@adbl/cells 0.0.0 → 0.0.1

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.
Binary file
@@ -25,6 +25,8 @@
25
25
  * An AbortSignal to be used to ignore the effect if it is aborted.
26
26
  * @property {string} [name]
27
27
  * The name of the effect for debugging purposes.
28
+ * @property {boolean} [weak]
29
+ * Whether the effect should be weakly referenced. This means that the effect will be garbage collected if there are no other references to it.
28
30
  * @property {number} [priority]
29
31
  * The priority of the effect. Higher priority effects are executed first. The default priority is 0.
30
32
  */
@@ -47,24 +49,81 @@
47
49
 
48
50
  import { activeComputedValues, root } from './root.js';
49
51
 
52
+ /**
53
+ * @template T
54
+ * @typedef {{
55
+ * deref: () => T | undefined
56
+ * }} Reference
57
+ */
58
+
59
+ /** @template T */
60
+ class Effect {
61
+ /**
62
+ * @type {EffectOptions | undefined}
63
+ */
64
+ options;
65
+
66
+ /**
67
+ * @type {WeakRef<(newValue: T) => void> | ((newValue: T) => void) }
68
+ */
69
+ #callback;
70
+
71
+ /**
72
+ * @param {(newValue: T) => void} callback
73
+ * @param {EffectOptions} [options]
74
+ */
75
+ constructor(callback, options) {
76
+ if (options?.weak) {
77
+ this.#callback = new WeakRef(callback);
78
+ } else {
79
+ this.#callback = callback;
80
+ }
81
+ this.options = options;
82
+ }
83
+
84
+ /**
85
+ * Returns the callback function, if it still exists.
86
+ * @returns {((newValue: T) => void) | undefined}
87
+ */
88
+ get callback() {
89
+ if (this.#callback instanceof WeakRef) {
90
+ return this.#callback.deref();
91
+ }
92
+ return this.#callback;
93
+ }
94
+ }
95
+
50
96
  /**
51
97
  * @template T
52
98
  */
53
99
  export class Cell {
54
100
  /**
55
- * @type {Array<({
56
- * effect: (newValue: T) => void,
57
- * options?: EffectOptions,
58
- * })>}
101
+ * @type {Array<Effect<T>>}
59
102
  * @protected
60
103
  */
61
- effects = [];
104
+ __effects = [];
62
105
 
63
106
  /**
64
- * @type {Array<WeakRef<DerivedCell<any>>>}
107
+ * @type {Array<[WeakRef<DerivedCell<any>>, () => any]>}
65
108
  * @protected
66
109
  */
67
- derivedCells = [];
110
+ __derivedCells = [];
111
+
112
+ /**
113
+ * @readonly
114
+ */
115
+ get effects() {
116
+ return this.__effects;
117
+ }
118
+
119
+ /**
120
+ * @readonly
121
+ * @returns {Array<DerivedCell<any>>}
122
+ */
123
+ get derivedCells() {
124
+ // @ts-ignore
125
+ return this.__derivedCells.map((cell) => cell.deref()).filter(Boolean);
126
+ }
68
127
 
69
128
  /**
70
129
  * @protected @type T
@@ -84,9 +143,22 @@ export class Cell {
84
143
  * @returns {T} The value of the Cell.
85
144
  */
86
145
  valueOf() {
146
+ return this.revalued;
147
+ }
148
+
149
+ get value() {
87
150
  return this.wvalue;
88
151
  }
89
152
 
153
+ /**
154
+ * Stringifies the value of the Cell.
155
+ * @returns {string}
156
+ */
157
+ toString() {
158
+ // @ts-ignore
159
+ return this.wvalue.toString();
160
+ }
161
+
90
162
  /**
91
163
  * The value stored in the Cell.
92
164
  * @protected @type {T}
@@ -95,12 +167,15 @@ export class Cell {
95
167
  const currentlyComputedValue = activeComputedValues.at(-1);
96
168
 
97
169
  if (currentlyComputedValue !== undefined) {
98
- const isAlreadySubscribed = this.derivedCells.some(
99
- (ref) => ref.deref() === currentlyComputedValue
170
+ const isAlreadySubscribed = this.__derivedCells.some(
171
+ (ref) => ref[0].deref() === currentlyComputedValue[0]
100
172
  );
101
173
  if (isAlreadySubscribed) return this.wvalue;
102
174
 
103
- this.derivedCells.push(new WeakRef(currentlyComputedValue));
175
+ this.__derivedCells.push([
176
+ new WeakRef(currentlyComputedValue[0]),
177
+ currentlyComputedValue[1],
178
+ ]);
104
179
  }
105
180
 
106
181
  return this.wvalue;
@@ -138,26 +213,25 @@ export class Cell {
138
213
  };
139
214
  }
140
215
 
141
- if (options?.name) {
142
- if (this.isListeningTo(options.name)) {
143
- throw new Error(
144
- `An effect with the name "${options.name}" is already listening to this cell.`
145
- );
146
- }
216
+ if (options?.name && this.isListeningTo(options.name)) {
217
+ throw new Error(
218
+ `An effect with the name "${options.name}" is already listening to this cell.`
219
+ );
147
220
  }
148
221
 
149
- if (this.effects.some(({ effect }) => effect === callback)) {
150
- throw new Error('This effect is already listening to this cell.');
151
- }
222
+ const isAlreadySubscribed = this.__effects.some((effect) => {
223
+ return effect.callback === callback;
224
+ });
152
225
 
153
- this.effects.push({ effect, options });
226
+ if (!isAlreadySubscribed) {
227
+ this.__effects.push(new Effect(callback, options));
228
+ }
154
229
 
155
- this.effects.sort((a, b) => {
230
+ this.__effects.sort((a, b) => {
156
231
  const aPriority = a.options?.priority ?? 0;
157
232
  const bPriority = b.options?.priority ?? 0;
158
- if (aPriority === bPriority) {
159
- return 0;
160
- }
233
+
234
+ if (aPriority === bPriority) return 0;
161
235
  return aPriority < bPriority ? 1 : -1;
162
236
  });
163
237
 
@@ -166,12 +240,46 @@ export class Cell {
166
240
 
167
241
  /**
168
242
  * 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.
169
- * @param {(newValue: T) => void} effect - The effect callback to add.
243
+ * @param {(newValue: T) => void} callback - The effect callback to add.
244
+ * @param {Partial<EffectOptions>} [options] - The options for the effect.
170
245
  * @returns {() => void} A function that can be called to remove the effect.
171
246
  */
172
- runAndListen(effect) {
173
- effect(this.wvalue);
174
- return this.listen(effect);
247
+ runAndListen(callback, options) {
248
+ const cb = callback;
249
+
250
+ cb(this.wvalue);
251
+
252
+ if (options?.signal?.aborted) {
253
+ return () => {};
254
+ }
255
+
256
+ options?.signal?.addEventListener('abort', () => {
257
+ this.ignore(cb);
258
+ });
259
+
260
+ if (options?.once) return () => this.ignore(cb);
261
+
262
+ if (options?.name && this.isListeningTo(options.name)) {
263
+ const message = `An effect with the name "${options.name}" is already listening to this cell.`;
264
+ throw new Error(message);
265
+ }
266
+
267
+ const isAlreadySubscribed = this.__effects.some((e) => {
268
+ return e.callback === callback;
269
+ });
270
+
271
+ if (!isAlreadySubscribed) {
272
+ this.__effects.push(new Effect(cb, options));
273
+ }
274
+
275
+ this.__effects.sort((a, b) => {
276
+ const aPriority = a.options?.priority ?? 0;
277
+ const bPriority = b.options?.priority ?? 0;
278
+ if (aPriority === bPriority) return 0;
279
+ return aPriority < bPriority ? 1 : -1;
280
+ });
281
+
282
+ return () => this.ignore(cb);
175
283
  }
176
284
 
177
285
  /**
@@ -179,10 +287,12 @@ export class Cell {
179
287
  * @param {(newValue: T) => void} callback - The effect callback to remove.
180
288
  */
181
289
  ignore(callback) {
182
- const index = this.effects.findIndex(({ effect }) => effect === callback);
290
+ const index = this.__effects.findIndex((e) => {
291
+ return e.callback === callback;
292
+ });
183
293
  if (index === -1) return;
184
294
 
185
- this.effects.splice(index, 1);
295
+ this.__effects.splice(index, 1);
186
296
  }
187
297
 
188
298
  /**
@@ -191,7 +301,9 @@ export class Cell {
191
301
  * @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
192
302
  */
193
303
  isListeningTo(name) {
194
- return this.effects.some(({ options }) => options?.name === name);
304
+ return this.__effects.some((effect) => {
305
+ return effect?.options?.name === name && effect.callback;
306
+ });
195
307
  }
196
308
 
197
309
  /**
@@ -199,12 +311,12 @@ export class Cell {
199
311
  * @param {string} name - The name of the watcher to stop listening to.
200
312
  */
201
313
  stopListeningTo(name) {
202
- const effectIndex = this.effects.findIndex(
203
- ({ options }) => options?.name === name
204
- );
314
+ const effectIndex = this.__effects.findIndex((e) => {
315
+ return e.options?.name === name;
316
+ });
205
317
  if (effectIndex === -1) return;
206
318
 
207
- this.effects.splice(effectIndex, 1);
319
+ this.__effects.splice(effectIndex, 1);
208
320
  }
209
321
 
210
322
  /**
@@ -213,7 +325,10 @@ export class Cell {
213
325
  */
214
326
  update() {
215
327
  // Run watchers.
216
- for (const { effect: watcher } of this.effects) {
328
+ for (const effect of this.__effects) {
329
+ let watcher = effect.callback;
330
+ if (watcher === undefined) continue;
331
+
217
332
  if (root.batchNestingLevel > 0) {
218
333
  root.batchedEffects.set(watcher, [this.wvalue]);
219
334
  continue;
@@ -222,16 +337,40 @@ export class Cell {
222
337
  watcher(this.wvalue);
223
338
  }
224
339
 
340
+ // Remove dead effects.
341
+ this.__effects = this.__effects.filter((effect) => effect.callback);
342
+
225
343
  // Run computed dependents.
226
- const computedDependents = this.derivedCells;
344
+ const computedDependents = this.__derivedCells;
227
345
  if (computedDependents !== undefined) {
228
346
  for (const dependent of computedDependents) {
229
- dependent.deref()?.update();
347
+ // global effects
348
+ for (const [options, effect] of root.globalPreEffects) {
349
+ if (options.ignoreDerivedCells) continue;
350
+
351
+ effect(this.wvalue);
352
+ }
353
+
354
+ const deref = dependent[0].deref();
355
+ if (deref === undefined) continue;
356
+
357
+ const computedCell = deref;
358
+ const computedFn = dependent[1];
359
+
360
+ if (root.batchNestingLevel > 0) {
361
+ root.batchedEffects.set(
362
+ () => computedCell.setValue(computedFn()),
363
+ []
364
+ );
365
+ } else {
366
+ computedCell.setValue(computedFn());
367
+ }
368
+ computedCell.update();
230
369
  }
231
370
  }
232
- // Periodically remove dead references.
233
- this.derivedCells = this.derivedCells.filter(
234
- (ref) => ref.deref() !== undefined
371
+ // Periodically drop dead references.
372
+ this.__derivedCells = this.__derivedCells.filter(
373
+ (ref) => ref[0].deref() !== undefined
235
374
  );
236
375
 
237
376
  // global effects
@@ -380,8 +519,10 @@ export class Cell {
380
519
 
381
520
  /**
382
521
  * Checks if the provided value is an instance of the Cell class.
383
- * @param {any} value - The value to check.
384
- * @returns {value is Cell<any>} True if the value is an instance of Cell, false otherwise.
522
+ * @template [T=any]
523
+ * @template [U=any]
524
+ * @param {Cell<T> | U} value - The value to check.
525
+ * @returns {value is Cell<T>} True if the value is an instance of Cell, false otherwise.
385
526
  */
386
527
  static isCell = (value) => value instanceof Cell;
387
528
 
@@ -510,19 +651,12 @@ export class Cell {
510
651
  * @extends {Cell<T>}
511
652
  */
512
653
  export class DerivedCell extends Cell {
513
- /**
514
- * @type {() => T}
515
- * @protected
516
- */
517
- computedFn;
518
-
519
654
  /**
520
655
  * @param {() => T} computedFn - A function that generates the value of the computed.
521
656
  */
522
657
  constructor(computedFn) {
523
658
  super();
524
- this.computedFn = computedFn;
525
- activeComputedValues.push(this);
659
+ activeComputedValues.push([this, computedFn]);
526
660
  this.setValue(computedFn());
527
661
  activeComputedValues.pop();
528
662
  }
@@ -540,26 +674,6 @@ export class DerivedCell extends Cell {
540
674
  set value(_) {
541
675
  throw new Error('Cannot set a derived Cell value.');
542
676
  }
543
-
544
- /**
545
- * Updates the current value with the result of the computed function.
546
- */
547
- update() {
548
- // global effects
549
- for (const [options, effect] of root.globalPreEffects) {
550
- if (options.ignoreDerivedCells) continue;
551
-
552
- effect(this.wvalue);
553
- }
554
-
555
- if (root.batchNestingLevel > 0) {
556
- root.batchedEffects.set(() => this.setValue(this.computedFn()), []);
557
- } else {
558
- this.setValue(this.computedFn());
559
- }
560
-
561
- super.update();
562
- }
563
677
  }
564
678
 
565
679
  /**
package/library/root.js CHANGED
@@ -39,6 +39,6 @@ export const root = {
39
39
  /**
40
40
  * A value representing the computed values that are currently being calculated.
41
41
  * It is an array so it can keep track of nested computed values.
42
- * @type {DerivedCell[]}
42
+ * @type {[DerivedCell, () => any][]}
43
43
  */
44
44
  export const activeComputedValues = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adbl/cells",
3
- "version": "0.0.0",
3
+ "version": "0.0.1",
4
4
  "description": "A simple implementation of reactive updates for JavaScript",
5
5
  "main": "index.js",
6
6
  "private": false,
@@ -96,31 +96,33 @@ export class Cell<T> {
96
96
  static batch: (callback: () => void) => void;
97
97
  /**
98
98
  * Checks if the provided value is an instance of the Cell class.
99
- * @param {any} value - The value to check.
100
- * @returns {value is Cell<any>} True if the value is an instance of Cell, false otherwise.
99
+ * @template [T=any]
100
+ * @template [U=any]
101
+ * @param {Cell<T> | U} value - The value to check.
102
+ * @returns {value is Cell<T>} True if the value is an instance of Cell, false otherwise.
101
103
  */
102
- static isCell: (value: any) => value is Cell<any>;
104
+ static isCell: <T_3 = any, U = any>(value: U | Cell<T_3>) => value is Cell<T_3>;
103
105
  /**
104
106
  * @template T
105
107
  * 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.
106
108
  * @param {T | Cell<T>} value - The value to be flattened.
107
109
  * @returns {T} The flattened value.
108
110
  */
109
- static flatten: <T_3>(value: T_3 | Cell<T_3>) => T_3;
111
+ static flatten: <T_4>(value: T_4 | Cell<T_4>) => T_4;
110
112
  /**
111
113
  * Flattens an array by applying the `flatten` function to each element.
112
114
  * @template T
113
115
  * @param {Array<T | Cell<T>>} array - The array to be flattened.
114
116
  * @returns {Array<T>} A new array with the flattened elements.
115
117
  */
116
- static flattenArray: <T_4>(array: (T_4 | Cell<T_4>)[]) => T_4[];
118
+ static flattenArray: <T_5>(array: (T_5 | Cell<T_5>)[]) => T_5[];
117
119
  /**
118
120
  * Flattens an object by applying the `flatten` function to each value.
119
121
  * @template {object} T
120
122
  * @param {T} object - The object to be flattened.
121
123
  * @returns {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} A new object with the flattened values.
122
124
  */
123
- static flattenObject: <T_5 extends object>(object: T_5) => { [K in keyof T_5]: T_5[K] extends Cell<infer U> ? U : T_5[K]; };
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]; };
124
126
  /**
125
127
  * Wraps an asynchronous function with managed state.
126
128
  *
@@ -140,21 +142,24 @@ export class Cell<T> {
140
142
  */
141
143
  static async<X, Y>(getter: (input: X) => Promise<Y>): AsyncRequestAtoms<X, Y>;
142
144
  /**
143
- * @type {Array<({
144
- * effect: (newValue: T) => void,
145
- * options?: EffectOptions,
146
- * })>}
145
+ * @type {Array<Effect<T>>}
147
146
  * @protected
148
147
  */
149
- protected effects: Array<({
150
- effect: (newValue: T) => void;
151
- options?: EffectOptions;
152
- })>;
148
+ protected __effects: Array<Effect<T>>;
153
149
  /**
154
- * @type {Array<WeakRef<DerivedCell<any>>>}
150
+ * @type {Array<WeakRef<[DerivedCell<any>, () => any]>>}
155
151
  * @protected
156
152
  */
157
- protected derivedCells: Array<WeakRef<DerivedCell<any>>>;
153
+ protected __derivedCells: Array<WeakRef<[DerivedCell<any>, () => any]>>;
154
+ /**
155
+ * @readonly
156
+ */
157
+ readonly get effects(): Effect<T>[];
158
+ /**
159
+ * @readonly
160
+ * @returns {Array<DerivedCell<any>>}
161
+ */
162
+ readonly get derivedCells(): DerivedCell<any>[];
158
163
  /**
159
164
  * @protected @type T
160
165
  */
@@ -169,6 +174,12 @@ export class Cell<T> {
169
174
  * @returns {T} The value of the Cell.
170
175
  */
171
176
  valueOf(): T;
177
+ get value(): T;
178
+ /**
179
+ * Stringifies the value of the Cell.
180
+ * @returns {string}
181
+ */
182
+ toString(): string;
172
183
  /**
173
184
  * The value stored in the Cell.
174
185
  * @protected @type {T}
@@ -188,10 +199,11 @@ export class Cell<T> {
188
199
  listen(callback: (newValue: T) => void, options?: EffectOptions | undefined): () => void;
189
200
  /**
190
201
  * 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.
191
- * @param {(newValue: T) => void} effect - The effect callback to add.
202
+ * @param {(newValue: T) => void} callback - The effect callback to add.
203
+ * @param {Partial<EffectOptions>} [options] - The options for the effect.
192
204
  * @returns {() => void} A function that can be called to remove the effect.
193
205
  */
194
- runAndListen(effect: (newValue: T) => void): () => void;
206
+ runAndListen(callback: (newValue: T) => void, options?: Partial<EffectOptions> | undefined): () => void;
195
207
  /**
196
208
  * Removes the specified effect callback from the list of effects for this cell.
197
209
  * @param {(newValue: T) => void} callback - The effect callback to remove.
@@ -203,6 +215,11 @@ export class Cell<T> {
203
215
  * @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
204
216
  */
205
217
  isListeningTo(name: string): boolean;
218
+ /**
219
+ * Removes the watcher with the specified name from the list of effects for this cell.
220
+ * @param {string} name - The name of the watcher to stop listening to.
221
+ */
222
+ stopListeningTo(name: string): void;
206
223
  /**
207
224
  * Updates the root object and notifies any registered watchers and computed dependents.
208
225
  * This method is called whenever the root object's value changes.
@@ -225,11 +242,6 @@ export class DerivedCell<T> extends Cell<T> {
225
242
  * @param {() => T} computedFn - A function that generates the value of the computed.
226
243
  */
227
244
  constructor(computedFn: () => T);
228
- /**
229
- * @type {() => T}
230
- * @protected
231
- */
232
- protected computedFn: () => T;
233
245
  /**
234
246
  * @readonly
235
247
  */
@@ -302,6 +314,10 @@ export type EffectOptions = {
302
314
  * The name of the effect for debugging purposes.
303
315
  */
304
316
  name?: string | undefined;
317
+ /**
318
+ * Whether the effect should be weakly referenced. This means that the effect will be garbage collected if there are no other references to it.
319
+ */
320
+ weak?: boolean | undefined;
305
321
  /**
306
322
  * The priority of the effect. Higher priority effects are executed first. The default priority is 0.
307
323
  */
@@ -322,3 +338,31 @@ export type CellOptions<T> = {
322
338
  equals?: ((oldValue: T, newValue: T) => boolean) | undefined;
323
339
  };
324
340
  export type NeverIfAny<T> = 0 extends (1 & T) ? never : T;
341
+ export type Reference<T> = {
342
+ deref: () => T | undefined;
343
+ };
344
+ /**
345
+ * @template T
346
+ * @typedef {{
347
+ * deref: () => T | undefined
348
+ * }} Reference
349
+ */
350
+ /** @template T */
351
+ declare class Effect<T> {
352
+ /**
353
+ * @param {(newValue: T) => void} callback
354
+ * @param {EffectOptions} [options]
355
+ */
356
+ constructor(callback: (newValue: T) => void, options?: EffectOptions | undefined);
357
+ /**
358
+ * @type {EffectOptions | undefined}
359
+ */
360
+ options: EffectOptions | undefined;
361
+ /**
362
+ * Returns the callback function, if it still exists.
363
+ * @returns {((newValue: T) => void) | undefined}
364
+ */
365
+ get callback(): ((newValue: T) => void) | undefined;
366
+ #private;
367
+ }
368
+ export {};
@@ -7,9 +7,9 @@ export namespace root {
7
7
  /**
8
8
  * A value representing the computed values that are currently being calculated.
9
9
  * It is an array so it can keep track of nested computed values.
10
- * @type {DerivedCell[]}
10
+ * @type {[DerivedCell, () => any][]}
11
11
  */
12
- export const activeComputedValues: import("./classes.js").DerivedCell<any>[];
12
+ export const activeComputedValues: [import("./classes.js").DerivedCell<any>, () => any][];
13
13
  export type Watchable = import('./classes.js').Cell<any>;
14
14
  export type DerivedCell = import('./classes.js').DerivedCell<any>;
15
15
  export type GlobalEffectOptions = {
package/jsconfig.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "noUnusedLocals": true,
7
- "noUnusedParameters": true,
8
- "noImplicitAny": true,
9
- "allowUnreachableCode": false,
10
- "allowJs": true,
11
-
12
- "noImplicitReturns": true,
13
-
14
- "noEmit": false,
15
- "emitDeclarationOnly": true,
16
- "declaration": true,
17
-
18
- "checkJs": true,
19
- "strict": true,
20
-
21
- "outDir": "types"
22
- },
23
- "include": ["library/**/*", "index.js"],
24
- "exclude": ["types/**/*", "tests/**/*"]
25
- }