@esportsplus/reactivity 0.31.3 → 0.33.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 (67) hide show
  1. package/README.md +43 -6
  2. package/bench/cellx.bench.ts +61 -0
  3. package/bench/dynamic.bench.ts +103 -0
  4. package/bench/kairo.bench.ts +368 -0
  5. package/bench/lib/reactive-adapter.ts +51 -0
  6. package/bench/molbench.bench.ts +75 -0
  7. package/{tests/bench/system.ts → bench/system.bench.ts} +3 -3
  8. package/build/compiler/array.d.ts +1 -1
  9. package/build/compiler/constants.d.ts +7 -6
  10. package/build/compiler/constants.js +6 -7
  11. package/build/compiler/object.d.ts +1 -1
  12. package/build/compiler/object.js +1 -2
  13. package/build/compiler/primitives.d.ts +1 -1
  14. package/build/constants.d.ts +3 -1
  15. package/build/constants.js +3 -1
  16. package/build/reactive/array.d.ts +7 -3
  17. package/build/reactive/array.js +32 -24
  18. package/build/reactive/index.d.ts +2 -2
  19. package/build/reactive/index.js +1 -1
  20. package/build/reactive/object.d.ts +4 -4
  21. package/build/reactive/object.js +8 -23
  22. package/build/system.d.ts +15 -6
  23. package/build/system.js +432 -69
  24. package/build/types.d.ts +22 -4
  25. package/package.json +10 -7
  26. package/src/compiler/array.ts +1 -1
  27. package/src/compiler/constants.ts +8 -6
  28. package/src/compiler/index.ts +2 -1
  29. package/src/compiler/object.ts +2 -2
  30. package/src/compiler/plugins/vite.ts +1 -0
  31. package/src/compiler/primitives.ts +1 -1
  32. package/src/constants.ts +6 -2
  33. package/src/reactive/array.ts +50 -35
  34. package/src/reactive/index.ts +6 -6
  35. package/src/reactive/object.ts +16 -41
  36. package/src/system.ts +635 -80
  37. package/src/types.ts +38 -9
  38. package/test/async-computed.test.ts +409 -0
  39. package/test/async-errors.test.ts +146 -0
  40. package/test/async-hardening.test.ts +73 -0
  41. package/test/async-iterable.test.ts +221 -0
  42. package/test/async-nested.test.ts +19 -0
  43. package/test/deep-graphs.test.ts +215 -0
  44. package/{tests/effects.ts → test/effects.test.ts} +122 -0
  45. package/test/equals.test.ts +142 -0
  46. package/test/errors.test.ts +201 -0
  47. package/test/flush.test.ts +185 -0
  48. package/test/glitch-freedom.test.ts +115 -0
  49. package/test/invalidate.test.ts +147 -0
  50. package/test/lib/wait-for.ts +28 -0
  51. package/test/pending-writes.test.ts +139 -0
  52. package/{tests/reactive.ts → test/reactive/reactive.test.ts} +20 -19
  53. package/test/read-dedup.test.ts +120 -0
  54. package/test/signal-selector.test.ts +187 -0
  55. package/{tests/system.ts → test/system.test.ts} +210 -19
  56. package/test/tsconfig.json +16 -0
  57. package/test/untrack.test.ts +146 -0
  58. package/vitest.config.ts +2 -2
  59. package/tests/async-computed.ts +0 -239
  60. /package/{tests/bench/array.ts → bench/reactive/array.bench.ts} +0 -0
  61. /package/{tests/bench/reactive-object.ts → bench/reactive/reactive-object.bench.ts} +0 -0
  62. /package/{tests → bench}/tsconfig.json +0 -0
  63. /package/{tests/compiler.ts → test/compiler/compiler.test.ts} +0 -0
  64. /package/{tests/primitives.ts → test/primitives.test.ts} +0 -0
  65. /package/{tests/array.ts → test/reactive/array.test.ts} +0 -0
  66. /package/{tests/nested.ts → test/reactive/nested.test.ts} +0 -0
  67. /package/{tests/objects.ts → test/reactive/objects.test.ts} +0 -0
@@ -0,0 +1,187 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { computed, effect, onCleanup, read, signal, write } from '~/system';
3
+
4
+ // signal.selector(node, key) rides a Map for entry lookup — SameValueZero semantics: NaN keys match
5
+ // themselves and ±0 collapse, slightly wider than the settled === for those two cases. Object keys
6
+ // compare by reference and MUST be stable (a re-allocated key object never matches). The initial
7
+ // node.value === key snapshot uses ===.
8
+
9
+ const settle = () => Promise.resolve().then(() => Promise.resolve());
10
+
11
+
12
+ describe('signal.selector per-key selector', () => {
13
+ it('re-runs on transitions into/out of the key, not on writes between other keys', async () => {
14
+ let runs = 0,
15
+ s = signal('a');
16
+
17
+ effect(() => {
18
+ runs++;
19
+ signal.selector(s, 'b');
20
+ });
21
+
22
+ runs = 0;
23
+
24
+ write(s, 'c');
25
+ await settle();
26
+ expect(runs).toBe(0);
27
+
28
+ write(s, 'b');
29
+ await settle();
30
+ expect(runs).toBe(1);
31
+
32
+ write(s, 'd');
33
+ await settle();
34
+ expect(runs).toBe(2);
35
+
36
+ write(s, 'e');
37
+ await settle();
38
+ expect(runs).toBe(2);
39
+ });
40
+
41
+ it('returns the correct boolean at first call, tracked and untracked', () => {
42
+ let s = signal('x'),
43
+ tracked: boolean | null = null;
44
+
45
+ expect(signal.selector(s, 'x')).toBe(true);
46
+ expect(signal.selector(s, 'y')).toBe(false);
47
+
48
+ effect(() => {
49
+ tracked = signal.selector(s, 'x');
50
+ });
51
+
52
+ expect(tracked).toBe(true);
53
+ });
54
+
55
+ it('is lazy: keys stays null until a tracked call, one entry per distinct key', () => {
56
+ let s = signal(0);
57
+
58
+ expect(s.keys).toBe(null);
59
+
60
+ signal.selector(s, 1);
61
+ expect(s.keys).toBe(null);
62
+
63
+ effect(() => {
64
+ signal.selector(s, 1);
65
+ signal.selector(s, 2);
66
+ signal.selector(s, 1);
67
+ });
68
+
69
+ expect(s.keys).not.toBe(null);
70
+ expect(s.keys!.size).toBe(2);
71
+ });
72
+
73
+ it('evicts entries on last unsubscribe and nulls keys when the map empties', () => {
74
+ let s = signal(0),
75
+ stop1 = effect(() => {
76
+ signal.selector(s, 1);
77
+ }),
78
+ stop2 = effect(() => {
79
+ signal.selector(s, 2);
80
+ });
81
+
82
+ expect(s.keys!.size).toBe(2);
83
+
84
+ stop1();
85
+ expect(s.keys!.size).toBe(1);
86
+
87
+ stop2();
88
+ expect(s.keys).toBe(null);
89
+ });
90
+
91
+ it('leaves the computed last-unsub auto-dispose behavior unchanged', () => {
92
+ let cleaned = 0,
93
+ s = signal(0);
94
+
95
+ let stop = effect(() => {
96
+ let c = computed(() => {
97
+ onCleanup(() => {
98
+ cleaned++;
99
+ });
100
+
101
+ return read(s);
102
+ });
103
+
104
+ read(c);
105
+ });
106
+
107
+ expect(cleaned).toBe(0);
108
+
109
+ // Disposing the effect unlinks c's last subscriber → c still auto-disposes and its cleanup fires.
110
+ stop();
111
+ expect(cleaned).toBe(1);
112
+ });
113
+
114
+ it('fans out keys independently; a write touches only the prev and next entries', async () => {
115
+ let s = signal('a'),
116
+ runs1 = 0,
117
+ runs2 = 0,
118
+ runs3 = 0;
119
+
120
+ effect(() => {
121
+ runs1++;
122
+ signal.selector(s, 'a');
123
+ });
124
+ effect(() => {
125
+ runs2++;
126
+ signal.selector(s, 'b');
127
+ });
128
+ effect(() => {
129
+ runs3++;
130
+ signal.selector(s, 'c');
131
+ });
132
+
133
+ runs1 = runs2 = runs3 = 0;
134
+
135
+ write(s, 'b');
136
+ await settle();
137
+
138
+ expect(runs1).toBe(1);
139
+ expect(runs2).toBe(1);
140
+ expect(runs3).toBe(0);
141
+ });
142
+
143
+ it('NaN key: the Map reuses one stable entry and transitions fan out (SameValueZero caveat)', async () => {
144
+ let observed: boolean[] = [],
145
+ runs = 0,
146
+ s = signal<number>(0);
147
+
148
+ effect(() => {
149
+ runs++;
150
+ observed.push(signal.selector(s, NaN));
151
+ });
152
+
153
+ runs = 0;
154
+ observed.length = 0;
155
+
156
+ expect(s.keys!.size).toBe(1);
157
+
158
+ write(s, NaN);
159
+ await settle();
160
+
161
+ expect(runs).toBe(1);
162
+ expect(observed[observed.length - 1]).toBe(true);
163
+ expect(s.keys!.size).toBe(1);
164
+ });
165
+
166
+ it('object key compares by reference: a stable key matches, a re-allocated one does not', async () => {
167
+ let k = { id: 1 },
168
+ observed: boolean[] = [],
169
+ s = signal<object>(k);
170
+
171
+ effect(() => {
172
+ observed.push(signal.selector(s, k));
173
+ });
174
+
175
+ expect(observed[0]).toBe(true);
176
+
177
+ observed.length = 0;
178
+
179
+ write(s, { id: 1 });
180
+ await settle();
181
+ expect(observed[observed.length - 1]).toBe(false);
182
+
183
+ write(s, k);
184
+ await settle();
185
+ expect(observed[observed.length - 1]).toBe(true);
186
+ });
187
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { computed, dispose, effect, isComputed, isSignal, onCleanup, read, root, signal, write } from '~/system';
3
+ import { waitFor } from './lib/wait-for';
3
4
 
4
5
 
5
6
  describe('signal', () => {
@@ -424,7 +425,7 @@ describe('onCleanup', () => {
424
425
  expect(fn).not.toHaveBeenCalled();
425
426
  });
426
427
 
427
- it('throwing cleanup skips subsequent cleanups in same array', () => {
428
+ it('throwing cleanup still runs subsequent cleanups and rethrows to the dispose caller', () => {
428
429
  let called: number[] = [],
429
430
  c = computed((onCleanup) => {
430
431
  onCleanup(() => { called.push(1); });
@@ -435,14 +436,89 @@ describe('onCleanup', () => {
435
436
 
436
437
  expect(called).toEqual([]);
437
438
 
438
- // dispose() calls cleanup() synchronously — no try/catch wraps it
439
- // cleanup iterates the array: index 0 (push 1), index 1 (push 2, throws)
440
- // index 2 (push 3) is never reached because no try/catch in cleanup()
441
439
  expect(() => dispose(c)).toThrow('cleanup boom');
442
440
 
443
- expect(called).toContain(1);
444
- expect(called).toContain(2);
445
- expect(called).not.toContain(3);
441
+ // Every disposer ran despite the throw
442
+ expect(called).toEqual([1, 2, 3]);
443
+ expect(c.cleanup).toBe(null);
444
+ });
445
+
446
+ it('single throwing cleanup rethrows the exact error', () => {
447
+ let error = new Error('solo boom'),
448
+ c = computed((onCleanup) => {
449
+ onCleanup(() => { throw error; });
450
+ return 1;
451
+ });
452
+
453
+ try {
454
+ dispose(c);
455
+ expect.unreachable();
456
+ }
457
+ catch (e) {
458
+ expect(e).toBe(error);
459
+ }
460
+ });
461
+
462
+ it('multiple throwing cleanups aggregate into AggregateError with all captured', () => {
463
+ let called: number[] = [],
464
+ c = computed((onCleanup) => {
465
+ onCleanup(() => { called.push(1); throw new Error('first'); });
466
+ onCleanup(() => { called.push(2); throw new Error('second'); });
467
+ onCleanup(() => { called.push(3); });
468
+ return 1;
469
+ });
470
+
471
+ try {
472
+ dispose(c);
473
+ expect.unreachable();
474
+ }
475
+ catch (e) {
476
+ expect(e).toBeInstanceOf(AggregateError);
477
+ expect((e as AggregateError).errors.map((x) => (x as Error).message)).toEqual(['first', 'second']);
478
+ }
479
+
480
+ expect(called).toEqual([1, 2, 3]);
481
+ expect(c.cleanup).toBe(null);
482
+ });
483
+
484
+ it('throwing cleanup during recompute does not abort the stabilize pass', async () => {
485
+ let s = signal(0),
486
+ captured: unknown[] = [],
487
+ listeners = process.listeners('uncaughtException'),
488
+ otherValues: number[] = [],
489
+ values: number[] = [];
490
+
491
+ effect(() => {
492
+ values.push(read(s));
493
+ onCleanup(() => { throw new Error('teardown boom'); });
494
+ });
495
+
496
+ effect(() => {
497
+ otherValues.push(read(s));
498
+ });
499
+
500
+ process.removeAllListeners('uncaughtException');
501
+ process.on('uncaughtException', (e) => { captured.push(e); });
502
+
503
+ try {
504
+ write(s, 1);
505
+ await waitFor(() => captured.length === 1, 'teardown error surfaces as uncaught');
506
+ }
507
+ finally {
508
+ process.removeAllListeners('uncaughtException');
509
+
510
+ for (let i = 0, n = listeners.length; i < n; i++) {
511
+ process.on('uncaughtException', listeners[i]);
512
+ }
513
+ }
514
+
515
+ // Both effects re-ran — the pass survived the teardown failure
516
+ expect(values).toEqual([0, 1]);
517
+ expect(otherValues).toEqual([0, 1]);
518
+
519
+ // And the teardown error surfaced as an uncaught microtask exception
520
+ expect(captured.length).toBe(1);
521
+ expect((captured[0] as Error).message).toBe('teardown boom');
446
522
  });
447
523
  });
448
524
 
@@ -610,10 +686,109 @@ describe('computed object size', () => {
610
686
  expect('type' in c).toBe(false);
611
687
  });
612
688
 
613
- it('has fewer own properties than 14 (old size)', () => {
689
+ it('has no more own properties than 16 (14 slimmed + gv + rv)', () => {
614
690
  let c = computed(() => 42);
615
691
 
616
- expect(Object.keys(c).length).toBeLessThan(14);
692
+ expect(Object.keys(c).length).toBeLessThanOrEqual(16);
693
+ });
694
+ });
695
+
696
+ describe('globalVersion fast path', () => {
697
+ it('a computed pulled by two tracked readers in one settle runs its fn at most once with correct values', async () => {
698
+ let cRuns = 0,
699
+ s = signal(0),
700
+ seen1: number[] = [],
701
+ seen2: number[] = [];
702
+
703
+ let c = computed(() => {
704
+ cRuns++;
705
+
706
+ return read(s) + 1;
707
+ });
708
+
709
+ effect(() => {
710
+ if (read(s) > 0) {
711
+ seen1.push(read(c));
712
+ }
713
+ });
714
+
715
+ effect(() => {
716
+ if (read(s) > 0) {
717
+ seen2.push(read(c));
718
+ }
719
+ });
720
+
721
+ cRuns = 0;
722
+
723
+ write(s, 1);
724
+ await Promise.resolve();
725
+ await Promise.resolve();
726
+
727
+ expect(cRuns).toBe(1);
728
+ expect(seen1[seen1.length - 1]).toBe(2);
729
+ expect(seen2[seen2.length - 1]).toBe(2);
730
+ });
731
+
732
+ it('write during recompute leaves the racing node validating normally (no stale skip)', async () => {
733
+ let s = signal(0),
734
+ t = signal(0);
735
+
736
+ let c = computed(() => {
737
+ let v = read(s);
738
+
739
+ if (v > 0) {
740
+ write(t, v * 10);
741
+ }
742
+
743
+ return v;
744
+ });
745
+
746
+ let d = computed(() => read(t) + read(c));
747
+
748
+ effect(() => {
749
+ read(d);
750
+ });
751
+
752
+ write(s, 2);
753
+ await Promise.resolve();
754
+ await Promise.resolve();
755
+ await Promise.resolve();
756
+
757
+ expect(read(t)).toBe(20);
758
+ expect(read(c)).toBe(2);
759
+ expect(read(d)).toBe(22);
760
+ });
761
+
762
+ it('a fresh signal write invalidates the fast path (no stale skip across a settle boundary)', async () => {
763
+ let cRuns = 0,
764
+ s = signal(1),
765
+ seen: number[] = [];
766
+
767
+ let c = computed(() => {
768
+ cRuns++;
769
+
770
+ return read(s) * 2;
771
+ });
772
+
773
+ effect(() => {
774
+ seen.push(read(c));
775
+ });
776
+
777
+ expect(seen[seen.length - 1]).toBe(2);
778
+
779
+ write(s, 5);
780
+ await Promise.resolve();
781
+ await Promise.resolve();
782
+
783
+ expect(read(c)).toBe(10);
784
+ expect(seen[seen.length - 1]).toBe(10);
785
+
786
+ write(s, 7);
787
+ await Promise.resolve();
788
+ await Promise.resolve();
789
+
790
+ expect(read(c)).toBe(14);
791
+ expect(seen[seen.length - 1]).toBe(14);
617
792
  });
618
793
  });
619
794
 
@@ -842,8 +1017,9 @@ describe('edge cases', () => {
842
1017
  expect(cValues).toEqual([0, 30]);
843
1018
  });
844
1019
 
845
- it('computed that throws on update retains previous value', async () => {
1020
+ it('computed that throws on update caches the error and rethrows at read', async () => {
846
1021
  let s = signal(0),
1022
+ effectErrors: unknown[] = [],
847
1023
  effectValues: number[] = [],
848
1024
  c = computed(() => {
849
1025
  let val = read(s);
@@ -856,7 +1032,12 @@ describe('edge cases', () => {
856
1032
  });
857
1033
 
858
1034
  effect(() => {
859
- effectValues.push(read(c));
1035
+ try {
1036
+ effectValues.push(read(c));
1037
+ }
1038
+ catch (e) {
1039
+ effectErrors.push(e);
1040
+ }
860
1041
  });
861
1042
 
862
1043
  expect(effectValues).toEqual([0]);
@@ -870,13 +1051,16 @@ describe('edge cases', () => {
870
1051
  write(s, 2);
871
1052
  await Promise.resolve();
872
1053
 
873
- // Value should remain 10 since throw prevented update
874
- expect(read(c)).toBe(10);
1054
+ // Error propagates to the subscribed effect and rethrows at read
1055
+ expect(() => read(c)).toThrow('boom');
1056
+ expect(effectErrors.length).toBe(1);
1057
+ expect((effectErrors[0] as Error).message).toBe('boom');
875
1058
  expect(effectValues).toEqual([0, 10]);
876
1059
  });
877
1060
 
878
1061
  it('computed alternates between throwing and succeeding', async () => {
879
1062
  let s = signal(0),
1063
+ effectErrors: unknown[] = [],
880
1064
  effectValues: number[] = [],
881
1065
  c = computed(() => {
882
1066
  let val = read(s);
@@ -889,7 +1073,12 @@ describe('edge cases', () => {
889
1073
  });
890
1074
 
891
1075
  effect(() => {
892
- effectValues.push(read(c));
1076
+ try {
1077
+ effectValues.push(read(c));
1078
+ }
1079
+ catch (e) {
1080
+ effectErrors.push(e);
1081
+ }
893
1082
  });
894
1083
 
895
1084
  expect(effectValues).toEqual([0]);
@@ -897,22 +1086,24 @@ describe('edge cases', () => {
897
1086
  write(s, 1);
898
1087
  await Promise.resolve();
899
1088
 
900
- // Threw on odd, value stays 0
901
- expect(read(c)).toBe(0);
1089
+ // Threw on odd, error cached and rethrown at read
1090
+ expect(() => read(c)).toThrow('odd');
1091
+ expect(effectErrors.length).toBe(1);
902
1092
  expect(effectValues).toEqual([0]);
903
1093
 
904
1094
  write(s, 2);
905
1095
  await Promise.resolve();
906
1096
 
907
- // Succeeds on even, value updates
1097
+ // Succeeds on even, error cleared, value updates
908
1098
  expect(read(c)).toBe(2);
909
1099
  expect(effectValues).toEqual([0, 2]);
910
1100
 
911
1101
  write(s, 3);
912
1102
  await Promise.resolve();
913
1103
 
914
- // Threw on odd again, value stays 2
915
- expect(read(c)).toBe(2);
1104
+ // Threw on odd again the error caches on the Nth failure too
1105
+ expect(() => read(c)).toThrow('odd');
1106
+ expect(effectErrors.length).toBe(2);
916
1107
  expect(effectValues).toEqual([0, 2]);
917
1108
 
918
1109
  write(s, 4);
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "paths": {
6
+ "~/*": [
7
+ "../src/*"
8
+ ]
9
+ },
10
+ "rootDir": ".."
11
+ },
12
+ "include": [
13
+ "./**/*",
14
+ "../src/**/*"
15
+ ]
16
+ }
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { computed, effect, peek, read, signal, untrack, write } from '~/system';
3
+
4
+
5
+ describe('untrack', () => {
6
+ it('does not re-run an effect for a signal read only inside untrack, and sees current values', async () => {
7
+ let a = signal(1),
8
+ b = signal(10),
9
+ calls = 0,
10
+ seenB: number[] = [];
11
+
12
+ effect(() => {
13
+ read(a);
14
+ seenB.push(untrack(() => read(b)));
15
+ calls++;
16
+ });
17
+
18
+ expect(calls).toBe(1);
19
+ expect(seenB).toEqual([10]);
20
+
21
+ write(b, 20);
22
+ await Promise.resolve();
23
+
24
+ // b changed but was only ever read inside untrack — the effect must not re-run
25
+ expect(calls).toBe(1);
26
+
27
+ write(a, 2);
28
+ await Promise.resolve();
29
+
30
+ // a re-runs the effect; the untracked read of b picks up its current value
31
+ expect(calls).toBe(2);
32
+ expect(seenB).toEqual([10, 20]);
33
+ });
34
+
35
+ it('restores the observer when fn throws, so a later read in the same effect still tracks', async () => {
36
+ let a = signal(1),
37
+ b = signal(1),
38
+ calls = 0;
39
+
40
+ effect(() => {
41
+ calls++;
42
+
43
+ try {
44
+ untrack(() => {
45
+ throw new Error('untrack boom');
46
+ });
47
+ }
48
+ catch {
49
+ // swallow — only the observer-restoration-after-throw behavior is under test
50
+ }
51
+
52
+ read(a);
53
+ read(b);
54
+ });
55
+
56
+ expect(calls).toBe(1);
57
+
58
+ write(a, 2);
59
+ await Promise.resolve();
60
+
61
+ expect(calls).toBe(2);
62
+
63
+ write(b, 2);
64
+ await Promise.resolve();
65
+
66
+ expect(calls).toBe(3);
67
+ });
68
+
69
+ it('returns the value fn produces', () => {
70
+ expect(untrack(() => 42)).toBe(42);
71
+ });
72
+ });
73
+
74
+
75
+ describe('peek', () => {
76
+ it('returns the current value of a signal', () => {
77
+ let s = signal(5);
78
+
79
+ expect(peek(s)).toBe(5);
80
+
81
+ write(s, 6);
82
+
83
+ expect(peek(s)).toBe(6);
84
+ });
85
+
86
+ it('returns the up-to-date value of a dirty computed without subscribing', async () => {
87
+ let s = signal(1),
88
+ c = computed(() => read(s) * 2),
89
+ writerRuns = 0;
90
+
91
+ effect(() => {
92
+ read(c);
93
+ });
94
+
95
+ effect(() => {
96
+ read(s);
97
+ writerRuns++;
98
+ });
99
+
100
+ expect(writerRuns).toBe(1);
101
+
102
+ write(s, 5);
103
+
104
+ // No await — c is queued/dirty, stabilize() has not run yet
105
+ expect(peek(c)).toBe(10);
106
+
107
+ // peek must not create a subscription: the writer effect's run count is untouched
108
+ expect(writerRuns).toBe(1);
109
+
110
+ await Promise.resolve();
111
+ await Promise.resolve();
112
+ });
113
+
114
+ it('does not add a dependency link when called from inside a tracking scope', async () => {
115
+ let s = signal(1),
116
+ c = computed(() => read(s) * 2),
117
+ caller = computed(() => peek(c));
118
+
119
+ expect(read(caller)).toBe(2);
120
+ expect(caller.deps).toBe(null);
121
+
122
+ write(s, 5);
123
+ await Promise.resolve();
124
+ await Promise.resolve();
125
+
126
+ // caller never subscribed to c, so it does not react to s changing
127
+ expect(read(caller)).toBe(2);
128
+ });
129
+
130
+ it('rethrows the cached error for an errored computed', () => {
131
+ let s = signal(0),
132
+ c = computed(() => {
133
+ if (read(s) > 0) {
134
+ throw new Error('peek boom');
135
+ }
136
+
137
+ return read(s);
138
+ });
139
+
140
+ expect(peek(c)).toBe(0);
141
+
142
+ write(s, 1);
143
+
144
+ expect(() => peek(c)).toThrow('peek boom');
145
+ });
146
+ });
package/vitest.config.ts CHANGED
@@ -11,8 +11,8 @@ export default defineConfig({
11
11
  },
12
12
  test: {
13
13
  benchmark: {
14
- include: ['tests/bench/**/*.ts']
14
+ include: ['bench/**/*.bench.ts']
15
15
  },
16
- include: ['tests/*.ts']
16
+ include: ['test/**/*.test.ts']
17
17
  }
18
18
  });