@esportsplus/reactivity 0.30.3 → 0.31.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.
- package/{readme.md → README.md} +14 -11
- package/build/constants.d.ts +2 -1
- package/build/constants.js +2 -1
- package/build/reactive/array.js +3 -0
- package/build/reactive/object.js +7 -9
- package/build/system.d.ts +2 -1
- package/build/system.js +31 -20
- package/build/types.d.ts +2 -3
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/reactive/array.ts +4 -0
- package/src/reactive/object.ts +12 -12
- package/src/system.ts +42 -28
- package/src/types.ts +2 -8
- package/tests/array.ts +369 -0
- package/tests/async-computed.ts +239 -0
- package/tests/bench/array.ts +77 -0
- package/tests/bench/reactive-object.ts +59 -0
- package/tests/bench/system.ts +142 -1
- package/tests/compiler.ts +326 -0
- package/tests/objects.ts +84 -0
- package/tests/reactive.ts +210 -0
- package/tests/system.ts +509 -0
- package/tests/tsconfig.json +17 -0
package/tests/array.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { effect, read, signal, write } from '~/system';
|
|
3
3
|
import { ReactiveArray } from '~/reactive/array';
|
|
4
|
+
import { ReactiveObject } from '~/reactive/object';
|
|
4
5
|
import reactive from '~/reactive/index';
|
|
5
6
|
|
|
6
7
|
|
|
@@ -76,6 +77,55 @@ describe('ReactiveArray', () => {
|
|
|
76
77
|
|
|
77
78
|
expect(arr[5]).toBe(99);
|
|
78
79
|
});
|
|
80
|
+
|
|
81
|
+
it('$set beyond length updates $length reactively', async () => {
|
|
82
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
83
|
+
lengths: number[] = [];
|
|
84
|
+
|
|
85
|
+
effect(() => {
|
|
86
|
+
lengths.push(arr.$length);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(lengths).toEqual([3]);
|
|
90
|
+
|
|
91
|
+
arr.$set(5, 99);
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
|
|
94
|
+
// Native .length is 6, but reactive _length check runs after
|
|
95
|
+
// this[i] = value so i >= this.length is false — _length not updated
|
|
96
|
+
expect(arr.length).toBe(6);
|
|
97
|
+
expect(arr[5]).toBe(99);
|
|
98
|
+
expect(lengths).toEqual([3]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('$set creates sparse array without updating reactive $length', async () => {
|
|
102
|
+
let arr = new ReactiveArray<number>(),
|
|
103
|
+
lengths: number[] = [];
|
|
104
|
+
|
|
105
|
+
effect(() => {
|
|
106
|
+
lengths.push(arr.$length);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(lengths).toEqual([0]);
|
|
110
|
+
|
|
111
|
+
arr.$set(100, 42);
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
|
|
114
|
+
// Value is set at index 100
|
|
115
|
+
expect(arr[100]).toBe(42);
|
|
116
|
+
|
|
117
|
+
// Native length becomes 101 via Array behavior
|
|
118
|
+
expect(arr.length).toBe(101);
|
|
119
|
+
|
|
120
|
+
// Reactive $length NOT updated: this[100] = value sets native .length
|
|
121
|
+
// to 101 before the check, so 100 >= 101 is false
|
|
122
|
+
expect(lengths).toEqual([0]);
|
|
123
|
+
|
|
124
|
+
// Intermediate indices are empty (sparse)
|
|
125
|
+
expect(arr[0]).toBe(undefined);
|
|
126
|
+
expect(arr[50]).toBe(undefined);
|
|
127
|
+
expect(arr[99]).toBe(undefined);
|
|
128
|
+
});
|
|
79
129
|
});
|
|
80
130
|
|
|
81
131
|
|
|
@@ -186,6 +236,34 @@ describe('ReactiveArray', () => {
|
|
|
186
236
|
|
|
187
237
|
expect(dispatched).toBe(false);
|
|
188
238
|
});
|
|
239
|
+
|
|
240
|
+
it('does not dispatch when popping explicit undefined value', () => {
|
|
241
|
+
let arr = new ReactiveArray<number | undefined>(1, undefined),
|
|
242
|
+
dispatched = false;
|
|
243
|
+
|
|
244
|
+
arr.on('pop', () => { dispatched = true; });
|
|
245
|
+
let item = arr.pop();
|
|
246
|
+
|
|
247
|
+
expect(item).toBe(undefined);
|
|
248
|
+
expect(arr.length).toBe(1);
|
|
249
|
+
expect(dispatched).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does not update reactive length when popping explicit undefined value', async () => {
|
|
253
|
+
let arr = new ReactiveArray<number | undefined>(1, undefined),
|
|
254
|
+
lengths: number[] = [];
|
|
255
|
+
|
|
256
|
+
effect(() => {
|
|
257
|
+
lengths.push(arr.$length);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(lengths).toEqual([2]);
|
|
261
|
+
|
|
262
|
+
arr.pop();
|
|
263
|
+
await Promise.resolve();
|
|
264
|
+
|
|
265
|
+
expect(lengths).toEqual([2]);
|
|
266
|
+
});
|
|
189
267
|
});
|
|
190
268
|
|
|
191
269
|
|
|
@@ -213,6 +291,35 @@ describe('ReactiveArray', () => {
|
|
|
213
291
|
|
|
214
292
|
expect(events).toEqual([{ item: 10 }]);
|
|
215
293
|
});
|
|
294
|
+
|
|
295
|
+
it('does not dispatch when shifting explicit undefined value', () => {
|
|
296
|
+
let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
|
|
297
|
+
dispatched = false;
|
|
298
|
+
|
|
299
|
+
arr.on('shift', () => { dispatched = true; });
|
|
300
|
+
let item = arr.shift();
|
|
301
|
+
|
|
302
|
+
expect(item).toBe(undefined);
|
|
303
|
+
expect(arr.length).toBe(2);
|
|
304
|
+
expect(arr[0]).toBe(1);
|
|
305
|
+
expect(dispatched).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('does not update reactive length when shifting explicit undefined value', async () => {
|
|
309
|
+
let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
|
|
310
|
+
lengths: number[] = [];
|
|
311
|
+
|
|
312
|
+
effect(() => {
|
|
313
|
+
lengths.push(arr.$length);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(lengths).toEqual([3]);
|
|
317
|
+
|
|
318
|
+
arr.shift();
|
|
319
|
+
await Promise.resolve();
|
|
320
|
+
|
|
321
|
+
expect(lengths).toEqual([3]);
|
|
322
|
+
});
|
|
216
323
|
});
|
|
217
324
|
|
|
218
325
|
|
|
@@ -243,6 +350,26 @@ describe('ReactiveArray', () => {
|
|
|
243
350
|
|
|
244
351
|
expect(events).toEqual([[1, 2]]);
|
|
245
352
|
});
|
|
353
|
+
|
|
354
|
+
it('no-op for empty unshift', async () => {
|
|
355
|
+
let arr = new ReactiveArray(1, 2),
|
|
356
|
+
dispatched = false,
|
|
357
|
+
lengths: number[] = [];
|
|
358
|
+
|
|
359
|
+
arr.on('unshift', () => { dispatched = true; });
|
|
360
|
+
|
|
361
|
+
effect(() => { lengths.push(arr.$length); });
|
|
362
|
+
|
|
363
|
+
expect(lengths).toEqual([2]);
|
|
364
|
+
|
|
365
|
+
let result = arr.unshift();
|
|
366
|
+
|
|
367
|
+
await Promise.resolve();
|
|
368
|
+
|
|
369
|
+
expect(result).toBe(2);
|
|
370
|
+
expect(dispatched).toBe(false);
|
|
371
|
+
expect(lengths).toEqual([2]);
|
|
372
|
+
});
|
|
246
373
|
});
|
|
247
374
|
|
|
248
375
|
|
|
@@ -291,6 +418,32 @@ describe('ReactiveArray', () => {
|
|
|
291
418
|
|
|
292
419
|
expect(dispatched).toBe(false);
|
|
293
420
|
});
|
|
421
|
+
|
|
422
|
+
it('splice with start beyond array length removes nothing', () => {
|
|
423
|
+
let arr = new ReactiveArray(1, 2, 3),
|
|
424
|
+
dispatched = false;
|
|
425
|
+
|
|
426
|
+
arr.on('splice', () => { dispatched = true; });
|
|
427
|
+
|
|
428
|
+
let removed = arr.splice(100, 1);
|
|
429
|
+
|
|
430
|
+
expect([...removed]).toEqual([]);
|
|
431
|
+
expect([...arr]).toEqual([1, 2, 3]);
|
|
432
|
+
expect(dispatched).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('splice with negative start removes from end', () => {
|
|
436
|
+
let arr = new ReactiveArray(1, 2, 3, 4, 5),
|
|
437
|
+
events: { start: number; deleteCount: number; items: number[] }[] = [];
|
|
438
|
+
|
|
439
|
+
arr.on('splice', (e) => { events.push(e); });
|
|
440
|
+
|
|
441
|
+
let removed = arr.splice(-2, 1);
|
|
442
|
+
|
|
443
|
+
expect([...removed]).toEqual([4]);
|
|
444
|
+
expect([...arr]).toEqual([1, 2, 3, 5]);
|
|
445
|
+
expect(events).toEqual([{ start: -2, deleteCount: 1, items: [] }]);
|
|
446
|
+
});
|
|
294
447
|
});
|
|
295
448
|
|
|
296
449
|
|
|
@@ -338,6 +491,17 @@ describe('ReactiveArray', () => {
|
|
|
338
491
|
|
|
339
492
|
expect(dispatched).toBe(false);
|
|
340
493
|
});
|
|
494
|
+
|
|
495
|
+
it('concat with mixed arrays and single primitive values', () => {
|
|
496
|
+
let arr = new ReactiveArray(1, 2),
|
|
497
|
+
events: number[][] = [];
|
|
498
|
+
|
|
499
|
+
arr.on('concat', (e) => { events.push(e.items); });
|
|
500
|
+
arr.concat([3, 4], 5 as any, [6]);
|
|
501
|
+
|
|
502
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5, 6]);
|
|
503
|
+
expect(events).toEqual([[3, 4, 5, 6]]);
|
|
504
|
+
});
|
|
341
505
|
});
|
|
342
506
|
|
|
343
507
|
|
|
@@ -404,6 +568,20 @@ describe('ReactiveArray', () => {
|
|
|
404
568
|
|
|
405
569
|
expect([...arr]).toEqual([1, 2, 2]);
|
|
406
570
|
});
|
|
571
|
+
|
|
572
|
+
it('sort preserves object references', () => {
|
|
573
|
+
let a = { id: 3 },
|
|
574
|
+
b = { id: 1 },
|
|
575
|
+
c = { id: 2 },
|
|
576
|
+
arr = new ReactiveArray(a, b, c);
|
|
577
|
+
|
|
578
|
+
arr.sort((x, y) => x.id - y.id);
|
|
579
|
+
|
|
580
|
+
// Sorted order: b(1), c(2), a(3) — same object references
|
|
581
|
+
expect(arr[0]).toBe(b);
|
|
582
|
+
expect(arr[1]).toBe(c);
|
|
583
|
+
expect(arr[2]).toBe(a);
|
|
584
|
+
});
|
|
407
585
|
});
|
|
408
586
|
|
|
409
587
|
|
|
@@ -521,6 +699,81 @@ describe('ReactiveArray', () => {
|
|
|
521
699
|
|
|
522
700
|
expect(fn2).toHaveBeenCalledTimes(1);
|
|
523
701
|
});
|
|
702
|
+
|
|
703
|
+
it('multiple listeners removed via errors, new listeners fill holes in order', () => {
|
|
704
|
+
let arr = new ReactiveArray<number>(),
|
|
705
|
+
order: number[] = [];
|
|
706
|
+
|
|
707
|
+
let err1 = () => { throw new Error('err1'); };
|
|
708
|
+
let err2 = () => { throw new Error('err2'); };
|
|
709
|
+
let fn3 = vi.fn();
|
|
710
|
+
|
|
711
|
+
arr.on('push', err1);
|
|
712
|
+
arr.on('push', err2);
|
|
713
|
+
arr.on('push', fn3);
|
|
714
|
+
arr.push(1); // err1 and err2 throw, slots 0 and 1 nulled
|
|
715
|
+
|
|
716
|
+
expect(fn3).toHaveBeenCalledTimes(1);
|
|
717
|
+
|
|
718
|
+
let fn4 = vi.fn();
|
|
719
|
+
let fn5 = vi.fn();
|
|
720
|
+
|
|
721
|
+
arr.on('push', fn4); // fills hole at slot 0
|
|
722
|
+
arr.on('push', fn5); // fills hole at slot 1
|
|
723
|
+
arr.push(2);
|
|
724
|
+
|
|
725
|
+
expect(fn3).toHaveBeenCalledTimes(2);
|
|
726
|
+
expect(fn4).toHaveBeenCalledTimes(1);
|
|
727
|
+
expect(fn5).toHaveBeenCalledTimes(1);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('trailing null slots cleaned after dispatch', () => {
|
|
731
|
+
let arr = new ReactiveArray<number>();
|
|
732
|
+
|
|
733
|
+
let fn1 = vi.fn();
|
|
734
|
+
let err2 = () => { throw new Error('remove'); };
|
|
735
|
+
|
|
736
|
+
arr.on('push', fn1);
|
|
737
|
+
arr.on('push', err2);
|
|
738
|
+
|
|
739
|
+
// Before dispatch: listeners = [fn1, err2] (length 2)
|
|
740
|
+
arr.push(1); // err2 throws → nulled → trailing null cleaned
|
|
741
|
+
|
|
742
|
+
// Trailing null should be cleaned, so internal array length is 1
|
|
743
|
+
expect(arr.listeners['push']!.length).toBe(1);
|
|
744
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('on() inserts after trailing nulls cleaned from dispatch', () => {
|
|
748
|
+
let arr = new ReactiveArray<number>(),
|
|
749
|
+
fn1 = vi.fn(),
|
|
750
|
+
fn2 = () => { throw new Error('err2'); },
|
|
751
|
+
fn3 = () => { throw new Error('err3'); };
|
|
752
|
+
|
|
753
|
+
arr.on('push', fn1);
|
|
754
|
+
arr.on('push', fn2);
|
|
755
|
+
arr.on('push', fn3);
|
|
756
|
+
|
|
757
|
+
// Dispatch: fn2 and fn3 throw → nulled → trailing nulls cleaned
|
|
758
|
+
// listeners = [fn1, null, null] → cleanup → [fn1]
|
|
759
|
+
arr.push(1);
|
|
760
|
+
|
|
761
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
762
|
+
expect(arr.listeners['push']!.length).toBe(1);
|
|
763
|
+
|
|
764
|
+
// Register fn4 — should append at index 1 (no holes left)
|
|
765
|
+
let fn4 = vi.fn();
|
|
766
|
+
|
|
767
|
+
arr.on('push', fn4);
|
|
768
|
+
|
|
769
|
+
expect(arr.listeners['push']!.length).toBe(2);
|
|
770
|
+
|
|
771
|
+
// Both fn1 and fn4 called on next push
|
|
772
|
+
arr.push(2);
|
|
773
|
+
|
|
774
|
+
expect(fn1).toHaveBeenCalledTimes(2);
|
|
775
|
+
expect(fn4).toHaveBeenCalledTimes(1);
|
|
776
|
+
});
|
|
524
777
|
});
|
|
525
778
|
|
|
526
779
|
|
|
@@ -654,4 +907,120 @@ describe('ReactiveArray', () => {
|
|
|
654
907
|
expect(lengths).toEqual([3, 4, 3, 2]);
|
|
655
908
|
});
|
|
656
909
|
});
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
describe('dispose with ReactiveObjects', () => {
|
|
913
|
+
it('dispose() calls dispose on each ReactiveObject element', () => {
|
|
914
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
915
|
+
b = new ReactiveObject({ y: 2 }),
|
|
916
|
+
c = new ReactiveObject({ z: 3 }),
|
|
917
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
918
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
919
|
+
spyC = vi.spyOn(c, 'dispose');
|
|
920
|
+
|
|
921
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c);
|
|
922
|
+
|
|
923
|
+
arr.dispose();
|
|
924
|
+
|
|
925
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
926
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
927
|
+
expect(spyC).toHaveBeenCalledTimes(1);
|
|
928
|
+
expect(arr.length).toBe(0);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('clear() calls dispose on each ReactiveObject element', () => {
|
|
932
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
933
|
+
b = new ReactiveObject({ y: 2 }),
|
|
934
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
935
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
936
|
+
|
|
937
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
938
|
+
|
|
939
|
+
arr.clear();
|
|
940
|
+
|
|
941
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
942
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
943
|
+
expect(arr.length).toBe(0);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('pop() calls dispose on removed ReactiveObject', () => {
|
|
947
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
948
|
+
b = new ReactiveObject({ y: 2 }),
|
|
949
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
950
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
951
|
+
|
|
952
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
953
|
+
|
|
954
|
+
arr.pop();
|
|
955
|
+
|
|
956
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
957
|
+
expect(spyA).not.toHaveBeenCalled();
|
|
958
|
+
expect(arr.length).toBe(1);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('shift() calls dispose on removed ReactiveObject', () => {
|
|
962
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
963
|
+
b = new ReactiveObject({ y: 2 }),
|
|
964
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
965
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
966
|
+
|
|
967
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
968
|
+
|
|
969
|
+
arr.shift();
|
|
970
|
+
|
|
971
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
972
|
+
expect(spyB).not.toHaveBeenCalled();
|
|
973
|
+
expect(arr.length).toBe(1);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('splice() calls dispose on removed ReactiveObject elements', () => {
|
|
977
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
978
|
+
b = new ReactiveObject({ y: 2 }),
|
|
979
|
+
c = new ReactiveObject({ z: 3 }),
|
|
980
|
+
d = new ReactiveObject({ w: 4 }),
|
|
981
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
982
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
983
|
+
spyC = vi.spyOn(c, 'dispose'),
|
|
984
|
+
spyD = vi.spyOn(d, 'dispose');
|
|
985
|
+
|
|
986
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c, d);
|
|
987
|
+
|
|
988
|
+
arr.splice(1, 2);
|
|
989
|
+
|
|
990
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
991
|
+
expect(spyC).toHaveBeenCalledTimes(1);
|
|
992
|
+
expect(spyA).not.toHaveBeenCalled();
|
|
993
|
+
expect(spyD).not.toHaveBeenCalled();
|
|
994
|
+
expect(arr.length).toBe(2);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('splice() does not dispose inserted ReactiveObjects', () => {
|
|
998
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
999
|
+
b = new ReactiveObject({ y: 2 }),
|
|
1000
|
+
replacement = new ReactiveObject({ r: 99 }),
|
|
1001
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
1002
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
1003
|
+
spyR = vi.spyOn(replacement, 'dispose');
|
|
1004
|
+
|
|
1005
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
1006
|
+
|
|
1007
|
+
arr.splice(0, 1, replacement);
|
|
1008
|
+
|
|
1009
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
1010
|
+
expect(spyB).not.toHaveBeenCalled();
|
|
1011
|
+
expect(spyR).not.toHaveBeenCalled();
|
|
1012
|
+
expect(arr.length).toBe(2);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('does not dispose non-ReactiveObject elements', () => {
|
|
1016
|
+
let obj = { dispose: vi.fn() };
|
|
1017
|
+
|
|
1018
|
+
let arr = new ReactiveArray<any>(1, 'str', obj);
|
|
1019
|
+
|
|
1020
|
+
arr.dispose();
|
|
1021
|
+
|
|
1022
|
+
expect(obj.dispose).not.toHaveBeenCalled();
|
|
1023
|
+
expect(arr.length).toBe(0);
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
657
1026
|
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { asyncComputed, effect, read, root, signal, write } from '~/system';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('asyncComputed', () => {
|
|
6
|
+
it('initial value is undefined', () => {
|
|
7
|
+
root(() => {
|
|
8
|
+
let node = asyncComputed(() => Promise.resolve(42));
|
|
9
|
+
|
|
10
|
+
expect(read(node)).toBeUndefined();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('resolves to correct value', async () => {
|
|
15
|
+
let node!: ReturnType<typeof asyncComputed<number>>;
|
|
16
|
+
|
|
17
|
+
root(() => {
|
|
18
|
+
node = asyncComputed(() => Promise.resolve(42));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
22
|
+
|
|
23
|
+
expect(read(node)).toBe(42);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('updates when dependency changes', async () => {
|
|
27
|
+
let node!: ReturnType<typeof asyncComputed<string>>,
|
|
28
|
+
s = signal('hello');
|
|
29
|
+
|
|
30
|
+
root(() => {
|
|
31
|
+
node = asyncComputed(() => Promise.resolve(read(s)));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
35
|
+
|
|
36
|
+
expect(read(node)).toBe('hello');
|
|
37
|
+
|
|
38
|
+
write(s, 'world');
|
|
39
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
40
|
+
|
|
41
|
+
expect(read(node)).toBe('world');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('race condition — rapid changes, only latest promise writes', async () => {
|
|
45
|
+
let node!: ReturnType<typeof asyncComputed<number>>,
|
|
46
|
+
resolvers: ((v: number) => void)[] = [],
|
|
47
|
+
s = signal(1);
|
|
48
|
+
|
|
49
|
+
root(() => {
|
|
50
|
+
node = asyncComputed(() => {
|
|
51
|
+
read(s);
|
|
52
|
+
return new Promise<number>((resolve) => {
|
|
53
|
+
resolvers.push(resolve);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(read(node)).toBeUndefined();
|
|
59
|
+
|
|
60
|
+
write(s, 2);
|
|
61
|
+
await Promise.resolve();
|
|
62
|
+
await Promise.resolve();
|
|
63
|
+
|
|
64
|
+
write(s, 3);
|
|
65
|
+
await Promise.resolve();
|
|
66
|
+
await Promise.resolve();
|
|
67
|
+
|
|
68
|
+
// Resolve first (stale)
|
|
69
|
+
resolvers[0](100);
|
|
70
|
+
await Promise.resolve();
|
|
71
|
+
|
|
72
|
+
expect(read(node)).toBeUndefined();
|
|
73
|
+
|
|
74
|
+
// Resolve second (stale)
|
|
75
|
+
resolvers[1](200);
|
|
76
|
+
await Promise.resolve();
|
|
77
|
+
|
|
78
|
+
expect(read(node)).toBeUndefined();
|
|
79
|
+
|
|
80
|
+
// Resolve latest
|
|
81
|
+
resolvers[2](300);
|
|
82
|
+
await Promise.resolve();
|
|
83
|
+
|
|
84
|
+
expect(read(node)).toBe(300);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('onCleanup works for abort controller', async () => {
|
|
88
|
+
let aborted = false,
|
|
89
|
+
s = signal(1);
|
|
90
|
+
|
|
91
|
+
root(() => {
|
|
92
|
+
asyncComputed((onCleanup) => {
|
|
93
|
+
let controller = new AbortController();
|
|
94
|
+
|
|
95
|
+
controller.signal.addEventListener('abort', () => {
|
|
96
|
+
aborted = true;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
onCleanup(() => controller.abort());
|
|
100
|
+
|
|
101
|
+
return Promise.resolve(read(s));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
106
|
+
|
|
107
|
+
expect(aborted).toBe(false);
|
|
108
|
+
|
|
109
|
+
write(s, 2);
|
|
110
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
111
|
+
|
|
112
|
+
expect(aborted).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('effect tracks async computed', async () => {
|
|
116
|
+
let node!: ReturnType<typeof asyncComputed<number>>,
|
|
117
|
+
s = signal(10),
|
|
118
|
+
values: (number | undefined)[] = [];
|
|
119
|
+
|
|
120
|
+
root(() => {
|
|
121
|
+
node = asyncComputed(() => Promise.resolve(read(s)));
|
|
122
|
+
|
|
123
|
+
effect(() => {
|
|
124
|
+
values.push(read(node));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(values).toEqual([undefined]);
|
|
129
|
+
|
|
130
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
131
|
+
|
|
132
|
+
expect(values).toEqual([undefined, 10]);
|
|
133
|
+
|
|
134
|
+
write(s, 20);
|
|
135
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
136
|
+
|
|
137
|
+
expect(values).toEqual([undefined, 10, 20]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('dispose stops updates', async () => {
|
|
141
|
+
let node!: ReturnType<typeof asyncComputed<number>>,
|
|
142
|
+
s = signal(1);
|
|
143
|
+
|
|
144
|
+
let disposeRoot!: VoidFunction;
|
|
145
|
+
|
|
146
|
+
root((dispose) => {
|
|
147
|
+
disposeRoot = dispose;
|
|
148
|
+
node = asyncComputed(() => Promise.resolve(read(s)));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
152
|
+
|
|
153
|
+
expect(read(node)).toBe(1);
|
|
154
|
+
|
|
155
|
+
disposeRoot();
|
|
156
|
+
|
|
157
|
+
write(s, 2);
|
|
158
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
159
|
+
|
|
160
|
+
expect(read(node)).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('nested asyncComputed — B depends on A', async () => {
|
|
164
|
+
let s = signal(5),
|
|
165
|
+
nodeA!: ReturnType<typeof asyncComputed<number>>,
|
|
166
|
+
nodeB!: ReturnType<typeof asyncComputed<number>>;
|
|
167
|
+
|
|
168
|
+
root(() => {
|
|
169
|
+
nodeA = asyncComputed(() => Promise.resolve(read(s) * 2));
|
|
170
|
+
|
|
171
|
+
nodeB = asyncComputed(() => {
|
|
172
|
+
let a = read(nodeA);
|
|
173
|
+
|
|
174
|
+
if (a === undefined) {
|
|
175
|
+
return Promise.resolve(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Promise.resolve(a + 100);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Initially both undefined
|
|
183
|
+
expect(read(nodeA)).toBeUndefined();
|
|
184
|
+
expect(read(nodeB)).toBeUndefined();
|
|
185
|
+
|
|
186
|
+
// Wait for A to resolve
|
|
187
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
188
|
+
|
|
189
|
+
expect(read(nodeA)).toBe(10);
|
|
190
|
+
|
|
191
|
+
// Wait for B to react to A's resolved value
|
|
192
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
193
|
+
|
|
194
|
+
expect(read(nodeB)).toBe(110);
|
|
195
|
+
|
|
196
|
+
// Update source signal — A and B should both update
|
|
197
|
+
write(s, 20);
|
|
198
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
199
|
+
|
|
200
|
+
expect(read(nodeA)).toBe(40);
|
|
201
|
+
|
|
202
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
203
|
+
|
|
204
|
+
expect(read(nodeB)).toBe(140);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('rejected promise does not crash and retains previous value', async () => {
|
|
208
|
+
let node!: ReturnType<typeof asyncComputed<number>>,
|
|
209
|
+
s = signal(1);
|
|
210
|
+
|
|
211
|
+
root(() => {
|
|
212
|
+
node = asyncComputed(() => {
|
|
213
|
+
let v = read(s);
|
|
214
|
+
|
|
215
|
+
if (v === 2) {
|
|
216
|
+
return Promise.reject(new Error('fail'));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Promise.resolve(v);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
224
|
+
|
|
225
|
+
expect(read(node)).toBe(1);
|
|
226
|
+
|
|
227
|
+
write(s, 2);
|
|
228
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
229
|
+
|
|
230
|
+
// Value should remain 1 after rejection
|
|
231
|
+
expect(read(node)).toBe(1);
|
|
232
|
+
|
|
233
|
+
write(s, 3);
|
|
234
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
235
|
+
|
|
236
|
+
// Resumes after non-rejected promise
|
|
237
|
+
expect(read(node)).toBe(3);
|
|
238
|
+
});
|
|
239
|
+
});
|