@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
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, 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
|
+
}
|
|
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);
|
|
186
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,165 @@ 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;
|
|
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;
|
|
348
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),
|
|
363
616
|
v = 0;
|
|
364
617
|
|
|
365
|
-
|
|
366
|
-
let
|
|
618
|
+
let stop = effect(() => {
|
|
619
|
+
let fail = (e: unknown) => {
|
|
620
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
621
|
+
write(error, e === undefined ? new Error('reactivity: async computed rejected with undefined') : e);
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
id = ++v,
|
|
625
|
+
// Heap membership (a write's eager insert) marks a pending re-run the notify mask alone misses.
|
|
626
|
+
result = read(factory);
|
|
627
|
+
|
|
628
|
+
if (isPromise(result)) {
|
|
629
|
+
(result as Promise<T>).then(
|
|
630
|
+
(value) => {
|
|
631
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
632
|
+
write(error, undefined);
|
|
633
|
+
write(node, value);
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
fail
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
else if (result != null && typeof (result as AsyncIterable<T>)[Symbol.asyncIterator] === 'function') {
|
|
640
|
+
let it = (result as AsyncIterable<T>)[Symbol.asyncIterator]();
|
|
641
|
+
|
|
642
|
+
onCleanup(() => {
|
|
643
|
+
it.return?.();
|
|
644
|
+
});
|
|
367
645
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
646
|
+
let step = (r: IteratorResult<T>) => {
|
|
647
|
+
if (id !== v || (factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!r.done) {
|
|
652
|
+
write(error, undefined);
|
|
653
|
+
write(node, r.value);
|
|
654
|
+
it.next().then(step, fail);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
untrack(() => it.next()).then(step, fail);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
write(error, undefined);
|
|
662
|
+
write(node, result as T);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
let wrapper = makeComputed<T | undefined>(() => {
|
|
667
|
+
let e = read(error);
|
|
668
|
+
|
|
669
|
+
if (e !== undefined) {
|
|
670
|
+
throw e;
|
|
371
671
|
}
|
|
372
|
-
}, () => {});
|
|
373
|
-
}));
|
|
374
672
|
|
|
375
|
-
|
|
376
|
-
};
|
|
673
|
+
return read(node);
|
|
674
|
+
});
|
|
377
675
|
|
|
378
|
-
|
|
676
|
+
asyncMeta.set(wrapper as Computed<unknown>, { factory: factory as Computed<unknown> });
|
|
677
|
+
wrapper.disposal = stop;
|
|
678
|
+
|
|
679
|
+
return wrapper;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Computed<T> {
|
|
379
683
|
let self: Computed<T> = {
|
|
380
684
|
cleanup: null,
|
|
381
685
|
deps: null,
|
|
382
686
|
depsTail: null,
|
|
687
|
+
disposal: null,
|
|
688
|
+
equals: null,
|
|
689
|
+
error: null,
|
|
383
690
|
fn: fn,
|
|
691
|
+
gv: 0,
|
|
384
692
|
height: 0,
|
|
385
693
|
nextHeap: undefined,
|
|
386
|
-
prevHeap: null as
|
|
694
|
+
prevHeap: null as unknown as Computed<unknown>,
|
|
695
|
+
rv: 0,
|
|
387
696
|
state: STATE_COMPUTED,
|
|
388
697
|
subs: null,
|
|
389
698
|
subsTail: null,
|
|
@@ -397,6 +706,13 @@ const computed = <T>(fn: Computed<T>['fn']): Computed<T> => {
|
|
|
397
706
|
self.height = observer.height;
|
|
398
707
|
recompute(self, false);
|
|
399
708
|
}
|
|
709
|
+
else if (eager) {
|
|
710
|
+
// computed() must know fn's return type to pick sync vs async. This probe runs BEFORE
|
|
711
|
+
// link() below, so self has no subs yet — recompute's propagate is a no-op and cannot
|
|
712
|
+
// re-run the parent. Deferring here (as effect() still does) would leave value unset.
|
|
713
|
+
self.height = observer.height + 1;
|
|
714
|
+
recompute(self, false);
|
|
715
|
+
}
|
|
400
716
|
else {
|
|
401
717
|
self.height = observer.height + 1;
|
|
402
718
|
insertIntoHeap(self);
|
|
@@ -416,32 +732,162 @@ const computed = <T>(fn: Computed<T>['fn']): Computed<T> => {
|
|
|
416
732
|
}
|
|
417
733
|
|
|
418
734
|
return self;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Reuses the same recompute-nesting counter schedule() consults, so writes inside fn defer
|
|
738
|
+
// scheduling until fn returns; pair with flush() for a synchronous transaction.
|
|
739
|
+
const batch = <T>(fn: () => T): T => {
|
|
740
|
+
depth++;
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
return fn();
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
depth--;
|
|
747
|
+
|
|
748
|
+
if (!depth) {
|
|
749
|
+
schedule();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
419
752
|
};
|
|
420
753
|
|
|
421
|
-
|
|
422
|
-
|
|
754
|
+
// A fn returning a Promise or AsyncIterable transparently becomes an async computed: the first run is
|
|
755
|
+
// the probe, reused as the factory (no duplicate dispatch). A plain fn returns the node directly.
|
|
756
|
+
const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<T>) => boolean) | null = null): Computed<Settled<T>> => {
|
|
757
|
+
// eager probe so self.value carries fn's return even when this is a non-first tracked op — the
|
|
758
|
+
// detection below cannot depend on the deferred branch, which never runs fn synchronously.
|
|
759
|
+
let o = observer,
|
|
760
|
+
self: Computed<unknown> = makeComputed(fn, true),
|
|
761
|
+
value = self.value;
|
|
423
762
|
|
|
424
|
-
|
|
763
|
+
if (isPromise(value) || (value != null && typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function')) {
|
|
764
|
+
// Built untracked: makeAsyncComputed's polling effect + wrapper take makeComputed's
|
|
765
|
+
// observer-null path (the proven top-level flow) instead of entangling with the enclosing
|
|
766
|
+
// observer's recompute, which deadlocks the scheduler. The probe is already linked+owned by
|
|
767
|
+
// the parent as the factory; the wrapper needs its own teardown tied to that same scope.
|
|
768
|
+
self = untrack(() => makeAsyncComputed(self as Computed<Promise<unknown> | AsyncIterable<unknown>>));
|
|
425
769
|
|
|
426
|
-
|
|
427
|
-
|
|
770
|
+
if (o) {
|
|
771
|
+
onCleanup(() => dispose(self));
|
|
772
|
+
}
|
|
428
773
|
}
|
|
429
774
|
|
|
430
|
-
|
|
775
|
+
self.equals = equals as ((a: unknown, b: unknown) => boolean) | null;
|
|
431
776
|
|
|
432
|
-
|
|
433
|
-
|
|
777
|
+
return self as Computed<Settled<T>>;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Forces a re-derivation without the dummy-signal-dependency hack. writes++ FIRST so a gv-stamped
|
|
781
|
+
// node cannot skip the forced re-run via update()'s clean-graph fast path; an async computed's wrapper
|
|
782
|
+
// redirects to its factory so the promise re-dispatches (a refetch).
|
|
783
|
+
computed.invalidate = <T>(c: Computed<T>): void => {
|
|
784
|
+
let meta = asyncMeta.get(c);
|
|
785
|
+
|
|
786
|
+
if (meta) {
|
|
787
|
+
computed.invalidate(meta.factory);
|
|
788
|
+
return;
|
|
434
789
|
}
|
|
790
|
+
|
|
791
|
+
writes++;
|
|
792
|
+
c.state |= STATE_DIRTY;
|
|
793
|
+
insertIntoHeap(c);
|
|
794
|
+
schedule();
|
|
435
795
|
};
|
|
436
796
|
|
|
437
|
-
|
|
438
|
-
|
|
797
|
+
// Teardown runs as an iterative LIFO drain, not recursion: a recursive dispose→unlink→dispose cascade
|
|
798
|
+
// overflows the call stack on deep chains. A dispose issued while a drain runs enqueues and returns,
|
|
799
|
+
// so the running drain picks it up. Field-nulling keeps the drain exactly-once, so a double dispose
|
|
800
|
+
// stays a no-op. The try/finally guarantees `draining` resets even if a cleanup/disposal callback throws.
|
|
801
|
+
const dispose = <T>(computed: Computed<T>): void => {
|
|
802
|
+
// A dispose issued mid-drain only enqueues; the running drain picks it up, so the first node is
|
|
803
|
+
// processed inline (no worklist node) and the pool is touched only for re-entrant deep cascades.
|
|
804
|
+
if (draining) {
|
|
805
|
+
disposeHead = walkPush(computed as Computed<unknown>, null, disposeHead);
|
|
806
|
+
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
draining = true;
|
|
811
|
+
|
|
812
|
+
let node: Computed<unknown> = computed as Computed<unknown>;
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
for (;;) {
|
|
816
|
+
deleteFromHeap(node);
|
|
817
|
+
|
|
818
|
+
let dep = node.deps;
|
|
819
|
+
|
|
820
|
+
while (dep) {
|
|
821
|
+
dep = unlink(dep);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
node.deps = null;
|
|
825
|
+
|
|
826
|
+
if (node.cleanup) {
|
|
827
|
+
cleanup(node);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (node.disposal) {
|
|
831
|
+
let d = node.disposal;
|
|
832
|
+
|
|
833
|
+
node.disposal = null;
|
|
834
|
+
d();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (disposeHead === null) {
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
node = disposeHead.computed!;
|
|
842
|
+
disposeHead = walkPop(disposeHead);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
draining = false;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const effect = <T>(fn: Computed<T>['fn'], onError?: (e: unknown) => void) => {
|
|
851
|
+
let c = makeComputed<T | undefined>(
|
|
852
|
+
onError
|
|
853
|
+
? (o) => {
|
|
854
|
+
try {
|
|
855
|
+
return fn(o);
|
|
856
|
+
}
|
|
857
|
+
catch (e) {
|
|
858
|
+
onError(e);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
: fn
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
c.state |= STATE_EFFECT;
|
|
865
|
+
|
|
866
|
+
// The creation run precedes the EFFECT tag, so its failure bypasses recompute's rethrow arm.
|
|
867
|
+
if (c.state & STATE_ERROR) {
|
|
868
|
+
let err = c.error;
|
|
869
|
+
|
|
870
|
+
microtask(() => {
|
|
871
|
+
throw err;
|
|
872
|
+
});
|
|
873
|
+
}
|
|
439
874
|
|
|
440
875
|
return () => {
|
|
441
876
|
dispose(c);
|
|
442
877
|
};
|
|
443
878
|
};
|
|
444
879
|
|
|
880
|
+
// RUNNING/RESCHEDULE means a pass is already draining (or a flush is already in this call chain);
|
|
881
|
+
// re-entering stabilize here would corrupt heap_i, so this is a deliberate no-op.
|
|
882
|
+
// Loops (not a single call): a write during a pass can target a height stabilize()'s current
|
|
883
|
+
// pass already scanned past, which only flips stabilizer to RESCHEDULE for the *next* microtask
|
|
884
|
+
// rather than draining in-pass — looping here is what actually settles that tail synchronously.
|
|
885
|
+
const flush = (): void => {
|
|
886
|
+
while (stabilizer === STABILIZER_SCHEDULED) {
|
|
887
|
+
stabilize();
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
445
891
|
const isComputed = (value: unknown): value is Computed<unknown> => {
|
|
446
892
|
return isObject(value) && !!((value as unknown as Computed<unknown>).state & STATE_COMPUTED);
|
|
447
893
|
};
|
|
@@ -472,11 +918,33 @@ const onCleanup = (fn: VoidFunction): typeof fn => {
|
|
|
472
918
|
return fn;
|
|
473
919
|
};
|
|
474
920
|
|
|
921
|
+
const peek = <T>(node: Signal<T> | Computed<T>): T => {
|
|
922
|
+
if ((node as Computed<T>).state & STATE_COMPUTED) {
|
|
923
|
+
if (pendingHead !== null) {
|
|
924
|
+
drainPending();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
pull(node as Computed<T>);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if ((node as Computed<T>).state & STATE_ERROR) {
|
|
931
|
+
throw (node as Computed<T>).error;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return node.value;
|
|
935
|
+
};
|
|
936
|
+
|
|
475
937
|
const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
476
938
|
if (observer) {
|
|
477
939
|
link(node, observer);
|
|
478
940
|
|
|
479
941
|
if ((node as Computed<unknown>).state & STATE_COMPUTED) {
|
|
942
|
+
// Invariant 1: a tracked mid-cycle read must see pending writes — drain so the heap
|
|
943
|
+
// and this node's notify bits reflect them before the broadcast condition is read
|
|
944
|
+
if (pendingHead !== null) {
|
|
945
|
+
drainPending();
|
|
946
|
+
}
|
|
947
|
+
|
|
480
948
|
let height = (node as Computed<T>).height;
|
|
481
949
|
|
|
482
950
|
if (height >= observer.height) {
|
|
@@ -484,21 +952,15 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
|
484
952
|
}
|
|
485
953
|
|
|
486
954
|
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>);
|
|
955
|
+
pull(node as Computed<T>);
|
|
498
956
|
}
|
|
499
957
|
}
|
|
500
958
|
}
|
|
501
959
|
|
|
960
|
+
if ((node as Computed<T>).state & STATE_ERROR) {
|
|
961
|
+
throw (node as Computed<T>).error;
|
|
962
|
+
}
|
|
963
|
+
|
|
502
964
|
return node.value;
|
|
503
965
|
};
|
|
504
966
|
|
|
@@ -536,8 +998,12 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
|
536
998
|
|
|
537
999
|
root.disposables = 0;
|
|
538
1000
|
|
|
539
|
-
const signal = <T>(value: T): Signal<T> => {
|
|
1001
|
+
const signal = <T>(value: T, equals: ((a: T, b: T) => boolean) | null = null): Signal<T> => {
|
|
540
1002
|
return {
|
|
1003
|
+
equals: equals as ((a: unknown, b: unknown) => boolean) | null,
|
|
1004
|
+
keys: null,
|
|
1005
|
+
nextPending: null,
|
|
1006
|
+
rv: 0,
|
|
541
1007
|
subs: null,
|
|
542
1008
|
subsTail: null,
|
|
543
1009
|
type: SIGNAL,
|
|
@@ -545,12 +1011,72 @@ const signal = <T>(value: T): Signal<T> => {
|
|
|
545
1011
|
};
|
|
546
1012
|
};
|
|
547
1013
|
|
|
1014
|
+
// SameValueZero (Map) key lookup: NaN matches itself, ±0 collapse — slightly wider than === for those.
|
|
1015
|
+
// Object keys compare by reference and must be stable. The initial snapshot below uses ===.
|
|
1016
|
+
signal.selector = <T>(node: Signal<T>, key: T): boolean => {
|
|
1017
|
+
if (!observer) {
|
|
1018
|
+
return node.value === key;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let keys = (node.keys ??= new Map()),
|
|
1022
|
+
entry = keys.get(key);
|
|
1023
|
+
|
|
1024
|
+
if (entry === undefined) {
|
|
1025
|
+
keys.set(key, entry = {
|
|
1026
|
+
equals: null,
|
|
1027
|
+
key,
|
|
1028
|
+
keys: null,
|
|
1029
|
+
nextPending: null,
|
|
1030
|
+
parent: node,
|
|
1031
|
+
rv: 0,
|
|
1032
|
+
subs: null,
|
|
1033
|
+
subsTail: null,
|
|
1034
|
+
type: SIGNAL,
|
|
1035
|
+
value: node.value === key
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return read(entry);
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const untrack = <T>(fn: () => T): T => {
|
|
1043
|
+
let o = observer;
|
|
1044
|
+
|
|
1045
|
+
observer = null;
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
return fn();
|
|
1049
|
+
}
|
|
1050
|
+
finally {
|
|
1051
|
+
observer = o;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
|
|
548
1055
|
const write = <T>(signal: Signal<T>, value: T) => {
|
|
549
|
-
|
|
1056
|
+
let prev = signal.value;
|
|
1057
|
+
|
|
1058
|
+
if (signal.equals === null ? prev === value : signal.equals(prev, value)) {
|
|
550
1059
|
return;
|
|
551
1060
|
}
|
|
552
1061
|
|
|
553
1062
|
signal.value = value;
|
|
1063
|
+
writes++;
|
|
1064
|
+
|
|
1065
|
+
// Per-key fan-out: only the leaving (prev) and entering (value) entries flip — O(2), key-count independent.
|
|
1066
|
+
// Runs before the subs === null exit: a parent may carry per-key subscribers yet zero direct subs.
|
|
1067
|
+
if (signal.keys !== null) {
|
|
1068
|
+
let entry = signal.keys.get(prev);
|
|
1069
|
+
|
|
1070
|
+
if (entry !== undefined) {
|
|
1071
|
+
write(entry, false);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
entry = signal.keys.get(value);
|
|
1075
|
+
|
|
1076
|
+
if (entry !== undefined) {
|
|
1077
|
+
write(entry, true);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
554
1080
|
|
|
555
1081
|
if (signal.subs === null) {
|
|
556
1082
|
return;
|
|
@@ -558,8 +1084,10 @@ const write = <T>(signal: Signal<T>, value: T) => {
|
|
|
558
1084
|
|
|
559
1085
|
notified = false;
|
|
560
1086
|
|
|
561
|
-
|
|
562
|
-
|
|
1087
|
+
// O(1) deferred mark: queue the signal (self-link = tail) unless already queued; fan-out defers to drain
|
|
1088
|
+
if (signal.nextPending === null) {
|
|
1089
|
+
signal.nextPending = pendingHead ?? signal;
|
|
1090
|
+
pendingHead = signal;
|
|
563
1091
|
}
|
|
564
1092
|
|
|
565
1093
|
schedule();
|
|
@@ -567,13 +1095,17 @@ const write = <T>(signal: Signal<T>, value: T) => {
|
|
|
567
1095
|
|
|
568
1096
|
|
|
569
1097
|
export {
|
|
570
|
-
|
|
1098
|
+
batch,
|
|
1099
|
+
computed,
|
|
571
1100
|
dispose,
|
|
572
1101
|
effect,
|
|
1102
|
+
flush,
|
|
573
1103
|
isComputed, isSignal,
|
|
574
1104
|
onCleanup,
|
|
1105
|
+
peek,
|
|
575
1106
|
read, root,
|
|
576
1107
|
signal,
|
|
1108
|
+
untrack,
|
|
577
1109
|
write
|
|
578
1110
|
};
|
|
579
1111
|
export type { Computed, Signal };
|