@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.
- package/README.md +43 -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 +7 -3
- package/build/reactive/array.js +32 -24
- 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 +432 -69
- package/build/types.d.ts +22 -4
- package/package.json +10 -7
- 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 +50 -35
- package/src/reactive/index.ts +6 -6
- package/src/reactive/object.ts +16 -41
- package/src/system.ts +635 -80
- package/src/types.ts +38 -9
- package/test/async-computed.test.ts +409 -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} +122 -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} +210 -19
- 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,147 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computed, effect, flush, peek, read, signal, write } from '~/system';
|
|
3
|
+
import { waitFor } from './lib/wait-for';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('invalidate()', () => {
|
|
7
|
+
it('re-runs the computed fn with no dependency change', () => {
|
|
8
|
+
let runs = 0,
|
|
9
|
+
s = signal(1);
|
|
10
|
+
|
|
11
|
+
let c = computed(() => {
|
|
12
|
+
runs++;
|
|
13
|
+
|
|
14
|
+
return read(s);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
effect(() => {
|
|
18
|
+
read(c);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(runs).toBe(1);
|
|
22
|
+
|
|
23
|
+
computed.invalidate(c);
|
|
24
|
+
flush();
|
|
25
|
+
|
|
26
|
+
expect(runs).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('dependents re-run only when the forced re-run changes the value', () => {
|
|
30
|
+
let changingRuns = 0,
|
|
31
|
+
s = signal(1),
|
|
32
|
+
stableRuns = 0,
|
|
33
|
+
ticks = 0;
|
|
34
|
+
|
|
35
|
+
let changing = computed(() => {
|
|
36
|
+
read(s);
|
|
37
|
+
|
|
38
|
+
return ++ticks;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let stable = computed(() => read(s));
|
|
42
|
+
|
|
43
|
+
effect(() => {
|
|
44
|
+
stableRuns++;
|
|
45
|
+
read(stable);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
effect(() => {
|
|
49
|
+
changingRuns++;
|
|
50
|
+
read(changing);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
computed.invalidate(stable);
|
|
54
|
+
flush();
|
|
55
|
+
|
|
56
|
+
expect(stableRuns).toBe(1);
|
|
57
|
+
|
|
58
|
+
computed.invalidate(changing);
|
|
59
|
+
flush();
|
|
60
|
+
|
|
61
|
+
expect(changingRuns).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('re-dispatches an asyncComputed factory (refetch via the asyncMeta redirect)', async () => {
|
|
65
|
+
let fetches = 0;
|
|
66
|
+
|
|
67
|
+
let node = computed(() => {
|
|
68
|
+
fetches++;
|
|
69
|
+
|
|
70
|
+
return Promise.resolve(fetches);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let stop = effect(() => {
|
|
74
|
+
read(node);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await waitFor(() => read(node) === 1, 'node resolves to 1');
|
|
78
|
+
|
|
79
|
+
expect(fetches).toBe(1);
|
|
80
|
+
expect(read(node)).toBe(1);
|
|
81
|
+
|
|
82
|
+
computed.invalidate(node);
|
|
83
|
+
await waitFor(() => read(node) === 2, 'node refetches to 2');
|
|
84
|
+
|
|
85
|
+
expect(fetches).toBe(2);
|
|
86
|
+
expect(read(node)).toBe(2);
|
|
87
|
+
|
|
88
|
+
stop();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('forces a side-effecting computed (the public effect encoding) to re-run without propagating', () => {
|
|
92
|
+
let keeperRuns = 0,
|
|
93
|
+
s = signal(1),
|
|
94
|
+
sideRuns = 0;
|
|
95
|
+
|
|
96
|
+
let c = computed<void>(() => {
|
|
97
|
+
read(s);
|
|
98
|
+
sideRuns++;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
effect(() => {
|
|
102
|
+
keeperRuns++;
|
|
103
|
+
read(c);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(sideRuns).toBe(1);
|
|
107
|
+
expect(keeperRuns).toBe(1);
|
|
108
|
+
|
|
109
|
+
computed.invalidate(c);
|
|
110
|
+
flush();
|
|
111
|
+
|
|
112
|
+
expect(sideRuns).toBe(2);
|
|
113
|
+
expect(keeperRuns).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('the global-version fast path does not swallow a forced re-run', () => {
|
|
117
|
+
let runs = 0,
|
|
118
|
+
s = signal(1);
|
|
119
|
+
|
|
120
|
+
let c = computed(() => {
|
|
121
|
+
runs++;
|
|
122
|
+
|
|
123
|
+
return read(s);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
effect(() => {
|
|
127
|
+
read(c);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
write(s, 2);
|
|
131
|
+
flush();
|
|
132
|
+
|
|
133
|
+
expect(runs).toBe(2);
|
|
134
|
+
|
|
135
|
+
// Fully settled: c carries the current gv stamp. Without writes++ inside invalidate,
|
|
136
|
+
// this synchronous pull would exit through the fast path and skip the re-run. peek()
|
|
137
|
+
// forces the pull unconditionally (read() outside an observer returns the cached value).
|
|
138
|
+
computed.invalidate(c);
|
|
139
|
+
|
|
140
|
+
expect(peek(c)).toBe(2);
|
|
141
|
+
expect(runs).toBe(3);
|
|
142
|
+
|
|
143
|
+
flush();
|
|
144
|
+
|
|
145
|
+
expect(runs).toBe(3);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const tick = (times = 1): Promise<void> => {
|
|
2
|
+
let p = Promise.resolve();
|
|
3
|
+
|
|
4
|
+
for (let i = 0; i < times; i++) {
|
|
5
|
+
p = p.then(() => new Promise<void>((resolve) => setTimeout(resolve, 0)));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return p;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const waitFor = (condition: () => boolean, description: string, timeoutMs = 1000): Promise<void> => {
|
|
12
|
+
return new Promise<void>((resolve, reject) => {
|
|
13
|
+
let deadline = Date.now() + timeoutMs,
|
|
14
|
+
timer = setInterval(() => {
|
|
15
|
+
if (condition()) {
|
|
16
|
+
clearInterval(timer);
|
|
17
|
+
resolve();
|
|
18
|
+
}
|
|
19
|
+
else if (Date.now() >= deadline) {
|
|
20
|
+
clearInterval(timer);
|
|
21
|
+
reject(new Error('wait-for: timed out waiting for ' + description));
|
|
22
|
+
}
|
|
23
|
+
}, 10);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export { tick, waitFor };
|
|
@@ -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
|
+
});
|