@adbl/cells 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -14
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/{types → dist}/library/classes.d.ts +25 -56
- package/dist/library/classes.js +958 -0
- package/dist/library/classes.js.map +1 -0
- package/{types → dist}/library/index.d.ts +2 -1
- package/{library → dist/library}/index.js +3 -5
- package/dist/library/index.js.map +1 -0
- package/index.js +0 -2
- package/package.json +12 -4
- package/.vscode/settings.json +0 -4
- package/bun.lockb +0 -0
- package/library/classes.js +0 -958
- package/library/root.js +0 -44
- package/types/library/root.d.ts +0 -24
- /package/{types → dist}/index.d.ts +0 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template Input, Output
|
|
3
|
+
* @typedef {Object} AsyncRequestAtoms
|
|
4
|
+
*
|
|
5
|
+
* @property {SourceCell<boolean>} pending
|
|
6
|
+
* Represents the loading state of an asynchronous request.
|
|
7
|
+
*
|
|
8
|
+
* @property {SourceCell<Output|null>} data
|
|
9
|
+
* Represents the data returned by the asynchronous request.
|
|
10
|
+
*
|
|
11
|
+
* @property {SourceCell<Error | null>} error
|
|
12
|
+
* Represents the errors returned by the asynchronous request, if any.
|
|
13
|
+
*
|
|
14
|
+
* @property {NeverIfAny<Input> extends never ? (input?: Input) => Promise<void> : (input: Input) => Promise<void>} run
|
|
15
|
+
* Triggers the asynchronous request.
|
|
16
|
+
*
|
|
17
|
+
* @property {(newInput?: Input, changeLoadingState?: boolean) => Promise<void>} reload Triggers the asynchronous request again with an optional new input and optionally changes the loading state.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} EffectOptions
|
|
21
|
+
* @property {boolean} [once]
|
|
22
|
+
* Whether the effect should be removed after the first run.
|
|
23
|
+
* @property {AbortSignal} [signal]
|
|
24
|
+
* An AbortSignal to be used to ignore the effect if it is aborted.
|
|
25
|
+
* @property {string} [name]
|
|
26
|
+
* The name of the effect for debugging purposes.
|
|
27
|
+
* @property {boolean} [weak]
|
|
28
|
+
* Whether the effect should be weakly referenced. This means that the effect will be garbage collected if there are no other references to it.
|
|
29
|
+
* @property {number} [priority]
|
|
30
|
+
* The priority of the effect. Higher priority effects are executed first. The default priority is 0.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* @template T
|
|
34
|
+
* @typedef {object} CellOptions
|
|
35
|
+
* @property {boolean} [immutable]
|
|
36
|
+
* Whether the cell should be immutable. If set to true, the cell will not allow updates and will throw an error if the value is changed.
|
|
37
|
+
* @property {boolean} [deep]
|
|
38
|
+
* Whether the cell should watch for changes deep into the given value. By default the cell only reacts to changes at the top level.
|
|
39
|
+
* @property {(oldValue: T, newValue: T) => boolean} [equals]
|
|
40
|
+
* A function that determines whether two values are equal. If not provided, the default equality function will be used.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* @template T
|
|
44
|
+
* @typedef {0 extends (1 & T) ? never : T} NeverIfAny
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* The nesting level of batch operations.
|
|
48
|
+
* This will prevent nested batch operations from triggering effects when they finish.
|
|
49
|
+
* @type {number}
|
|
50
|
+
*/
|
|
51
|
+
let batchNestingLevel = 0;
|
|
52
|
+
/**
|
|
53
|
+
* A map of effect tuples to be executed in a batch.
|
|
54
|
+
* The key in each entry is the effect, and the value is the argument to call it with.
|
|
55
|
+
* All callbacks in this map will be executed only once in a batch.
|
|
56
|
+
* @type {Map<Function, any>}
|
|
57
|
+
*/
|
|
58
|
+
let batchedEffects = new Map();
|
|
59
|
+
/**
|
|
60
|
+
* A value representing the computed values that are currently being calculated.
|
|
61
|
+
* It is an array so it can keep track of nested computed values.
|
|
62
|
+
* @type {DerivedCell<any>[]}
|
|
63
|
+
*/
|
|
64
|
+
const activeComputedValues = [];
|
|
65
|
+
/**
|
|
66
|
+
* Tracks cells that need to be updated during the update cycle.
|
|
67
|
+
* Cells are added to this stack to be processed and updated sequentially.
|
|
68
|
+
* @type {Set<Cell<any>>}
|
|
69
|
+
*/
|
|
70
|
+
const updateBuffer = new Set();
|
|
71
|
+
/** @type {WeakMap<Cell<any>, Set<WeakRef<DerivedCell<any>>>>} */
|
|
72
|
+
const derivedCellMap = new WeakMap();
|
|
73
|
+
let isUpdating = false;
|
|
74
|
+
/** @type {Error[]} */
|
|
75
|
+
const cellErrors = [];
|
|
76
|
+
/**
|
|
77
|
+
* Processes and updates cells in the update queue.
|
|
78
|
+
*
|
|
79
|
+
* Iterates through cells in the update queue, computing their new values,
|
|
80
|
+
* and updating them if their values have changed. Clears the update queue
|
|
81
|
+
* after processing all cells. It ensures that derived cells are updated
|
|
82
|
+
* in a breadth-first order, which is important for preventing multiple
|
|
83
|
+
* updates of the same cell.
|
|
84
|
+
*/
|
|
85
|
+
function triggerUpdate() {
|
|
86
|
+
isUpdating = true;
|
|
87
|
+
for (const cell of updateBuffer) {
|
|
88
|
+
if (cell instanceof DerivedCell) {
|
|
89
|
+
const newValue = cell.computedFn();
|
|
90
|
+
if (deepEqual(cell.peek(), newValue))
|
|
91
|
+
continue;
|
|
92
|
+
// @ts-ignore: wvalue is protected.
|
|
93
|
+
cell.wvalue = newValue;
|
|
94
|
+
}
|
|
95
|
+
// Run computed dependents.
|
|
96
|
+
const computedDependents = derivedCellMap.get(cell);
|
|
97
|
+
if (computedDependents)
|
|
98
|
+
for (const dependent of computedDependents) {
|
|
99
|
+
const deref = dependent.deref();
|
|
100
|
+
if (deref === undefined) {
|
|
101
|
+
computedDependents.delete(dependent);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const computedCell = deref;
|
|
105
|
+
if (batchNestingLevel > 0) {
|
|
106
|
+
batchedEffects.set(() => {
|
|
107
|
+
if (!computedCell.initialized)
|
|
108
|
+
return;
|
|
109
|
+
updateBuffer.add(computedCell);
|
|
110
|
+
}, undefined);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (!computedCell.initialized)
|
|
114
|
+
continue;
|
|
115
|
+
updateBuffer.add(computedCell);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
cell.update();
|
|
119
|
+
}
|
|
120
|
+
updateBuffer.clear();
|
|
121
|
+
isUpdating = false;
|
|
122
|
+
throwAnyErrors();
|
|
123
|
+
}
|
|
124
|
+
function throwAnyErrors() {
|
|
125
|
+
if (cellErrors.length > 0) {
|
|
126
|
+
const errors = [...cellErrors];
|
|
127
|
+
cellErrors.length = 0;
|
|
128
|
+
throw new CellUpdateError(errors);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const mutativeMethods = {
|
|
132
|
+
Map: {
|
|
133
|
+
set: Symbol('set'),
|
|
134
|
+
delete: Symbol('delete'),
|
|
135
|
+
clear: Symbol('clear'),
|
|
136
|
+
},
|
|
137
|
+
Set: {
|
|
138
|
+
add: Symbol('add'),
|
|
139
|
+
delete: Symbol('delete'),
|
|
140
|
+
clear: Symbol('clear'),
|
|
141
|
+
},
|
|
142
|
+
Array: {
|
|
143
|
+
push: Symbol('push'),
|
|
144
|
+
pop: Symbol('pop'),
|
|
145
|
+
shift: Symbol('shift'),
|
|
146
|
+
unshift: Symbol('unshift'),
|
|
147
|
+
splice: Symbol('splice'),
|
|
148
|
+
sort: Symbol('sort'),
|
|
149
|
+
reverse: Symbol('reverse'),
|
|
150
|
+
},
|
|
151
|
+
Date: {
|
|
152
|
+
setDate: Symbol('setDate'),
|
|
153
|
+
setMonth: Symbol('setMonth'),
|
|
154
|
+
setFullYear: Symbol('setFullYear'),
|
|
155
|
+
setHours: Symbol('setHours'),
|
|
156
|
+
setMinutes: Symbol('setMinutes'),
|
|
157
|
+
setSeconds: Symbol('setSeconds'),
|
|
158
|
+
setMilliseconds: Symbol('setMilliseconds'),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const mutativeMapMethods = /^(set|delete|clear)$/;
|
|
162
|
+
const mutativeSetMethods = /^(add|delete|clear)$/;
|
|
163
|
+
const mutativeArrayMethods = /^(push|pop|shift|unshift|splice|sort|reverse)$/;
|
|
164
|
+
const mutativeDateMethods = /^(setDate|setMonth|setFullYear|setHours|setMinutes|setSeconds|setMilliseconds)$/;
|
|
165
|
+
/**
|
|
166
|
+
* Proxies mutative methods of a given value to trigger cell updates when called.
|
|
167
|
+
*
|
|
168
|
+
* @template {object} T
|
|
169
|
+
* @param {T} value - The object whose methods are to be proxied.
|
|
170
|
+
* @param {keyof typeof mutativeMethods} prototypeName - The name of the prototype (e.g., 'Map', 'Set') whose methods are being proxied.
|
|
171
|
+
* @param {Cell<any>} cell - The cell to be updated when a mutative method is called.
|
|
172
|
+
*/
|
|
173
|
+
const proxyMutativeMethods = (value, prototypeName, cell) => {
|
|
174
|
+
for (const method in mutativeMethods[prototypeName]) {
|
|
175
|
+
Reflect.set(value, Reflect.get(mutativeMethods[prototypeName], method),
|
|
176
|
+
/**
|
|
177
|
+
* @param {...any} args - The arguments passed to the mutative method.
|
|
178
|
+
* @returns {any} The result of calling the original method.
|
|
179
|
+
*/
|
|
180
|
+
(...args) => {
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
const innerMethod = value[method]; // Direct access is faster than Reflection here.
|
|
183
|
+
const result = innerMethod.apply(value, args);
|
|
184
|
+
updateBuffer.add(cell);
|
|
185
|
+
if (!isUpdating)
|
|
186
|
+
triggerUpdate();
|
|
187
|
+
return result;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* @template T
|
|
193
|
+
* @typedef {{
|
|
194
|
+
* deref: () => T | undefined
|
|
195
|
+
* }} Reference
|
|
196
|
+
*/
|
|
197
|
+
/** @template T */
|
|
198
|
+
class Effect {
|
|
199
|
+
/**
|
|
200
|
+
* @type {EffectOptions | undefined}
|
|
201
|
+
*/
|
|
202
|
+
options;
|
|
203
|
+
/**
|
|
204
|
+
* @type {WeakRef<(newValue: T) => void> | ((newValue: T) => void) }
|
|
205
|
+
*/
|
|
206
|
+
#callback;
|
|
207
|
+
/**
|
|
208
|
+
* @param {(newValue: T) => void} callback
|
|
209
|
+
* @param {EffectOptions} [options]
|
|
210
|
+
*/
|
|
211
|
+
constructor(callback, options) {
|
|
212
|
+
if (options?.weak) {
|
|
213
|
+
this.#callback = new WeakRef(callback);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
this.#callback = callback;
|
|
217
|
+
}
|
|
218
|
+
this.options = options;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Returns the callback function, if it still exists.
|
|
222
|
+
* @returns {((newValue: T) => void) | undefined}
|
|
223
|
+
*/
|
|
224
|
+
get callback() {
|
|
225
|
+
if (this.#callback instanceof WeakRef) {
|
|
226
|
+
return this.#callback.deref();
|
|
227
|
+
}
|
|
228
|
+
return this.#callback;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* @template {*} T
|
|
233
|
+
*/
|
|
234
|
+
export class Cell {
|
|
235
|
+
/**
|
|
236
|
+
* @type {Array<Effect<T>>}
|
|
237
|
+
*/
|
|
238
|
+
#effects = [];
|
|
239
|
+
/**
|
|
240
|
+
* @readonly
|
|
241
|
+
* @returns {Array<DerivedCell<any>>}
|
|
242
|
+
*/
|
|
243
|
+
get derivedCells() {
|
|
244
|
+
const dependents = derivedCellMap.get(this);
|
|
245
|
+
const cells = [];
|
|
246
|
+
if (dependents) {
|
|
247
|
+
for (const cell of dependents) {
|
|
248
|
+
const cellDeref = cell.deref();
|
|
249
|
+
if (cellDeref)
|
|
250
|
+
cells.push(cellDeref);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return cells;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* @protected @type T
|
|
257
|
+
*/
|
|
258
|
+
wvalue = /** @type {T} */ (null);
|
|
259
|
+
/**
|
|
260
|
+
* @protected
|
|
261
|
+
* @param {T} value
|
|
262
|
+
*/
|
|
263
|
+
setValue(value) {
|
|
264
|
+
this.wvalue = value;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Overrides `Object.prototype.valueOf()` to return the value stored in the Cell.
|
|
268
|
+
* @returns {T} The value of the Cell.
|
|
269
|
+
*/
|
|
270
|
+
valueOf() {
|
|
271
|
+
return this.revalued;
|
|
272
|
+
}
|
|
273
|
+
get value() {
|
|
274
|
+
return this.wvalue;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Stringifies the value of the Cell.
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
toString() {
|
|
281
|
+
// @ts-ignore
|
|
282
|
+
return String(this.wvalue);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The value stored in the Cell.
|
|
286
|
+
* @protected @type {T}
|
|
287
|
+
*/
|
|
288
|
+
get revalued() {
|
|
289
|
+
const currentlyComputedValue = activeComputedValues[activeComputedValues.length - 1];
|
|
290
|
+
if (currentlyComputedValue === undefined)
|
|
291
|
+
return this.wvalue;
|
|
292
|
+
let dependents = derivedCellMap.get(this);
|
|
293
|
+
if (dependents === undefined) {
|
|
294
|
+
dependents = new Set();
|
|
295
|
+
derivedCellMap.set(this, dependents);
|
|
296
|
+
}
|
|
297
|
+
const isAlreadySubscribed = dependents?.has(currentlyComputedValue.ref);
|
|
298
|
+
if (isAlreadySubscribed)
|
|
299
|
+
return this.wvalue;
|
|
300
|
+
dependents.add(currentlyComputedValue.ref);
|
|
301
|
+
return this.wvalue;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Adds the provided effect callback to the list of effects for this cell, and returns a function that can be called to remove the effect.
|
|
305
|
+
* @param {(newValue: T) => void} callback - The effect callback to add.
|
|
306
|
+
* @param {EffectOptions} [options] - The options for the effect.
|
|
307
|
+
* @returns {() => void} A function that can be called to remove the effect.
|
|
308
|
+
*/
|
|
309
|
+
listen(callback, options) {
|
|
310
|
+
let effect = callback;
|
|
311
|
+
if (options?.signal?.aborted) {
|
|
312
|
+
return () => { };
|
|
313
|
+
}
|
|
314
|
+
options?.signal?.addEventListener('abort', () => {
|
|
315
|
+
this.ignore(effect);
|
|
316
|
+
});
|
|
317
|
+
if (options?.once) {
|
|
318
|
+
effect = () => {
|
|
319
|
+
callback(this.wvalue);
|
|
320
|
+
this.ignore(effect);
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (options?.name && this.isListeningTo(options.name)) {
|
|
324
|
+
throw new Error(`An effect with the name "${options.name}" is already listening to this cell.`);
|
|
325
|
+
}
|
|
326
|
+
const isAlreadySubscribed = this.#effects.some((effect) => {
|
|
327
|
+
return effect.callback === callback;
|
|
328
|
+
});
|
|
329
|
+
if (!isAlreadySubscribed) {
|
|
330
|
+
this.#effects.push(new Effect(effect, options));
|
|
331
|
+
}
|
|
332
|
+
this.#effects.sort((a, b) => {
|
|
333
|
+
const aPriority = a.options?.priority ?? 0;
|
|
334
|
+
const bPriority = b.options?.priority ?? 0;
|
|
335
|
+
if (aPriority === bPriority)
|
|
336
|
+
return 0;
|
|
337
|
+
return aPriority < bPriority ? 1 : -1;
|
|
338
|
+
});
|
|
339
|
+
return () => this.ignore(effect);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Creates an effect that is immediately executed with the current value of the cell, and then added to the list of effects for the cell.
|
|
343
|
+
* @param {(newValue: T) => void} callback - The effect callback to add.
|
|
344
|
+
* @param {Partial<EffectOptions>} [options] - The options for the effect.
|
|
345
|
+
* @returns {() => void} A function that can be called to remove the effect.
|
|
346
|
+
*/
|
|
347
|
+
runAndListen(callback, options) {
|
|
348
|
+
const cb = callback;
|
|
349
|
+
try {
|
|
350
|
+
cb(this.wvalue);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
if (e instanceof Error) {
|
|
354
|
+
cellErrors.push(e);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (options?.signal?.aborted) {
|
|
358
|
+
return () => { };
|
|
359
|
+
}
|
|
360
|
+
options?.signal?.addEventListener('abort', () => {
|
|
361
|
+
this.ignore(cb);
|
|
362
|
+
});
|
|
363
|
+
if (options?.once)
|
|
364
|
+
return () => this.ignore(cb);
|
|
365
|
+
if (options?.name && this.isListeningTo(options.name)) {
|
|
366
|
+
const message = `An effect with the name "${options.name}" is already listening to this cell.`;
|
|
367
|
+
throw new Error(message);
|
|
368
|
+
}
|
|
369
|
+
const isAlreadySubscribed = this.#effects.some((e) => {
|
|
370
|
+
return e.callback === callback;
|
|
371
|
+
});
|
|
372
|
+
if (!isAlreadySubscribed) {
|
|
373
|
+
this.#effects.push(new Effect(cb, options));
|
|
374
|
+
}
|
|
375
|
+
this.#effects.sort((a, b) => {
|
|
376
|
+
const aPriority = a.options?.priority ?? 0;
|
|
377
|
+
const bPriority = b.options?.priority ?? 0;
|
|
378
|
+
if (aPriority === bPriority)
|
|
379
|
+
return 0;
|
|
380
|
+
return aPriority < bPriority ? 1 : -1;
|
|
381
|
+
});
|
|
382
|
+
throwAnyErrors();
|
|
383
|
+
return () => this.ignore(cb);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Removes the specified effect callback from the list of effects for this cell.
|
|
387
|
+
* @param {(newValue: T) => void} callback - The effect callback to remove.
|
|
388
|
+
*/
|
|
389
|
+
ignore(callback) {
|
|
390
|
+
const index = this.#effects.findIndex((e) => {
|
|
391
|
+
return e.callback === callback;
|
|
392
|
+
});
|
|
393
|
+
if (index === -1)
|
|
394
|
+
return;
|
|
395
|
+
this.#effects.splice(index, 1);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Checks if the cell is listening to a watcher with the specified name.
|
|
399
|
+
* @param {string} name - The name of the watcher to check for.
|
|
400
|
+
* @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
|
|
401
|
+
*/
|
|
402
|
+
isListeningTo(name) {
|
|
403
|
+
return this.#effects.some((effect) => {
|
|
404
|
+
return effect?.options?.name === name && effect.callback;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Removes the watcher with the specified name from the list of effects for this cell.
|
|
409
|
+
* @param {string} name - The name of the watcher to stop listening to.
|
|
410
|
+
*/
|
|
411
|
+
stopListeningTo(name) {
|
|
412
|
+
const effectIndex = this.#effects.findIndex((e) => {
|
|
413
|
+
return e.options?.name === name;
|
|
414
|
+
});
|
|
415
|
+
if (effectIndex === -1)
|
|
416
|
+
return;
|
|
417
|
+
this.#effects.splice(effectIndex, 1);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Updates the root object and notifies any registered watchers and computed dependents.
|
|
421
|
+
* This method is called whenever the root object's value changes.
|
|
422
|
+
*/
|
|
423
|
+
update() {
|
|
424
|
+
// Run watchers.
|
|
425
|
+
const wvalue = this.peek();
|
|
426
|
+
const effects = this.#effects;
|
|
427
|
+
let len = effects.length;
|
|
428
|
+
for (let i = 0; i < len; i++) {
|
|
429
|
+
const watcher = effects[i].callback;
|
|
430
|
+
if (watcher === undefined) {
|
|
431
|
+
effects.splice(i, 1);
|
|
432
|
+
i--;
|
|
433
|
+
len--;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (batchNestingLevel > 0) {
|
|
437
|
+
batchedEffects.set(watcher, wvalue);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
try {
|
|
441
|
+
watcher(wvalue);
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
if (e instanceof Error) {
|
|
445
|
+
cellErrors.push(e);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Returns the current value of the cell without registering a watcher.
|
|
453
|
+
* @returns {T} - The current value of the cell.
|
|
454
|
+
*/
|
|
455
|
+
peek() {
|
|
456
|
+
return this.wvalue;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* @template T
|
|
460
|
+
* Creates a new Cell instance with the provided value.
|
|
461
|
+
* @param {T} value - The value to be stored in the Cell.
|
|
462
|
+
* @param {Partial<CellOptions<T>>} [options] - The options for the cell.
|
|
463
|
+
* @returns {SourceCell<T>} A new Cell instance.
|
|
464
|
+
* ```
|
|
465
|
+
* import { Cell } from '@adbl/cells';
|
|
466
|
+
*
|
|
467
|
+
* const cell = Cell.source('Hello world');
|
|
468
|
+
* console.log(cell.value); // Hello world.
|
|
469
|
+
*
|
|
470
|
+
* cell.value = 'Greetings!';
|
|
471
|
+
* console.log(cell.value) // Greetings!
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
static source = (value, options) => new SourceCell(value, options);
|
|
475
|
+
/**
|
|
476
|
+
* @template T
|
|
477
|
+
* Creates a new Derived instance with the provided callback function.
|
|
478
|
+
* @param {() => T} callback - The callback function to be used by the Derived instance.
|
|
479
|
+
* @returns {DerivedCell<T>} A new Derived instance.
|
|
480
|
+
* ```
|
|
481
|
+
* import { Cell } from '@adbl/cells';
|
|
482
|
+
*
|
|
483
|
+
* const cell = Cell.source(2);
|
|
484
|
+
* const derived = Cell.derived(() => cell.value * 2);
|
|
485
|
+
*
|
|
486
|
+
* console.log(derived.value); // 4
|
|
487
|
+
*
|
|
488
|
+
* cell.value = 3;
|
|
489
|
+
* console.log(derived.value); // 6
|
|
490
|
+
* ```
|
|
491
|
+
*/
|
|
492
|
+
static derived = (callback) => new DerivedCell(callback);
|
|
493
|
+
/**
|
|
494
|
+
* @template X
|
|
495
|
+
* Batches all the effects created to run only once.
|
|
496
|
+
* @param {() => X} callback - The function to be executed in a batched manner.
|
|
497
|
+
* @returns {X} The return value of the callback.
|
|
498
|
+
*/
|
|
499
|
+
static batch = (callback) => {
|
|
500
|
+
batchNestingLevel++;
|
|
501
|
+
/** @type {X | undefined} */
|
|
502
|
+
let value = undefined;
|
|
503
|
+
let error;
|
|
504
|
+
try {
|
|
505
|
+
value = callback();
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
error = e;
|
|
509
|
+
}
|
|
510
|
+
batchNestingLevel--;
|
|
511
|
+
if (error instanceof Error) {
|
|
512
|
+
cellErrors.push(error);
|
|
513
|
+
}
|
|
514
|
+
if (batchNestingLevel === 0) {
|
|
515
|
+
for (const [effect, args] of batchedEffects) {
|
|
516
|
+
effect(args);
|
|
517
|
+
}
|
|
518
|
+
if (!isUpdating)
|
|
519
|
+
triggerUpdate();
|
|
520
|
+
batchedEffects = new Map();
|
|
521
|
+
}
|
|
522
|
+
throwAnyErrors();
|
|
523
|
+
return /** @type {X} */ (value);
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* Checks if the provided value is an instance of the Cell class.
|
|
527
|
+
* @template [T=any]
|
|
528
|
+
* @template [U=any]
|
|
529
|
+
* @param {Cell<T> | U} value - The value to check.
|
|
530
|
+
* @returns {value is Cell<T>} True if the value is an instance of Cell, false otherwise.
|
|
531
|
+
*/
|
|
532
|
+
static isCell = (value) => value instanceof Cell;
|
|
533
|
+
/**
|
|
534
|
+
* @template T
|
|
535
|
+
* Flattens the provided value by returning the value if it is not a Cell instance, or the value of the Cell instance if it is.
|
|
536
|
+
* @param {T | Cell<T>} value - The value to be flattened.
|
|
537
|
+
* @returns {T} The flattened value.
|
|
538
|
+
*/
|
|
539
|
+
static flatten = (value) => {
|
|
540
|
+
if (value instanceof Cell) {
|
|
541
|
+
if (value instanceof DerivedCell) {
|
|
542
|
+
if (value.initialized) {
|
|
543
|
+
return Cell.flatten(value.wvalue);
|
|
544
|
+
}
|
|
545
|
+
value.setValue(value.computedFn());
|
|
546
|
+
return Cell.flatten(value.wvalue);
|
|
547
|
+
}
|
|
548
|
+
return Cell.flatten(value.wvalue);
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(value)) {
|
|
551
|
+
// @ts-ignore:
|
|
552
|
+
return Cell.flattenArray(value);
|
|
553
|
+
}
|
|
554
|
+
if (value instanceof Object) {
|
|
555
|
+
// @ts-ignore:
|
|
556
|
+
return Cell.flattenObject(value);
|
|
557
|
+
}
|
|
558
|
+
return value;
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* Flattens an array by applying the `flatten` function to each element.
|
|
562
|
+
* @template T
|
|
563
|
+
* @param {Array<T | Cell<T>>} array - The array to be flattened.
|
|
564
|
+
* @returns {Array<T>} A new array with the flattened elements.
|
|
565
|
+
*/
|
|
566
|
+
static flattenArray = (array) => array.map(Cell.flatten);
|
|
567
|
+
/**
|
|
568
|
+
* Flattens an object by applying the `flatten` function to each value.
|
|
569
|
+
* @template {object} T
|
|
570
|
+
* @param {T} object - The object to be flattened.
|
|
571
|
+
* @returns {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} A new object with the flattened values.
|
|
572
|
+
*/
|
|
573
|
+
static flattenObject = (object) => {
|
|
574
|
+
const result =
|
|
575
|
+
/** @type {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} */ ({});
|
|
576
|
+
for (const [key, value] of Object.entries(object)) {
|
|
577
|
+
Reflect.set(result, key, Cell.flatten(value));
|
|
578
|
+
}
|
|
579
|
+
return result;
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Wraps an asynchronous function with managed state.
|
|
583
|
+
*
|
|
584
|
+
* @template X - The type of the input parameter for the getter function.
|
|
585
|
+
* @template Y - The type of the output returned by the getter function.
|
|
586
|
+
* @param {(input: X) => Promise<Y>} getter - A function that performs the asynchronous operation.
|
|
587
|
+
* @returns {AsyncRequestAtoms<X, Y>} An object containing cells for pending, data, and error states,
|
|
588
|
+
* as well as functions to run and reload the operation.
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* const { pending, data, error, run, reload } = Cell.async(async (input) => {
|
|
592
|
+
* const response = await fetch(`https://example.com/api/data?input=${input}`);
|
|
593
|
+
* return response.json();
|
|
594
|
+
* });
|
|
595
|
+
*
|
|
596
|
+
* run('input');
|
|
597
|
+
*/
|
|
598
|
+
static async(getter) {
|
|
599
|
+
const pending = Cell.source(false);
|
|
600
|
+
const data = Cell.source(/** @type {Y | null} */ (null));
|
|
601
|
+
const error = Cell.source(/** @type {Error | null} */ (null));
|
|
602
|
+
/** @type {X | undefined} */
|
|
603
|
+
let initialInput = undefined;
|
|
604
|
+
async function run(input = initialInput) {
|
|
605
|
+
pending.value = true;
|
|
606
|
+
error.value = null;
|
|
607
|
+
data.value = null;
|
|
608
|
+
await Cell.batch(async () => {
|
|
609
|
+
try {
|
|
610
|
+
initialInput = input;
|
|
611
|
+
const result = await getter(/** @type {X} */ (input));
|
|
612
|
+
data.value = result;
|
|
613
|
+
}
|
|
614
|
+
catch (e) {
|
|
615
|
+
if (e instanceof Error) {
|
|
616
|
+
error.value = e;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
throw e;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
finally {
|
|
623
|
+
pending.value = false;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* @param {X} [newInput]
|
|
629
|
+
* @param {boolean} [changeLoadingState]
|
|
630
|
+
*/
|
|
631
|
+
async function reload(newInput, changeLoadingState = true) {
|
|
632
|
+
if (changeLoadingState) {
|
|
633
|
+
pending.value = true;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
const result = await getter(
|
|
637
|
+
/** @type {X} */ (newInput ?? initialInput));
|
|
638
|
+
data.value = result;
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
if (e instanceof Error) {
|
|
642
|
+
error.value = e;
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
throw e;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
if (changeLoadingState) {
|
|
650
|
+
pending.value = false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
pending,
|
|
656
|
+
data,
|
|
657
|
+
error,
|
|
658
|
+
run,
|
|
659
|
+
reload,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* A class that represents a computed value that depends on other reactive values.
|
|
665
|
+
* The computed value is automatically updated when any of its dependencies change.
|
|
666
|
+
* @template {*} T
|
|
667
|
+
* @extends {Cell<T>}
|
|
668
|
+
*/
|
|
669
|
+
export class DerivedCell extends Cell {
|
|
670
|
+
/**
|
|
671
|
+
* @param {() => T} computedFn - A function that generates the value of the computed.
|
|
672
|
+
*/
|
|
673
|
+
constructor(computedFn) {
|
|
674
|
+
super();
|
|
675
|
+
this.ref = new WeakRef(this);
|
|
676
|
+
// Ensures that the cell is derived every time the computing function is called.
|
|
677
|
+
const derivationWrapper = () => {
|
|
678
|
+
activeComputedValues.push(this);
|
|
679
|
+
let value = this.wvalue;
|
|
680
|
+
try {
|
|
681
|
+
value = computedFn();
|
|
682
|
+
}
|
|
683
|
+
catch (e) {
|
|
684
|
+
if (e instanceof Error)
|
|
685
|
+
cellErrors.push(e);
|
|
686
|
+
}
|
|
687
|
+
activeComputedValues.pop();
|
|
688
|
+
return value;
|
|
689
|
+
};
|
|
690
|
+
this.computedFn = /** @type {() => T} */ (derivationWrapper);
|
|
691
|
+
this.initialized = false;
|
|
692
|
+
}
|
|
693
|
+
/** @type {() => T} */
|
|
694
|
+
computedFn;
|
|
695
|
+
/** @type {WeakRef<this>} */
|
|
696
|
+
ref;
|
|
697
|
+
/**
|
|
698
|
+
* @readonly
|
|
699
|
+
*/
|
|
700
|
+
get value() {
|
|
701
|
+
if (!this.initialized) {
|
|
702
|
+
this.initialized = true;
|
|
703
|
+
this.setValue(this.computedFn());
|
|
704
|
+
throwAnyErrors();
|
|
705
|
+
}
|
|
706
|
+
return this.revalued;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Listens for changes to the cell, initializing the value if not already done.
|
|
710
|
+
* @param {(newValue: T) => void} callback - The function to call when the cell's value changes.
|
|
711
|
+
* @param {object} [options] - Optional configuration for listening.
|
|
712
|
+
*/
|
|
713
|
+
listen(callback, options) {
|
|
714
|
+
if (!this.initialized) {
|
|
715
|
+
this.initialized = true;
|
|
716
|
+
this.setValue(this.computedFn());
|
|
717
|
+
throwAnyErrors();
|
|
718
|
+
}
|
|
719
|
+
return super.listen(callback, options);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Runs the callback and sets up a listener, initializing the cell's value if not already done.
|
|
723
|
+
* @param {(newValue: T) => void} callback - The function to call when the cell's value changes.
|
|
724
|
+
* @param {object} [options] - Optional configuration for listening and running.
|
|
725
|
+
* @returns {*} The result of the parent class's runAndListen method.
|
|
726
|
+
*/
|
|
727
|
+
runAndListen(callback, options) {
|
|
728
|
+
if (!this.initialized) {
|
|
729
|
+
this.initialized = true;
|
|
730
|
+
this.setValue(this.computedFn());
|
|
731
|
+
throwAnyErrors();
|
|
732
|
+
}
|
|
733
|
+
return super.runAndListen(callback, options);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* @readonly
|
|
737
|
+
*/
|
|
738
|
+
set value(_) {
|
|
739
|
+
throw new Error('Cannot set a derived Cell value.');
|
|
740
|
+
}
|
|
741
|
+
deproxy() {
|
|
742
|
+
throw new Error('Cannot deproxy a derived cell.');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* @template {*} T
|
|
747
|
+
* @extends {Cell<T>}
|
|
748
|
+
*/
|
|
749
|
+
export class SourceCell extends Cell {
|
|
750
|
+
/** @type {object | undefined} */
|
|
751
|
+
#originalObject;
|
|
752
|
+
/**
|
|
753
|
+
* Creates a new Cell with the provided value.
|
|
754
|
+
* @param {T} value
|
|
755
|
+
* @param {Partial<CellOptions<T>>} [options]
|
|
756
|
+
*/
|
|
757
|
+
constructor(value, options) {
|
|
758
|
+
super();
|
|
759
|
+
if (options !== undefined)
|
|
760
|
+
this.options = options;
|
|
761
|
+
this.setValue(this.#proxy(value));
|
|
762
|
+
if (typeof value === 'object' && value !== null) {
|
|
763
|
+
this.#originalObject = value;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* For cells containing objects, returns the object itself.
|
|
768
|
+
* This can be useful in scenarios where unfettered access to the original object is needed,
|
|
769
|
+
* such as when using the object as a cache.
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* const cell = new SourceCell({ a: 1, b: 2 });
|
|
773
|
+
* console.log(cell.deproxy()); // { a: 1, b: 2 }
|
|
774
|
+
*
|
|
775
|
+
* cell.value = { a: 3, b: 4 };
|
|
776
|
+
* console.log(cell.deproxy()); // { a: 3, b: 4 }
|
|
777
|
+
*
|
|
778
|
+
* @returns {T extends object ? T : never} The original object if T is an object, otherwise never.
|
|
779
|
+
*/
|
|
780
|
+
deproxy() {
|
|
781
|
+
const originalObject = this.#originalObject;
|
|
782
|
+
if (typeof originalObject === 'object' && originalObject !== null) {
|
|
783
|
+
return /** @type {T extends object ? T : never} */ (originalObject);
|
|
784
|
+
}
|
|
785
|
+
throw new Error('Cannot deproxy a non-object cell.');
|
|
786
|
+
}
|
|
787
|
+
peek() {
|
|
788
|
+
const originalObject = this.#originalObject;
|
|
789
|
+
if (typeof originalObject === 'object' && originalObject !== null) {
|
|
790
|
+
return /** @type {T extends object ? T : never} */ (originalObject);
|
|
791
|
+
}
|
|
792
|
+
return this.wvalue;
|
|
793
|
+
}
|
|
794
|
+
get value() {
|
|
795
|
+
return this.revalued;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Sets the value stored in the Cell and triggers an update.
|
|
799
|
+
* @param {T} value
|
|
800
|
+
*/
|
|
801
|
+
set value(value) {
|
|
802
|
+
if (this.options?.immutable) {
|
|
803
|
+
throw new Error('Cannot set the value of an immutable cell.');
|
|
804
|
+
}
|
|
805
|
+
const oldValue = this.peek();
|
|
806
|
+
const isEqual = this.options?.equals
|
|
807
|
+
? this.options.equals(oldValue, value)
|
|
808
|
+
: deepEqual(oldValue, value);
|
|
809
|
+
if (isEqual)
|
|
810
|
+
return;
|
|
811
|
+
this.setValue(this.#proxy(value));
|
|
812
|
+
if (typeof value === 'object' && value !== null) {
|
|
813
|
+
this.#originalObject = value;
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
this.#originalObject = undefined;
|
|
817
|
+
}
|
|
818
|
+
updateBuffer.add(this);
|
|
819
|
+
if (!isUpdating)
|
|
820
|
+
triggerUpdate();
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Proxies the provided value deeply, allowing it to be observed and updated.
|
|
824
|
+
* @template T
|
|
825
|
+
* @param {T} value - The value to be proxied.
|
|
826
|
+
* @returns {T} - The proxied value.
|
|
827
|
+
*/
|
|
828
|
+
#proxy(value) {
|
|
829
|
+
if (typeof value !== 'object' || value === null) {
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
if (value instanceof Map) {
|
|
833
|
+
proxyMutativeMethods(value, 'Map', this);
|
|
834
|
+
}
|
|
835
|
+
else if (value instanceof Set) {
|
|
836
|
+
proxyMutativeMethods(value, 'Set', this);
|
|
837
|
+
}
|
|
838
|
+
else if (value instanceof Date) {
|
|
839
|
+
proxyMutativeMethods(value, 'Date', this);
|
|
840
|
+
}
|
|
841
|
+
else if (ArrayBuffer.isView(value) || Array.isArray(value)) {
|
|
842
|
+
proxyMutativeMethods(value, 'Array', this);
|
|
843
|
+
}
|
|
844
|
+
return new Proxy(value, {
|
|
845
|
+
get: (target, prop) => {
|
|
846
|
+
this.revalued;
|
|
847
|
+
if (this.options?.deep) {
|
|
848
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
849
|
+
return this.#proxy(target[prop]);
|
|
850
|
+
}
|
|
851
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
852
|
+
let value = target[prop];
|
|
853
|
+
if (typeof value === 'function') {
|
|
854
|
+
value = value.bind(target);
|
|
855
|
+
}
|
|
856
|
+
if (typeof prop === 'string') {
|
|
857
|
+
if (target instanceof Map && mutativeMapMethods.test(prop)) {
|
|
858
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
859
|
+
return target[mutativeMethods.Map[prop]];
|
|
860
|
+
}
|
|
861
|
+
if (target instanceof Set && mutativeSetMethods.test(prop)) {
|
|
862
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
863
|
+
return target[mutativeMethods.Set[prop]];
|
|
864
|
+
}
|
|
865
|
+
if (target instanceof Date && mutativeDateMethods.test(prop)) {
|
|
866
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
867
|
+
return target[mutativeMethods.Date[prop]];
|
|
868
|
+
}
|
|
869
|
+
if ((ArrayBuffer.isView(target) || Array.isArray(target)) &&
|
|
870
|
+
mutativeArrayMethods.test(prop)) {
|
|
871
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
872
|
+
return target[mutativeMethods.Array[prop]];
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return value;
|
|
876
|
+
},
|
|
877
|
+
set: (target, prop, value) => {
|
|
878
|
+
const formerValue = Reflect.get(target, prop);
|
|
879
|
+
Reflect.set(target, prop, value);
|
|
880
|
+
const isEqual = deepEqual(formerValue, value);
|
|
881
|
+
if (!isEqual) {
|
|
882
|
+
updateBuffer.add(this);
|
|
883
|
+
if (!isUpdating)
|
|
884
|
+
triggerUpdate();
|
|
885
|
+
}
|
|
886
|
+
return true;
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
export class CellUpdateError extends Error {
|
|
892
|
+
/** @param {Error[]} errors */
|
|
893
|
+
constructor(errors) {
|
|
894
|
+
super('Errors occurred during cell update cycle');
|
|
895
|
+
this.errors = errors;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Recursively compares two values for deep equality.
|
|
900
|
+
* @param {any} a - The first value to compare.
|
|
901
|
+
* @param {any} b - The second value to compare.
|
|
902
|
+
* @returns {boolean} - True if the values are deeply equal, false otherwise.
|
|
903
|
+
*/
|
|
904
|
+
function deepEqual(a, b) {
|
|
905
|
+
if (a === b)
|
|
906
|
+
return true;
|
|
907
|
+
if (typeof a !== typeof b ||
|
|
908
|
+
typeof a !== 'object' ||
|
|
909
|
+
a === null ||
|
|
910
|
+
b === null)
|
|
911
|
+
return false;
|
|
912
|
+
if (a instanceof Date) {
|
|
913
|
+
if (!(b instanceof Date)) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
if (a.getTime() !== b.getTime()) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (a instanceof Map) {
|
|
921
|
+
if (!(b instanceof Map)) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
if (a.size !== b.size) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
for (const [key, value] of a.entries()) {
|
|
928
|
+
if (!deepEqual(b.get(key), value)) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (Array.isArray(a)) {
|
|
934
|
+
const aLength = a.length;
|
|
935
|
+
if (!Array.isArray(b) || aLength !== b.length)
|
|
936
|
+
return false;
|
|
937
|
+
for (let i = 0; i < aLength; i++) {
|
|
938
|
+
if (!deepEqual(a[i], b[i]))
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
const keysA = Object.keys(a);
|
|
944
|
+
const keysB = Object.keys(b);
|
|
945
|
+
const keysALength = keysA.length;
|
|
946
|
+
if (keysALength !== keysB.length)
|
|
947
|
+
return false;
|
|
948
|
+
for (let i = 0; i < keysALength; i++) {
|
|
949
|
+
const key = keysA[i];
|
|
950
|
+
if (a === b)
|
|
951
|
+
return true;
|
|
952
|
+
if (!(key in b) || !deepEqual(a[key], b[key]))
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
//# sourceMappingURL=classes.js.map
|