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