@esportsplus/reactivity 0.34.0 → 0.36.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.
Files changed (44) hide show
  1. package/README.md +9 -7
  2. package/bench/reactive/array.bench.ts +17 -17
  3. package/build/compiler/array.d.ts +2 -2
  4. package/build/compiler/array.js +10 -52
  5. package/build/compiler/constants.d.ts +4 -4
  6. package/build/compiler/constants.js +20 -4
  7. package/build/compiler/index.js +25 -38
  8. package/build/compiler/object.d.ts +2 -2
  9. package/build/compiler/object.js +5 -16
  10. package/build/compiler/primitives.d.ts +6 -2
  11. package/build/compiler/primitives.js +31 -38
  12. package/build/compiler/types.d.ts +3 -1
  13. package/build/constants.d.ts +2 -2
  14. package/build/constants.js +2 -2
  15. package/build/reactive/array.d.ts +1 -1
  16. package/build/reactive/array.js +26 -15
  17. package/build/reactive/index.js +5 -14
  18. package/build/reactive/object.js +1 -1
  19. package/build/system.d.ts +1 -4
  20. package/build/system.js +105 -83
  21. package/build/types.d.ts +2 -7
  22. package/package.json +5 -5
  23. package/pnpm-workspace.yaml +2 -1
  24. package/src/compiler/array.ts +12 -62
  25. package/src/compiler/constants.ts +21 -5
  26. package/src/compiler/index.ts +38 -69
  27. package/src/compiler/object.ts +7 -24
  28. package/src/compiler/primitives.ts +51 -49
  29. package/src/compiler/types.ts +4 -1
  30. package/src/constants.ts +4 -4
  31. package/src/reactive/array.ts +33 -17
  32. package/src/reactive/index.ts +5 -17
  33. package/src/reactive/object.ts +1 -1
  34. package/src/system.ts +138 -101
  35. package/src/types.ts +2 -9
  36. package/test/async-computed.test.ts +2 -2
  37. package/test/compiler/compiler.test.ts +38 -22
  38. package/test/effects.test.ts +37 -1
  39. package/test/lib/uncaught.ts +26 -0
  40. package/test/reactive/array.test.ts +169 -99
  41. package/test/reactive/nested.test.ts +14 -14
  42. package/test/reactive/objects.test.ts +1 -1
  43. package/test/reactive/reactive.test.ts +49 -0
  44. package/test/system.test.ts +130 -22
@@ -63,7 +63,7 @@ describe('nested reactive patterns', () => {
63
63
  let todo1 = new ReactiveObject({ done: false, text: 'Learn reactivity' }) as any,
64
64
  todo2 = new ReactiveObject({ done: true, text: 'Build app' }) as any,
65
65
  todo3 = new ReactiveObject({ done: false, text: 'Test everything' }) as any,
66
- todos = new ReactiveArray(todo1, todo2, todo3),
66
+ todos = new ReactiveArray([todo1, todo2, todo3]),
67
67
  completedCounts: number[] = [];
68
68
 
69
69
  effect(() => {
@@ -88,7 +88,7 @@ describe('nested reactive patterns', () => {
88
88
 
89
89
  it('push new reactive object to array', () => {
90
90
  let todo1 = new ReactiveObject({ done: false, text: 'First' }) as any,
91
- todos = new ReactiveArray(todo1);
91
+ todos = new ReactiveArray([todo1]);
92
92
 
93
93
  expect(todos.length).toBe(1);
94
94
 
@@ -104,10 +104,10 @@ describe('nested reactive patterns', () => {
104
104
 
105
105
  describe('matrix pattern', () => {
106
106
  it('array of arrays', () => {
107
- let row1 = new ReactiveArray(1, 2, 3),
108
- row2 = new ReactiveArray(4, 5, 6),
109
- row3 = new ReactiveArray(7, 8, 9),
110
- matrix = new ReactiveArray(row1, row2, row3);
107
+ let row1 = new ReactiveArray([1, 2, 3]),
108
+ row2 = new ReactiveArray([4, 5, 6]),
109
+ row3 = new ReactiveArray([7, 8, 9]),
110
+ matrix = new ReactiveArray([row1, row2, row3]);
111
111
 
112
112
  expect(matrix.length).toBe(3);
113
113
  expect([...matrix[0]]).toEqual([1, 2, 3]);
@@ -116,9 +116,9 @@ describe('nested reactive patterns', () => {
116
116
  });
117
117
 
118
118
  it('modifying inner array', () => {
119
- let row1 = new ReactiveArray(1, 2, 3),
120
- row2 = new ReactiveArray(4, 5, 6),
121
- matrix = new ReactiveArray(row1, row2);
119
+ let row1 = new ReactiveArray([1, 2, 3]),
120
+ row2 = new ReactiveArray([4, 5, 6]),
121
+ matrix = new ReactiveArray([row1, row2]);
122
122
 
123
123
  row1.$set(0, 100);
124
124
  row2.push(60);
@@ -128,10 +128,10 @@ describe('nested reactive patterns', () => {
128
128
  });
129
129
 
130
130
  it('push new row to matrix', () => {
131
- let row1 = new ReactiveArray(1, 2, 3),
132
- matrix = new ReactiveArray(row1);
131
+ let row1 = new ReactiveArray([1, 2, 3]),
132
+ matrix = new ReactiveArray([row1]);
133
133
 
134
- let row2 = new ReactiveArray(4, 5, 6);
134
+ let row2 = new ReactiveArray([4, 5, 6]);
135
135
 
136
136
  matrix.push(row2);
137
137
 
@@ -143,7 +143,7 @@ describe('nested reactive patterns', () => {
143
143
 
144
144
  describe('cross-object array + multiplier', () => {
145
145
  it('computed sum reacts to multiplier changes', async () => {
146
- let items = new ReactiveArray(10, 20, 30),
146
+ let items = new ReactiveArray([10, 20, 30]),
147
147
  multiplier = signal(2),
148
148
  sum = computed(() => {
149
149
  let total = 0;
@@ -171,7 +171,7 @@ describe('nested reactive patterns', () => {
171
171
  });
172
172
 
173
173
  it('computed sum reacts to reactive length changes', async () => {
174
- let items = new ReactiveArray(10, 20, 30),
174
+ let items = new ReactiveArray([10, 20, 30]),
175
175
  multiplier = signal(2),
176
176
  sum = computed(() => {
177
177
  let length = items.$length,
@@ -126,7 +126,7 @@ describe('reactive object patterns', () => {
126
126
  });
127
127
 
128
128
  it('array computed using reduce via external signal', async () => {
129
- let items = new ReactiveArray(1, 2, 3),
129
+ let items = new ReactiveArray([1, 2, 3]),
130
130
  total = computed(() => {
131
131
  let sum = 0;
132
132
 
@@ -391,6 +391,55 @@ describe('reactive()', () => {
391
391
  expect(() => reactive('hello' as any)).toThrow();
392
392
  });
393
393
  });
394
+
395
+
396
+ describe('auto-dispose', () => {
397
+ it('disposes an object with a computed property when its owning effect stops', async () => {
398
+ let s = signal(1),
399
+ runs = 0,
400
+ stop = effect(() => {
401
+ reactive({
402
+ doubled: () => {
403
+ runs++;
404
+ return read(s) * 2;
405
+ }
406
+ });
407
+ });
408
+
409
+ expect(runs).toBe(1);
410
+
411
+ stop();
412
+
413
+ write(s, 2);
414
+ await Promise.resolve();
415
+
416
+ expect(runs).toBe(1);
417
+ });
418
+
419
+ it('disposes nested objects of an array when its owning effect stops', () => {
420
+ let inner = new ReactiveObject({ x: 1 }),
421
+ spy = vi.spyOn(inner, 'dispose'),
422
+ stop = effect(() => {
423
+ reactive([inner]);
424
+ });
425
+
426
+ stop();
427
+
428
+ expect(spy).toHaveBeenCalledTimes(1);
429
+ });
430
+
431
+ it('top-level reactive() is not auto-disposed', async () => {
432
+ let s = signal(1),
433
+ obj = reactive({ doubled: () => read(s) * 2 }) as any;
434
+
435
+ expect(obj.doubled).toBe(2);
436
+
437
+ write(s, 3);
438
+ await Promise.resolve();
439
+
440
+ expect(obj.doubled).toBe(6);
441
+ });
442
+ });
394
443
  });
395
444
 
396
445
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { computed, dispose, effect, isComputed, isSignal, onCleanup, read, root, signal, write } from '~/system';
2
+ import { computed, dispose, effect, flush, isComputed, isSignal, onCleanup, peek, read, root, signal, write } from '~/system';
3
3
  import { waitFor } from './lib/wait-for';
4
4
 
5
5
 
@@ -576,32 +576,32 @@ describe('root', () => {
576
576
  expect(inner).toBe(1);
577
577
  });
578
578
 
579
- it('tracks disposables counter for unowned computeds', () => {
580
- let before = root.disposables;
579
+ it('restores scope when an inner root throws', () => {
580
+ let spy = vi.fn(),
581
+ stop: VoidFunction | undefined;
581
582
 
582
- root(() => {
583
- computed(() => 1);
584
- computed(() => 2);
585
- computed(() => 3);
586
- });
583
+ root((dispose) => {
584
+ stop = dispose;
587
585
 
588
- // root restores disposables to outer value after execution
589
- expect(root.disposables).toBe(before);
586
+ try {
587
+ root((inner) => {
588
+ inner;
589
+ throw new Error('reactivity: inner boom');
590
+ });
591
+ }
592
+ catch {
593
+ }
590
594
 
591
- // Nested: inner root creates computeds, outer root creates computeds
592
- root(() => {
593
- computed(() => 10);
595
+ onCleanup(spy);
596
+ });
594
597
 
595
- root(() => {
596
- computed(() => 20);
597
- computed(() => 30);
598
- });
598
+ expect(spy).not.toHaveBeenCalled();
599
599
 
600
- computed(() => 40);
601
- });
600
+ stop!();
602
601
 
603
- expect(root.disposables).toBe(before);
602
+ expect(spy).toHaveBeenCalledTimes(1);
604
603
  });
604
+
605
605
  });
606
606
 
607
607
 
@@ -686,10 +686,19 @@ describe('computed object size', () => {
686
686
  expect('type' in c).toBe(false);
687
687
  });
688
688
 
689
- it('has no more own properties than 16 (14 slimmed + gv + rv)', () => {
689
+ it('has no more own properties than 17 (14 slimmed + gv + rv + pending)', () => {
690
690
  let c = computed(() => 42);
691
691
 
692
- expect(Object.keys(c).length).toBeLessThanOrEqual(16);
692
+ expect(Object.keys(c).length).toBeLessThanOrEqual(17);
693
+ });
694
+
695
+ it('shares one shape between sync and async computeds', () => {
696
+ let a = computed(() => 42),
697
+ b = computed(() => Promise.resolve(42));
698
+
699
+ expect(Object.keys(a)).toEqual(Object.keys(b));
700
+ expect(a.pending).toBeNull();
701
+ expect(isSignal(b.pending)).toBe(true);
693
702
  });
694
703
  });
695
704
 
@@ -793,6 +802,105 @@ describe('globalVersion fast path', () => {
793
802
  });
794
803
 
795
804
 
805
+ describe('stale reads after a pass', () => {
806
+ it('an observer created between a write and its flush never sees the stale value', async () => {
807
+ let s = signal(0),
808
+ c = computed(() => read(s)),
809
+ seen: number[] = [];
810
+
811
+ write(s, 1);
812
+ flush();
813
+
814
+ write(s, 2);
815
+
816
+ effect(() => {
817
+ seen.push(read(c));
818
+ });
819
+
820
+ await Promise.resolve();
821
+ await Promise.resolve();
822
+
823
+ expect(seen[0]).toBe(2);
824
+ expect(seen).not.toContain(1);
825
+ expect(read(c)).toBe(2);
826
+ });
827
+
828
+ it('a computed created between a write and its flush computes from the new value', async () => {
829
+ let s = signal(0),
830
+ seen: number[] = [];
831
+
832
+ write(s, 1);
833
+ flush();
834
+
835
+ write(s, 2);
836
+
837
+ effect(() => {
838
+ let c = computed(() => read(s) * 10);
839
+
840
+ seen.push(read(c));
841
+ });
842
+
843
+ await Promise.resolve();
844
+ await Promise.resolve();
845
+
846
+ expect(seen[0]).toBe(20);
847
+ expect(seen).not.toContain(10);
848
+ });
849
+ });
850
+
851
+
852
+ describe('same-height stabilization', () => {
853
+ it('settles a sibling pulled from the active heap bucket', () => {
854
+ let s = signal(0),
855
+ flag = signal(false),
856
+ M = computed(() => 0),
857
+ N = computed(() => read(flag) ? (read(M), read(s), 0) : 0),
858
+ R = computed(() => read(s) + read(N));
859
+
860
+ effect(() => { read(R); });
861
+ effect(() => { read(N); });
862
+ effect(() => { read(M); });
863
+
864
+ write(flag, true);
865
+ flush();
866
+ write(s, 1);
867
+
868
+ expect(() => {
869
+ peek(M);
870
+ flush();
871
+ }).not.toThrow();
872
+ expect(read(R)).toBe(1);
873
+ });
874
+
875
+ it('observes one settled value per write when a sibling grows', () => {
876
+ let s = signal(0),
877
+ flag = signal(false),
878
+ M = computed(() => 0),
879
+ N = computed(() => read(flag) ? read(M) + read(s) : 0),
880
+ values: number[] = [],
881
+ R = computed(() => {
882
+ let value = read(s) + read(N);
883
+
884
+ values.push(value);
885
+
886
+ return value;
887
+ });
888
+
889
+ effect(() => { read(R); });
890
+ effect(() => { read(N); });
891
+ effect(() => { read(M); });
892
+
893
+ write(flag, true);
894
+ flush();
895
+ values.length = 0;
896
+ write(s, 1);
897
+ flush();
898
+
899
+ expect(values).toEqual([2]);
900
+ });
901
+ });
902
+
903
+
796
904
  describe('edge cases', () => {
797
905
  it('diamond graph dedup — notify state mask prevents redundant recomputation', async () => {
798
906
  let s = signal(1),