@esportsplus/reactivity 0.30.3 → 0.31.0
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/build/constants.d.ts +2 -1
- package/build/constants.js +2 -1
- 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/object.ts +12 -12
- package/src/system.ts +42 -28
- package/src/types.ts +2 -8
- package/tests/array.ts +249 -0
- package/tests/async-computed.ts +195 -0
- package/tests/bench/array.ts +65 -0
- package/tests/bench/reactive-object.ts +54 -0
- package/tests/bench/system.ts +88 -1
- package/tests/compiler.ts +326 -0
- package/tests/reactive.ts +210 -0
- package/tests/system.ts +359 -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,26 @@ 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
|
+
});
|
|
79
100
|
});
|
|
80
101
|
|
|
81
102
|
|
|
@@ -186,6 +207,34 @@ describe('ReactiveArray', () => {
|
|
|
186
207
|
|
|
187
208
|
expect(dispatched).toBe(false);
|
|
188
209
|
});
|
|
210
|
+
|
|
211
|
+
it('does not dispatch when popping explicit undefined value', () => {
|
|
212
|
+
let arr = new ReactiveArray<number | undefined>(1, undefined),
|
|
213
|
+
dispatched = false;
|
|
214
|
+
|
|
215
|
+
arr.on('pop', () => { dispatched = true; });
|
|
216
|
+
let item = arr.pop();
|
|
217
|
+
|
|
218
|
+
expect(item).toBe(undefined);
|
|
219
|
+
expect(arr.length).toBe(1);
|
|
220
|
+
expect(dispatched).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not update reactive length when popping explicit undefined value', async () => {
|
|
224
|
+
let arr = new ReactiveArray<number | undefined>(1, undefined),
|
|
225
|
+
lengths: number[] = [];
|
|
226
|
+
|
|
227
|
+
effect(() => {
|
|
228
|
+
lengths.push(arr.$length);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(lengths).toEqual([2]);
|
|
232
|
+
|
|
233
|
+
arr.pop();
|
|
234
|
+
await Promise.resolve();
|
|
235
|
+
|
|
236
|
+
expect(lengths).toEqual([2]);
|
|
237
|
+
});
|
|
189
238
|
});
|
|
190
239
|
|
|
191
240
|
|
|
@@ -213,6 +262,35 @@ describe('ReactiveArray', () => {
|
|
|
213
262
|
|
|
214
263
|
expect(events).toEqual([{ item: 10 }]);
|
|
215
264
|
});
|
|
265
|
+
|
|
266
|
+
it('does not dispatch when shifting explicit undefined value', () => {
|
|
267
|
+
let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
|
|
268
|
+
dispatched = false;
|
|
269
|
+
|
|
270
|
+
arr.on('shift', () => { dispatched = true; });
|
|
271
|
+
let item = arr.shift();
|
|
272
|
+
|
|
273
|
+
expect(item).toBe(undefined);
|
|
274
|
+
expect(arr.length).toBe(2);
|
|
275
|
+
expect(arr[0]).toBe(1);
|
|
276
|
+
expect(dispatched).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('does not update reactive length when shifting explicit undefined value', async () => {
|
|
280
|
+
let arr = new ReactiveArray<number | undefined>(undefined, 1, 2),
|
|
281
|
+
lengths: number[] = [];
|
|
282
|
+
|
|
283
|
+
effect(() => {
|
|
284
|
+
lengths.push(arr.$length);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(lengths).toEqual([3]);
|
|
288
|
+
|
|
289
|
+
arr.shift();
|
|
290
|
+
await Promise.resolve();
|
|
291
|
+
|
|
292
|
+
expect(lengths).toEqual([3]);
|
|
293
|
+
});
|
|
216
294
|
});
|
|
217
295
|
|
|
218
296
|
|
|
@@ -338,6 +416,17 @@ describe('ReactiveArray', () => {
|
|
|
338
416
|
|
|
339
417
|
expect(dispatched).toBe(false);
|
|
340
418
|
});
|
|
419
|
+
|
|
420
|
+
it('concat with mixed arrays and single primitive values', () => {
|
|
421
|
+
let arr = new ReactiveArray(1, 2),
|
|
422
|
+
events: number[][] = [];
|
|
423
|
+
|
|
424
|
+
arr.on('concat', (e) => { events.push(e.items); });
|
|
425
|
+
arr.concat([3, 4], 5 as any, [6]);
|
|
426
|
+
|
|
427
|
+
expect([...arr]).toEqual([1, 2, 3, 4, 5, 6]);
|
|
428
|
+
expect(events).toEqual([[3, 4, 5, 6]]);
|
|
429
|
+
});
|
|
341
430
|
});
|
|
342
431
|
|
|
343
432
|
|
|
@@ -521,6 +610,50 @@ describe('ReactiveArray', () => {
|
|
|
521
610
|
|
|
522
611
|
expect(fn2).toHaveBeenCalledTimes(1);
|
|
523
612
|
});
|
|
613
|
+
|
|
614
|
+
it('multiple listeners removed via errors, new listeners fill holes in order', () => {
|
|
615
|
+
let arr = new ReactiveArray<number>(),
|
|
616
|
+
order: number[] = [];
|
|
617
|
+
|
|
618
|
+
let err1 = () => { throw new Error('err1'); };
|
|
619
|
+
let err2 = () => { throw new Error('err2'); };
|
|
620
|
+
let fn3 = vi.fn();
|
|
621
|
+
|
|
622
|
+
arr.on('push', err1);
|
|
623
|
+
arr.on('push', err2);
|
|
624
|
+
arr.on('push', fn3);
|
|
625
|
+
arr.push(1); // err1 and err2 throw, slots 0 and 1 nulled
|
|
626
|
+
|
|
627
|
+
expect(fn3).toHaveBeenCalledTimes(1);
|
|
628
|
+
|
|
629
|
+
let fn4 = vi.fn();
|
|
630
|
+
let fn5 = vi.fn();
|
|
631
|
+
|
|
632
|
+
arr.on('push', fn4); // fills hole at slot 0
|
|
633
|
+
arr.on('push', fn5); // fills hole at slot 1
|
|
634
|
+
arr.push(2);
|
|
635
|
+
|
|
636
|
+
expect(fn3).toHaveBeenCalledTimes(2);
|
|
637
|
+
expect(fn4).toHaveBeenCalledTimes(1);
|
|
638
|
+
expect(fn5).toHaveBeenCalledTimes(1);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('trailing null slots cleaned after dispatch', () => {
|
|
642
|
+
let arr = new ReactiveArray<number>();
|
|
643
|
+
|
|
644
|
+
let fn1 = vi.fn();
|
|
645
|
+
let err2 = () => { throw new Error('remove'); };
|
|
646
|
+
|
|
647
|
+
arr.on('push', fn1);
|
|
648
|
+
arr.on('push', err2);
|
|
649
|
+
|
|
650
|
+
// Before dispatch: listeners = [fn1, err2] (length 2)
|
|
651
|
+
arr.push(1); // err2 throws → nulled → trailing null cleaned
|
|
652
|
+
|
|
653
|
+
// Trailing null should be cleaned, so internal array length is 1
|
|
654
|
+
expect(arr.listeners['push']!.length).toBe(1);
|
|
655
|
+
expect(fn1).toHaveBeenCalledTimes(1);
|
|
656
|
+
});
|
|
524
657
|
});
|
|
525
658
|
|
|
526
659
|
|
|
@@ -654,4 +787,120 @@ describe('ReactiveArray', () => {
|
|
|
654
787
|
expect(lengths).toEqual([3, 4, 3, 2]);
|
|
655
788
|
});
|
|
656
789
|
});
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
describe('dispose with ReactiveObjects', () => {
|
|
793
|
+
it('dispose() calls dispose on each ReactiveObject element', () => {
|
|
794
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
795
|
+
b = new ReactiveObject({ y: 2 }),
|
|
796
|
+
c = new ReactiveObject({ z: 3 }),
|
|
797
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
798
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
799
|
+
spyC = vi.spyOn(c, 'dispose');
|
|
800
|
+
|
|
801
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c);
|
|
802
|
+
|
|
803
|
+
arr.dispose();
|
|
804
|
+
|
|
805
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
806
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
807
|
+
expect(spyC).toHaveBeenCalledTimes(1);
|
|
808
|
+
expect(arr.length).toBe(0);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('clear() calls dispose on each ReactiveObject element', () => {
|
|
812
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
813
|
+
b = new ReactiveObject({ y: 2 }),
|
|
814
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
815
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
816
|
+
|
|
817
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
818
|
+
|
|
819
|
+
arr.clear();
|
|
820
|
+
|
|
821
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
822
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
823
|
+
expect(arr.length).toBe(0);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('pop() calls dispose on removed ReactiveObject', () => {
|
|
827
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
828
|
+
b = new ReactiveObject({ y: 2 }),
|
|
829
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
830
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
831
|
+
|
|
832
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
833
|
+
|
|
834
|
+
arr.pop();
|
|
835
|
+
|
|
836
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
837
|
+
expect(spyA).not.toHaveBeenCalled();
|
|
838
|
+
expect(arr.length).toBe(1);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('shift() calls dispose on removed ReactiveObject', () => {
|
|
842
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
843
|
+
b = new ReactiveObject({ y: 2 }),
|
|
844
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
845
|
+
spyB = vi.spyOn(b, 'dispose');
|
|
846
|
+
|
|
847
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
848
|
+
|
|
849
|
+
arr.shift();
|
|
850
|
+
|
|
851
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
852
|
+
expect(spyB).not.toHaveBeenCalled();
|
|
853
|
+
expect(arr.length).toBe(1);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('splice() calls dispose on removed ReactiveObject elements', () => {
|
|
857
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
858
|
+
b = new ReactiveObject({ y: 2 }),
|
|
859
|
+
c = new ReactiveObject({ z: 3 }),
|
|
860
|
+
d = new ReactiveObject({ w: 4 }),
|
|
861
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
862
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
863
|
+
spyC = vi.spyOn(c, 'dispose'),
|
|
864
|
+
spyD = vi.spyOn(d, 'dispose');
|
|
865
|
+
|
|
866
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b, c, d);
|
|
867
|
+
|
|
868
|
+
arr.splice(1, 2);
|
|
869
|
+
|
|
870
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
871
|
+
expect(spyC).toHaveBeenCalledTimes(1);
|
|
872
|
+
expect(spyA).not.toHaveBeenCalled();
|
|
873
|
+
expect(spyD).not.toHaveBeenCalled();
|
|
874
|
+
expect(arr.length).toBe(2);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('splice() does not dispose inserted ReactiveObjects', () => {
|
|
878
|
+
let a = new ReactiveObject({ x: 1 }),
|
|
879
|
+
b = new ReactiveObject({ y: 2 }),
|
|
880
|
+
replacement = new ReactiveObject({ r: 99 }),
|
|
881
|
+
spyA = vi.spyOn(a, 'dispose'),
|
|
882
|
+
spyB = vi.spyOn(b, 'dispose'),
|
|
883
|
+
spyR = vi.spyOn(replacement, 'dispose');
|
|
884
|
+
|
|
885
|
+
let arr = new ReactiveArray<ReactiveObject<any>>(a, b);
|
|
886
|
+
|
|
887
|
+
arr.splice(0, 1, replacement);
|
|
888
|
+
|
|
889
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
890
|
+
expect(spyB).not.toHaveBeenCalled();
|
|
891
|
+
expect(spyR).not.toHaveBeenCalled();
|
|
892
|
+
expect(arr.length).toBe(2);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('does not dispose non-ReactiveObject elements', () => {
|
|
896
|
+
let obj = { dispose: vi.fn() };
|
|
897
|
+
|
|
898
|
+
let arr = new ReactiveArray<any>(1, 'str', obj);
|
|
899
|
+
|
|
900
|
+
arr.dispose();
|
|
901
|
+
|
|
902
|
+
expect(obj.dispose).not.toHaveBeenCalled();
|
|
903
|
+
expect(arr.length).toBe(0);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
657
906
|
});
|
|
@@ -0,0 +1,195 @@
|
|
|
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('rejected promise does not crash and retains previous value', async () => {
|
|
164
|
+
let node!: ReturnType<typeof asyncComputed<number>>,
|
|
165
|
+
s = signal(1);
|
|
166
|
+
|
|
167
|
+
root(() => {
|
|
168
|
+
node = asyncComputed(() => {
|
|
169
|
+
let v = read(s);
|
|
170
|
+
|
|
171
|
+
if (v === 2) {
|
|
172
|
+
return Promise.reject(new Error('fail'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Promise.resolve(v);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
180
|
+
|
|
181
|
+
expect(read(node)).toBe(1);
|
|
182
|
+
|
|
183
|
+
write(s, 2);
|
|
184
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
185
|
+
|
|
186
|
+
// Value should remain 1 after rejection
|
|
187
|
+
expect(read(node)).toBe(1);
|
|
188
|
+
|
|
189
|
+
write(s, 3);
|
|
190
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
191
|
+
|
|
192
|
+
// Resumes after non-rejected promise
|
|
193
|
+
expect(read(node)).toBe(3);
|
|
194
|
+
});
|
|
195
|
+
});
|
package/tests/bench/array.ts
CHANGED
|
@@ -149,6 +149,71 @@ describe('ReactiveArray events', () => {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
describe('ReactiveArray dispose/clear', () => {
|
|
153
|
+
bench('dispose with 100 items', () => {
|
|
154
|
+
let items = [];
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < 100; i++) {
|
|
157
|
+
items.push(i);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let arr = new ReactiveArray(...items);
|
|
161
|
+
|
|
162
|
+
arr.dispose();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
bench('clear with 100 items', () => {
|
|
166
|
+
let items = [];
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < 100; i++) {
|
|
169
|
+
items.push(i);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let arr = new ReactiveArray(...items);
|
|
173
|
+
|
|
174
|
+
arr.clear();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
describe('ReactiveArray concat/unshift/shift/reverse', () => {
|
|
180
|
+
bench('concat 100 items', () => {
|
|
181
|
+
let arr = new ReactiveArray<number>(),
|
|
182
|
+
items = [];
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < 100; i++) {
|
|
185
|
+
items.push(i);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
arr.concat(items);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
bench('unshift 10 items', () => {
|
|
192
|
+
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
193
|
+
|
|
194
|
+
arr.unshift(10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
bench('shift', () => {
|
|
198
|
+
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
199
|
+
|
|
200
|
+
arr.shift();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
bench('reverse 100 items', () => {
|
|
204
|
+
let items = [];
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < 100; i++) {
|
|
207
|
+
items.push(i);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let arr = new ReactiveArray(...items);
|
|
211
|
+
|
|
212
|
+
arr.reverse();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
|
|
152
217
|
describe('ReactiveArray reactive length', () => {
|
|
153
218
|
bench('read $length in effect', () => {
|
|
154
219
|
let arr = new ReactiveArray(1, 2, 3);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { computed, dispose, effect, read, root, signal, write } from '~/system';
|
|
3
|
+
import { ReactiveArray } from '~/reactive/array';
|
|
4
|
+
import { ReactiveObject } from '~/reactive/object';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe('ReactiveObject creation', () => {
|
|
8
|
+
bench('create with 5 signal properties', () => {
|
|
9
|
+
new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
bench('create with computed properties', () => {
|
|
13
|
+
new ReactiveObject({
|
|
14
|
+
a: 1,
|
|
15
|
+
b: 2,
|
|
16
|
+
sum: () => 0
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
describe('ReactiveObject read/write', () => {
|
|
23
|
+
bench('read property (signal-backed)', () => {
|
|
24
|
+
let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
|
|
25
|
+
|
|
26
|
+
(obj as any).a;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
bench('write property (signal-backed)', () => {
|
|
30
|
+
let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 }),
|
|
31
|
+
i = 0;
|
|
32
|
+
|
|
33
|
+
(obj as any).a = ++i;
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
describe('ReactiveObject dispose', () => {
|
|
39
|
+
bench('dispose with 5 properties', () => {
|
|
40
|
+
let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
|
|
41
|
+
|
|
42
|
+
obj.dispose();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
bench('dispose with arrays + computeds', () => {
|
|
46
|
+
let obj = new ReactiveObject({
|
|
47
|
+
items: [1, 2, 3],
|
|
48
|
+
name: 'test',
|
|
49
|
+
total: () => 0
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
obj.dispose();
|
|
53
|
+
});
|
|
54
|
+
});
|
package/tests/bench/system.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bench, describe } from 'vitest';
|
|
2
|
-
import { computed, dispose, effect, read, root, signal, write } from '~/system';
|
|
2
|
+
import { computed, dispose, effect, onCleanup, read, root, signal, write } from '~/system';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
describe('signal', () => {
|
|
@@ -228,3 +228,90 @@ describe('memory', () => {
|
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
describe('effect stress', () => {
|
|
234
|
+
bench('create + dispose 1000 effects (pool recycling)', () => {
|
|
235
|
+
for (let i = 0; i < 1000; i++) {
|
|
236
|
+
let stop = effect(() => {});
|
|
237
|
+
|
|
238
|
+
stop();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
describe('deep propagation', () => {
|
|
245
|
+
bench('deep chain (50 computeds)', () => {
|
|
246
|
+
let s = signal(0),
|
|
247
|
+
chain: ReturnType<typeof computed>[] = [],
|
|
248
|
+
i = 0;
|
|
249
|
+
|
|
250
|
+
chain[0] = computed(() => read(s) + 1);
|
|
251
|
+
|
|
252
|
+
for (let j = 1; j < 50; j++) {
|
|
253
|
+
let prev = chain[j - 1];
|
|
254
|
+
|
|
255
|
+
chain[j] = computed(() => read(prev) + 1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
effect(() => {
|
|
259
|
+
read(chain[49]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
write(s, ++i);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
describe('stabilization', () => {
|
|
268
|
+
bench('write during stabilization (reschedule path)', () => {
|
|
269
|
+
let a = signal(0),
|
|
270
|
+
b = signal(0),
|
|
271
|
+
i = 0;
|
|
272
|
+
|
|
273
|
+
effect(() => {
|
|
274
|
+
let val = read(a);
|
|
275
|
+
|
|
276
|
+
if (val > 0) {
|
|
277
|
+
write(b, val * 10);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
effect(() => {
|
|
282
|
+
read(b);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
write(a, ++i);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
describe('root', () => {
|
|
291
|
+
bench('root scope creation + disposal', () => {
|
|
292
|
+
root((dispose) => {
|
|
293
|
+
dispose();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
describe('onCleanup', () => {
|
|
300
|
+
bench('register 1 cleanup', () => {
|
|
301
|
+
root(() => {
|
|
302
|
+
effect(() => {
|
|
303
|
+
onCleanup(() => {});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
bench('register 10 cleanups', () => {
|
|
309
|
+
root(() => {
|
|
310
|
+
effect(() => {
|
|
311
|
+
for (let i = 0; i < 10; i++) {
|
|
312
|
+
onCleanup(() => {});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|