@adbl/cells 0.0.4 → 0.0.6

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 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
- shallowProxied: boolean, // If true, only top-level properties are proxied
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
  ```
@@ -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} [shallowProxied]
40
- * Whether the cell's value should be shallowly proxied. If set to true, the cell will only proxy the top-level properties of the value, preventing any changes to nested properties. This can be useful for performance optimizations.
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
@@ -94,27 +157,18 @@ class Effect {
94
157
  }
95
158
 
96
159
  /**
97
- * @template T
160
+ * @template {*} T
98
161
  */
99
162
  export class Cell {
100
163
  /**
101
164
  * @type {Array<Effect<T>>}
102
- * @protected
103
165
  */
104
- __effects = [];
166
+ #effects = [];
105
167
 
106
168
  /**
107
169
  * @type {Array<[WeakRef<DerivedCell<any>>, () => any]>}
108
- * @protected
109
- */
110
- __derivedCells = [];
111
-
112
- /**
113
- * @readonly
114
170
  */
115
- get effects() {
116
- return this.__effects;
117
- }
171
+ #derivedCells = [];
118
172
 
119
173
  /**
120
174
  * @readonly
@@ -122,7 +176,7 @@ export class Cell {
122
176
  */
123
177
  get derivedCells() {
124
178
  // @ts-ignore
125
- return this.__derivedCells.map((cell) => cell.deref()).filter(Boolean);
179
+ return this.#derivedCells.map((cell) => cell.deref()).filter(Boolean);
126
180
  }
127
181
 
128
182
  /**
@@ -167,12 +221,12 @@ export class Cell {
167
221
  const currentlyComputedValue = activeComputedValues.at(-1);
168
222
 
169
223
  if (currentlyComputedValue !== undefined) {
170
- const isAlreadySubscribed = this.__derivedCells.some(
224
+ const isAlreadySubscribed = this.#derivedCells.some(
171
225
  (ref) => ref[0].deref() === currentlyComputedValue[0]
172
226
  );
173
227
  if (isAlreadySubscribed) return this.wvalue;
174
228
 
175
- this.__derivedCells.push([
229
+ this.#derivedCells.push([
176
230
  new WeakRef(currentlyComputedValue[0]),
177
231
  currentlyComputedValue[1],
178
232
  ]);
@@ -181,14 +235,6 @@ export class Cell {
181
235
  return this.wvalue;
182
236
  }
183
237
 
184
- /**
185
- * Sets a callback function that will be called whenever the value of the Cell changes.
186
- * @param {(newValue: T) => void} callback - The function to be called when the value changes.
187
- */
188
- set onchange(callback) {
189
- this.listen(callback);
190
- }
191
-
192
238
  /**
193
239
  * 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.
194
240
  * @param {(newValue: T) => void} callback - The effect callback to add.
@@ -219,15 +265,15 @@ export class Cell {
219
265
  );
220
266
  }
221
267
 
222
- const isAlreadySubscribed = this.__effects.some((effect) => {
268
+ const isAlreadySubscribed = this.#effects.some((effect) => {
223
269
  return effect.callback === callback;
224
270
  });
225
271
 
226
272
  if (!isAlreadySubscribed) {
227
- this.__effects.push(new Effect(callback, options));
273
+ this.#effects.push(new Effect(callback, options));
228
274
  }
229
275
 
230
- this.__effects.sort((a, b) => {
276
+ this.#effects.sort((a, b) => {
231
277
  const aPriority = a.options?.priority ?? 0;
232
278
  const bPriority = b.options?.priority ?? 0;
233
279
 
@@ -264,15 +310,15 @@ export class Cell {
264
310
  throw new Error(message);
265
311
  }
266
312
 
267
- const isAlreadySubscribed = this.__effects.some((e) => {
313
+ const isAlreadySubscribed = this.#effects.some((e) => {
268
314
  return e.callback === callback;
269
315
  });
270
316
 
271
317
  if (!isAlreadySubscribed) {
272
- this.__effects.push(new Effect(cb, options));
318
+ this.#effects.push(new Effect(cb, options));
273
319
  }
274
320
 
275
- this.__effects.sort((a, b) => {
321
+ this.#effects.sort((a, b) => {
276
322
  const aPriority = a.options?.priority ?? 0;
277
323
  const bPriority = b.options?.priority ?? 0;
278
324
  if (aPriority === bPriority) return 0;
@@ -287,12 +333,12 @@ export class Cell {
287
333
  * @param {(newValue: T) => void} callback - The effect callback to remove.
288
334
  */
289
335
  ignore(callback) {
290
- const index = this.__effects.findIndex((e) => {
336
+ const index = this.#effects.findIndex((e) => {
291
337
  return e.callback === callback;
292
338
  });
293
339
  if (index === -1) return;
294
340
 
295
- this.__effects.splice(index, 1);
341
+ this.#effects.splice(index, 1);
296
342
  }
297
343
 
298
344
  /**
@@ -301,7 +347,7 @@ export class Cell {
301
347
  * @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
302
348
  */
303
349
  isListeningTo(name) {
304
- return this.__effects.some((effect) => {
350
+ return this.#effects.some((effect) => {
305
351
  return effect?.options?.name === name && effect.callback;
306
352
  });
307
353
  }
@@ -311,12 +357,12 @@ export class Cell {
311
357
  * @param {string} name - The name of the watcher to stop listening to.
312
358
  */
313
359
  stopListeningTo(name) {
314
- const effectIndex = this.__effects.findIndex((e) => {
360
+ const effectIndex = this.#effects.findIndex((e) => {
315
361
  return e.options?.name === name;
316
362
  });
317
363
  if (effectIndex === -1) return;
318
364
 
319
- this.__effects.splice(effectIndex, 1);
365
+ this.#effects.splice(effectIndex, 1);
320
366
  }
321
367
 
322
368
  /**
@@ -325,23 +371,27 @@ export class Cell {
325
371
  */
326
372
  update() {
327
373
  // Run watchers.
328
- for (const effect of this.__effects) {
329
- const watcher = effect.callback;
374
+ const batchNestingLevel = root.batchNestingLevel;
375
+ const wvalue = this.wvalue;
376
+ const effects = this.#effects;
377
+ const len = effects.length;
378
+
379
+ for (let i = 0; i < len; i++) {
380
+ const watcher = effects[i].callback;
330
381
  if (watcher === undefined) continue;
331
382
 
332
- if (root.batchNestingLevel > 0) {
333
- root.batchedEffects.set(watcher, [this.wvalue]);
334
- continue;
383
+ if (batchNestingLevel > 0) {
384
+ root.batchedEffects.set(watcher, [wvalue]);
385
+ } else {
386
+ watcher(wvalue);
335
387
  }
336
-
337
- watcher(this.wvalue);
338
388
  }
339
389
 
340
390
  // Remove dead effects.
341
- this.__effects = this.__effects.filter((effect) => effect.callback);
391
+ this.#effects = this.#effects.filter((effect) => effect.callback);
342
392
 
343
393
  // Run computed dependents.
344
- const computedDependents = this.__derivedCells;
394
+ const computedDependents = this.#derivedCells;
345
395
  if (computedDependents !== undefined) {
346
396
  for (const dependent of computedDependents) {
347
397
  // global effects
@@ -369,7 +419,7 @@ export class Cell {
369
419
  }
370
420
  }
371
421
  // Periodically drop dead references.
372
- this.__derivedCells = this.__derivedCells.filter(
422
+ this.#derivedCells = this.#derivedCells.filter(
373
423
  (ref) => ref[0].deref() !== undefined
374
424
  );
375
425
 
@@ -649,7 +699,7 @@ export class Cell {
649
699
  /**
650
700
  * A class that represents a computed value that depends on other reactive values.
651
701
  * The computed value is automatically updated when any of its dependencies change.
652
- * @template T
702
+ * @template {*} T
653
703
  * @extends {Cell<T>}
654
704
  */
655
705
  export class DerivedCell extends Cell {
@@ -658,9 +708,14 @@ export class DerivedCell extends Cell {
658
708
  */
659
709
  constructor(computedFn) {
660
710
  super();
661
- activeComputedValues.push([this, computedFn]);
662
- this.setValue(computedFn());
663
- activeComputedValues.pop();
711
+ // Ensures that the cell is derived every time the computing function is called.
712
+ const derivationWrapper = () => {
713
+ activeComputedValues.push([this, derivationWrapper]);
714
+ const value = computedFn();
715
+ activeComputedValues.pop();
716
+ return value;
717
+ };
718
+ this.setValue(derivationWrapper());
664
719
  }
665
720
 
666
721
  /**
@@ -679,7 +734,7 @@ export class DerivedCell extends Cell {
679
734
  }
680
735
 
681
736
  /**
682
- * @template T
737
+ * @template {*} T
683
738
  * @extends {Cell<T>}
684
739
  */
685
740
  export class SourceCell extends Cell {
@@ -697,8 +752,8 @@ export class SourceCell extends Cell {
697
752
  constructor(value, options) {
698
753
  super();
699
754
 
700
- this.setValue(options?.shallowProxied ? value : this.proxy(value));
701
755
  this.options = options ?? {};
756
+ this.setValue(this.#proxy(value));
702
757
 
703
758
  if (typeof value === 'object' && value !== null) {
704
759
  this.#originalObject = value;
@@ -759,7 +814,7 @@ export class SourceCell extends Cell {
759
814
  }
760
815
  }
761
816
 
762
- this.setValue(this.options?.shallowProxied ? value : this.proxy(value));
817
+ this.setValue(this.#proxy(value));
763
818
  if (typeof value === 'object' && value !== null) {
764
819
  this.#originalObject = value;
765
820
  }
@@ -771,17 +826,62 @@ export class SourceCell extends Cell {
771
826
  * @template T
772
827
  * @param {T} value - The value to be proxied.
773
828
  * @returns {T} - The proxied value.
774
- * @private
775
829
  */
776
- proxy(value) {
830
+ #proxy(value) {
777
831
  if (typeof value !== 'object' || value === null) {
778
832
  return value;
779
833
  }
780
834
 
835
+ if (value instanceof Map) {
836
+ proxyMutativeMethods(value, 'Map', this);
837
+ } else if (value instanceof Set) {
838
+ proxyMutativeMethods(value, 'Set', this);
839
+ } else if (value instanceof Date) {
840
+ proxyMutativeMethods(value, 'Date', this);
841
+ } else if (ArrayBuffer.isView(value) || Array.isArray(value)) {
842
+ proxyMutativeMethods(value, 'Array', this);
843
+ }
844
+
781
845
  return new Proxy(value, {
782
846
  get: (target, prop) => {
783
847
  this.revalued;
784
- return this.proxy(Reflect.get(target, prop));
848
+ if (this.options.deep) {
849
+ // @ts-ignore: Direct access is faster than Reflection here.
850
+ return this.#proxy(target[prop]);
851
+ }
852
+ // @ts-ignore: Direct access is faster than Reflection here.
853
+ let value = target[prop];
854
+
855
+ if (typeof value === 'function') {
856
+ value = value.bind(target);
857
+ }
858
+
859
+ if (typeof prop === 'string') {
860
+ if (target instanceof Map && mutativeMapMethods.test(prop)) {
861
+ // @ts-ignore: Direct access is faster than Reflection here.
862
+ return target[mutativeMethods.Map[prop]];
863
+ }
864
+
865
+ if (target instanceof Set && mutativeSetMethods.test(prop)) {
866
+ // @ts-ignore: Direct access is faster than Reflection here.
867
+ return target[mutativeMethods.Set[prop]];
868
+ }
869
+
870
+ if (target instanceof Date && mutativeDateMethods.test(prop)) {
871
+ // @ts-ignore: Direct access is faster than Reflection here.
872
+ return target[mutativeMethods.Date[prop]];
873
+ }
874
+
875
+ if (
876
+ (ArrayBuffer.isView(target) || Array.isArray(target)) &&
877
+ mutativeArrayMethods.test(prop)
878
+ ) {
879
+ // @ts-ignore: Direct access is faster than Reflection here.
880
+ return target[mutativeMethods.Array[prop]];
881
+ }
882
+ }
883
+
884
+ return value;
785
885
  },
786
886
  set: (target, prop, value) => {
787
887
  const formerValue = Reflect.get(target, prop);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adbl/cells",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A simple implementation of reactive updates for JavaScript",
5
5
  "main": "index.js",
6
6
  "private": false,
@@ -1,7 +1,7 @@
1
1
  /**
2
- * @template T
2
+ * @template {*} T
3
3
  */
4
- export class Cell<T> {
4
+ export class Cell<T extends unknown> {
5
5
  /**
6
6
  * Adds a global effect that runs before any Cell is updated.
7
7
  * @param {(value: unknown) => void} effect - The effect function.
@@ -122,7 +122,7 @@ export class Cell<T> {
122
122
  * @param {T} object - The object to be flattened.
123
123
  * @returns {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} A new object with the flattened values.
124
124
  */
125
- static flattenObject: <T_6 extends object>(object: T_6) => { [K in keyof T_6]: T_6[K] extends Cell<infer U_1> ? U_1 : T_6[K]; };
125
+ static flattenObject: <T_6 extends object>(object: T_6) => { [K in keyof T_6]: T_6[K] extends Cell<infer U_1 extends unknown> ? U_1 : T_6[K]; };
126
126
  /**
127
127
  * Wraps an asynchronous function with managed state.
128
128
  *
@@ -141,20 +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
- /**
155
- * @readonly
156
- */
157
- readonly get effects(): Effect<T>[];
158
144
  /**
159
145
  * @readonly
160
146
  * @returns {Array<DerivedCell<any>>}
@@ -185,11 +171,6 @@ export class Cell<T> {
185
171
  * @protected @type {T}
186
172
  */
187
173
  protected get revalued(): T;
188
- /**
189
- * Sets a callback function that will be called whenever the value of the Cell changes.
190
- * @param {(newValue: T) => void} callback - The function to be called when the value changes.
191
- */
192
- set onchange(callback: (newValue: T) => void);
193
174
  /**
194
175
  * 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.
195
176
  * @param {(newValue: T) => void} callback - The effect callback to add.
@@ -230,14 +211,15 @@ export class Cell<T> {
230
211
  * @returns {T} - The current value of the cell.
231
212
  */
232
213
  peek(): T;
214
+ #private;
233
215
  }
234
216
  /**
235
217
  * A class that represents a computed value that depends on other reactive values.
236
218
  * The computed value is automatically updated when any of its dependencies change.
237
- * @template T
219
+ * @template {*} T
238
220
  * @extends {Cell<T>}
239
221
  */
240
- export class DerivedCell<T> extends Cell<T> {
222
+ export class DerivedCell<T extends unknown> extends Cell<T> {
241
223
  /**
242
224
  * @param {() => T} computedFn - A function that generates the value of the computed.
243
225
  */
@@ -252,10 +234,10 @@ export class DerivedCell<T> extends Cell<T> {
252
234
  readonly get value(): T;
253
235
  }
254
236
  /**
255
- * @template T
237
+ * @template {*} T
256
238
  * @extends {Cell<T>}
257
239
  */
258
- export class SourceCell<T> extends Cell<T> {
240
+ export class SourceCell<T extends unknown> extends Cell<T> {
259
241
  /**
260
242
  * Creates a new Cell with the provided value.
261
243
  * @param {T} value
@@ -285,14 +267,6 @@ export class SourceCell<T> extends Cell<T> {
285
267
  */
286
268
  set value(value: T);
287
269
  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
270
  #private;
297
271
  }
298
272
  export type AsyncRequestAtoms<Input, Output> = {
@@ -345,9 +319,9 @@ export type CellOptions<T> = {
345
319
  */
346
320
  immutable?: boolean | undefined;
347
321
  /**
348
- * Whether the cell's value should be shallowly proxied. If set to true, the cell will only proxy the top-level properties of the value, preventing any changes to nested properties. This can be useful for performance optimizations.
322
+ * 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
323
  */
350
- shallowProxied?: boolean | undefined;
324
+ deep?: boolean | undefined;
351
325
  /**
352
326
  * A function that determines whether two values are equal. If not provided, the default equality function will be used.
353
327
  */
@@ -357,28 +331,3 @@ export type NeverIfAny<T> = 0 extends (1 & T) ? never : T;
357
331
  export type Reference<T> = {
358
332
  deref: () => T | undefined;
359
333
  };
360
- /**
361
- * @template T
362
- * @typedef {{
363
- * deref: () => T | undefined
364
- * }} Reference
365
- */
366
- /** @template T */
367
- declare class Effect<T> {
368
- /**
369
- * @param {(newValue: T) => void} callback
370
- * @param {EffectOptions} [options]
371
- */
372
- constructor(callback: (newValue: T) => void, options?: EffectOptions | undefined);
373
- /**
374
- * @type {EffectOptions | undefined}
375
- */
376
- options: EffectOptions | undefined;
377
- /**
378
- * Returns the callback function, if it still exists.
379
- * @returns {((newValue: T) => void) | undefined}
380
- */
381
- get callback(): ((newValue: T) => void) | undefined;
382
- #private;
383
- }
384
- export {};