@adbl/cells 0.0.4 → 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 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
@@ -99,21 +162,19 @@ class Effect {
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
170
  */
110
- __derivedCells = [];
171
+ #derivedCells = [];
111
172
 
112
173
  /**
113
174
  * @readonly
114
175
  */
115
176
  get effects() {
116
- return this.__effects;
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.__derivedCells.map((cell) => cell.deref()).filter(Boolean);
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.__derivedCells.some(
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.__derivedCells.push([
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.__effects.some((effect) => {
283
+ const isAlreadySubscribed = this.#effects.some((effect) => {
223
284
  return effect.callback === callback;
224
285
  });
225
286
 
226
287
  if (!isAlreadySubscribed) {
227
- this.__effects.push(new Effect(callback, options));
288
+ this.#effects.push(new Effect(callback, options));
228
289
  }
229
290
 
230
- this.__effects.sort((a, b) => {
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.__effects.some((e) => {
328
+ const isAlreadySubscribed = this.#effects.some((e) => {
268
329
  return e.callback === callback;
269
330
  });
270
331
 
271
332
  if (!isAlreadySubscribed) {
272
- this.__effects.push(new Effect(cb, options));
333
+ this.#effects.push(new Effect(cb, options));
273
334
  }
274
335
 
275
- this.__effects.sort((a, b) => {
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.__effects.findIndex((e) => {
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.__effects.splice(index, 1);
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.__effects.some((effect) => {
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.__effects.findIndex((e) => {
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.__effects.splice(effectIndex, 1);
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.__effects) {
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.__effects = this.__effects.filter((effect) => effect.callback);
402
+ this.#effects = this.#effects.filter((effect) => effect.callback);
342
403
 
343
404
  // Run computed dependents.
344
- const computedDependents = this.__derivedCells;
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.__derivedCells = this.__derivedCells.filter(
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.options?.shallowProxied ? value : this.proxy(value));
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
- return this.proxy(Reflect.get(target, prop));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adbl/cells",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "A simple implementation of reactive updates for JavaScript",
5
5
  "main": "index.js",
6
6
  "private": false,
@@ -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'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.
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
- shallowProxied?: boolean | undefined;
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
  */