@adbl/cells 0.0.3 → 0.0.5
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 +1 -1
- package/index.js +1 -1
- package/library/classes.js +136 -30
- package/package.json +1 -1
- package/types/library/classes.d.ts +3 -20
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ When creating a source cell, you have fine-grained control over its behavior:
|
|
|
186
186
|
```javascript
|
|
187
187
|
const cell = Cell.source(initialValue, {
|
|
188
188
|
immutable: boolean, // If true, the cell will not allow updates
|
|
189
|
-
|
|
189
|
+
deep: boolean, // By default, the cell only reacts to changes at the top level of objects. Setting deep to true will proxy the cell to all nested properties and trigger updates when they change as well.
|
|
190
190
|
equals: (oldValue, newValue) => boolean, // Custom equality function
|
|
191
191
|
});
|
|
192
192
|
```
|
package/index.js
CHANGED
package/library/classes.js
CHANGED
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
* @typedef {object} CellOptions
|
|
37
37
|
* @property {boolean} [immutable]
|
|
38
38
|
* 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.
|
|
39
|
-
* @property {boolean} [
|
|
40
|
-
* Whether the cell
|
|
39
|
+
* @property {boolean} [deep]
|
|
40
|
+
* Whether the cell should watch for changes deep into the given value. By default the cell only reacts to changes at the top level.
|
|
41
41
|
* @property {(oldValue: T, newValue: T) => boolean} [equals]
|
|
42
42
|
* A function that determines whether two values are equal. If not provided, the default equality function will be used.
|
|
43
43
|
*/
|
|
@@ -48,6 +48,69 @@
|
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
import { activeComputedValues, root } from './root.js';
|
|
51
|
+
const mutativeMethods = {
|
|
52
|
+
Map: {
|
|
53
|
+
set: Symbol('set'),
|
|
54
|
+
delete: Symbol('delete'),
|
|
55
|
+
clear: Symbol('clear'),
|
|
56
|
+
},
|
|
57
|
+
Set: {
|
|
58
|
+
add: Symbol('add'),
|
|
59
|
+
delete: Symbol('delete'),
|
|
60
|
+
clear: Symbol('clear'),
|
|
61
|
+
},
|
|
62
|
+
Array: {
|
|
63
|
+
push: Symbol('push'),
|
|
64
|
+
pop: Symbol('pop'),
|
|
65
|
+
shift: Symbol('shift'),
|
|
66
|
+
unshift: Symbol('unshift'),
|
|
67
|
+
splice: Symbol('splice'),
|
|
68
|
+
sort: Symbol('sort'),
|
|
69
|
+
reverse: Symbol('reverse'),
|
|
70
|
+
},
|
|
71
|
+
Date: {
|
|
72
|
+
setDate: Symbol('setDate'),
|
|
73
|
+
setMonth: Symbol('setMonth'),
|
|
74
|
+
setFullYear: Symbol('setFullYear'),
|
|
75
|
+
setHours: Symbol('setHours'),
|
|
76
|
+
setMinutes: Symbol('setMinutes'),
|
|
77
|
+
setSeconds: Symbol('setSeconds'),
|
|
78
|
+
setMilliseconds: Symbol('setMilliseconds'),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const mutativeMapMethods = /^(set|delete|clear)$/;
|
|
82
|
+
const mutativeSetMethods = /^(add|delete|clear)$/;
|
|
83
|
+
const mutativeArrayMethods = /^(push|pop|shift|unshift|splice|sort|reverse)$/;
|
|
84
|
+
const mutativeDateMethods =
|
|
85
|
+
/^(setDate|setMonth|setFullYear|setHours|setMinutes|setSeconds|setMilliseconds)$/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Proxies mutative methods of a given value to trigger cell updates when called.
|
|
89
|
+
*
|
|
90
|
+
* @template {object} T
|
|
91
|
+
* @param {T} value - The object whose methods are to be proxied.
|
|
92
|
+
* @param {keyof typeof mutativeMethods} prototypeName - The name of the prototype (e.g., 'Map', 'Set') whose methods are being proxied.
|
|
93
|
+
* @param {Cell<any>} cell - The cell to be updated when a mutative method is called.
|
|
94
|
+
*/
|
|
95
|
+
const proxyMutativeMethods = (value, prototypeName, cell) => {
|
|
96
|
+
for (const method in mutativeMethods[prototypeName]) {
|
|
97
|
+
Reflect.set(
|
|
98
|
+
value,
|
|
99
|
+
Reflect.get(mutativeMethods[prototypeName], method),
|
|
100
|
+
/**
|
|
101
|
+
* @param {...any} args - The arguments passed to the mutative method.
|
|
102
|
+
* @returns {any} The result of calling the original method.
|
|
103
|
+
*/
|
|
104
|
+
(...args) => {
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
const innerMethod = value[method]; // Direct access is faster than Reflection here.
|
|
107
|
+
const result = innerMethod.apply(value, args);
|
|
108
|
+
cell.update();
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
51
114
|
|
|
52
115
|
/**
|
|
53
116
|
* @template T
|
|
@@ -99,21 +162,19 @@ class Effect {
|
|
|
99
162
|
export class Cell {
|
|
100
163
|
/**
|
|
101
164
|
* @type {Array<Effect<T>>}
|
|
102
|
-
* @protected
|
|
103
165
|
*/
|
|
104
|
-
|
|
166
|
+
#effects = [];
|
|
105
167
|
|
|
106
168
|
/**
|
|
107
169
|
* @type {Array<[WeakRef<DerivedCell<any>>, () => any]>}
|
|
108
|
-
* @protected
|
|
109
170
|
*/
|
|
110
|
-
|
|
171
|
+
#derivedCells = [];
|
|
111
172
|
|
|
112
173
|
/**
|
|
113
174
|
* @readonly
|
|
114
175
|
*/
|
|
115
176
|
get effects() {
|
|
116
|
-
return this
|
|
177
|
+
return this.#effects;
|
|
117
178
|
}
|
|
118
179
|
|
|
119
180
|
/**
|
|
@@ -122,7 +183,7 @@ export class Cell {
|
|
|
122
183
|
*/
|
|
123
184
|
get derivedCells() {
|
|
124
185
|
// @ts-ignore
|
|
125
|
-
return this.
|
|
186
|
+
return this.#derivedCells.map((cell) => cell.deref()).filter(Boolean);
|
|
126
187
|
}
|
|
127
188
|
|
|
128
189
|
/**
|
|
@@ -167,12 +228,12 @@ export class Cell {
|
|
|
167
228
|
const currentlyComputedValue = activeComputedValues.at(-1);
|
|
168
229
|
|
|
169
230
|
if (currentlyComputedValue !== undefined) {
|
|
170
|
-
const isAlreadySubscribed = this.
|
|
231
|
+
const isAlreadySubscribed = this.#derivedCells.some(
|
|
171
232
|
(ref) => ref[0].deref() === currentlyComputedValue[0]
|
|
172
233
|
);
|
|
173
234
|
if (isAlreadySubscribed) return this.wvalue;
|
|
174
235
|
|
|
175
|
-
this.
|
|
236
|
+
this.#derivedCells.push([
|
|
176
237
|
new WeakRef(currentlyComputedValue[0]),
|
|
177
238
|
currentlyComputedValue[1],
|
|
178
239
|
]);
|
|
@@ -219,15 +280,15 @@ export class Cell {
|
|
|
219
280
|
);
|
|
220
281
|
}
|
|
221
282
|
|
|
222
|
-
const isAlreadySubscribed = this.
|
|
283
|
+
const isAlreadySubscribed = this.#effects.some((effect) => {
|
|
223
284
|
return effect.callback === callback;
|
|
224
285
|
});
|
|
225
286
|
|
|
226
287
|
if (!isAlreadySubscribed) {
|
|
227
|
-
this.
|
|
288
|
+
this.#effects.push(new Effect(callback, options));
|
|
228
289
|
}
|
|
229
290
|
|
|
230
|
-
this.
|
|
291
|
+
this.#effects.sort((a, b) => {
|
|
231
292
|
const aPriority = a.options?.priority ?? 0;
|
|
232
293
|
const bPriority = b.options?.priority ?? 0;
|
|
233
294
|
|
|
@@ -264,15 +325,15 @@ export class Cell {
|
|
|
264
325
|
throw new Error(message);
|
|
265
326
|
}
|
|
266
327
|
|
|
267
|
-
const isAlreadySubscribed = this.
|
|
328
|
+
const isAlreadySubscribed = this.#effects.some((e) => {
|
|
268
329
|
return e.callback === callback;
|
|
269
330
|
});
|
|
270
331
|
|
|
271
332
|
if (!isAlreadySubscribed) {
|
|
272
|
-
this.
|
|
333
|
+
this.#effects.push(new Effect(cb, options));
|
|
273
334
|
}
|
|
274
335
|
|
|
275
|
-
this.
|
|
336
|
+
this.#effects.sort((a, b) => {
|
|
276
337
|
const aPriority = a.options?.priority ?? 0;
|
|
277
338
|
const bPriority = b.options?.priority ?? 0;
|
|
278
339
|
if (aPriority === bPriority) return 0;
|
|
@@ -287,12 +348,12 @@ export class Cell {
|
|
|
287
348
|
* @param {(newValue: T) => void} callback - The effect callback to remove.
|
|
288
349
|
*/
|
|
289
350
|
ignore(callback) {
|
|
290
|
-
const index = this.
|
|
351
|
+
const index = this.#effects.findIndex((e) => {
|
|
291
352
|
return e.callback === callback;
|
|
292
353
|
});
|
|
293
354
|
if (index === -1) return;
|
|
294
355
|
|
|
295
|
-
this.
|
|
356
|
+
this.#effects.splice(index, 1);
|
|
296
357
|
}
|
|
297
358
|
|
|
298
359
|
/**
|
|
@@ -301,7 +362,7 @@ export class Cell {
|
|
|
301
362
|
* @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
|
|
302
363
|
*/
|
|
303
364
|
isListeningTo(name) {
|
|
304
|
-
return this.
|
|
365
|
+
return this.#effects.some((effect) => {
|
|
305
366
|
return effect?.options?.name === name && effect.callback;
|
|
306
367
|
});
|
|
307
368
|
}
|
|
@@ -311,12 +372,12 @@ export class Cell {
|
|
|
311
372
|
* @param {string} name - The name of the watcher to stop listening to.
|
|
312
373
|
*/
|
|
313
374
|
stopListeningTo(name) {
|
|
314
|
-
const effectIndex = this.
|
|
375
|
+
const effectIndex = this.#effects.findIndex((e) => {
|
|
315
376
|
return e.options?.name === name;
|
|
316
377
|
});
|
|
317
378
|
if (effectIndex === -1) return;
|
|
318
379
|
|
|
319
|
-
this.
|
|
380
|
+
this.#effects.splice(effectIndex, 1);
|
|
320
381
|
}
|
|
321
382
|
|
|
322
383
|
/**
|
|
@@ -325,7 +386,7 @@ export class Cell {
|
|
|
325
386
|
*/
|
|
326
387
|
update() {
|
|
327
388
|
// Run watchers.
|
|
328
|
-
for (const effect of this
|
|
389
|
+
for (const effect of this.#effects) {
|
|
329
390
|
const watcher = effect.callback;
|
|
330
391
|
if (watcher === undefined) continue;
|
|
331
392
|
|
|
@@ -338,10 +399,10 @@ export class Cell {
|
|
|
338
399
|
}
|
|
339
400
|
|
|
340
401
|
// Remove dead effects.
|
|
341
|
-
this
|
|
402
|
+
this.#effects = this.#effects.filter((effect) => effect.callback);
|
|
342
403
|
|
|
343
404
|
// Run computed dependents.
|
|
344
|
-
const computedDependents = this
|
|
405
|
+
const computedDependents = this.#derivedCells;
|
|
345
406
|
if (computedDependents !== undefined) {
|
|
346
407
|
for (const dependent of computedDependents) {
|
|
347
408
|
// global effects
|
|
@@ -369,7 +430,7 @@ export class Cell {
|
|
|
369
430
|
}
|
|
370
431
|
}
|
|
371
432
|
// Periodically drop dead references.
|
|
372
|
-
this
|
|
433
|
+
this.#derivedCells = this.#derivedCells.filter(
|
|
373
434
|
(ref) => ref[0].deref() !== undefined
|
|
374
435
|
);
|
|
375
436
|
|
|
@@ -697,8 +758,8 @@ export class SourceCell extends Cell {
|
|
|
697
758
|
constructor(value, options) {
|
|
698
759
|
super();
|
|
699
760
|
|
|
700
|
-
this.setValue(options?.shallowProxied ? value : this.proxy(value));
|
|
701
761
|
this.options = options ?? {};
|
|
762
|
+
this.setValue(this.#proxy(value));
|
|
702
763
|
|
|
703
764
|
if (typeof value === 'object' && value !== null) {
|
|
704
765
|
this.#originalObject = value;
|
|
@@ -759,7 +820,7 @@ export class SourceCell extends Cell {
|
|
|
759
820
|
}
|
|
760
821
|
}
|
|
761
822
|
|
|
762
|
-
this.setValue(this
|
|
823
|
+
this.setValue(this.#proxy(value));
|
|
763
824
|
if (typeof value === 'object' && value !== null) {
|
|
764
825
|
this.#originalObject = value;
|
|
765
826
|
}
|
|
@@ -771,17 +832,62 @@ export class SourceCell extends Cell {
|
|
|
771
832
|
* @template T
|
|
772
833
|
* @param {T} value - The value to be proxied.
|
|
773
834
|
* @returns {T} - The proxied value.
|
|
774
|
-
* @private
|
|
775
835
|
*/
|
|
776
|
-
proxy(value) {
|
|
836
|
+
#proxy(value) {
|
|
777
837
|
if (typeof value !== 'object' || value === null) {
|
|
778
838
|
return value;
|
|
779
839
|
}
|
|
780
840
|
|
|
841
|
+
if (value instanceof Map) {
|
|
842
|
+
proxyMutativeMethods(value, 'Map', this);
|
|
843
|
+
} else if (value instanceof Set) {
|
|
844
|
+
proxyMutativeMethods(value, 'Set', this);
|
|
845
|
+
} else if (value instanceof Date) {
|
|
846
|
+
proxyMutativeMethods(value, 'Date', this);
|
|
847
|
+
} else if (ArrayBuffer.isView(value) || Array.isArray(value)) {
|
|
848
|
+
proxyMutativeMethods(value, 'Array', this);
|
|
849
|
+
}
|
|
850
|
+
|
|
781
851
|
return new Proxy(value, {
|
|
782
852
|
get: (target, prop) => {
|
|
783
853
|
this.revalued;
|
|
784
|
-
|
|
854
|
+
if (this.options.deep) {
|
|
855
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
856
|
+
return this.#proxy(target[prop]);
|
|
857
|
+
}
|
|
858
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
859
|
+
let value = target[prop];
|
|
860
|
+
|
|
861
|
+
if (typeof value === 'function') {
|
|
862
|
+
value = value.bind(target);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (typeof prop === 'string') {
|
|
866
|
+
if (target instanceof Map && mutativeMapMethods.test(prop)) {
|
|
867
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
868
|
+
return target[mutativeMethods.Map[prop]];
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (target instanceof Set && mutativeSetMethods.test(prop)) {
|
|
872
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
873
|
+
return target[mutativeMethods.Set[prop]];
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (target instanceof Date && mutativeDateMethods.test(prop)) {
|
|
877
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
878
|
+
return target[mutativeMethods.Date[prop]];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (
|
|
882
|
+
(ArrayBuffer.isView(target) || Array.isArray(target)) &&
|
|
883
|
+
mutativeArrayMethods.test(prop)
|
|
884
|
+
) {
|
|
885
|
+
// @ts-ignore: Direct access is faster than Reflection here.
|
|
886
|
+
return target[mutativeMethods.Array[prop]];
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return value;
|
|
785
891
|
},
|
|
786
892
|
set: (target, prop, value) => {
|
|
787
893
|
const formerValue = Reflect.get(target, prop);
|
package/package.json
CHANGED
|
@@ -141,16 +141,6 @@ export class Cell<T> {
|
|
|
141
141
|
* run('input');
|
|
142
142
|
*/
|
|
143
143
|
static async<X, Y>(getter: (input: X) => Promise<Y>): AsyncRequestAtoms<X, Y>;
|
|
144
|
-
/**
|
|
145
|
-
* @type {Array<Effect<T>>}
|
|
146
|
-
* @protected
|
|
147
|
-
*/
|
|
148
|
-
protected __effects: Array<Effect<T>>;
|
|
149
|
-
/**
|
|
150
|
-
* @type {Array<[WeakRef<DerivedCell<any>>, () => any]>}
|
|
151
|
-
* @protected
|
|
152
|
-
*/
|
|
153
|
-
protected __derivedCells: Array<[WeakRef<DerivedCell<any>>, () => any]>;
|
|
154
144
|
/**
|
|
155
145
|
* @readonly
|
|
156
146
|
*/
|
|
@@ -230,6 +220,7 @@ export class Cell<T> {
|
|
|
230
220
|
* @returns {T} - The current value of the cell.
|
|
231
221
|
*/
|
|
232
222
|
peek(): T;
|
|
223
|
+
#private;
|
|
233
224
|
}
|
|
234
225
|
/**
|
|
235
226
|
* A class that represents a computed value that depends on other reactive values.
|
|
@@ -285,14 +276,6 @@ export class SourceCell<T> extends Cell<T> {
|
|
|
285
276
|
*/
|
|
286
277
|
set value(value: T);
|
|
287
278
|
get value(): T;
|
|
288
|
-
/**
|
|
289
|
-
* Proxies the provided value deeply, allowing it to be observed and updated.
|
|
290
|
-
* @template T
|
|
291
|
-
* @param {T} value - The value to be proxied.
|
|
292
|
-
* @returns {T} - The proxied value.
|
|
293
|
-
* @private
|
|
294
|
-
*/
|
|
295
|
-
private proxy;
|
|
296
279
|
#private;
|
|
297
280
|
}
|
|
298
281
|
export type AsyncRequestAtoms<Input, Output> = {
|
|
@@ -345,9 +328,9 @@ export type CellOptions<T> = {
|
|
|
345
328
|
*/
|
|
346
329
|
immutable?: boolean | undefined;
|
|
347
330
|
/**
|
|
348
|
-
* Whether the cell
|
|
331
|
+
* Whether the cell should watch for changes deep into the given value. By default the cell only reacts to changes at the top level.
|
|
349
332
|
*/
|
|
350
|
-
|
|
333
|
+
deep?: boolean | undefined;
|
|
351
334
|
/**
|
|
352
335
|
* A function that determines whether two values are equal. If not provided, the default equality function will be used.
|
|
353
336
|
*/
|