@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
package/src/system.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
+
PACKAGE_NAME,
|
|
2
3
|
SIGNAL,
|
|
3
4
|
STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
|
|
4
|
-
STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING
|
|
5
|
+
STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING
|
|
5
6
|
} from './constants';
|
|
6
|
-
import { Computed, Link, Signal } from './types';
|
|
7
|
-
import { isObject } from '@esportsplus/utilities';
|
|
7
|
+
import { Computed, ComputedResult, Link, SelectorSignal, Settled, Signal } from './types';
|
|
8
|
+
import { isObject, isPromise } from '@esportsplus/utilities';
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// Shared free-listed stack node for the iterative notify()/update()/dispose() walks: `link` carries
|
|
12
|
+
// a notify sibling-continuation or an update dep-cursor; `computed` carries an update/dispose node.
|
|
13
|
+
type Walk = {
|
|
14
|
+
computed: Computed<unknown> | null;
|
|
15
|
+
link: Link | null;
|
|
16
|
+
prev: Walk | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let asyncMeta = new WeakMap<Computed<unknown>, { factory: Computed<unknown> }>(),
|
|
20
|
+
depth = 0,
|
|
21
|
+
disposeHead: Walk | null = null,
|
|
22
|
+
draining = false,
|
|
11
23
|
heap: (Computed<unknown> | undefined)[] = new Array(64),
|
|
12
24
|
heap_i = 0,
|
|
13
25
|
heap_n = 0,
|
|
@@ -15,9 +27,39 @@ let depth = 0,
|
|
|
15
27
|
microtask = queueMicrotask,
|
|
16
28
|
notified = false,
|
|
17
29
|
observer: Computed<unknown> | null = null,
|
|
30
|
+
pendingHead: Signal<unknown> | null = null,
|
|
18
31
|
scope: Computed<unknown> | null = null,
|
|
19
32
|
stabilizer = STABILIZER_IDLE,
|
|
20
|
-
version = 0
|
|
33
|
+
version = 0,
|
|
34
|
+
walkPoolHead: Walk | null = null,
|
|
35
|
+
writes = 0;
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
function walkPop(walk: Walk): Walk | null {
|
|
39
|
+
let prev = walk.prev;
|
|
40
|
+
|
|
41
|
+
walk.computed = null;
|
|
42
|
+
walk.link = null;
|
|
43
|
+
walk.prev = walkPoolHead;
|
|
44
|
+
walkPoolHead = walk;
|
|
45
|
+
|
|
46
|
+
return prev;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function walkPush(computed: Computed<unknown> | null, link: Link | null, prev: Walk | null): Walk {
|
|
50
|
+
let walk = walkPoolHead;
|
|
51
|
+
|
|
52
|
+
if (walk !== null) {
|
|
53
|
+
walkPoolHead = walk.prev;
|
|
54
|
+
walk.computed = computed;
|
|
55
|
+
walk.link = link;
|
|
56
|
+
walk.prev = prev;
|
|
57
|
+
|
|
58
|
+
return walk;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { computed, link, prev };
|
|
62
|
+
}
|
|
21
63
|
|
|
22
64
|
|
|
23
65
|
function cleanup<T>(computed: Computed<T>): void {
|
|
@@ -25,18 +67,33 @@ function cleanup<T>(computed: Computed<T>): void {
|
|
|
25
67
|
return;
|
|
26
68
|
}
|
|
27
69
|
|
|
28
|
-
let
|
|
70
|
+
let errors: unknown[] = [],
|
|
71
|
+
value = computed.cleanup;
|
|
72
|
+
|
|
73
|
+
computed.cleanup = null;
|
|
29
74
|
|
|
30
75
|
if (typeof value === 'function') {
|
|
31
|
-
|
|
76
|
+
try {
|
|
77
|
+
value();
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
errors.push(e);
|
|
81
|
+
}
|
|
32
82
|
}
|
|
33
83
|
else {
|
|
34
84
|
for (let i = 0, n = value.length; i < n; i++) {
|
|
35
|
-
|
|
85
|
+
try {
|
|
86
|
+
value[i]();
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
errors.push(e);
|
|
90
|
+
}
|
|
36
91
|
}
|
|
37
92
|
}
|
|
38
93
|
|
|
39
|
-
|
|
94
|
+
if (errors.length) {
|
|
95
|
+
throw errors.length === 1 ? errors[0] : new AggregateError(errors, `${PACKAGE_NAME}: cleanup produced multiple errors`);
|
|
96
|
+
}
|
|
40
97
|
}
|
|
41
98
|
|
|
42
99
|
function deleteFromHeap<T>(computed: Computed<T>) {
|
|
@@ -72,6 +129,26 @@ function deleteFromHeap<T>(computed: Computed<T>) {
|
|
|
72
129
|
computed.prevHeap = computed;
|
|
73
130
|
}
|
|
74
131
|
|
|
132
|
+
// Reconstructs main's eager write() fan-out in one batched pass: N writes to one signal
|
|
133
|
+
// queue it once, so each subscriber is heap-inserted once. Self-linked nextPending marks the tail.
|
|
134
|
+
function drainPending() {
|
|
135
|
+
let node = pendingHead;
|
|
136
|
+
|
|
137
|
+
pendingHead = null;
|
|
138
|
+
|
|
139
|
+
while (node !== null) {
|
|
140
|
+
let next = node.nextPending === node ? null : node.nextPending;
|
|
141
|
+
|
|
142
|
+
node.nextPending = null;
|
|
143
|
+
|
|
144
|
+
for (let link: Link | null = node.subs; link; link = link.nextSub) {
|
|
145
|
+
insertIntoHeap(link.sub);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
node = next;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
75
152
|
function insertIntoHeap<T>(computed: Computed<T>) {
|
|
76
153
|
let state = computed.state;
|
|
77
154
|
|
|
@@ -107,9 +184,15 @@ function insertIntoHeap<T>(computed: Computed<T>) {
|
|
|
107
184
|
|
|
108
185
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
109
186
|
function link<T>(dep: Signal<T> | Computed<T>, sub: Computed<T>) {
|
|
187
|
+
// rv === version proves this dep already linked to the current observer during this run
|
|
188
|
+
if (dep.rv === version) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
110
192
|
let prevDep = sub.depsTail;
|
|
111
193
|
|
|
112
194
|
if (prevDep && prevDep.dep === dep) {
|
|
195
|
+
dep.rv = version;
|
|
113
196
|
return;
|
|
114
197
|
}
|
|
115
198
|
|
|
@@ -119,6 +202,7 @@ function link<T>(dep: Signal<T> | Computed<T>, sub: Computed<T>) {
|
|
|
119
202
|
nextDep = prevDep ? prevDep.nextDep : sub.deps;
|
|
120
203
|
|
|
121
204
|
if (nextDep && nextDep.dep === dep) {
|
|
205
|
+
dep.rv = version;
|
|
122
206
|
nextDep.version = version;
|
|
123
207
|
sub.depsTail = nextDep;
|
|
124
208
|
return;
|
|
@@ -133,9 +217,12 @@ function link<T>(dep: Signal<T> | Computed<T>, sub: Computed<T>) {
|
|
|
133
217
|
prevSub.version === version &&
|
|
134
218
|
prevSub.sub === sub
|
|
135
219
|
) {
|
|
220
|
+
dep.rv = version;
|
|
136
221
|
return;
|
|
137
222
|
}
|
|
138
223
|
|
|
224
|
+
dep.rv = version;
|
|
225
|
+
|
|
139
226
|
let pooled = linkPoolHead,
|
|
140
227
|
newLink =
|
|
141
228
|
sub.depsTail =
|
|
@@ -172,6 +259,9 @@ function link<T>(dep: Signal<T> | Computed<T>, sub: Computed<T>) {
|
|
|
172
259
|
}
|
|
173
260
|
}
|
|
174
261
|
|
|
262
|
+
// Iterative sub-tree walk over an explicit stack (call-stack recursion overflows on deep graphs).
|
|
263
|
+
// Only the ROOT receives newState; every descendant receives STATE_CHECK. A node already CHECK/DIRTY
|
|
264
|
+
// prunes the descent (the `< STATE_CHECK` guard).
|
|
175
265
|
function notify<T>(computed: Computed<T>, newState: number) {
|
|
176
266
|
let state = computed.state;
|
|
177
267
|
|
|
@@ -181,9 +271,72 @@ function notify<T>(computed: Computed<T>, newState: number) {
|
|
|
181
271
|
|
|
182
272
|
computed.state = state | newState;
|
|
183
273
|
|
|
184
|
-
|
|
185
|
-
|
|
274
|
+
let link: Link | null = computed.subs,
|
|
275
|
+
stack: Walk | null = null;
|
|
276
|
+
|
|
277
|
+
for (;;) {
|
|
278
|
+
while (link !== null) {
|
|
279
|
+
let sub = link.sub,
|
|
280
|
+
subState = sub.state;
|
|
281
|
+
|
|
282
|
+
if ((subState & STATE_NOTIFY_MASK) < STATE_CHECK) {
|
|
283
|
+
sub.state = subState | STATE_CHECK;
|
|
284
|
+
|
|
285
|
+
if (sub.subs !== null) {
|
|
286
|
+
if (link.nextSub !== null) {
|
|
287
|
+
stack = walkPush(null, link.nextSub, stack);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
link = sub.subs;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
link = link.nextSub;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (stack === null) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
link = stack.link;
|
|
303
|
+
stack = walkPop(stack);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Shared by read()'s tracked pull and peek()'s untracked pull. observer is nulled around update()
|
|
308
|
+
// so a recompute triggered here tracks into the node's own scope, never the caller's.
|
|
309
|
+
function pull<T>(node: Computed<T>): void {
|
|
310
|
+
if (!notified) {
|
|
311
|
+
notified = true;
|
|
312
|
+
|
|
313
|
+
for (let i = 0; i <= heap_n; i++) {
|
|
314
|
+
for (let computed = heap[i]; computed !== undefined; computed = computed.nextHeap) {
|
|
315
|
+
notify(computed, STATE_DIRTY);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
186
318
|
}
|
|
319
|
+
|
|
320
|
+
let o = observer;
|
|
321
|
+
|
|
322
|
+
observer = null;
|
|
323
|
+
update(node);
|
|
324
|
+
observer = o;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function propagate<T>(computed: Computed<T>) {
|
|
328
|
+
for (let c = computed.subs; c; c = c.nextSub) {
|
|
329
|
+
let s = c.sub,
|
|
330
|
+
state = s.state;
|
|
331
|
+
|
|
332
|
+
if (state & STATE_CHECK) {
|
|
333
|
+
s.state = state | STATE_DIRTY;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
insertIntoHeap(s);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
schedule();
|
|
187
340
|
}
|
|
188
341
|
|
|
189
342
|
function recompute<T>(computed: Computed<T>, del: boolean) {
|
|
@@ -196,16 +349,28 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
|
|
|
196
349
|
}
|
|
197
350
|
|
|
198
351
|
if (computed.cleanup) {
|
|
199
|
-
|
|
352
|
+
// A failing PREVIOUS generation's teardown must not poison this recompute or the stabilize pass
|
|
353
|
+
try {
|
|
354
|
+
cleanup(computed);
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
microtask(() => {
|
|
358
|
+
throw e;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
200
361
|
}
|
|
201
362
|
|
|
202
|
-
let
|
|
363
|
+
let err: unknown,
|
|
364
|
+
flags = computed.state & STATE_EFFECT,
|
|
365
|
+
hadError = computed.state & STATE_ERROR,
|
|
366
|
+
o = observer,
|
|
203
367
|
ok = true,
|
|
204
|
-
value
|
|
368
|
+
value,
|
|
369
|
+
w = writes;
|
|
205
370
|
|
|
206
371
|
observer = computed;
|
|
207
372
|
computed.depsTail = null;
|
|
208
|
-
computed.state = STATE_COMPUTED | STATE_RECOMPUTING;
|
|
373
|
+
computed.state = STATE_COMPUTED | STATE_RECOMPUTING | flags;
|
|
209
374
|
|
|
210
375
|
depth++;
|
|
211
376
|
version++;
|
|
@@ -215,11 +380,16 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
|
|
|
215
380
|
}
|
|
216
381
|
catch (e) {
|
|
217
382
|
ok = false;
|
|
383
|
+
err = e;
|
|
218
384
|
}
|
|
219
385
|
|
|
220
386
|
depth--;
|
|
387
|
+
// Fresh version so rv/link stamps from this run (incl. nested creations) go stale — false negatives only
|
|
388
|
+
version++;
|
|
221
389
|
observer = o;
|
|
222
|
-
computed.state = STATE_COMPUTED;
|
|
390
|
+
computed.state = STATE_COMPUTED | flags;
|
|
391
|
+
// Entry snapshot, not current writes: a node whose fn wrote mid-run must stay gv < writes and validate normally
|
|
392
|
+
computed.gv = w;
|
|
223
393
|
|
|
224
394
|
let depsTail = computed.depsTail as Link | null,
|
|
225
395
|
remove = depsTail ? depsTail.nextDep : computed.deps;
|
|
@@ -238,21 +408,34 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
|
|
|
238
408
|
}
|
|
239
409
|
}
|
|
240
410
|
|
|
241
|
-
if (ok
|
|
242
|
-
computed.
|
|
411
|
+
if (ok) {
|
|
412
|
+
computed.error = null;
|
|
243
413
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
414
|
+
// A value comparator can't compare a missing prior value (and would fault dereferencing
|
|
415
|
+
// undefined), so a recompute whose PREVIOUS value was undefined falls back to === — not
|
|
416
|
+
// only the first run.
|
|
417
|
+
let changed = computed.equals === null || computed.value === undefined
|
|
418
|
+
? value !== computed.value
|
|
419
|
+
: !computed.equals(computed.value, value);
|
|
247
420
|
|
|
248
|
-
|
|
249
|
-
s.state = state | STATE_DIRTY;
|
|
250
|
-
}
|
|
421
|
+
computed.value = value as T;
|
|
251
422
|
|
|
252
|
-
|
|
423
|
+
if (changed || hadError) {
|
|
424
|
+
propagate(computed);
|
|
253
425
|
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
computed.error = err;
|
|
429
|
+
computed.state |= STATE_ERROR;
|
|
254
430
|
|
|
255
|
-
|
|
431
|
+
if (flags) {
|
|
432
|
+
microtask(() => {
|
|
433
|
+
throw err;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
propagate(computed);
|
|
438
|
+
}
|
|
256
439
|
}
|
|
257
440
|
}
|
|
258
441
|
|
|
@@ -277,6 +460,12 @@ function stabilize() {
|
|
|
277
460
|
stabilizer = STABILIZER_RUNNING;
|
|
278
461
|
|
|
279
462
|
for (heap_i = 0; heap_i <= heap_n; heap_i++) {
|
|
463
|
+
// Drain before scanning each height so writes emitted by a lower level's recompute
|
|
464
|
+
// land their subscribers in this same pass, matching main's eager same-pass pickup
|
|
465
|
+
if (pendingHead !== null) {
|
|
466
|
+
drainPending();
|
|
467
|
+
}
|
|
468
|
+
|
|
280
469
|
let computed = heap[heap_i];
|
|
281
470
|
|
|
282
471
|
heap[heap_i] = undefined;
|
|
@@ -322,11 +511,22 @@ function unlink(link: Link): Link | null {
|
|
|
322
511
|
if (prevSub) {
|
|
323
512
|
prevSub.nextSub = nextSub;
|
|
324
513
|
}
|
|
325
|
-
else if ((dep.subs = nextSub) === null
|
|
326
|
-
|
|
514
|
+
else if ((dep.subs = nextSub) === null) {
|
|
515
|
+
if ((dep as Computed<unknown>).state & STATE_COMPUTED) {
|
|
516
|
+
dispose(dep as Computed<unknown>);
|
|
517
|
+
}
|
|
518
|
+
else if ((dep as SelectorSignal<unknown>).parent !== undefined) {
|
|
519
|
+
let parent = (dep as SelectorSignal<unknown>).parent;
|
|
520
|
+
|
|
521
|
+
parent.keys!.delete((dep as SelectorSignal<unknown>).key);
|
|
522
|
+
|
|
523
|
+
if (parent.keys!.size === 0) {
|
|
524
|
+
parent.keys = null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
327
527
|
}
|
|
328
528
|
|
|
329
|
-
link.dep = link.sub = null as
|
|
529
|
+
link.dep = link.sub = null as unknown as Computed<unknown>;
|
|
330
530
|
link.nextSub = link.prevSub = null;
|
|
331
531
|
link.nextDep = linkPoolHead;
|
|
332
532
|
linkPoolHead = link;
|
|
@@ -334,56 +534,183 @@ function unlink(link: Link): Link | null {
|
|
|
334
534
|
return nextDep;
|
|
335
535
|
}
|
|
336
536
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
537
|
+
// Iterative CHECK-pull over an explicit frame stack (call-stack recursion overflows on deep graphs).
|
|
538
|
+
// `w` is the entry snapshot of writes so the gv === writes clean-graph exit stays consistent across
|
|
539
|
+
// one pull even if a mid-walk recompute bumps the global. The DIRTY-break stops checking further deps
|
|
540
|
+
// once a node is known dirty; the notify mask is cleared on every node left.
|
|
541
|
+
function update<T>(root: Computed<T>): void {
|
|
542
|
+
let link: Link | null = null,
|
|
543
|
+
node: Computed<unknown> = root as Computed<unknown>,
|
|
544
|
+
resuming = false,
|
|
545
|
+
stack: Walk | null = null,
|
|
546
|
+
w = writes;
|
|
547
|
+
|
|
548
|
+
for (;;) {
|
|
549
|
+
if (!resuming) {
|
|
550
|
+
if (node.gv === w) {
|
|
551
|
+
node.state &= ~STATE_NOTIFY_MASK;
|
|
552
|
+
}
|
|
553
|
+
else if (node.state & STATE_CHECK) {
|
|
554
|
+
link = node.deps;
|
|
555
|
+
resuming = true;
|
|
556
|
+
}
|
|
557
|
+
else if (node.state & STATE_DIRTY) {
|
|
558
|
+
recompute(node, true);
|
|
559
|
+
node.state &= ~STATE_NOTIFY_MASK;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
node.gv = w;
|
|
563
|
+
node.state &= ~STATE_NOTIFY_MASK;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
341
566
|
|
|
342
|
-
|
|
343
|
-
|
|
567
|
+
if (resuming) {
|
|
568
|
+
let descended = false;
|
|
344
569
|
|
|
345
|
-
|
|
570
|
+
while (link !== null && !(node.state & STATE_DIRTY)) {
|
|
571
|
+
if ((link.dep as Computed<unknown>).state & STATE_COMPUTED) {
|
|
572
|
+
stack = walkPush(node, link.nextDep, stack);
|
|
573
|
+
node = link.dep as Computed<unknown>;
|
|
574
|
+
link = null;
|
|
575
|
+
resuming = false;
|
|
576
|
+
descended = true;
|
|
346
577
|
break;
|
|
347
578
|
}
|
|
579
|
+
|
|
580
|
+
link = link.nextDep;
|
|
348
581
|
}
|
|
582
|
+
|
|
583
|
+
if (descended) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (node.state & STATE_DIRTY) {
|
|
588
|
+
recompute(node, true);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
node.gv = w;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
node.state &= ~STATE_NOTIFY_MASK;
|
|
595
|
+
resuming = false;
|
|
349
596
|
}
|
|
350
|
-
}
|
|
351
597
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
598
|
+
if (stack === null) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
355
601
|
|
|
356
|
-
|
|
602
|
+
node = stack.computed!;
|
|
603
|
+
link = stack.link;
|
|
604
|
+
stack = walkPop(stack);
|
|
605
|
+
resuming = true;
|
|
606
|
+
}
|
|
357
607
|
}
|
|
358
608
|
|
|
359
609
|
|
|
360
|
-
|
|
361
|
-
|
|
610
|
+
// Drives an async factory — a computed whose fn returns a Promise or AsyncIterable — into a settled
|
|
611
|
+
// wrapper: the polling effect awaits each dispatch and writes the value/error signals the wrapper
|
|
612
|
+
// reads. `id` plus the dirty-gap guard enforce latest-wins across re-dispatches.
|
|
613
|
+
function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> | T>): Computed<T | undefined> {
|
|
614
|
+
let error = signal<unknown>(undefined),
|
|
362
615
|
node = signal<T | undefined>(undefined),
|
|
616
|
+
pending = signal(false),
|
|
363
617
|
v = 0;
|
|
364
618
|
|
|
365
|
-
|
|
366
|
-
let
|
|
619
|
+
let stop = effect(() => {
|
|
620
|
+
let fail = (e: unknown) => {
|
|
621
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
622
|
+
write(error, e === undefined ? new Error('reactivity: async computed rejected with undefined') : e);
|
|
623
|
+
write(pending, false);
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
id = ++v,
|
|
627
|
+
// Heap membership (a write's eager insert) marks a pending re-run the notify mask alone misses.
|
|
628
|
+
result = read(factory);
|
|
629
|
+
|
|
630
|
+
if (isPromise(result)) {
|
|
631
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
632
|
+
write(pending, true);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
(result as Promise<T>).then(
|
|
636
|
+
(value) => {
|
|
637
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
638
|
+
write(error, undefined);
|
|
639
|
+
write(node, value);
|
|
640
|
+
write(pending, false);
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
fail
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
else if (result != null && typeof (result as AsyncIterable<T>)[Symbol.asyncIterator] === 'function') {
|
|
647
|
+
let it = (result as AsyncIterable<T>)[Symbol.asyncIterator]();
|
|
367
648
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
649
|
+
onCleanup(() => {
|
|
650
|
+
it.return?.();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
let step = (r: IteratorResult<T>) => {
|
|
654
|
+
if (id !== v || (factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!r.done) {
|
|
659
|
+
write(error, undefined);
|
|
660
|
+
write(node, r.value);
|
|
661
|
+
write(pending, false);
|
|
662
|
+
it.next().then(step, fail);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
write(pending, false);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
670
|
+
write(pending, true);
|
|
371
671
|
}
|
|
372
|
-
}, () => {});
|
|
373
|
-
}));
|
|
374
672
|
|
|
375
|
-
|
|
376
|
-
}
|
|
673
|
+
untrack(() => it.next()).then(step, fail);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
write(error, undefined);
|
|
677
|
+
write(node, result as T);
|
|
678
|
+
write(pending, false);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
let wrapper = makeComputed<T | undefined>(() => {
|
|
683
|
+
let e = read(error);
|
|
684
|
+
|
|
685
|
+
if (e !== undefined) {
|
|
686
|
+
throw e;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return read(node);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
(wrapper as Computed<T | undefined> & { pending: Signal<boolean> }).pending = pending;
|
|
693
|
+
|
|
694
|
+
asyncMeta.set(wrapper as Computed<unknown>, { factory: factory as Computed<unknown> });
|
|
695
|
+
wrapper.disposal = stop;
|
|
377
696
|
|
|
378
|
-
|
|
697
|
+
return wrapper;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Computed<T> {
|
|
379
701
|
let self: Computed<T> = {
|
|
380
702
|
cleanup: null,
|
|
381
703
|
deps: null,
|
|
382
704
|
depsTail: null,
|
|
705
|
+
disposal: null,
|
|
706
|
+
equals: null,
|
|
707
|
+
error: null,
|
|
383
708
|
fn: fn,
|
|
709
|
+
gv: 0,
|
|
384
710
|
height: 0,
|
|
385
711
|
nextHeap: undefined,
|
|
386
|
-
prevHeap: null as
|
|
712
|
+
prevHeap: null as unknown as Computed<unknown>,
|
|
713
|
+
rv: 0,
|
|
387
714
|
state: STATE_COMPUTED,
|
|
388
715
|
subs: null,
|
|
389
716
|
subsTail: null,
|
|
@@ -397,6 +724,13 @@ const computed = <T>(fn: Computed<T>['fn']): Computed<T> => {
|
|
|
397
724
|
self.height = observer.height;
|
|
398
725
|
recompute(self, false);
|
|
399
726
|
}
|
|
727
|
+
else if (eager) {
|
|
728
|
+
// computed() must know fn's return type to pick sync vs async. This probe runs BEFORE
|
|
729
|
+
// link() below, so self has no subs yet — recompute's propagate is a no-op and cannot
|
|
730
|
+
// re-run the parent. Deferring here (as effect() still does) would leave value unset.
|
|
731
|
+
self.height = observer.height + 1;
|
|
732
|
+
recompute(self, false);
|
|
733
|
+
}
|
|
400
734
|
else {
|
|
401
735
|
self.height = observer.height + 1;
|
|
402
736
|
insertIntoHeap(self);
|
|
@@ -416,32 +750,163 @@ const computed = <T>(fn: Computed<T>['fn']): Computed<T> => {
|
|
|
416
750
|
}
|
|
417
751
|
|
|
418
752
|
return self;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Reuses the same recompute-nesting counter schedule() consults, so writes inside fn defer
|
|
756
|
+
// scheduling until fn returns; pair with flush() for a synchronous transaction.
|
|
757
|
+
const batch = <T>(fn: () => T): T => {
|
|
758
|
+
depth++;
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
return fn();
|
|
762
|
+
}
|
|
763
|
+
finally {
|
|
764
|
+
depth--;
|
|
765
|
+
|
|
766
|
+
if (!depth) {
|
|
767
|
+
schedule();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// A fn returning a Promise or AsyncIterable transparently becomes an async computed: the first run is
|
|
773
|
+
// the probe, reused as the factory (no duplicate dispatch). A plain fn returns the node directly.
|
|
774
|
+
const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<T>) => boolean) | null = null): ComputedResult<T> => {
|
|
775
|
+
// eager probe so self.value carries fn's return even when this is a non-first tracked op — the
|
|
776
|
+
// detection below cannot depend on the deferred branch, which never runs fn synchronously.
|
|
777
|
+
let o = observer,
|
|
778
|
+
self: Computed<unknown> = makeComputed(fn, true),
|
|
779
|
+
value = self.value;
|
|
780
|
+
|
|
781
|
+
if (isPromise(value) || (value != null && typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function')) {
|
|
782
|
+
// Built untracked: makeAsyncComputed's polling effect + wrapper take makeComputed's
|
|
783
|
+
// observer-null path (the proven top-level flow) instead of entangling with the enclosing
|
|
784
|
+
// observer's recompute, which deadlocks the scheduler. The probe is already linked+owned by
|
|
785
|
+
// the parent as the factory; the wrapper needs its own teardown tied to that same scope.
|
|
786
|
+
self = untrack(() => makeAsyncComputed(self as Computed<Promise<unknown> | AsyncIterable<unknown>>));
|
|
787
|
+
|
|
788
|
+
if (o) {
|
|
789
|
+
onCleanup(() => dispose(self));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
self.equals = equals as ((a: unknown, b: unknown) => boolean) | null;
|
|
794
|
+
|
|
795
|
+
return self as unknown as ComputedResult<T>;
|
|
419
796
|
};
|
|
420
797
|
|
|
421
|
-
|
|
422
|
-
|
|
798
|
+
// Forces a re-derivation without the dummy-signal-dependency hack. writes++ FIRST so a gv-stamped
|
|
799
|
+
// node cannot skip the forced re-run via update()'s clean-graph fast path; an async computed's wrapper
|
|
800
|
+
// redirects to its factory so the promise re-dispatches (a refetch).
|
|
801
|
+
computed.invalidate = <T>(c: Computed<T>): void => {
|
|
802
|
+
let meta = asyncMeta.get(c);
|
|
423
803
|
|
|
424
|
-
|
|
804
|
+
if (meta) {
|
|
805
|
+
computed.invalidate(meta.factory);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
425
808
|
|
|
426
|
-
|
|
427
|
-
|
|
809
|
+
writes++;
|
|
810
|
+
c.state |= STATE_DIRTY;
|
|
811
|
+
insertIntoHeap(c);
|
|
812
|
+
schedule();
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// Teardown runs as an iterative LIFO drain, not recursion: a recursive dispose→unlink→dispose cascade
|
|
816
|
+
// overflows the call stack on deep chains. A dispose issued while a drain runs enqueues and returns,
|
|
817
|
+
// so the running drain picks it up. Field-nulling keeps the drain exactly-once, so a double dispose
|
|
818
|
+
// stays a no-op. The try/finally guarantees `draining` resets even if a cleanup/disposal callback throws.
|
|
819
|
+
const dispose = <T>(computed: Computed<T>): void => {
|
|
820
|
+
// A dispose issued mid-drain only enqueues; the running drain picks it up, so the first node is
|
|
821
|
+
// processed inline (no worklist node) and the pool is touched only for re-entrant deep cascades.
|
|
822
|
+
if (draining) {
|
|
823
|
+
disposeHead = walkPush(computed as Computed<unknown>, null, disposeHead);
|
|
824
|
+
return;
|
|
428
825
|
}
|
|
429
826
|
|
|
430
|
-
|
|
827
|
+
draining = true;
|
|
431
828
|
|
|
432
|
-
|
|
433
|
-
|
|
829
|
+
let node: Computed<unknown> = computed as Computed<unknown>;
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
for (;;) {
|
|
833
|
+
deleteFromHeap(node);
|
|
834
|
+
|
|
835
|
+
let dep = node.deps;
|
|
836
|
+
|
|
837
|
+
while (dep) {
|
|
838
|
+
dep = unlink(dep);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
node.deps = null;
|
|
842
|
+
|
|
843
|
+
if (node.cleanup) {
|
|
844
|
+
cleanup(node);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (node.disposal) {
|
|
848
|
+
let d = node.disposal;
|
|
849
|
+
|
|
850
|
+
node.disposal = null;
|
|
851
|
+
d();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (disposeHead === null) {
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
node = disposeHead.computed!;
|
|
859
|
+
disposeHead = walkPop(disposeHead);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
finally {
|
|
863
|
+
draining = false;
|
|
434
864
|
}
|
|
435
865
|
};
|
|
436
866
|
|
|
437
|
-
const effect = <T>(fn: Computed<T>['fn']) => {
|
|
438
|
-
let
|
|
867
|
+
const effect = <T>(fn: Computed<T>['fn'], apply?: (value: T, prev: T | undefined) => void) => {
|
|
868
|
+
let prev: T | undefined;
|
|
869
|
+
|
|
870
|
+
let c = makeComputed<T | undefined>(
|
|
871
|
+
apply
|
|
872
|
+
? (o) => {
|
|
873
|
+
let v = fn(o);
|
|
874
|
+
|
|
875
|
+
untrack(() => apply(v, prev));
|
|
876
|
+
prev = v;
|
|
877
|
+
|
|
878
|
+
return v;
|
|
879
|
+
}
|
|
880
|
+
: fn
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
c.state |= STATE_EFFECT;
|
|
884
|
+
|
|
885
|
+
// The creation run precedes the EFFECT tag, so its failure bypasses recompute's rethrow arm.
|
|
886
|
+
if (c.state & STATE_ERROR) {
|
|
887
|
+
let err = c.error;
|
|
888
|
+
|
|
889
|
+
microtask(() => {
|
|
890
|
+
throw err;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
439
893
|
|
|
440
894
|
return () => {
|
|
441
895
|
dispose(c);
|
|
442
896
|
};
|
|
443
897
|
};
|
|
444
898
|
|
|
899
|
+
// RUNNING/RESCHEDULE means a pass is already draining (or a flush is already in this call chain);
|
|
900
|
+
// re-entering stabilize here would corrupt heap_i, so this is a deliberate no-op.
|
|
901
|
+
// Loops (not a single call): a write during a pass can target a height stabilize()'s current
|
|
902
|
+
// pass already scanned past, which only flips stabilizer to RESCHEDULE for the *next* microtask
|
|
903
|
+
// rather than draining in-pass — looping here is what actually settles that tail synchronously.
|
|
904
|
+
const flush = (): void => {
|
|
905
|
+
while (stabilizer === STABILIZER_SCHEDULED) {
|
|
906
|
+
stabilize();
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
445
910
|
const isComputed = (value: unknown): value is Computed<unknown> => {
|
|
446
911
|
return isObject(value) && !!((value as unknown as Computed<unknown>).state & STATE_COMPUTED);
|
|
447
912
|
};
|
|
@@ -472,11 +937,33 @@ const onCleanup = (fn: VoidFunction): typeof fn => {
|
|
|
472
937
|
return fn;
|
|
473
938
|
};
|
|
474
939
|
|
|
940
|
+
const peek = <T>(node: Signal<T> | Computed<T>): T => {
|
|
941
|
+
if ((node as Computed<T>).state & STATE_COMPUTED) {
|
|
942
|
+
if (pendingHead !== null) {
|
|
943
|
+
drainPending();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
pull(node as Computed<T>);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if ((node as Computed<T>).state & STATE_ERROR) {
|
|
950
|
+
throw (node as Computed<T>).error;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return node.value;
|
|
954
|
+
};
|
|
955
|
+
|
|
475
956
|
const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
476
957
|
if (observer) {
|
|
477
958
|
link(node, observer);
|
|
478
959
|
|
|
479
960
|
if ((node as Computed<unknown>).state & STATE_COMPUTED) {
|
|
961
|
+
// Invariant 1: a tracked mid-cycle read must see pending writes — drain so the heap
|
|
962
|
+
// and this node's notify bits reflect them before the broadcast condition is read
|
|
963
|
+
if (pendingHead !== null) {
|
|
964
|
+
drainPending();
|
|
965
|
+
}
|
|
966
|
+
|
|
480
967
|
let height = (node as Computed<T>).height;
|
|
481
968
|
|
|
482
969
|
if (height >= observer.height) {
|
|
@@ -484,21 +971,15 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
|
484
971
|
}
|
|
485
972
|
|
|
486
973
|
if (height >= heap_i || (node as Computed<T>).state & STATE_NOTIFY_MASK) {
|
|
487
|
-
|
|
488
|
-
notified = true;
|
|
489
|
-
|
|
490
|
-
for (let i = 0; i <= heap_n; i++) {
|
|
491
|
-
for (let computed = heap[i]; computed !== undefined; computed = computed.nextHeap) {
|
|
492
|
-
notify(computed, STATE_DIRTY);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
update(node as Computed<T>);
|
|
974
|
+
pull(node as Computed<T>);
|
|
498
975
|
}
|
|
499
976
|
}
|
|
500
977
|
}
|
|
501
978
|
|
|
979
|
+
if ((node as Computed<T>).state & STATE_ERROR) {
|
|
980
|
+
throw (node as Computed<T>).error;
|
|
981
|
+
}
|
|
982
|
+
|
|
502
983
|
return node.value;
|
|
503
984
|
};
|
|
504
985
|
|
|
@@ -536,8 +1017,15 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
|
536
1017
|
|
|
537
1018
|
root.disposables = 0;
|
|
538
1019
|
|
|
539
|
-
const signal = <T>(value: T): Signal<T> => {
|
|
1020
|
+
const signal = <T>(value: T, equals: ((a: T, b: T) => boolean) | null = null): Signal<T> => {
|
|
540
1021
|
return {
|
|
1022
|
+
equals: equals as ((a: unknown, b: unknown) => boolean) | null,
|
|
1023
|
+
key: undefined,
|
|
1024
|
+
keys: null,
|
|
1025
|
+
nextPending: null,
|
|
1026
|
+
parent: undefined,
|
|
1027
|
+
rv: 0,
|
|
1028
|
+
state: 0,
|
|
541
1029
|
subs: null,
|
|
542
1030
|
subsTail: null,
|
|
543
1031
|
type: SIGNAL,
|
|
@@ -545,12 +1033,73 @@ const signal = <T>(value: T): Signal<T> => {
|
|
|
545
1033
|
};
|
|
546
1034
|
};
|
|
547
1035
|
|
|
1036
|
+
// SameValueZero (Map) key lookup: NaN matches itself, ±0 collapse — slightly wider than === for those.
|
|
1037
|
+
// Object keys compare by reference and must be stable. The initial snapshot below uses ===.
|
|
1038
|
+
signal.selector = <T>(node: Signal<T>, key: T): boolean => {
|
|
1039
|
+
if (!observer) {
|
|
1040
|
+
return node.value === key;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
let keys = (node.keys ??= new Map()),
|
|
1044
|
+
entry = keys.get(key);
|
|
1045
|
+
|
|
1046
|
+
if (entry === undefined) {
|
|
1047
|
+
keys.set(key, entry = {
|
|
1048
|
+
equals: null,
|
|
1049
|
+
key,
|
|
1050
|
+
keys: null,
|
|
1051
|
+
nextPending: null,
|
|
1052
|
+
parent: node,
|
|
1053
|
+
rv: 0,
|
|
1054
|
+
state: 0,
|
|
1055
|
+
subs: null,
|
|
1056
|
+
subsTail: null,
|
|
1057
|
+
type: SIGNAL,
|
|
1058
|
+
value: node.value === key
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return read(entry);
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const untrack = <T>(fn: () => T): T => {
|
|
1066
|
+
let o = observer;
|
|
1067
|
+
|
|
1068
|
+
observer = null;
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
return fn();
|
|
1072
|
+
}
|
|
1073
|
+
finally {
|
|
1074
|
+
observer = o;
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
548
1078
|
const write = <T>(signal: Signal<T>, value: T) => {
|
|
549
|
-
|
|
1079
|
+
let prev = signal.value;
|
|
1080
|
+
|
|
1081
|
+
if (signal.equals === null ? prev === value : signal.equals(prev, value)) {
|
|
550
1082
|
return;
|
|
551
1083
|
}
|
|
552
1084
|
|
|
553
1085
|
signal.value = value;
|
|
1086
|
+
writes++;
|
|
1087
|
+
|
|
1088
|
+
// Per-key fan-out: only the leaving (prev) and entering (value) entries flip — O(2), key-count independent.
|
|
1089
|
+
// Runs before the subs === null exit: a parent may carry per-key subscribers yet zero direct subs.
|
|
1090
|
+
if (signal.keys !== null) {
|
|
1091
|
+
let entry = signal.keys.get(prev);
|
|
1092
|
+
|
|
1093
|
+
if (entry !== undefined) {
|
|
1094
|
+
write(entry, false);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
entry = signal.keys.get(value);
|
|
1098
|
+
|
|
1099
|
+
if (entry !== undefined) {
|
|
1100
|
+
write(entry, true);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
554
1103
|
|
|
555
1104
|
if (signal.subs === null) {
|
|
556
1105
|
return;
|
|
@@ -558,8 +1107,10 @@ const write = <T>(signal: Signal<T>, value: T) => {
|
|
|
558
1107
|
|
|
559
1108
|
notified = false;
|
|
560
1109
|
|
|
561
|
-
|
|
562
|
-
|
|
1110
|
+
// O(1) deferred mark: queue the signal (self-link = tail) unless already queued; fan-out defers to drain
|
|
1111
|
+
if (signal.nextPending === null) {
|
|
1112
|
+
signal.nextPending = pendingHead ?? signal;
|
|
1113
|
+
pendingHead = signal;
|
|
563
1114
|
}
|
|
564
1115
|
|
|
565
1116
|
schedule();
|
|
@@ -567,13 +1118,17 @@ const write = <T>(signal: Signal<T>, value: T) => {
|
|
|
567
1118
|
|
|
568
1119
|
|
|
569
1120
|
export {
|
|
570
|
-
|
|
1121
|
+
batch,
|
|
1122
|
+
computed,
|
|
571
1123
|
dispose,
|
|
572
1124
|
effect,
|
|
1125
|
+
flush,
|
|
573
1126
|
isComputed, isSignal,
|
|
574
1127
|
onCleanup,
|
|
1128
|
+
peek,
|
|
575
1129
|
read, root,
|
|
576
1130
|
signal,
|
|
1131
|
+
untrack,
|
|
577
1132
|
write
|
|
578
1133
|
};
|
|
579
1134
|
export type { Computed, Signal };
|