@esportsplus/reactivity 0.31.3 → 0.32.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/.claude/CHANGELOG.md +43 -0
- package/README.md +13 -6
- package/bench/cellx.bench.ts +61 -0
- package/bench/dynamic.bench.ts +103 -0
- package/bench/kairo.bench.ts +368 -0
- package/bench/lib/reactive-adapter.ts +51 -0
- package/bench/molbench.bench.ts +75 -0
- package/{tests/bench/system.ts → bench/system.bench.ts} +3 -3
- package/build/compiler/array.d.ts +1 -1
- package/build/compiler/constants.d.ts +7 -6
- package/build/compiler/constants.js +6 -7
- package/build/compiler/object.d.ts +1 -1
- package/build/compiler/object.js +1 -2
- package/build/compiler/primitives.d.ts +1 -1
- package/build/constants.d.ts +3 -1
- package/build/constants.js +3 -1
- package/build/reactive/array.d.ts +8 -5
- package/build/reactive/array.js +14 -14
- package/build/reactive/index.d.ts +2 -2
- package/build/reactive/index.js +1 -1
- package/build/reactive/object.d.ts +4 -4
- package/build/reactive/object.js +8 -23
- package/build/system.d.ts +15 -6
- package/build/system.js +415 -69
- package/build/types.d.ts +16 -4
- package/package.json +5 -2
- package/src/compiler/array.ts +1 -1
- package/src/compiler/constants.ts +8 -6
- package/src/compiler/index.ts +2 -1
- package/src/compiler/object.ts +2 -2
- package/src/compiler/plugins/vite.ts +1 -0
- package/src/compiler/primitives.ts +1 -1
- package/src/constants.ts +6 -2
- package/src/reactive/array.ts +26 -23
- package/src/reactive/index.ts +6 -6
- package/src/reactive/object.ts +16 -41
- package/src/system.ts +612 -80
- package/src/types.ts +29 -9
- package/test/async-computed.test.ts +323 -0
- package/test/async-errors.test.ts +146 -0
- package/test/async-hardening.test.ts +73 -0
- package/test/async-iterable.test.ts +221 -0
- package/test/async-nested.test.ts +19 -0
- package/test/deep-graphs.test.ts +215 -0
- package/{tests/effects.ts → test/effects.test.ts} +60 -0
- package/test/equals.test.ts +142 -0
- package/test/errors.test.ts +201 -0
- package/test/flush.test.ts +185 -0
- package/test/glitch-freedom.test.ts +115 -0
- package/test/invalidate.test.ts +147 -0
- package/test/lib/wait-for.ts +28 -0
- package/test/pending-writes.test.ts +139 -0
- package/{tests/reactive.ts → test/reactive/reactive.test.ts} +20 -19
- package/test/read-dedup.test.ts +120 -0
- package/test/signal-selector.test.ts +187 -0
- package/{tests/system.ts → test/system.test.ts} +214 -23
- package/test/tsconfig.json +16 -0
- package/test/untrack.test.ts +146 -0
- package/vitest.config.ts +2 -2
- package/tests/async-computed.ts +0 -239
- /package/{tests/bench/array.ts → bench/reactive/array.bench.ts} +0 -0
- /package/{tests/bench/reactive-object.ts → bench/reactive/reactive-object.bench.ts} +0 -0
- /package/{tests → bench}/tsconfig.json +0 -0
- /package/{tests/compiler.ts → test/compiler/compiler.test.ts} +0 -0
- /package/{tests/primitives.ts → test/primitives.test.ts} +0 -0
- /package/{tests/array.ts → test/reactive/array.test.ts} +0 -0
- /package/{tests/nested.ts → test/reactive/nested.test.ts} +0 -0
- /package/{tests/objects.ts → test/reactive/objects.test.ts} +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computed, effect, read, signal, write } from '~/system';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('pending-only writes', () => {
|
|
6
|
+
it('N writes to one signal between settles produce exactly one fan-out', async () => {
|
|
7
|
+
let runs = 0,
|
|
8
|
+
s = signal(0);
|
|
9
|
+
|
|
10
|
+
let c = computed(() => {
|
|
11
|
+
runs++;
|
|
12
|
+
|
|
13
|
+
return read(s);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
effect(() => {
|
|
17
|
+
read(c);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
runs = 0;
|
|
21
|
+
|
|
22
|
+
write(s, 1);
|
|
23
|
+
write(s, 2);
|
|
24
|
+
write(s, 3);
|
|
25
|
+
write(s, 4);
|
|
26
|
+
write(s, 5);
|
|
27
|
+
|
|
28
|
+
await Promise.resolve();
|
|
29
|
+
await Promise.resolve();
|
|
30
|
+
|
|
31
|
+
// One drain, one recompute — not one per write
|
|
32
|
+
expect(runs).toBe(1);
|
|
33
|
+
expect(read(c)).toBe(5);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('an unchanged recompute does not cascade to downstream subscribers', async () => {
|
|
37
|
+
let downstream = 0,
|
|
38
|
+
s = signal(0);
|
|
39
|
+
|
|
40
|
+
// Parity: value only changes on even inputs, so odd writes recompute c but must not re-run d
|
|
41
|
+
let c = computed(() => read(s) - (read(s) % 2));
|
|
42
|
+
|
|
43
|
+
let d = computed(() => {
|
|
44
|
+
downstream++;
|
|
45
|
+
|
|
46
|
+
return read(c);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
effect(() => {
|
|
50
|
+
read(d);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
downstream = 0;
|
|
54
|
+
|
|
55
|
+
write(s, 1);
|
|
56
|
+
await Promise.resolve();
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
|
|
59
|
+
// c recomputed to the same value (0) → no propagation to d
|
|
60
|
+
expect(downstream).toBe(0);
|
|
61
|
+
expect(read(d)).toBe(0);
|
|
62
|
+
|
|
63
|
+
write(s, 2);
|
|
64
|
+
await Promise.resolve();
|
|
65
|
+
await Promise.resolve();
|
|
66
|
+
|
|
67
|
+
expect(downstream).toBe(1);
|
|
68
|
+
expect(read(d)).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('tracked reads converge on pending writes at settle (invariant 1, parity with main)', async () => {
|
|
72
|
+
let observed: number[] = [],
|
|
73
|
+
s = signal(0),
|
|
74
|
+
t = signal(10);
|
|
75
|
+
|
|
76
|
+
let c = computed(() => read(t));
|
|
77
|
+
|
|
78
|
+
// Effect writes t then reads c, and a downstream effect reads c too — at settle every
|
|
79
|
+
// tracked reader must see the pending write reflected (main's coherence contract, which
|
|
80
|
+
// the deferred queue must preserve rather than strand a write un-fanned-out)
|
|
81
|
+
effect(() => {
|
|
82
|
+
let v = read(s);
|
|
83
|
+
|
|
84
|
+
if (v > 0) {
|
|
85
|
+
write(t, v * 100);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
observed.push(read(c));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
let downstream = 0;
|
|
92
|
+
|
|
93
|
+
effect(() => {
|
|
94
|
+
read(c);
|
|
95
|
+
downstream++;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
observed.length = 0;
|
|
99
|
+
downstream = 0;
|
|
100
|
+
|
|
101
|
+
write(s, 1);
|
|
102
|
+
await Promise.resolve();
|
|
103
|
+
await Promise.resolve();
|
|
104
|
+
|
|
105
|
+
// Both readers and the node itself settle on the written value — the queue never drops it
|
|
106
|
+
expect(read(c)).toBe(100);
|
|
107
|
+
expect(observed[observed.length - 1]).toBe(100);
|
|
108
|
+
expect(downstream).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('repeated writes across two signals coalesce, final graph values correct', async () => {
|
|
112
|
+
let runs = 0,
|
|
113
|
+
a = signal(1),
|
|
114
|
+
b = signal(1);
|
|
115
|
+
|
|
116
|
+
let sum = computed(() => {
|
|
117
|
+
runs++;
|
|
118
|
+
|
|
119
|
+
return read(a) + read(b);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
effect(() => {
|
|
123
|
+
read(sum);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
runs = 0;
|
|
127
|
+
|
|
128
|
+
write(a, 2);
|
|
129
|
+
write(a, 3);
|
|
130
|
+
write(b, 4);
|
|
131
|
+
write(b, 5);
|
|
132
|
+
|
|
133
|
+
await Promise.resolve();
|
|
134
|
+
await Promise.resolve();
|
|
135
|
+
|
|
136
|
+
expect(runs).toBe(1);
|
|
137
|
+
expect(read(sum)).toBe(8);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -4,6 +4,7 @@ import { SIGNAL } from '~/constants';
|
|
|
4
4
|
import { ReactiveObject, isReactiveObject } from '~/reactive/object';
|
|
5
5
|
import reactive from '~/reactive/index';
|
|
6
6
|
import { ReactiveArray } from '~/reactive/array';
|
|
7
|
+
import { tick, waitFor } from '../lib/wait-for';
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
describe('ReactiveObject', () => {
|
|
@@ -120,7 +121,7 @@ describe('ReactiveObject', () => {
|
|
|
120
121
|
|
|
121
122
|
expect(obj.data).toBeUndefined();
|
|
122
123
|
|
|
123
|
-
await
|
|
124
|
+
await waitFor(() => obj.data === 42, 'obj.data resolves to 42');
|
|
124
125
|
|
|
125
126
|
expect(obj.data).toBe(42);
|
|
126
127
|
});
|
|
@@ -131,12 +132,12 @@ describe('ReactiveObject', () => {
|
|
|
131
132
|
data: () => Promise.resolve(read(s))
|
|
132
133
|
}) as any;
|
|
133
134
|
|
|
134
|
-
await
|
|
135
|
+
await waitFor(() => obj.data === 'hello', 'obj.data resolves to hello');
|
|
135
136
|
|
|
136
137
|
expect(obj.data).toBe('hello');
|
|
137
138
|
|
|
138
139
|
write(s, 'world');
|
|
139
|
-
await
|
|
140
|
+
await waitFor(() => obj.data === 'world', 'obj.data updates to world');
|
|
140
141
|
|
|
141
142
|
expect(obj.data).toBe('world');
|
|
142
143
|
});
|
|
@@ -153,6 +154,9 @@ describe('ReactiveObject', () => {
|
|
|
153
154
|
|
|
154
155
|
expect(obj.data).toBeUndefined();
|
|
155
156
|
|
|
157
|
+
// Creation dispatches once: the probe is reused as the factory (no separate probe dispatch).
|
|
158
|
+
expect(resolvers.length).toBe(1);
|
|
159
|
+
|
|
156
160
|
// Trigger second computation
|
|
157
161
|
write(s, 2);
|
|
158
162
|
await Promise.resolve();
|
|
@@ -163,26 +167,24 @@ describe('ReactiveObject', () => {
|
|
|
163
167
|
await Promise.resolve();
|
|
164
168
|
await Promise.resolve();
|
|
165
169
|
|
|
166
|
-
//
|
|
167
|
-
resolvers
|
|
168
|
-
await Promise.resolve();
|
|
169
|
-
|
|
170
|
-
expect(obj.data).toBeUndefined();
|
|
170
|
+
// [0] creation, [1] after write(2), [2] after write(3) — the latest
|
|
171
|
+
expect(resolvers.length).toBe(3);
|
|
171
172
|
|
|
172
|
-
// Resolve
|
|
173
|
-
resolvers[
|
|
173
|
+
// Resolve the stale computations — all ignored
|
|
174
|
+
resolvers[0](50);
|
|
175
|
+
resolvers[1](100);
|
|
174
176
|
await Promise.resolve();
|
|
175
177
|
|
|
176
178
|
expect(obj.data).toBeUndefined();
|
|
177
179
|
|
|
178
|
-
// Resolve latest promise —
|
|
180
|
+
// Resolve latest promise — lands after the stabilize microtask
|
|
179
181
|
resolvers[2](300);
|
|
180
|
-
await
|
|
182
|
+
await waitFor(() => obj.data === 300, 'obj.data resolves to latest 300');
|
|
181
183
|
|
|
182
184
|
expect(obj.data).toBe(300);
|
|
183
185
|
});
|
|
184
186
|
|
|
185
|
-
it('dispose
|
|
187
|
+
it('dispose stops in-flight resolves from landing', async () => {
|
|
186
188
|
let s = signal(1),
|
|
187
189
|
resolver: ((v: number) => void) | null = null,
|
|
188
190
|
obj = new ReactiveObject({
|
|
@@ -200,18 +202,17 @@ describe('ReactiveObject', () => {
|
|
|
200
202
|
write(s, 2);
|
|
201
203
|
await Promise.resolve();
|
|
202
204
|
|
|
203
|
-
// Resolve the original in-flight promise
|
|
205
|
+
// Resolve the original in-flight promise — teardown is permanent, nothing lands
|
|
204
206
|
resolver!(999);
|
|
205
207
|
await Promise.resolve();
|
|
206
208
|
|
|
207
|
-
|
|
208
|
-
expect(obj.data).toBe(999);
|
|
209
|
+
expect(obj.data).toBeUndefined();
|
|
209
210
|
|
|
210
|
-
//
|
|
211
|
+
// And no further computations occur from new dependency changes
|
|
211
212
|
write(s, 3);
|
|
212
|
-
await
|
|
213
|
+
await tick();
|
|
213
214
|
|
|
214
|
-
expect(obj.data).
|
|
215
|
+
expect(obj.data).toBeUndefined();
|
|
215
216
|
});
|
|
216
217
|
});
|
|
217
218
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computed, effect, read, signal, write } from '~/system';
|
|
3
|
+
import type { Computed } from '~/system';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function countDeps(node: Computed<unknown>): number {
|
|
7
|
+
let n = 0;
|
|
8
|
+
|
|
9
|
+
for (let link = node.deps; link; link = link.nextDep) {
|
|
10
|
+
n++;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
describe('read-version dedup', () => {
|
|
18
|
+
it('interleaved repeat reads (x, y, x) hold exactly 2 dep links and re-run once per write', async () => {
|
|
19
|
+
let runs = 0,
|
|
20
|
+
x = signal(0),
|
|
21
|
+
y = signal(0);
|
|
22
|
+
|
|
23
|
+
let c = computed(() => {
|
|
24
|
+
runs++;
|
|
25
|
+
|
|
26
|
+
return read(x) + read(y) + read(x);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
effect(() => {
|
|
30
|
+
read(c);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(countDeps(c as Computed<unknown>)).toBe(2);
|
|
34
|
+
expect(runs).toBe(1);
|
|
35
|
+
|
|
36
|
+
write(x, 1);
|
|
37
|
+
await Promise.resolve();
|
|
38
|
+
|
|
39
|
+
// Steady-state re-run exercises the RECOMPUTING reuse paths
|
|
40
|
+
expect(countDeps(c as Computed<unknown>)).toBe(2);
|
|
41
|
+
expect(runs).toBe(2);
|
|
42
|
+
expect(read(c)).toBe(2);
|
|
43
|
+
|
|
44
|
+
write(x, 2);
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
|
|
47
|
+
expect(countDeps(c as Computed<unknown>)).toBe(2);
|
|
48
|
+
expect(runs).toBe(3);
|
|
49
|
+
expect(read(c)).toBe(4);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('outer first-read of a dep stamped only by a nested creation still links (no false positive)', async () => {
|
|
53
|
+
let outerRuns = 0,
|
|
54
|
+
s = signal(1);
|
|
55
|
+
|
|
56
|
+
effect(() => {
|
|
57
|
+
outerRuns++;
|
|
58
|
+
|
|
59
|
+
// Inline creation: depsTail is null, so the inner recompute nests here and stamps s.
|
|
60
|
+
// Constant value keeps the inner from ever propagating — the outer re-runs on s
|
|
61
|
+
// writes ONLY through its own direct link.
|
|
62
|
+
computed(() => (read(s), 0));
|
|
63
|
+
|
|
64
|
+
read(s);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(outerRuns).toBe(1);
|
|
68
|
+
|
|
69
|
+
write(s, 2);
|
|
70
|
+
await Promise.resolve();
|
|
71
|
+
|
|
72
|
+
expect(outerRuns).toBe(2);
|
|
73
|
+
|
|
74
|
+
write(s, 3);
|
|
75
|
+
await Promise.resolve();
|
|
76
|
+
|
|
77
|
+
expect(outerRuns).toBe(3);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('repeat reads across a nested-recompute boundary re-link or reuse, never drop', async () => {
|
|
81
|
+
let runs = 0,
|
|
82
|
+
s = signal(1);
|
|
83
|
+
|
|
84
|
+
let c = computed(() => {
|
|
85
|
+
runs++;
|
|
86
|
+
|
|
87
|
+
// Inline nested recompute reads s and stamps it under the inner run's version.
|
|
88
|
+
// Undefined return keeps the inner from ever propagating back into c.
|
|
89
|
+
computed(() => {
|
|
90
|
+
read(s);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return read(s) + read(s);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
effect(() => {
|
|
97
|
+
read(c);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(read(c)).toBe(2);
|
|
101
|
+
expect(runs).toBe(1);
|
|
102
|
+
|
|
103
|
+
// inner + one s link — the repeat read fast-exits, the boundary read still links
|
|
104
|
+
expect(countDeps(c as Computed<unknown>)).toBe(2);
|
|
105
|
+
|
|
106
|
+
write(s, 2);
|
|
107
|
+
await Promise.resolve();
|
|
108
|
+
|
|
109
|
+
expect(read(c)).toBe(4);
|
|
110
|
+
expect(runs).toBe(2);
|
|
111
|
+
expect(countDeps(c as Computed<unknown>)).toBe(2);
|
|
112
|
+
|
|
113
|
+
write(s, 5);
|
|
114
|
+
await Promise.resolve();
|
|
115
|
+
|
|
116
|
+
expect(read(c)).toBe(10);
|
|
117
|
+
expect(runs).toBe(3);
|
|
118
|
+
expect(countDeps(c as Computed<unknown>)).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -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
|
+
});
|