-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathobjects.ts
177 lines (151 loc) · 4.51 KB
/
objects.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import { isArray, isObject, Obj } from './types';
import { ToolkitError } from '../toolkit/error';
/**
* Return a new object by adding missing keys into another object
*/
export function applyDefaults(hash: any, defaults: any) {
const result: any = { };
Object.keys(hash).forEach(k => result[k] = hash[k]);
Object.keys(defaults)
.filter(k => !(k in result))
.forEach(k => result[k] = defaults[k]);
return result;
}
/**
* Return whether the given parameter is an empty object or empty list.
*/
export function isEmpty(x: any) {
if (x == null) { return false; }
if (isArray(x)) { return x.length === 0; }
return Object.keys(x).length === 0;
}
/**
* Deep clone a tree of objects, lists or scalars
*
* Does not support cycles.
*/
export function deepClone(x: any): any {
if (typeof x === 'undefined') { return undefined; }
if (x === null) { return null; }
if (isArray(x)) { return x.map(deepClone); }
if (isObject(x)) { return makeObject(mapObject(x, (k, v) => [k, deepClone(v)] as [string, any])); }
return x;
}
/**
* Map over an object, treating it as a dictionary
*/
export function mapObject<T, U>(x: Obj<T>, fn: (key: string, value: T) => U): U[] {
const ret: U[] = [];
Object.keys(x).forEach(key => {
ret.push(fn(key, x[key]));
});
return ret;
}
/**
* Construct an object from a list of (k, v) pairs
*/
export function makeObject<T>(pairs: Array<[string, T]>): Obj<T> {
const ret: Obj<T> = {};
for (const pair of pairs) {
ret[pair[0]] = pair[1];
}
return ret;
}
/**
* Deep get a value from a tree of nested objects
*
* Returns undefined if any part of the path was unset or
* not an object.
*/
export function deepGet(x: any, path: string[]): any {
path = path.slice();
while (path.length > 0 && isObject(x)) {
const key = path.shift()!;
x = x[key];
}
return path.length === 0 ? x : undefined;
}
/**
* Deep set a value in a tree of nested objects
*
* Throws an error if any part of the path is not an object.
*/
export function deepSet(x: any, path: string[], value: any) {
path = path.slice();
if (path.length === 0) {
throw new ToolkitError('Path may not be empty');
}
while (path.length > 1 && isObject(x)) {
const key = path.shift()!;
if (!(key in x)) { x[key] = {}; }
x = x[key];
}
if (!isObject(x)) {
throw new ToolkitError(`Expected an object, got '${x}'`);
}
if (value !== undefined) {
x[path[0]] = value;
} else {
delete x[path[0]];
}
}
/**
* Recursively merge objects together
*
* The leftmost object is mutated and returned. Arrays are not merged
* but overwritten just like scalars.
*
* If an object is merged into a non-object, the non-object is lost.
*/
export function deepMerge(...objects: Array<Obj<any> | undefined>) {
function mergeOne(target: Obj<any>, source: Obj<any>) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor') {
continue;
}
const value = source[key];
if (isObject(value)) {
if (!isObject(target[key])) { target[key] = {}; } // Overwrite on purpose
mergeOne(target[key], value);
} else if (typeof value !== 'undefined') {
target[key] = value;
}
}
}
const others = objects.filter(x => x != null) as Array<Obj<any>>;
if (others.length === 0) { return {}; }
const into = others.splice(0, 1)[0];
others.forEach(other => mergeOne(into, other));
return into;
}
/**
* Splits the given object into two, such that:
*
* 1. The size of the first object (after stringified in UTF-8) is less than or equal to the provided size limit.
* 2. Merging the two objects results in the original one.
*/
export function splitBySize(data: any, maxSizeBytes: number): [any, any] {
if (maxSizeBytes < 2) {
// It's impossible to fit anything in the first object
return [undefined, data];
}
const entries = Object.entries(data);
return recurse(0, 0);
function recurse(index: number, runningTotalSize: number): [any, any] {
if (index >= entries.length) {
// Everything fits in the first object
return [data, undefined];
}
const size = runningTotalSize + entrySize(entries[index]);
return (size > maxSizeBytes) ? cutAt(index) : recurse(index + 1, size);
}
function entrySize(entry: [string, unknown]) {
return Buffer.byteLength(JSON.stringify(Object.fromEntries([entry])));
}
function cutAt(index: number): [any, any] {
return [
Object.fromEntries(entries.slice(0, index)),
Object.fromEntries(entries.slice(index)),
];
}
}