@solidjs/signals 2.0.0-rc.4 → 2.0.0-rc.6
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/dist/dev.js +1618 -232
- package/dist/node.cjs +2262 -1199
- package/dist/prod/boundaries.js +4 -1
- package/dist/prod/core/async.js +152 -101
- package/dist/prod/core/constants.js +55 -1
- package/dist/prod/core/core.js +305 -234
- package/dist/prod/core/effect.js +28 -28
- package/dist/prod/core/error.js +13 -1
- package/dist/prod/core/external.js +2 -2
- package/dist/prod/core/graph.js +27 -27
- package/dist/prod/core/heap.js +30 -30
- package/dist/prod/core/lanes.js +32 -32
- package/dist/prod/core/optimistic.js +57 -57
- package/dist/prod/core/owner.js +34 -34
- package/dist/prod/core/scheduler.js +346 -152
- package/dist/prod/core/verdict.js +122 -65
- package/dist/prod/index.js +3 -3
- package/dist/prod/map.js +106 -106
- package/dist/prod/signals.js +321 -26
- package/dist/prod/store/next/optimistic.js +363 -151
- package/dist/prod/store/next/patch.js +6 -6
- package/dist/prod/store/next/projection.js +25 -21
- package/dist/prod/store/next/store.js +223 -113
- package/dist/prod/store/store.js +2 -2
- package/dist/types/core/async.d.ts +2 -0
- package/dist/types/core/attribution-hooks.d.ts +11 -0
- package/dist/types/core/attribution.d.ts +57 -4
- package/dist/types/core/constants.d.ts +54 -0
- package/dist/types/core/core.d.ts +19 -20
- package/dist/types/core/dev.d.ts +8 -2
- package/dist/types/core/error.d.ts +9 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/scheduler.d.ts +39 -0
- package/dist/types/core/types.d.ts +12 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/signals.d.ts +108 -11
- package/dist/types/store/next/optimistic.d.ts +13 -10
- package/dist/types/store/next/projection.d.ts +1 -1
- package/dist/types/store/next/store.d.ts +22 -0
- package/dist/types/store/next/target.d.ts +25 -0
- package/dist/types-cjs/core/async.d.cts +2 -0
- package/dist/types-cjs/core/attribution-hooks.d.cts +11 -0
- package/dist/types-cjs/core/attribution.d.cts +57 -4
- package/dist/types-cjs/core/constants.d.cts +54 -0
- package/dist/types-cjs/core/core.d.cts +19 -20
- package/dist/types-cjs/core/dev.d.cts +8 -2
- package/dist/types-cjs/core/error.d.cts +9 -0
- package/dist/types-cjs/core/index.d.cts +2 -2
- package/dist/types-cjs/core/scheduler.d.cts +39 -0
- package/dist/types-cjs/core/types.d.cts +12 -0
- package/dist/types-cjs/index.d.cts +3 -3
- package/dist/types-cjs/signals.d.cts +108 -11
- package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
- package/dist/types-cjs/store/next/projection.d.cts +1 -1
- package/dist/types-cjs/store/next/store.d.cts +22 -0
- package/dist/types-cjs/store/next/target.d.cts +25 -0
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -42,6 +42,18 @@ class StatusError extends Error {
|
|
|
42
42
|
function unwrapStatusError(error) {
|
|
43
43
|
return error instanceof StatusError ? error.cause : error;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Rejection value of `until(fn, { timeout })` when the predicate does not turn
|
|
47
|
+
* truthy within the window. Inside an `action()`, the rejection is thrown back
|
|
48
|
+
* in at the `yield` point — catchable there, or the action fails and its
|
|
49
|
+
* optimistic state reverts.
|
|
50
|
+
*/
|
|
51
|
+
class TimeoutError extends Error {
|
|
52
|
+
constructor(message = "Timed out waiting for condition") {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "TimeoutError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
class NoOwnerError extends Error {
|
|
46
58
|
constructor() {
|
|
47
59
|
super("Context can only be accessed under a reactive root.");
|
|
@@ -117,6 +129,60 @@ const CONFIG_CHILD_COMPANIONS = 1 << 11;
|
|
|
117
129
|
* moved into the cold extension (§12), and an unconditional `_x` deref per
|
|
118
130
|
* marked node measurably taxed the propagation hot path (diamond -22%). */
|
|
119
131
|
const CONFIG_FW_CHILDREN = 1 << 12;
|
|
132
|
+
/** Authoritative-view reader (`until()`): while this node computes, reads
|
|
133
|
+
* dodge active optimistic OVERRIDES only — the predicate must observe
|
|
134
|
+
* arriving truth, never the caller's own tentative writes (which would
|
|
135
|
+
* trivially satisfy it). Everything else reads normally, INCLUDING
|
|
136
|
+
* transition-staged `_pendingValue`: staged data is authoritative (optimism
|
|
137
|
+
* lives only in override slots), and a hold that refused staged reads would
|
|
138
|
+
* deadlock on data the open transaction itself is holding (a refresh the
|
|
139
|
+
* action issued lands staged and cannot commit until the hold releases).
|
|
140
|
+
* read() checks the bit on the reading computation (`context`) directly — no
|
|
141
|
+
* ambient flag — so a shared computed the predicate pulls recomputes as
|
|
142
|
+
* itself (no bit) under the normal view, and its cache never forks. */
|
|
143
|
+
const CONFIG_AUTHORITATIVE_READ = 1 << 13;
|
|
144
|
+
/** Sticky mark: an authoritative-view reader read this node PAST an active
|
|
145
|
+
* override. The ack shape — an authoritative arrival EQUAL to the override —
|
|
146
|
+
* rides paths that are deliberately silent under A17 (every ordinary reader
|
|
147
|
+
* sees the override, so an equal landing changes nothing for them). A marked
|
|
148
|
+
* node notifies those readers on such paths anyway, so the landed truth is
|
|
149
|
+
* seen without re-firing ordinary subscribers. Never cleared — only nodes an
|
|
150
|
+
* until() predicate observed mid-override pay. */
|
|
151
|
+
const CONFIG_AUTHORITATIVE_OBSERVED = 1 << 14;
|
|
152
|
+
/** Promise-delivery effect (resolve()/until()): commits its computed value
|
|
153
|
+
* directly even when recomputing under its own held transition. These
|
|
154
|
+
* effects deliver applies on a microtask (#2930) instead of the stashed
|
|
155
|
+
* effect queues, so the value must ride the same immediate schedule — a
|
|
156
|
+
* staged value with an immediate apply delivers stale state (resolve) or
|
|
157
|
+
* deadlocks the hold (until). Safe because the node is a private leaf: no
|
|
158
|
+
* subscriber reads an effect's value, only its own apply does. */
|
|
159
|
+
const CONFIG_DIRECT_COMMIT = 1 << 15;
|
|
160
|
+
/** Fresh-pull reader (awaitable `refresh()`'s waiter effect): a read of a
|
|
161
|
+
* dirty source recomputes it inline even when the height gate defers to the
|
|
162
|
+
* flush. Closes the same-flush ordering race where a waiter created
|
|
163
|
+
* alongside a refresh() mark read the PRE-re-ask value as settled and
|
|
164
|
+
* delivered stale; with the pull, the waiter either parks on the re-ask's
|
|
165
|
+
* pending window (async — woken by the settle walk, which runs on every
|
|
166
|
+
* landing including equal-value ones) or serves its sync answer. resolve()
|
|
167
|
+
* deliberately keeps that race — its contract is "first settled value"
|
|
168
|
+
* (#2930), not "next quiescent state". */
|
|
169
|
+
const CONFIG_FRESH_READ = 1 << 16;
|
|
170
|
+
/** HELD truth (#3164): this node's staged `_pendingValue` is confirming
|
|
171
|
+
* truth riding a transaction that retains optimism, revealed only at that
|
|
172
|
+
* transaction's settle. Two arming sites, one meaning: the store fold
|
|
173
|
+
* (a landing staged into the retaining transaction) and until()'s
|
|
174
|
+
* flip-entanglement (a foreign carrier's staged write, stolen when it
|
|
175
|
+
* flipped the awaited predicate truthy). Until the reveal, ordinary
|
|
176
|
+
* readers — lane and speculative recomputes included — keep committed:
|
|
177
|
+
* the staging notified subscribers as a plain write, so without the mask
|
|
178
|
+
* a mid-hold recompute composes live optimism with the confirming truth,
|
|
179
|
+
* a frame no timeline contains (GabbeV's union tear). Authoritative
|
|
180
|
+
* readers (until()'s predicate) and latest() tunnel through — the
|
|
181
|
+
* exemption that keeps holds deadlock-free. Override-covered nodes never
|
|
182
|
+
* arm: the override is their display and its revert their notification
|
|
183
|
+
* (A17). Cleared at commit (the commit IS the reveal); subscribers masked
|
|
184
|
+
* during the hold are woken by finalizePureQueue's post-revert pass. */
|
|
185
|
+
const CONFIG_HELD_TRUTH = 1 << 17;
|
|
120
186
|
const STATUS_PENDING = 1 << 0;
|
|
121
187
|
const STATUS_ERROR = 1 << 1;
|
|
122
188
|
const STATUS_UNINITIALIZED = 1 << 2;
|
|
@@ -166,7 +232,8 @@ const defaultOptions = {
|
|
|
166
232
|
wideDeps: 30,
|
|
167
233
|
hotTime: { budgetMs: 8, windowMs: 1000 },
|
|
168
234
|
unstableMemos: 4,
|
|
169
|
-
wideWrites: 250
|
|
235
|
+
wideWrites: 250,
|
|
236
|
+
waterfalls: { minFlightMs: 50 }
|
|
170
237
|
};
|
|
171
238
|
let options = { ...defaultOptions };
|
|
172
239
|
const listeners = new Set();
|
|
@@ -359,10 +426,13 @@ function checkDepWidth(el) {
|
|
|
359
426
|
});
|
|
360
427
|
console.warn(message);
|
|
361
428
|
}
|
|
429
|
+
const hotCauses = new Map();
|
|
430
|
+
const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
362
431
|
/**
|
|
363
432
|
* Hot-scope warning — flags a scope that re-ran more than `count` times
|
|
364
433
|
* inside one `windowMs` window. Warned once per window, with the most recent
|
|
365
434
|
* cause chain named so the leaking signal is identified in the message.
|
|
435
|
+
* Fan-out spam is folded per root cause (see HotCauseWindow above).
|
|
366
436
|
*/
|
|
367
437
|
function checkHotRuns(el, event) {
|
|
368
438
|
const cfg = options.hotRuns;
|
|
@@ -377,22 +447,55 @@ function checkHotRuns(el, event) {
|
|
|
377
447
|
node._devWinCount = (node._devWinCount ?? 0) + 1;
|
|
378
448
|
if (node._devHotWarned || node._devWinCount < cfg.count) return;
|
|
379
449
|
node._devHotWarned = true;
|
|
380
|
-
|
|
450
|
+
// Root-cause key: the set of originating writes behind this scope's latest
|
|
451
|
+
// re-run. Scopes hot from the SAME roots share one aggregation window.
|
|
452
|
+
const roots = new Set();
|
|
453
|
+
rootsOf(event.causes, roots);
|
|
454
|
+
const causeKey = roots.size > 0 ? [...roots].sort().join(", ") : "(untracked)";
|
|
455
|
+
let window = hotCauses.get(causeKey);
|
|
456
|
+
if (window === undefined || now - window.winStart > cfg.windowMs) {
|
|
457
|
+
window = { winStart: now, scopes: 0, runs: 0, nextMilestone: HOT_FANOUT_FIRST_MILESTONE };
|
|
458
|
+
hotCauses.set(causeKey, window);
|
|
459
|
+
}
|
|
460
|
+
window.scopes++;
|
|
461
|
+
window.runs += node._devWinCount;
|
|
462
|
+
if (window.scopes === 1) {
|
|
463
|
+
const rootCause = event.causes.map(c => `"${c.name}" (${c.kind})`).join(", ");
|
|
464
|
+
const message =
|
|
465
|
+
`[HOT_SCOPE_RERUNS] ${event.nodeKind} "${event.nodeName}" re-ran ${node._devWinCount} times ` +
|
|
466
|
+
`in ${Math.max(1, now - node._devWinStart)}ms — a hot signal is likely leaking into this ` +
|
|
467
|
+
`scope. Latest cause: ${rootCause || "(untracked pull)"}`;
|
|
468
|
+
emitDiagnostic({
|
|
469
|
+
code: "HOT_SCOPE_RERUNS",
|
|
470
|
+
kind: "perf",
|
|
471
|
+
severity: "warn",
|
|
472
|
+
message,
|
|
473
|
+
nodeName: event.nodeName,
|
|
474
|
+
data: {
|
|
475
|
+
runs: node._devWinCount,
|
|
476
|
+
windowMs: cfg.windowMs,
|
|
477
|
+
causes: event.causes.map(c => c.name)
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
console.warn(message);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Additional scopes hot from the same cause: silent until a milestone —
|
|
484
|
+
// the culprit is the cause, and it has already been named once.
|
|
485
|
+
if (window.scopes < window.nextMilestone) return;
|
|
486
|
+
window.nextMilestone *= 10;
|
|
381
487
|
const message =
|
|
382
|
-
`[
|
|
383
|
-
|
|
384
|
-
`
|
|
488
|
+
`[HOT_SCOPE_FANOUT] ${window.scopes} scopes have gone hot (${window.runs} re-runs) within ` +
|
|
489
|
+
`${cfg.windowMs}ms, all driven by ${causeKey} — one hot cause is re-running a large part ` +
|
|
490
|
+
`of the graph. Per-scope warnings are suppressed; fix the cause. If consumers ask keyed ` +
|
|
491
|
+
`questions of it, invert with createSelector or createProjection.`;
|
|
385
492
|
emitDiagnostic({
|
|
386
|
-
code: "
|
|
493
|
+
code: "HOT_SCOPE_FANOUT",
|
|
387
494
|
kind: "perf",
|
|
388
495
|
severity: "warn",
|
|
389
496
|
message,
|
|
390
|
-
nodeName:
|
|
391
|
-
data: {
|
|
392
|
-
runs: node._devWinCount,
|
|
393
|
-
windowMs: cfg.windowMs,
|
|
394
|
-
causes: event.causes.map(c => c.name)
|
|
395
|
-
}
|
|
497
|
+
nodeName: causeKey,
|
|
498
|
+
data: { cause: causeKey, scopes: window.scopes, runs: window.runs, windowMs: cfg.windowMs }
|
|
396
499
|
});
|
|
397
500
|
console.warn(message);
|
|
398
501
|
}
|
|
@@ -571,6 +674,120 @@ function checkUnstableOutput(el, prevValue, newValue) {
|
|
|
571
674
|
});
|
|
572
675
|
console.warn(message);
|
|
573
676
|
}
|
|
677
|
+
// WeakMaps: an errored/abandoned flight must not leak its node or block GC.
|
|
678
|
+
const liveFlights = new WeakMap();
|
|
679
|
+
const landedFlights = new WeakMap();
|
|
680
|
+
/**
|
|
681
|
+
* Flight-object identity → earliest known start. Fed by markFlight() (the
|
|
682
|
+
* cooperative preload/cache declaration — populated even while attribution
|
|
683
|
+
* is disabled, so navigation-time marks survive a later enable()) and by
|
|
684
|
+
* first sightings at registration.
|
|
685
|
+
*/
|
|
686
|
+
const flightOrigins = new WeakMap();
|
|
687
|
+
let waterfallLog = [];
|
|
688
|
+
/** Deepest landed-flight cause reachable through a cause list (derived links included). */
|
|
689
|
+
function flightCauseIn(causes) {
|
|
690
|
+
let best = null;
|
|
691
|
+
for (const c of causes) {
|
|
692
|
+
let found = null;
|
|
693
|
+
if (c.kind === "async") found = landedFlights.get(c) ?? null;
|
|
694
|
+
else if (c.kind === "derived" && c.causes) found = flightCauseIn(c.causes);
|
|
695
|
+
if (found !== null && (best === null || found.chain.length > best.chain.length)) best = found;
|
|
696
|
+
}
|
|
697
|
+
return best;
|
|
698
|
+
}
|
|
699
|
+
function trackFlightStart(el, flight) {
|
|
700
|
+
if (options.waterfalls === false) return;
|
|
701
|
+
const at = now();
|
|
702
|
+
const origin = flightOrigins.get(flight) ?? at;
|
|
703
|
+
if (origin === at) flightOrigins.set(flight, at);
|
|
704
|
+
// Nearest enclosing frame with causes: create runs carry null (a node born
|
|
705
|
+
// inside a parent's recompute inherits the parent's causality — the
|
|
706
|
+
// boundary-reveal case, and the lazy sibling whose first pull is gated
|
|
707
|
+
// behind an earlier not-ready read), so walk down to the first re-run frame.
|
|
708
|
+
let parent = null;
|
|
709
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
710
|
+
const causes = frames[i].causes;
|
|
711
|
+
if (causes !== null) {
|
|
712
|
+
parent = flightCauseIn(causes);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// The sequentiality test. A marked/previously-seen flight whose origin
|
|
717
|
+
// predates the upstream landing was in the air alongside it: parallel.
|
|
718
|
+
if (parent !== null && origin < parent.landedAt) parent = null;
|
|
719
|
+
liveFlights.set(el, {
|
|
720
|
+
origin,
|
|
721
|
+
startSeq: changeSeq,
|
|
722
|
+
chain: parent === null ? [] : [...parent.chain, { name: parent.name, ms: parent.ms }]
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Flight landed (whether or not the value committed — the wall time was
|
|
727
|
+
* spent either way). Attach the measurement to the landing's fresh "async"
|
|
728
|
+
* stamp so downstream flights can chain through it, then judge the chain.
|
|
729
|
+
*/
|
|
730
|
+
function finalizeFlight(el) {
|
|
731
|
+
const flight = liveFlights.get(el);
|
|
732
|
+
if (flight === undefined) return;
|
|
733
|
+
liveFlights.delete(el);
|
|
734
|
+
const landedAt = now();
|
|
735
|
+
const ms = landedAt - flight.origin;
|
|
736
|
+
const record = el._devChange;
|
|
737
|
+
// Only a stamp this landing produced may carry the measurement — a stale
|
|
738
|
+
// async record from a previous landing must not be re-labeled.
|
|
739
|
+
if (record !== undefined && record.kind === "async" && record.seq > flight.startSeq)
|
|
740
|
+
landedFlights.set(record, { name: nodeName(el), ms, chain: flight.chain, landedAt });
|
|
741
|
+
checkWaterfall(el, flight.chain, ms);
|
|
742
|
+
}
|
|
743
|
+
function checkWaterfall(el, chain, ms) {
|
|
744
|
+
const cfg = options.waterfalls;
|
|
745
|
+
if (cfg === false) return;
|
|
746
|
+
if (chain.length > 0) {
|
|
747
|
+
waterfallLog.push({
|
|
748
|
+
chain: [...chain, { name: nodeName(el), ms }],
|
|
749
|
+
sequentialMs: chain.reduce((sum, l) => sum + l.ms, ms)
|
|
750
|
+
});
|
|
751
|
+
if (waterfallLog.length > options.historyLimit) waterfallLog.shift();
|
|
752
|
+
}
|
|
753
|
+
// The verdict: trailing run of links that were each a real wait. A fast
|
|
754
|
+
// tail (settled preload/cache hit) or a fast upstream breaks the sequence.
|
|
755
|
+
if (ms < cfg.minFlightMs) return;
|
|
756
|
+
let seq = 1;
|
|
757
|
+
let totalMs = ms;
|
|
758
|
+
for (let i = chain.length - 1; i >= 0 && chain[i].ms >= cfg.minFlightMs; i--) {
|
|
759
|
+
seq++;
|
|
760
|
+
totalMs += chain[i].ms;
|
|
761
|
+
}
|
|
762
|
+
if (seq < 2) return;
|
|
763
|
+
const node = el;
|
|
764
|
+
if ((node._devWaterfallWarnedAt ?? 0) >= seq) return;
|
|
765
|
+
node._devWaterfallWarnedAt = seq;
|
|
766
|
+
const links = [...chain.slice(chain.length - (seq - 1)), { name: nodeName(el), ms }];
|
|
767
|
+
const path = links.map(l => `"${l.name}" (${l.ms.toFixed(0)}ms)`).join(" → ");
|
|
768
|
+
const message =
|
|
769
|
+
`[ASYNC_WATERFALL] ${seq} sequential async flights — ${path} — ` +
|
|
770
|
+
`${totalMs.toFixed(0)}ms serialized: each began only after the previous resolved ` +
|
|
771
|
+
`(as far as this graph can see). If a later request doesn't need the earlier ` +
|
|
772
|
+
`response, derive both from the same inputs so they start together; if the ` +
|
|
773
|
+
`dependency is intrinsic, preload the dependent data or join the requests ` +
|
|
774
|
+
`server-side. If this work WAS already started elsewhere (a preloader or request ` +
|
|
775
|
+
`cache), have that layer stamp its promises with DEV.attribution.markFlight().`;
|
|
776
|
+
// Depth 2 is advisory-only (structured consumers see it; the console does
|
|
777
|
+
// not): a 2-chain can be an intrinsic data dependency or an unmarked
|
|
778
|
+
// preload. A 3+ chain that survived the origin test is near-certainly
|
|
779
|
+
// structural — that one earns the console.
|
|
780
|
+
const severity = seq > 2 ? "warn" : "info";
|
|
781
|
+
emitDiagnostic({
|
|
782
|
+
code: "ASYNC_WATERFALL",
|
|
783
|
+
kind: "perf",
|
|
784
|
+
severity,
|
|
785
|
+
message,
|
|
786
|
+
nodeName: nodeName(el),
|
|
787
|
+
data: { chain: links.map(l => ({ name: l.name, ms: l.ms })), sequentialMs: totalMs }
|
|
788
|
+
});
|
|
789
|
+
if (severity === "warn") console.warn(message);
|
|
790
|
+
}
|
|
574
791
|
// The engine's implementation of the core's dev hook points. Installed by
|
|
575
792
|
// enable(), uninstalled by disable() — while uninstalled the core pays one
|
|
576
793
|
// null check per site and nothing else.
|
|
@@ -600,6 +817,18 @@ const engineHooks = {
|
|
|
600
817
|
const totalMs = now() - frame.start;
|
|
601
818
|
if (frames.length > 0) frames[frames.length - 1].childMs += totalMs;
|
|
602
819
|
const selfMs = Math.max(0, totalMs - frame.childMs);
|
|
820
|
+
// Effect-output honesty: effects run with `_equals: false`, so core
|
|
821
|
+
// reports EVERY effect recompute as changed — which made effect waste
|
|
822
|
+
// invisible to costs() (and compiled JSX bindings are effects: the
|
|
823
|
+
// fan-out waste a naive selected-row produces is all effects). The
|
|
824
|
+
// engine re-derives the fact from its own snapshot: an identical
|
|
825
|
+
// committed compute output is an unchanged run. `undefined` outputs are
|
|
826
|
+
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
827
|
+
// identity of `undefined` proves nothing.
|
|
828
|
+
if (changed && frame.causes !== null && el._type) {
|
|
829
|
+
const committed = el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
|
|
830
|
+
if (committed !== undefined && committed === frame.prevValue) changed = false;
|
|
831
|
+
}
|
|
603
832
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
604
833
|
// committed change. The fresh value sits in `_pendingValue` for held
|
|
605
834
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
@@ -632,6 +861,9 @@ const engineHooks = {
|
|
|
632
861
|
refreshed(el) {
|
|
633
862
|
stampWrite(el, "refresh");
|
|
634
863
|
},
|
|
864
|
+
flightStart(el, flight) {
|
|
865
|
+
trackFlightStart(el, flight);
|
|
866
|
+
},
|
|
635
867
|
asyncStart(el) {
|
|
636
868
|
asyncStartSeq = el._devChange?.seq ?? 0;
|
|
637
869
|
asyncStartTime = el._time;
|
|
@@ -648,6 +880,9 @@ const engineHooks = {
|
|
|
648
880
|
const committed =
|
|
649
881
|
el._value !== asyncStartValue || el._time !== asyncStartTime || el._pendingValue === value;
|
|
650
882
|
if (committed) stampWrite(el, "async", prev === undefined ? NO_VALUES : prev, value);
|
|
883
|
+
// Flight over either way — an equality-swallowed landing still spent
|
|
884
|
+
// the wall time (finalizeFlight only chains through a fresh stamp).
|
|
885
|
+
finalizeFlight(el);
|
|
651
886
|
return;
|
|
652
887
|
}
|
|
653
888
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
@@ -656,6 +891,7 @@ const engineHooks = {
|
|
|
656
891
|
const change = el._devChange;
|
|
657
892
|
if (change !== undefined && change.seq > asyncStartSeq && change.kind === "write")
|
|
658
893
|
stampWrite(el, "async", NO_VALUES, value);
|
|
894
|
+
finalizeFlight(el);
|
|
659
895
|
}
|
|
660
896
|
};
|
|
661
897
|
const attribution = {
|
|
@@ -664,6 +900,8 @@ const attribution = {
|
|
|
664
900
|
frames.length = 0;
|
|
665
901
|
scopeCosts.clear();
|
|
666
902
|
writeCosts.clear();
|
|
903
|
+
waterfallLog = [];
|
|
904
|
+
hotCauses.clear();
|
|
667
905
|
setAttributionHooks(engineHooks);
|
|
668
906
|
},
|
|
669
907
|
disable() {
|
|
@@ -672,6 +910,8 @@ const attribution = {
|
|
|
672
910
|
frames.length = 0;
|
|
673
911
|
scopeCosts.clear();
|
|
674
912
|
writeCosts.clear();
|
|
913
|
+
waterfallLog = [];
|
|
914
|
+
hotCauses.clear();
|
|
675
915
|
setAttributionHooks(null);
|
|
676
916
|
},
|
|
677
917
|
subscribe(listener) {
|
|
@@ -697,6 +937,15 @@ const attribution = {
|
|
|
697
937
|
writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
|
|
698
938
|
};
|
|
699
939
|
},
|
|
940
|
+
waterfalls() {
|
|
941
|
+
return waterfallLog;
|
|
942
|
+
},
|
|
943
|
+
markFlight(flight, startedAt = now()) {
|
|
944
|
+
// Earliest wins: re-marking (a cache re-serving the same promise) must
|
|
945
|
+
// not move the origin later.
|
|
946
|
+
const existing = flightOrigins.get(flight);
|
|
947
|
+
if (existing === undefined || startedAt < existing) flightOrigins.set(flight, startedAt);
|
|
948
|
+
},
|
|
700
949
|
format: formatRerun
|
|
701
950
|
};
|
|
702
951
|
|
|
@@ -1248,6 +1497,107 @@ function mergeTransitionState(target, outgoing) {
|
|
|
1248
1497
|
}
|
|
1249
1498
|
for (const sub of outgoing._gatedSubs) target._gatedSubs.add(sub);
|
|
1250
1499
|
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Flip-entanglement (#3164 follow-up): `until()` is a declaration of
|
|
1502
|
+
* relatedness — the predicate names the condition that confirms the awaiting
|
|
1503
|
+
* transaction. When the predicate settles truthy, every live foreign
|
|
1504
|
+
* transition whose staged write it read IS the confirming event by the
|
|
1505
|
+
* user's own definition, so it merges into the awaiting transaction and
|
|
1506
|
+
* reveals at the joint settle — the cross-primitive twin of the family fold
|
|
1507
|
+
* (a landing on an optimism-carrying family joins the retaining
|
|
1508
|
+
* transaction). Non-flipping updates never pass through here: falsy
|
|
1509
|
+
* evaluations don't entangle, so unrelated traffic on the watched sources
|
|
1510
|
+
* reveals freely on its own schedule.
|
|
1511
|
+
*
|
|
1512
|
+
* Runs inside the predicate's compute (pure phase) — the confirming
|
|
1513
|
+
* transition's stamps are still live and its commit decision hasn't run, so
|
|
1514
|
+
* the merge lands before any reveal. Only the tree-shaken graphs that call
|
|
1515
|
+
* `until()` retain this.
|
|
1516
|
+
*/
|
|
1517
|
+
function entangleConfirmingTransitions(obs, target) {
|
|
1518
|
+
target = currentTransition(target);
|
|
1519
|
+
if (target._done === true) return;
|
|
1520
|
+
// The confirming evidence is a dep whose value is STAGED (pending,
|
|
1521
|
+
// uncommitted) at flip evaluation — committed deps are public already and
|
|
1522
|
+
// carry nothing to entangle. A staged dep lives in one of two carriers: a
|
|
1523
|
+
// stamped transition, or the queue's current batch (ambient registrations
|
|
1524
|
+
// don't stamp; "ambient work IS a transaction" — the batch is the
|
|
1525
|
+
// carrier). The entangle STEALS the carrier's staged cargo — its pending
|
|
1526
|
+
// nodes move (re-stamped) into the awaiting transaction and reveal at its
|
|
1527
|
+
// settle — but never the carrier itself: its async reporters, actions,
|
|
1528
|
+
// and stashes are its own future (a live stream's flight must not chain
|
|
1529
|
+
// the awaiting transaction to landings that haven't happened; a merged
|
|
1530
|
+
// reporter deadlocked exactly that way).
|
|
1531
|
+
let stole = false;
|
|
1532
|
+
for (let l = obs._deps; l !== null; l = l._nextDep) {
|
|
1533
|
+
const dep = l._dep;
|
|
1534
|
+
if (dep._pendingValue !== NOT_PENDING) {
|
|
1535
|
+
const stamp = dep._transition;
|
|
1536
|
+
const t = stamp != null ? currentTransition(stamp) : null;
|
|
1537
|
+
// Skip the awaiting transaction's own cargo (t === target: a
|
|
1538
|
+
// fold-staged landing or a write the action itself issued — hold and
|
|
1539
|
+
// reveal already correct) and dead carriers. Ambient-batch staging
|
|
1540
|
+
// (t === null) must leave the batch NOW — it commits at this flush's
|
|
1541
|
+
// end, which would reveal the confirmation under the live optimism
|
|
1542
|
+
// it just confirmed.
|
|
1543
|
+
const carrier =
|
|
1544
|
+
t === null
|
|
1545
|
+
? currentBatch._pendingNodes
|
|
1546
|
+
: t !== target && t._done !== true
|
|
1547
|
+
? t._pendingNodes
|
|
1548
|
+
: null;
|
|
1549
|
+
if (carrier !== null) stole = stealEntangledCargo(carrier, target) || stole;
|
|
1550
|
+
}
|
|
1551
|
+
if (l === obs._depsTail) break;
|
|
1552
|
+
}
|
|
1553
|
+
// The steal never activates the awaiting transaction: the predicate can
|
|
1554
|
+
// flip inside another transaction's finalize heap, and adopting the queue
|
|
1555
|
+
// batch there hands the stolen cargo to that finalize's commit sweep — a
|
|
1556
|
+
// premature reveal at a foreign settle. Subscribers that computed against
|
|
1557
|
+
// the pre-steal world were re-dirtied by the steal itself, so this
|
|
1558
|
+
// flush's applies paint the masked (mid-hold) view; the cargo commits at
|
|
1559
|
+
// the awaiting transaction's own settle.
|
|
1560
|
+
}
|
|
1561
|
+
/** Move a confirming carrier's staged nodes into the awaiting transaction:
|
|
1562
|
+
* re-stamp and arm the held-truth mask (override-covered nodes skip it —
|
|
1563
|
+
* the override already hides their staged value per A17, and is usually
|
|
1564
|
+
* the very optimism this confirmation settles); the mask's commit
|
|
1565
|
+
* registers the settle-side post-revert wake. The carrier's array is
|
|
1566
|
+
* emptied so its own commit point commits none of the stolen cargo.
|
|
1567
|
+
*
|
|
1568
|
+
* EFFECT subs of stolen nodes re-run: any that recomputed against the
|
|
1569
|
+
* staging BEFORE the steal (the carrier's landing notified them as a plain
|
|
1570
|
+
* write) hold a private torn result — override composed with confirming
|
|
1571
|
+
* truth — that the next paint gate (stash-point lane run, a foreign
|
|
1572
|
+
* flush's completion drain) would show. Re-running them under the mask
|
|
1573
|
+
* re-derives the mid-hold view in this same heap pass. PURE computeds are
|
|
1574
|
+
* deliberately NOT re-run: a torn staged value of theirs is itself stolen
|
|
1575
|
+
* cargo — masked at read, so ordinary readers already serve their
|
|
1576
|
+
* committed value — while re-running them would re-derive the OLD world
|
|
1577
|
+
* and re-stage it over the held truth. The reveal re-notifies
|
|
1578
|
+
* (commitPendingNodes), which is when they re-derive for real. */
|
|
1579
|
+
function stealEntangledCargo(carrier, target) {
|
|
1580
|
+
if (carrier === target._pendingNodes || carrier.length === 0) return false;
|
|
1581
|
+
for (let i = 0; i < carrier.length; i++) {
|
|
1582
|
+
const node = carrier[i];
|
|
1583
|
+
node._transition = target;
|
|
1584
|
+
target._pendingNodes.push(node);
|
|
1585
|
+
// Override-covered nodes stay silent AND unmasked: the override is the
|
|
1586
|
+
// display (A17 — its staging never notified, its revert will), so their
|
|
1587
|
+
// subs saw nothing and re-running one would break the silence with a
|
|
1588
|
+
// duplicate fire of an unchanged view.
|
|
1589
|
+
if (!hasActiveOverride$1(node)) {
|
|
1590
|
+
node._config |= CONFIG_HELD_TRUTH;
|
|
1591
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
1592
|
+
const sub = s._sub;
|
|
1593
|
+
if (sub._type && !(sub._config & CONFIG_AUTHORITATIVE_READ)) enqueueSub(sub);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
carrier.length = 0;
|
|
1598
|
+
transitions.add(target);
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1251
1601
|
function schedule() {
|
|
1252
1602
|
if (halted) {
|
|
1253
1603
|
notifyHalted();
|
|
@@ -1446,6 +1796,10 @@ class GlobalQueue extends Queue {
|
|
|
1446
1796
|
static _laneReadsCommitted = null;
|
|
1447
1797
|
static _recomputeLane = null;
|
|
1448
1798
|
static _laneAsyncPending = null;
|
|
1799
|
+
/** Authoritative-view reader wakeup (until()): installed at first until() call.
|
|
1800
|
+
* Call sites are gated by CONFIG_AUTHORITATIVE_OBSERVED, which only until()'s
|
|
1801
|
+
* carve-out read can set, so `!` invocations are safe once the gate holds. */
|
|
1802
|
+
static _notifyAuthoritativeObservers = null;
|
|
1449
1803
|
static _laneAsyncSettled = null;
|
|
1450
1804
|
static _trackOptimisticStore = null;
|
|
1451
1805
|
flush() {
|
|
@@ -1589,8 +1943,17 @@ class GlobalQueue extends Queue {
|
|
|
1589
1943
|
return false;
|
|
1590
1944
|
}
|
|
1591
1945
|
initTransition(transition) {
|
|
1592
|
-
if (transition)
|
|
1593
|
-
|
|
1946
|
+
if (transition) {
|
|
1947
|
+
transition = currentTransition(transition);
|
|
1948
|
+
// A finished transaction cannot be re-entered: its state is committed
|
|
1949
|
+
// or reverted, so "rejoining" it (A26) is meaningless and re-activating
|
|
1950
|
+
// it spins the drain loop (#3140). The refusal must be a bare return —
|
|
1951
|
+
// redirecting the caller to a fresh batch would re-arm the loop with a
|
|
1952
|
+
// new transaction identity each pass. Stamps are cleared at commit, so
|
|
1953
|
+
// this is a belt for paths that hand over a chased-dead reference
|
|
1954
|
+
// (merged chains, async settles racing completion).
|
|
1955
|
+
if (transition._done === true || transition === activeTransition) return;
|
|
1956
|
+
}
|
|
1594
1957
|
if (!transition && activeTransition && activeTransition._time === clock) return;
|
|
1595
1958
|
if (!activeTransition) {
|
|
1596
1959
|
activeTransition = transition ?? createBatch();
|
|
@@ -1634,11 +1997,25 @@ class GlobalQueue extends Queue {
|
|
|
1634
1997
|
for (const lane of activeLanes) {
|
|
1635
1998
|
if (!lane._transition) lane._transition = activeTransition;
|
|
1636
1999
|
}
|
|
2000
|
+
// A transaction's ambient window is one flush. Entering must therefore
|
|
2001
|
+
// guarantee a flush: a transaction opened with no writes (an action whose
|
|
2002
|
+
// first statements only await) otherwise leaves activeTransition and the
|
|
2003
|
+
// adopted batch armed across the async gap, and the next unrelated work
|
|
2004
|
+
// to arrive — an optimistic store's authoritative landing, a plain async
|
|
2005
|
+
// settle — is adopted into a transaction it has nothing to do with
|
|
2006
|
+
// (#3141). The scheduled flush parks the incomplete transaction through
|
|
2007
|
+
// the normal machinery and detaches the ambient slots first.
|
|
2008
|
+
schedule();
|
|
1637
2009
|
}
|
|
1638
2010
|
}
|
|
1639
2011
|
function queuePendingNode(node) {
|
|
2012
|
+
lastStagedNodeName = node._name ?? null;
|
|
1640
2013
|
currentBatch._pendingNodes.push(node);
|
|
1641
2014
|
}
|
|
2015
|
+
// Dev-only attribution for the flush loop guard (#3140): when the guard
|
|
2016
|
+
// trips, naming what the loop kept chewing on lets the app author attribute
|
|
2017
|
+
// the runaway without patching dist.
|
|
2018
|
+
let lastStagedNodeName = null;
|
|
1642
2019
|
// Sticky: flips true on the first refresh() ever (the only setter of
|
|
1643
2020
|
// REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
|
|
1644
2021
|
// clear entirely in apps that never refresh.
|
|
@@ -1714,6 +2091,12 @@ function commitPendingNode(n) {
|
|
|
1714
2091
|
n._pendingValue = NOT_PENDING;
|
|
1715
2092
|
// Set _modified for effects, but not for tracked effects (they handle their own scheduling)
|
|
1716
2093
|
if (n._type && n._type !== EFFECT_TRACKED) n._modified = true;
|
|
2094
|
+
// A quiet re-ask classification preserved through a held landing dies
|
|
2095
|
+
// with the value commit — the commit IS the reveal (#3178). Gated on the
|
|
2096
|
+
// staged value: status propagation queues pending nodes whose windows
|
|
2097
|
+
// are still OPEN (no staged value), and their live classification must
|
|
2098
|
+
// survive this sweep.
|
|
2099
|
+
if (n._x) n._x._reask = false;
|
|
1717
2100
|
}
|
|
1718
2101
|
// The committed hold is the first observable answer for a loading-window
|
|
1719
2102
|
// node — the window closes here, not at compute time (#2990). Unconditional
|
|
@@ -1744,10 +2127,34 @@ let patchCommitHook = null;
|
|
|
1744
2127
|
function setPatchCommitHook(fn) {
|
|
1745
2128
|
patchCommitHook = fn;
|
|
1746
2129
|
}
|
|
2130
|
+
/** Held truth committed this finalize, awaiting its post-revert wake (see
|
|
2131
|
+
* finalizePureQueue): the commit IS the reveal, but subscribers must not
|
|
2132
|
+
* re-derive until the settling transaction's optimistic overrides have
|
|
2133
|
+
* reverted — a commit-time wake recomputes them in the window where
|
|
2134
|
+
* confirming truth is committed and the override still displays, a torn
|
|
2135
|
+
* frame no timeline contains. */
|
|
2136
|
+
const heldRevealed = [];
|
|
1747
2137
|
function commitPendingNodes() {
|
|
1748
2138
|
const pendingNodes = currentBatch._pendingNodes;
|
|
1749
2139
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
1750
|
-
|
|
2140
|
+
const node = pendingNodes[i];
|
|
2141
|
+
commitPendingNode(node);
|
|
2142
|
+
// The stamp dies with the commit (#3143) — symmetric with
|
|
2143
|
+
// resolveOptimisticNodes clearing optimistic stamps. A stamp outliving
|
|
2144
|
+
// its transaction let any later write (even a value-equal no-op, which
|
|
2145
|
+
// re-opens before the equality bail) resurrect the finished transaction;
|
|
2146
|
+
// a boundary flag rewritten every finalize pass then spun the drain loop
|
|
2147
|
+
// forever (#3140). The held-truth mark dies the same death — the commit
|
|
2148
|
+
// IS the reveal — but its wake defers to the post-revert pass: ordinary
|
|
2149
|
+
// subscribers were masked to committed all hold (some re-derived against
|
|
2150
|
+
// that old view and cached it), and commits are otherwise silent
|
|
2151
|
+
// (staging already notified), so without a wake they'd hold the old
|
|
2152
|
+
// world forever.
|
|
2153
|
+
node._transition = null;
|
|
2154
|
+
if (node._config & CONFIG_HELD_TRUTH) {
|
|
2155
|
+
node._config &= ~CONFIG_HELD_TRUTH;
|
|
2156
|
+
heldRevealed.push(node);
|
|
2157
|
+
}
|
|
1751
2158
|
}
|
|
1752
2159
|
pendingNodes.length = 0;
|
|
1753
2160
|
storeCommitHook?.();
|
|
@@ -1799,6 +2206,22 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
1799
2206
|
// completing transition scopes the clear to its own layer keys (#2899).
|
|
1800
2207
|
if (batch._optimisticStores.size)
|
|
1801
2208
|
GlobalQueue._clearOptimisticStores(batch._optimisticStores, completingTransition);
|
|
2209
|
+
// Held-truth reveal wake (#3164), post-revert by construction: this
|
|
2210
|
+
// finalize committed confirming truth whose subscribers were masked all
|
|
2211
|
+
// hold — some re-derived against the committed view (the staging, or a
|
|
2212
|
+
// confirming carrier's landing, notified them as a plain write) and
|
|
2213
|
+
// cached it, and stash-restored applies may carry those torn values.
|
|
2214
|
+
// Waking and recomputing HERE — after _resolveOptimistic and the store
|
|
2215
|
+
// clears above — means every apply paints the settled view; a wake at
|
|
2216
|
+
// commit time would recompute them in the window where truth is
|
|
2217
|
+
// committed but the settling transaction's overrides still display.
|
|
2218
|
+
if (heldRevealed.length !== 0) {
|
|
2219
|
+
while (heldRevealed.length) insertSubs(heldRevealed.pop());
|
|
2220
|
+
if (dirtyQueue._max >= dirtyQueue._min) {
|
|
2221
|
+
runHeap(dirtyQueue, GlobalQueue._update);
|
|
2222
|
+
commitPendingNodes();
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
1802
2225
|
sweepTransientStoreNodes();
|
|
1803
2226
|
// Lanes only enter activeLanes through the engine's getOrCreateLane.
|
|
1804
2227
|
if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
|
|
@@ -1879,7 +2302,20 @@ function flush(fn) {
|
|
|
1879
2302
|
// `flush()` is an explicit drain point, so it must also process an active
|
|
1880
2303
|
// transition even if no microtask was scheduled for it yet.
|
|
1881
2304
|
while (scheduled$1 || activeTransition) {
|
|
1882
|
-
if (++count === 1e5)
|
|
2305
|
+
if (++count === 1e5) {
|
|
2306
|
+
// Attribution beats a bare guard (#3140): say what kept the loop alive.
|
|
2307
|
+
// A completed transition being re-activated reads `done=true` here —
|
|
2308
|
+
// the corpse-revival signature — while application-driven runaways
|
|
2309
|
+
// (#2843) usually show staged work naming the culprit node.
|
|
2310
|
+
const t = activeTransition;
|
|
2311
|
+
throw new Error(
|
|
2312
|
+
`Potential Infinite Loop Detected. Kept alive by ${scheduled$1 ? "scheduled work" : "an active transition"}${
|
|
2313
|
+
t
|
|
2314
|
+
? `; transition: done=${t._done === true}, pending=${t._pendingNodes.length}, optimistic=${t._optimisticNodes.length}, asyncReporters=${t._asyncReporters.size}`
|
|
2315
|
+
: ""
|
|
2316
|
+
}${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
1883
2319
|
globalQueue.flush();
|
|
1884
2320
|
}
|
|
1885
2321
|
}
|
|
@@ -1928,6 +2364,13 @@ function transitionComplete(transition) {
|
|
|
1928
2364
|
done && (transition._done = true);
|
|
1929
2365
|
return done;
|
|
1930
2366
|
}
|
|
2367
|
+
/** A fresh, unentered transaction (#3146): the optimistic store's truth
|
|
2368
|
+
* flight DECLARES an owned transaction instead of relying on whatever the
|
|
2369
|
+
* ambient adoption machinery stamped on its firewall. Activate it with
|
|
2370
|
+
* initTransition; it is a plain batch until then. */
|
|
2371
|
+
function createTransition() {
|
|
2372
|
+
return createBatch();
|
|
2373
|
+
}
|
|
1931
2374
|
function currentTransition(transition) {
|
|
1932
2375
|
while (transition._done && typeof transition._done === "object") transition = transition._done;
|
|
1933
2376
|
return transition;
|
|
@@ -1941,6 +2384,29 @@ function runInTransition(transition, fn) {
|
|
|
1941
2384
|
activeTransition = prevTransition;
|
|
1942
2385
|
}
|
|
1943
2386
|
}
|
|
2387
|
+
/** Run `fn` with `transition` as BOTH the ambient transaction and the
|
|
2388
|
+
* registration batch, restoring both after. runInTransition alone is not
|
|
2389
|
+
* enough for code that WRITES on behalf of a transaction from inside someone
|
|
2390
|
+
* else's window (optimistic replay re-arming a still-open action's edits
|
|
2391
|
+
* during a landing commit, #3123): registrations route through the queue's
|
|
2392
|
+
* batch pointer, and a bare activeTransition swap leaves them in the ambient
|
|
2393
|
+
* batch — a plain batch "completes" at the next flush and reverts optimistic
|
|
2394
|
+
* registrations that were supposed to live with the transaction.
|
|
2395
|
+
* initTransition is the wrong tool here: it MERGES the currently ambient
|
|
2396
|
+
* transaction into the target, entangling whatever the interrupted window
|
|
2397
|
+
* belonged to. */
|
|
2398
|
+
function runAsTransitionBatch(transition, fn) {
|
|
2399
|
+
const prevTransition = activeTransition;
|
|
2400
|
+
const prevBatch = globalQueue._batch;
|
|
2401
|
+
try {
|
|
2402
|
+
activeTransition = currentTransition(transition);
|
|
2403
|
+
currentBatch = globalQueue._batch = activeTransition;
|
|
2404
|
+
return fn();
|
|
2405
|
+
} finally {
|
|
2406
|
+
activeTransition = prevTransition;
|
|
2407
|
+
currentBatch = globalQueue._batch = prevBatch;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
1944
2410
|
|
|
1945
2411
|
/** The queue a node belongs to, picked from its own zombie flag. */
|
|
1946
2412
|
function queueFor(n) {
|
|
@@ -2569,12 +3035,14 @@ function addPendingSource(el, source) {
|
|
|
2569
3035
|
return true;
|
|
2570
3036
|
}
|
|
2571
3037
|
function removePendingSource(el, source) {
|
|
2572
|
-
|
|
2573
|
-
if (
|
|
3038
|
+
const sources = el._x?._pendingSources;
|
|
3039
|
+
if (!sources?.delete(source)) return false;
|
|
3040
|
+
if (!sources.size) el._x._pendingSources = undefined;
|
|
2574
3041
|
return true;
|
|
2575
3042
|
}
|
|
2576
3043
|
function clearPendingSources(el) {
|
|
2577
|
-
|
|
3044
|
+
// This set is node-owned and never shared; dropping the sole reference
|
|
3045
|
+
// releases the set and every entry without a redundant clear() walk.
|
|
2578
3046
|
if (el._x !== null) el._x._pendingSources = undefined;
|
|
2579
3047
|
}
|
|
2580
3048
|
// A rejection-pending only resolves through the settle sweep over the
|
|
@@ -2691,6 +3159,49 @@ function settleErroredDependents(el, error) {
|
|
|
2691
3159
|
if (scheduled) schedule();
|
|
2692
3160
|
}
|
|
2693
3161
|
function settlePendingSource(el) {
|
|
3162
|
+
// Invariant: walking a settle implies truth exists. A caller reaching this
|
|
3163
|
+
// with an uninitialized source is announcing a settle that has not
|
|
3164
|
+
// happened — parked readers would wake into a value that was never
|
|
3165
|
+
// produced (the rc.5 regression: the recompute-side walk fired on a
|
|
3166
|
+
// projection driver whose first flight was superseded before any commit
|
|
3167
|
+
// reached the observable store). "Uninitialized" alone is not the tell,
|
|
3168
|
+
// though: a first landing whose commit is transition-held (streamed
|
|
3169
|
+
// hydration rides this) parks its value in `_pendingValue` with the flag
|
|
3170
|
+
// still set, and a comparator throw on that landing leaves the node
|
|
3171
|
+
// uninitialized but errored — both have real truth to reveal. Only an
|
|
3172
|
+
// uninitialized node with neither a held value nor an error is a settle
|
|
3173
|
+
// that never happened. Silent in production; loud in dev so a future
|
|
3174
|
+
// call site that violates the contract fails in its author's test run
|
|
3175
|
+
// instead of wedging a downstream app.
|
|
3176
|
+
{
|
|
3177
|
+
const sources = el._x?._pendingSources;
|
|
3178
|
+
if (
|
|
3179
|
+
el._statusFlags & STATUS_UNINITIALIZED &&
|
|
3180
|
+
el._pendingValue === NOT_PENDING &&
|
|
3181
|
+
!el._x?._error &&
|
|
3182
|
+
// A replacement source makes this a cleanup-only transfer: removing
|
|
3183
|
+
// self leaves the source and every propagated dependent parked. No
|
|
3184
|
+
// sources (or self alone) would release readers without truth.
|
|
3185
|
+
!(sources?.size && (sources.size > 1 || !sources.has(el)))
|
|
3186
|
+
) {
|
|
3187
|
+
emitDiagnostic({
|
|
3188
|
+
code: "SETTLE_WALK_UNINITIALIZED_SOURCE",
|
|
3189
|
+
kind: "lifecycle",
|
|
3190
|
+
severity: "error",
|
|
3191
|
+
message:
|
|
3192
|
+
"[SETTLE_WALK_UNINITIALIZED_SOURCE] settlePendingSource was called on a source that " +
|
|
3193
|
+
"never produced a value. Settling parked readers requires truth to reveal — an " +
|
|
3194
|
+
"uninitialized source waking its dependents serves them its initial face instead of " +
|
|
3195
|
+
"settled data.",
|
|
3196
|
+
ownerId: el.id,
|
|
3197
|
+
ownerName: el._name
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
// The normal landing path already cleared the source's own set. Superseded
|
|
3202
|
+
// re-parks arrive here with an abandoned self entry, which must retire in
|
|
3203
|
+
// the same walk as its propagated copies.
|
|
3204
|
+
removePendingSource(el, el);
|
|
2694
3205
|
let scheduled = false;
|
|
2695
3206
|
let released;
|
|
2696
3207
|
const visited = new Set();
|
|
@@ -2709,11 +3220,11 @@ function settlePendingSource(el) {
|
|
|
2709
3220
|
const errored = node._statusFlags & STATUS_ERROR;
|
|
2710
3221
|
if (remaining) {
|
|
2711
3222
|
if (!errored) setPendingError(node, remaining);
|
|
2712
|
-
updateCompanions
|
|
3223
|
+
updateCompanions?.(node);
|
|
2713
3224
|
} else {
|
|
2714
3225
|
node._statusFlags &= ~STATUS_PENDING;
|
|
2715
3226
|
if (!errored) setPendingError(node);
|
|
2716
|
-
updateCompanions
|
|
3227
|
+
updateCompanions?.(node);
|
|
2717
3228
|
if (node._x?._blocked) {
|
|
2718
3229
|
enqueueSub(node);
|
|
2719
3230
|
scheduled = true;
|
|
@@ -2736,6 +3247,14 @@ function settlePendingSource(el) {
|
|
|
2736
3247
|
function isThenable(value) {
|
|
2737
3248
|
return value != null && typeof value === "object" && typeof value.then === "function";
|
|
2738
3249
|
}
|
|
3250
|
+
/** Fire and clear a node's iterator-flight cancellation hook (#3122). */
|
|
3251
|
+
function releaseFlightTeardown(el) {
|
|
3252
|
+
const teardown = el._x?._flightTeardown;
|
|
3253
|
+
if (teardown != null) {
|
|
3254
|
+
el._x._flightTeardown = null;
|
|
3255
|
+
teardown();
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
2739
3258
|
function handleAsync(el, result, setter) {
|
|
2740
3259
|
let iterator = false;
|
|
2741
3260
|
let thenable = false;
|
|
@@ -2773,7 +3292,18 @@ function handleAsync(el, result, setter) {
|
|
|
2773
3292
|
});
|
|
2774
3293
|
throw new Error(message);
|
|
2775
3294
|
}
|
|
3295
|
+
// Flight replacement relies on recompute's supersede release for iterator
|
|
3296
|
+
// teardown (#3122): every handleAsync call — including the projection
|
|
3297
|
+
// self-registration — runs during a recompute of `el`, which has already
|
|
3298
|
+
// fired _flightTeardown. A future non-recompute registration path must
|
|
3299
|
+
// release it here before overwriting _inFlight.
|
|
2776
3300
|
ext(el)._inFlight = result;
|
|
3301
|
+
// Attribution hook: a new flight is registered. Fired here (not in the
|
|
3302
|
+
// branches below) so every flight shape — plain thenable, iterator, the
|
|
3303
|
+
// flattened combinations — is announced exactly once, while the recompute
|
|
3304
|
+
// frame that caused it is still on the engine's stack. Not inside a try
|
|
3305
|
+
// (#2883 — see attribution-hooks.ts).
|
|
3306
|
+
if (attrHooks !== null) attrHooks.flightStart(el, result);
|
|
2777
3307
|
let syncValue;
|
|
2778
3308
|
// Settle-time transition re-entry. The loading rail is invisible to
|
|
2779
3309
|
// transactions (#2933): a boundary-caught first load never registers as an
|
|
@@ -2832,6 +3362,10 @@ function handleAsync(el, result, setter) {
|
|
|
2832
3362
|
}
|
|
2833
3363
|
settleTransition();
|
|
2834
3364
|
notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
|
|
3365
|
+
// A NotReady rejection is a landing into another pending source. The
|
|
3366
|
+
// rejected flight will never settle its self entry, so transfer ownership
|
|
3367
|
+
// after notifyStatus has propagated the replacement source.
|
|
3368
|
+
if (stillPending) settlePendingSource(el);
|
|
2835
3369
|
el._time = clock;
|
|
2836
3370
|
// A real error settles derivatively-pending dependents (notifyStatus
|
|
2837
3371
|
// cleared their pending sources), so stranded lazy ones release here —
|
|
@@ -2846,8 +3380,16 @@ function handleAsync(el, result, setter) {
|
|
|
2846
3380
|
if (el._flags & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
|
|
2847
3381
|
settleTransition();
|
|
2848
3382
|
const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
|
|
3383
|
+
// Captured before clearStatus wipes it: a quiet re-ask's landing may be
|
|
3384
|
+
// transition-held below, and the displayed value keeps answering the same
|
|
3385
|
+
// question until the hold commits — the classification must survive to
|
|
3386
|
+
// that reveal or companion synchronization briefly classifies the held
|
|
3387
|
+
// old value as pending, a one-frame pulse to direct observers (#3178).
|
|
3388
|
+
// A truthy capture implies `_x` exists, so the restore writes it directly.
|
|
3389
|
+
const wasReask = el._x?._reask;
|
|
2849
3390
|
trimStaleDeps(el);
|
|
2850
3391
|
clearStatus(el);
|
|
3392
|
+
if (wasReask) el._x._reask = true;
|
|
2851
3393
|
const lane = resolveLane(el);
|
|
2852
3394
|
if (lane) lane._pendingAsync.delete(el);
|
|
2853
3395
|
// Attribution hook: lets the engine snapshot state before the landing
|
|
@@ -2876,10 +3418,21 @@ function handleAsync(el, result, setter) {
|
|
|
2876
3418
|
// only notified when the hold is visible to them: under an active
|
|
2877
3419
|
// override every reader sees the override (A17), so waking subs would
|
|
2878
3420
|
// re-show an unchanged view — the revert is the notification point.
|
|
2879
|
-
GlobalQueue._syncCompanions
|
|
3421
|
+
GlobalQueue._syncCompanions?.(el, value);
|
|
2880
3422
|
if (!hasActiveOverride$1(el)) {
|
|
2881
3423
|
if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
|
|
2882
3424
|
insertSubs(el);
|
|
3425
|
+
} else if (el._config & CONFIG_AUTHORITATIVE_OBSERVED) {
|
|
3426
|
+
// A17 silence is stated over ordinary readers; an authoritative-view
|
|
3427
|
+
// reader (until()'s predicate) observed this node PAST its override
|
|
3428
|
+
// and is waiting for exactly this staged truth. Without the wake the
|
|
3429
|
+
// hold deadlocks: the landing waits on the transaction, the
|
|
3430
|
+
// transaction on the action, the action on an until() that was never
|
|
3431
|
+
// re-notified (#3164). Same selective wake as the equal-landing
|
|
3432
|
+
// branch in recompute(). Optional call: the bit implies the
|
|
3433
|
+
// optimistic engine WAS consulted, but the hook only installs with
|
|
3434
|
+
// it — a bare-core build must not crash here.
|
|
3435
|
+
GlobalQueue._notifyAuthoritativeObservers?.(el);
|
|
2883
3436
|
}
|
|
2884
3437
|
el._time = clock;
|
|
2885
3438
|
} else if (lane) {
|
|
@@ -2894,7 +3447,7 @@ function handleAsync(el, result, setter) {
|
|
|
2894
3447
|
// The latest() shadow write gives latest() effects independent lanes; the
|
|
2895
3448
|
// _pendingSignal update is a no-op repeat of the clearStatus() call above
|
|
2896
3449
|
// (computePendingState doesn't read _value).
|
|
2897
|
-
GlobalQueue._syncCompanions
|
|
3450
|
+
GlobalQueue._syncCompanions?.(el, value);
|
|
2898
3451
|
insertSubs(el, true);
|
|
2899
3452
|
}
|
|
2900
3453
|
} catch (e) {
|
|
@@ -2930,8 +3483,12 @@ function handleAsync(el, result, setter) {
|
|
|
2930
3483
|
// (`_pendingValue` set above or inside setSignal) is not — the verdict's
|
|
2931
3484
|
// held-value branch is window-gated, and commitPendingNode closes the
|
|
2932
3485
|
// window when the hold commits, so no one-frame isPending pulse can leak
|
|
2933
|
-
// to live observers between the landing and its commit (#2990).
|
|
2934
|
-
|
|
3486
|
+
// to live observers between the landing and its commit (#2990). The
|
|
3487
|
+
// quiet re-ask classification follows the same schedule (#3178).
|
|
3488
|
+
if (el._pendingValue === NOT_PENDING) {
|
|
3489
|
+
el._loading = false;
|
|
3490
|
+
if (wasReask) el._x._reask = false;
|
|
3491
|
+
}
|
|
2935
3492
|
settlePendingSource(el);
|
|
2936
3493
|
schedule();
|
|
2937
3494
|
flush();
|
|
@@ -2976,6 +3533,11 @@ function handleAsync(el, result, setter) {
|
|
|
2976
3533
|
} catch {}
|
|
2977
3534
|
};
|
|
2978
3535
|
registerClose ? registerClose(close) : cleanup(close);
|
|
3536
|
+
// Flight-identity cancellation (#3122): the registration above is the
|
|
3537
|
+
// owner-death backstop, but its disposal list can be zombie-deferred
|
|
3538
|
+
// until the SUPERSEDING flight settles. The teardown slot fires at the
|
|
3539
|
+
// _inFlight release sites so supersede stops this stream immediately.
|
|
3540
|
+
ext(el)._flightTeardown = close;
|
|
2979
3541
|
// Release check before each next pull: an unobserved lazy node must tear
|
|
2980
3542
|
// down (its close above runs via disposal, closing the iterator) instead
|
|
2981
3543
|
// of pumping the stream forever with zero subscribers (#2935).
|
|
@@ -3207,7 +3769,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3207
3769
|
status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
|
|
3208
3770
|
ext(el)._error = error;
|
|
3209
3771
|
}
|
|
3210
|
-
GlobalQueue._updatePendingSignal
|
|
3772
|
+
GlobalQueue._updatePendingSignal?.(el);
|
|
3211
3773
|
if (
|
|
3212
3774
|
el._x?._child &&
|
|
3213
3775
|
el._config & CONFIG_CHILD_COMPANIONS &&
|
|
@@ -3352,7 +3914,15 @@ function recompute(el, create = false) {
|
|
|
3352
3914
|
if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
|
|
3353
3915
|
globalQueue.initTransition(el._transition);
|
|
3354
3916
|
deleteFromHeap(el, queueFor(el));
|
|
3355
|
-
if (el._x !== null)
|
|
3917
|
+
if (el._x !== null) {
|
|
3918
|
+
el._x._inFlight = null;
|
|
3919
|
+
// Supersede is where an iterator flight dies (#3122): close it now.
|
|
3920
|
+
// Its cleanup(close) registration may sit in a zombie-deferred
|
|
3921
|
+
// disposal list that a held transition only drains when the
|
|
3922
|
+
// SUPERSEDING flight settles — cancellation must not wait for the
|
|
3923
|
+
// work that replaced it. Idempotent with the cleanup-channel close.
|
|
3924
|
+
releaseFlightTeardown(el);
|
|
3925
|
+
}
|
|
3356
3926
|
// Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
|
|
3357
3927
|
if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
|
|
3358
3928
|
else if (el._firstChild !== null || el._disposal !== null) {
|
|
@@ -3376,6 +3946,13 @@ function recompute(el, create = false) {
|
|
|
3376
3946
|
// recovers to an unchanged value, dependents still holding this object must
|
|
3377
3947
|
// be swept (settleErroredDependents, #2949).
|
|
3378
3948
|
const outgoingError = el._statusFlags & STATUS_ERROR ? el._x?._error : undefined;
|
|
3949
|
+
// Pending SOURCE-hood, captured before the compute clears status: a node
|
|
3950
|
+
// whose own flight parked dependents self-registers in _pendingSources
|
|
3951
|
+
// (notifyStatus, isSource). If this recompute supersedes that flight and
|
|
3952
|
+
// settles synchronously, those dependents settle HERE — asyncWrite's
|
|
3953
|
+
// settlePendingSource walk never runs for a landing that was preempted
|
|
3954
|
+
// (#3181).
|
|
3955
|
+
const wasPendingSource = el._x?._pendingSources?.has(el);
|
|
3379
3956
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
3380
3957
|
// the recompute wipes _flags below.
|
|
3381
3958
|
const hadReask = (el._flags & REACTIVE_REASK) !== 0;
|
|
@@ -3489,6 +4066,9 @@ function recompute(el, create = false) {
|
|
|
3489
4066
|
undefined,
|
|
3490
4067
|
notReady ? el._x?._optimisticLane : undefined
|
|
3491
4068
|
);
|
|
4069
|
+
// The replacement source is fully propagated now. If no new flight
|
|
4070
|
+
// re-owned self, retire the superseded flight and its dependent copies.
|
|
4071
|
+
if (notReady && wasPendingSource && !el._x?._inFlight) settlePendingSource(el);
|
|
3492
4072
|
if (reaskChanged) GlobalQueue._repollVerdicts(el);
|
|
3493
4073
|
}
|
|
3494
4074
|
} finally {
|
|
@@ -3550,7 +4130,14 @@ function recompute(el, create = false) {
|
|
|
3550
4130
|
// values directly — the pending round-trip (queuePendingNode +
|
|
3551
4131
|
// commitPendingNodes) exists to sequence transition reveals, and
|
|
3552
4132
|
// paying it per effect on the plain path is pure overhead.
|
|
3553
|
-
|
|
4133
|
+
// DIRECT_COMMIT effects (resolve/until) commit directly even under
|
|
4134
|
+
// their own held transition: their applies deliver on a microtask,
|
|
4135
|
+
// not the stashed queues, so a staged value would hand the immediate
|
|
4136
|
+
// apply stale state — see CONFIG_DIRECT_COMMIT.
|
|
4137
|
+
(isEffect &&
|
|
4138
|
+
(activeTransition !== el._transition ||
|
|
4139
|
+
activeTransition === null ||
|
|
4140
|
+
el._config & CONFIG_DIRECT_COMMIT)) ||
|
|
3554
4141
|
isOptimisticDirty
|
|
3555
4142
|
// NOTE (stage-3, 2026-08-21): a quiet-world MEMO direct-commit was
|
|
3556
4143
|
// attempted here and REVERTED — memo staging is load-bearing beyond
|
|
@@ -3596,6 +4183,12 @@ function recompute(el, create = false) {
|
|
|
3596
4183
|
if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
|
|
3597
4184
|
el._pendingValue = value;
|
|
3598
4185
|
if (wasLoading) el._loading = true; // see the held branch above (#2990)
|
|
4186
|
+
// A authoritative-view reader (until()) observed this node past its
|
|
4187
|
+
// override — and "authoritative arrival equal to the override" is
|
|
4188
|
+
// exactly the acknowledgment it waits for. Wake those readers only;
|
|
4189
|
+
// A17 silence holds for every ordinary subscriber. (Hook installed by
|
|
4190
|
+
// until(), the only setter of the gating bit.)
|
|
4191
|
+
if (el._config & CONFIG_AUTHORITATIVE_OBSERVED) GlobalQueue._notifyAuthoritativeObservers(el);
|
|
3599
4192
|
} else if (el._height != oldHeight) {
|
|
3600
4193
|
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
3601
4194
|
insertIntoHeapHeight(s._sub, queueFor(s._sub));
|
|
@@ -3608,6 +4201,11 @@ function recompute(el, create = false) {
|
|
|
3608
4201
|
// (el._x?._error re-set), so this only runs on a genuinely clean recovery.
|
|
3609
4202
|
if (outgoingError !== undefined && !valueChanged && !el._x?._error)
|
|
3610
4203
|
settleErroredDependents(el, outgoingError);
|
|
4204
|
+
// #3181: a synchronous settle supersedes the old landing callback, so
|
|
4205
|
+
// recompute owns its pending-source sweep. An uninitialized node without
|
|
4206
|
+
// a replacement source still has no truth to reveal and must stay parked.
|
|
4207
|
+
if (wasPendingSource && !(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)))
|
|
4208
|
+
settlePendingSource(el);
|
|
3611
4209
|
}
|
|
3612
4210
|
// Attribution hook: fired before the lane restore so `currentOptimisticLane`
|
|
3613
4211
|
// still reflects THIS run's posture. The facts distinguish an overlay
|
|
@@ -3656,7 +4254,9 @@ function updateIfNecessary(el) {
|
|
|
3656
4254
|
// (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
|
|
3657
4255
|
// A mid-pass mark stays latched for recompute's own tail to reschedule
|
|
3658
4256
|
// (#3037); readers meanwhile serve the values the pass has so far.
|
|
3659
|
-
|
|
4257
|
+
// Never recompute a DISPOSED node either: recompute rewrites _flags and
|
|
4258
|
+
// would resurrect it (#2983) — readers serve its last value.
|
|
4259
|
+
if (el._flags & (REACTIVE_RECOMPUTING_DEPS | REACTIVE_DISPOSED)) return;
|
|
3660
4260
|
if (el._flags & REACTIVE_CHECK) {
|
|
3661
4261
|
for (let d = el._deps; d; d = d._nextDep) {
|
|
3662
4262
|
const dep1 = d._dep;
|
|
@@ -3692,7 +4292,7 @@ function computed(fn, options) {
|
|
|
3692
4292
|
(options?.sync ? CONFIG_SYNC : 0) |
|
|
3693
4293
|
(options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0) |
|
|
3694
4294
|
(snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
3695
|
-
_equals: options?.equals
|
|
4295
|
+
_equals: options?.equals ?? isEqual,
|
|
3696
4296
|
_disposal: null,
|
|
3697
4297
|
_queue: context?._queue ?? globalQueue,
|
|
3698
4298
|
_context: context?._context ?? defaultContext,
|
|
@@ -3744,6 +4344,7 @@ function ext(el) {
|
|
|
3744
4344
|
_parentSource: undefined,
|
|
3745
4345
|
_affectsCount: 0,
|
|
3746
4346
|
_inFlight: null,
|
|
4347
|
+
_flightTeardown: null,
|
|
3747
4348
|
_error: undefined,
|
|
3748
4349
|
_blocked: undefined,
|
|
3749
4350
|
_pendingSources: undefined,
|
|
@@ -3771,6 +4372,7 @@ function createEffectNode(fn, effectFn, errorFn, type, options) {
|
|
|
3771
4372
|
(transparent ? CONFIG_TRANSPARENT : 0) |
|
|
3772
4373
|
(options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
|
|
3773
4374
|
(options?.sync ? CONFIG_SYNC : 0) |
|
|
4375
|
+
(options?._extraConfig ?? 0) |
|
|
3774
4376
|
(snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
3775
4377
|
_equals: false,
|
|
3776
4378
|
_disposal: null,
|
|
@@ -3832,8 +4434,7 @@ function setEffectStatusNotify(fn) {
|
|
|
3832
4434
|
* "display consumer" membership test in the status walks, exactly as the
|
|
3833
4435
|
* per-node field did when every effect carried one. */
|
|
3834
4436
|
function statusNotifierOf(el) {
|
|
3835
|
-
const
|
|
3836
|
-
const own = x !== null && x !== undefined ? x._notifyStatus : undefined;
|
|
4437
|
+
const own = el._x?._notifyStatus;
|
|
3837
4438
|
if (own !== undefined) return own;
|
|
3838
4439
|
return el._type ? (effectStatusNotify ?? undefined) : undefined;
|
|
3839
4440
|
}
|
|
@@ -3876,7 +4477,7 @@ function setupComputedNode(self, options) {
|
|
|
3876
4477
|
}
|
|
3877
4478
|
function signal(v, options, firewall = null) {
|
|
3878
4479
|
const s = {
|
|
3879
|
-
_equals: options?.equals
|
|
4480
|
+
_equals: options?.equals ?? isEqual,
|
|
3880
4481
|
_config:
|
|
3881
4482
|
(options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
|
|
3882
4483
|
(options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0),
|
|
@@ -4013,6 +4614,35 @@ const READ_SLOW = Symbol("read-slow");
|
|
|
4013
4614
|
* snapshot / transition / lane / dev-strictRead state all take the full
|
|
4014
4615
|
* resolution. Anything slow returns READ_SLOW; the caller then calls read().
|
|
4015
4616
|
*/
|
|
4617
|
+
/**
|
|
4618
|
+
* Wake only authoritative-view readers (until() predicates) subscribed to `el`.
|
|
4619
|
+
* The A17-silent ack paths — an authoritative arrival equal to the active
|
|
4620
|
+
* override — use this so the predicate re-evaluates without re-firing
|
|
4621
|
+
* ordinary subscribers whose visible (override) value did not change.
|
|
4622
|
+
* Pay-for-use: reached through GlobalQueue._notifyAuthoritativeObservers,
|
|
4623
|
+
* installed at first until() call — apps that never use until() shake it.
|
|
4624
|
+
*/
|
|
4625
|
+
function notifyAuthoritativeObservers(el) {
|
|
4626
|
+
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
4627
|
+
const sub = s._sub;
|
|
4628
|
+
if (!(sub._config & CONFIG_AUTHORITATIVE_READ)) continue;
|
|
4629
|
+
// Missed-wake latch (#3037), same contract as insertSubs: the reader may
|
|
4630
|
+
// itself have pulled this recompute (updateIfNecessary from its own
|
|
4631
|
+
// read), and the heap refuses RECOMPUTING nodes — latch so recompute's
|
|
4632
|
+
// tail reschedules it with the staged value visible.
|
|
4633
|
+
if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
|
|
4634
|
+
sub._flags |= REACTIVE_MISSED_WAKE;
|
|
4635
|
+
enqueueSub(sub);
|
|
4636
|
+
}
|
|
4637
|
+
schedule();
|
|
4638
|
+
}
|
|
4639
|
+
/** Installs the until() machinery hook. Idempotent; called by until() before
|
|
4640
|
+
* any authoritative-view read happens (same late-binding contract as the
|
|
4641
|
+
* optimistic engine). */
|
|
4642
|
+
function installAuthoritativeRead() {
|
|
4643
|
+
if (GlobalQueue._notifyAuthoritativeObservers === null)
|
|
4644
|
+
GlobalQueue._notifyAuthoritativeObservers = notifyAuthoritativeObservers;
|
|
4645
|
+
}
|
|
4016
4646
|
function readNodeFast(el) {
|
|
4017
4647
|
if (
|
|
4018
4648
|
latestReadActive ||
|
|
@@ -4092,6 +4722,13 @@ function read(el) {
|
|
|
4092
4722
|
markHeap(elQueue);
|
|
4093
4723
|
updateIfNecessary(owner);
|
|
4094
4724
|
}
|
|
4725
|
+
// Fresh-pull readers (awaitable refresh's waiter) recompute a dirty
|
|
4726
|
+
// source inline even when the height gate defers to the flush: the
|
|
4727
|
+
// waiter must park on the re-ask's window (or serve its sync answer),
|
|
4728
|
+
// never read the PRE-re-ask value as settled. Self-guarded: a clean
|
|
4729
|
+
// node no-ops and updateIfNecessary refuses disposed nodes (#2983) —
|
|
4730
|
+
// a dead target serves its last value, which is already quiescent.
|
|
4731
|
+
else if (c._config & CONFIG_FRESH_READ) updateIfNecessary(owner);
|
|
4095
4732
|
const height = owner._height;
|
|
4096
4733
|
// parent check is shallow, might need to be recursive
|
|
4097
4734
|
if (height >= c._height && el._parent !== c) {
|
|
@@ -4170,8 +4807,18 @@ function read(el) {
|
|
|
4170
4807
|
nodeName: owner?._name
|
|
4171
4808
|
});
|
|
4172
4809
|
if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
|
|
4173
|
-
// A17: the override IS the value for every reader
|
|
4174
|
-
|
|
4810
|
+
// A17: the override IS the value for every reader — except an authoritative
|
|
4811
|
+
// reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
|
|
4812
|
+
// observe independently-arriving truth, and serving it the caller's own
|
|
4813
|
+
// tentative write would trivially satisfy the predicate. The bit is checked
|
|
4814
|
+
// on the reading computation itself, so a shared computed the predicate
|
|
4815
|
+
// pulls recomputes as ITSELF (context = the memo, no bit) under the normal
|
|
4816
|
+
// view. Fall through to normal value selection (staged `_pendingValue` is
|
|
4817
|
+
// authoritative — optimism never lives there); the sticky mark makes the
|
|
4818
|
+
// A17-silent "landing equals override" paths notify this node's subs so
|
|
4819
|
+
// the reader re-runs when truth arrives.
|
|
4820
|
+
if (!(c && c._config & CONFIG_AUTHORITATIVE_READ)) return unwrapOverride(el._x?._overrideValue);
|
|
4821
|
+
el._config |= CONFIG_AUTHORITATIVE_OBSERVED;
|
|
4175
4822
|
}
|
|
4176
4823
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
4177
4824
|
// a pending mid-transition write sees the committed value. Projection-store
|
|
@@ -4199,7 +4846,18 @@ function read(el) {
|
|
|
4199
4846
|
(currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
|
|
4200
4847
|
el._pendingValue === NOT_PENDING ||
|
|
4201
4848
|
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
4202
|
-
(stale && el._transition && activeTransition !== el._transition)
|
|
4849
|
+
(stale && el._transition && activeTransition !== el._transition) ||
|
|
4850
|
+
// A17 for HELD truth (#3164, see CONFIG_HELD_TRUTH): staged confirming
|
|
4851
|
+
// truth — fold-staged onto an armed family, or entangle-stolen by an
|
|
4852
|
+
// awaited until() — is masked from ordinary readers until its
|
|
4853
|
+
// transaction's reveal; the retaining transaction's own speculative
|
|
4854
|
+
// recomputes included (partial override coverage would otherwise
|
|
4855
|
+
// compose override + staged truth into a state no timeline contains).
|
|
4856
|
+
// Authoritative readers (until()'s predicate) and latest() see the
|
|
4857
|
+
// staged truth — the tunnel that keeps the hold deadlock-free.
|
|
4858
|
+
(el._config & CONFIG_HELD_TRUTH &&
|
|
4859
|
+
!latestReadActive &&
|
|
4860
|
+
!(c._config & CONFIG_AUTHORITATIVE_READ))
|
|
4203
4861
|
? el._value
|
|
4204
4862
|
: el._pendingValue;
|
|
4205
4863
|
// Record that this isPending() probe observed the fresh pending value, so
|
|
@@ -4244,9 +4902,17 @@ function devGuardStoreSetterWrite() {
|
|
|
4244
4902
|
ownerName: context._name,
|
|
4245
4903
|
data: { operation: "setStore" }
|
|
4246
4904
|
});
|
|
4247
|
-
|
|
4905
|
+
// the owner name reaches the THROWN message too, not just the
|
|
4906
|
+
// diagnostics channel apps don't subscribe to by default (#3157)
|
|
4907
|
+
throw new Error(ownedScopeWriteMessage(context));
|
|
4248
4908
|
}
|
|
4249
4909
|
}
|
|
4910
|
+
function ownedScopeWriteMessage(owner) {
|
|
4911
|
+
const name = owner._name;
|
|
4912
|
+
return name
|
|
4913
|
+
? `${REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE} (in ${name})`
|
|
4914
|
+
: REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE;
|
|
4915
|
+
}
|
|
4250
4916
|
function setSignal(el, v) {
|
|
4251
4917
|
if (
|
|
4252
4918
|
!(el._config & CONFIG_OWNED_WRITE) &&
|
|
@@ -4264,7 +4930,7 @@ function setSignal(el, v) {
|
|
|
4264
4930
|
nodeName: el._name,
|
|
4265
4931
|
data: { operation: "setSignal" }
|
|
4266
4932
|
});
|
|
4267
|
-
throw new Error(
|
|
4933
|
+
throw new Error(ownedScopeWriteMessage(context));
|
|
4268
4934
|
}
|
|
4269
4935
|
if (el._transition && activeTransition !== el._transition)
|
|
4270
4936
|
globalQueue.initTransition(el._transition);
|
|
@@ -4390,41 +5056,13 @@ function staleValues(fn, set = true) {
|
|
|
4390
5056
|
}
|
|
4391
5057
|
}
|
|
4392
5058
|
/**
|
|
4393
|
-
*
|
|
4394
|
-
*
|
|
4395
|
-
*
|
|
4396
|
-
*
|
|
4397
|
-
*
|
|
4398
|
-
* write-like invalidation operation: it does not read the target's value, and
|
|
4399
|
-
* refreshing a plain signal accessor is a no-op.
|
|
4400
|
-
*
|
|
4401
|
-
* Use it to invalidate cached async values (e.g. force a re-fetch) without
|
|
4402
|
-
* tearing the consumer down.
|
|
4403
|
-
*
|
|
4404
|
-
* @example
|
|
4405
|
-
* ```ts
|
|
4406
|
-
* const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
|
|
4407
|
-
*
|
|
4408
|
-
* // Re-fetch on demand
|
|
4409
|
-
* <button onClick={() => refresh(user)}>Reload</button>
|
|
4410
|
-
* ```
|
|
5059
|
+
* Core marking half of `refresh()` (the public wrapper lives in signals.ts —
|
|
5060
|
+
* it validates the target, marks through here, then builds the quiescence
|
|
5061
|
+
* promise on the resolve()/until() effect machinery). Flags the node's next
|
|
5062
|
+
* recompute as a quiet re-ask and schedules it; no-ops for non-derived or
|
|
5063
|
+
* disposed targets and for same-tick manual writes.
|
|
4411
5064
|
*/
|
|
4412
|
-
function
|
|
4413
|
-
const node = target?.[$REFRESH];
|
|
4414
|
-
if (!node) {
|
|
4415
|
-
{
|
|
4416
|
-
const message =
|
|
4417
|
-
"[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
|
|
4418
|
-
"Pass the original source target, not a wrapper function or derived property read.";
|
|
4419
|
-
emitDiagnostic({
|
|
4420
|
-
code: "INVALID_REFRESH_TARGET",
|
|
4421
|
-
kind: "write",
|
|
4422
|
-
severity: "error",
|
|
4423
|
-
message
|
|
4424
|
-
});
|
|
4425
|
-
throw new Error(message);
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
5065
|
+
function markRefresh(node) {
|
|
4428
5066
|
if (
|
|
4429
5067
|
context &&
|
|
4430
5068
|
!((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
|
|
@@ -5076,7 +5714,11 @@ function computePendingState(el) {
|
|
|
5076
5714
|
) {
|
|
5077
5715
|
if (hasActiveOverride$1(el))
|
|
5078
5716
|
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._x?._overrideValue));
|
|
5079
|
-
|
|
5717
|
+
// A quiet re-ask's held landing still answers the same question: the
|
|
5718
|
+
// classification survives the landing (asyncWrite) and dies with the
|
|
5719
|
+
// commit (commitPendingNode) — verdict-quiet through the reveal, like
|
|
5720
|
+
// the loading window above (#3178).
|
|
5721
|
+
if (!comp._x?._reask) return true;
|
|
5080
5722
|
}
|
|
5081
5723
|
return newQuestionInFlight(comp);
|
|
5082
5724
|
}
|
|
@@ -5236,7 +5878,22 @@ function latestRead(el) {
|
|
|
5236
5878
|
!(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
5237
5879
|
) {
|
|
5238
5880
|
markHeap(queue);
|
|
5239
|
-
|
|
5881
|
+
// Suspend probe collection during the pull (mirrors pendingCheckRead's
|
|
5882
|
+
// prepare): a probe through latest() answers for the SHADOW — the
|
|
5883
|
+
// read() dispatch collects it deliberately, so the verdict reflects
|
|
5884
|
+
// async still in flight for the latest view, not the parent's held
|
|
5885
|
+
// write. A stale shadow recomputing HERE ran its `read(parent)` with
|
|
5886
|
+
// the probe still live and collected the parent too, so the verdict
|
|
5887
|
+
// depended on whether anything had pulled the shadow current earlier
|
|
5888
|
+
// in the tick (#3104: reading latest(m) flipped a later
|
|
5889
|
+
// latest(() => isPending(x)) from true to false).
|
|
5890
|
+
const prevCheck = pendingCheckActive;
|
|
5891
|
+
setPendingCheckActive(false);
|
|
5892
|
+
try {
|
|
5893
|
+
prepareComputed(pendingComputed, true);
|
|
5894
|
+
} finally {
|
|
5895
|
+
setPendingCheckActive(prevCheck);
|
|
5896
|
+
}
|
|
5240
5897
|
}
|
|
5241
5898
|
value = read(pendingComputed);
|
|
5242
5899
|
} catch (e) {
|
|
@@ -5266,12 +5923,42 @@ function latestRead(el) {
|
|
|
5266
5923
|
return pendingComputed._pendingValue;
|
|
5267
5924
|
return value;
|
|
5268
5925
|
}
|
|
5926
|
+
/**
|
|
5927
|
+
* A latest() shadow that is uninitialized only because it was CREATED during
|
|
5928
|
+
* an active flight — its parent source already has a committed value, so
|
|
5929
|
+
* latest() serves that as the visible value and a tracked reader has
|
|
5930
|
+
* something to pair a verdict with (#3166). Same parent resolution as
|
|
5931
|
+
* computePendingState. The pending signal companion also carries
|
|
5932
|
+
* `_parentSource` but is a plain signal (no `_fn`) that never goes pending,
|
|
5933
|
+
* so the `_fn` check is belt-and-braces for this call site.
|
|
5934
|
+
*/
|
|
5935
|
+
function latestShadowWithInitializedParent(owner) {
|
|
5936
|
+
if (typeof owner._fn !== "function") return false;
|
|
5937
|
+
const parentNode = owner._x?._parentSource;
|
|
5938
|
+
if (parentNode === undefined) return false;
|
|
5939
|
+
const parent = parentNode._firewall || parentNode;
|
|
5940
|
+
return !(parent._statusFlags & STATUS_UNINITIALIZED);
|
|
5941
|
+
}
|
|
5269
5942
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
5270
5943
|
function pendingCheckRead(el, c, owner, firewall) {
|
|
5271
5944
|
setPendingCheckActive(false);
|
|
5272
5945
|
if (typeof el._fn === "function") prepareComputed(el, true);
|
|
5273
5946
|
const ownerStatus = owner._statusFlags;
|
|
5274
|
-
if (
|
|
5947
|
+
if (
|
|
5948
|
+
c &&
|
|
5949
|
+
ownerStatus & STATUS_PENDING &&
|
|
5950
|
+
ownerStatus & STATUS_UNINITIALIZED &&
|
|
5951
|
+
// The suspend-throw is for a genuinely-first-load source: the tracked
|
|
5952
|
+
// reader has nothing to pair a verdict with, so it parks on the source.
|
|
5953
|
+
// A latest() SHADOW created lazily mid-flight is born uninitialized even
|
|
5954
|
+
// though its parent has a committed value latest() will serve — throwing
|
|
5955
|
+
// here (swallowed by latestRead's fallback) dropped the shadow from the
|
|
5956
|
+
// probe, so a tracked latest(isPending()) probe created during a
|
|
5957
|
+
// new-question flight cached `false` for that whole flight (#3166).
|
|
5958
|
+
// Defer to the PARENT's initialization state and fall through to normal
|
|
5959
|
+
// collection; the plain pending throw downstream still links the reader.
|
|
5960
|
+
!latestShadowWithInitializedParent(owner)
|
|
5961
|
+
) {
|
|
5275
5962
|
if (tracking && el !== c) link(el, c);
|
|
5276
5963
|
setPendingCheckActive(true);
|
|
5277
5964
|
throw owner._x?._error;
|
|
@@ -5352,6 +6039,17 @@ function isPending(fn) {
|
|
|
5352
6039
|
});
|
|
5353
6040
|
const collectPending = () => {
|
|
5354
6041
|
setPendingCheckActive(false);
|
|
6042
|
+
// Companion reads are mode-neutral plumbing: under an outer latest()
|
|
6043
|
+
// (isPending inside a latest window — #3104's memo shape) leaving latest
|
|
6044
|
+
// mode active dispatched these reads through latestRead, which built a
|
|
6045
|
+
// SHADOW OF THE PENDING SIGNAL itself. The next updatePendingSignal then
|
|
6046
|
+
// wrote that companion-on-companion from inside a recompute
|
|
6047
|
+
// (syncCompanions → setSignal on a shadow created without ownedWrite)
|
|
6048
|
+
// and halted dev with the owned-scope write guard. The creation paths
|
|
6049
|
+
// (getLatestValueComputed / getPendingSignal) already suspend both
|
|
6050
|
+
// modes; this read site must too.
|
|
6051
|
+
const prevLatest = latestReadActive;
|
|
6052
|
+
setLatestReadActive(false);
|
|
5355
6053
|
const prevStrictRead = strictRead;
|
|
5356
6054
|
setStrictRead(false);
|
|
5357
6055
|
try {
|
|
@@ -5363,6 +6061,7 @@ function isPending(fn) {
|
|
|
5363
6061
|
});
|
|
5364
6062
|
} finally {
|
|
5365
6063
|
setStrictRead(prevStrictRead);
|
|
6064
|
+
setLatestReadActive(prevLatest);
|
|
5366
6065
|
setPendingCheckActive(true);
|
|
5367
6066
|
}
|
|
5368
6067
|
// A "not pending" verdict that exists only because this reader saw the
|
|
@@ -5868,6 +6567,74 @@ function createSignal(first, second) {
|
|
|
5868
6567
|
function createMemo(compute, options) {
|
|
5869
6568
|
return accessor(computed(compute, options));
|
|
5870
6569
|
}
|
|
6570
|
+
/**
|
|
6571
|
+
* Creates a reactive effect with **separate compute and effect phases**.
|
|
6572
|
+
*
|
|
6573
|
+
* - `compute(prev)` runs reactively — *put all reactive reads here*. The
|
|
6574
|
+
* returned value is passed to `effect` and is also the new "previous" value
|
|
6575
|
+
* for the next run.
|
|
6576
|
+
* - `effect(next, prev?)` runs imperatively (untracked) after the queue
|
|
6577
|
+
* flushes. *Put DOM writes / fetch / logging / subscriptions here.* It may
|
|
6578
|
+
* return a cleanup function which runs before the next effect or on
|
|
6579
|
+
* disposal.
|
|
6580
|
+
*
|
|
6581
|
+
* Reactive reads inside `effect` will *not* re-trigger this effect — that's
|
|
6582
|
+
* intentional. If you need a single-phase tracked effect, use
|
|
6583
|
+
* `createTrackedEffect` (with the tradeoffs noted there).
|
|
6584
|
+
*
|
|
6585
|
+
* Pass an `EffectBundle` (`{ effect, error }`) instead of a plain function to
|
|
6586
|
+
* intercept **compute-phase** errors — errors thrown by `compute` or arriving
|
|
6587
|
+
* from upstream reactive sources (including async rejections), which your own
|
|
6588
|
+
* code has no frame to `try/catch`. The `error` handler is the error arm of
|
|
6589
|
+
* the effect phase: it runs on the same schedule and in the same imperative,
|
|
6590
|
+
* writable scope as `effect` (setting error state via signals is fine), and
|
|
6591
|
+
* only for *settled* errors — a transient error that recovers before the
|
|
6592
|
+
* effect phase runs `effect` with the recovered value instead, and a held
|
|
6593
|
+
* transition defers it exactly as it defers `effect`. Without an `error`
|
|
6594
|
+
* handler a compute-phase error is logged and the effect simply skips that
|
|
6595
|
+
* run — a non-render effect's reactivity failing does not crash the app.
|
|
6596
|
+
* Rethrowing from `error` escalates it to the nearest error boundary
|
|
6597
|
+
* (halting the system if none exists).
|
|
6598
|
+
*
|
|
6599
|
+
* The **effect phase is different**: it is your own imperative code, so handle
|
|
6600
|
+
* failures with `try/catch` where they occur. An uncaught effect-phase throw
|
|
6601
|
+
* is treated as an unhandled application error — caught by the nearest
|
|
6602
|
+
* `createErrorBoundary`/`<Errored>`, and permanently halting the reactive
|
|
6603
|
+
* system if there is none. It is *not* routed to the bundle's `error` handler.
|
|
6604
|
+
*
|
|
6605
|
+
* ```typescript
|
|
6606
|
+
* createEffect<T>(compute, effectFn | { effect, error }, options?: EffectOptions);
|
|
6607
|
+
* ```
|
|
6608
|
+
* @param compute a function that receives its previous value and returns a new value used to react on a computation
|
|
6609
|
+
* @param effectFn a function that receives the new value and is used to perform side effects (return a cleanup function), or an `EffectBundle` with `effect` and `error` handlers
|
|
6610
|
+
* @param options `EffectOptions` -- name, defer, schedule, transparent
|
|
6611
|
+
*
|
|
6612
|
+
* @example
|
|
6613
|
+
* ```ts
|
|
6614
|
+
* const [count, setCount] = createSignal(0);
|
|
6615
|
+
*
|
|
6616
|
+
* createEffect(
|
|
6617
|
+
* () => count(), // compute: tracks `count`
|
|
6618
|
+
* value => console.log(value) // effect: side effect
|
|
6619
|
+
* );
|
|
6620
|
+
*
|
|
6621
|
+
* setCount(1); // logs 1 after the next flush
|
|
6622
|
+
* ```
|
|
6623
|
+
*
|
|
6624
|
+
* @example
|
|
6625
|
+
* ```ts
|
|
6626
|
+
* createEffect(
|
|
6627
|
+
* () => userId(),
|
|
6628
|
+
* id => {
|
|
6629
|
+
* const ctrl = new AbortController();
|
|
6630
|
+
* fetch(`/users/${id}`, { signal: ctrl.signal });
|
|
6631
|
+
* return () => ctrl.abort(); // cleanup before next run / disposal
|
|
6632
|
+
* }
|
|
6633
|
+
* );
|
|
6634
|
+
* ```
|
|
6635
|
+
*
|
|
6636
|
+
* @description https://docs.solidjs.com/reference/basic-reactivity/create-effect
|
|
6637
|
+
*/
|
|
5871
6638
|
function createEffect(compute, effectFn, options) {
|
|
5872
6639
|
if (effectFn === undefined) {
|
|
5873
6640
|
const message =
|
|
@@ -6086,11 +6853,259 @@ function resolve(fn) {
|
|
|
6086
6853
|
rej(err);
|
|
6087
6854
|
dispose();
|
|
6088
6855
|
},
|
|
6089
|
-
|
|
6856
|
+
// DIRECT_COMMIT: a source settling INTO the held transaction (e.g. a
|
|
6857
|
+
// refresh this action issued) stages its landing; the effect's own
|
|
6858
|
+
// recompute must not stage too, or the microtask apply reads the
|
|
6859
|
+
// stale mainline value and resolves with old data.
|
|
6860
|
+
{ user: true, _extraConfig: CONFIG_DIRECT_COMMIT }
|
|
6090
6861
|
);
|
|
6091
6862
|
});
|
|
6092
6863
|
});
|
|
6093
6864
|
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Invalidates one reactive source, forcing it to re-execute even if its inputs
|
|
6867
|
+
* haven't changed, and returns a promise for the target's NEXT QUIESCENT
|
|
6868
|
+
* STATE — the re-ask (and anything that supersedes it) has settled.
|
|
6869
|
+
*
|
|
6870
|
+
* Pass either a Solid-created accessor or a projected store created from
|
|
6871
|
+
* `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
|
|
6872
|
+
* write-like invalidation operation: it does not read the target's value, and
|
|
6873
|
+
* refreshing a plain signal accessor is a no-op that resolves immediately.
|
|
6874
|
+
*
|
|
6875
|
+
* The returned promise is safe to ignore (fire-and-forget refresh is
|
|
6876
|
+
* unchanged, and a failed refetch will not surface an unhandled rejection).
|
|
6877
|
+
* Awaiting it gives imperative flows the settle point without a reactive
|
|
6878
|
+
* read:
|
|
6879
|
+
* - Accessor targets resolve with the settled value; store targets resolve
|
|
6880
|
+
* with the store node passed (reads through it are fresh after the await).
|
|
6881
|
+
* - A failed re-ask rejects with the error (inside an action's generator,
|
|
6882
|
+
* `yield refresh(x)` throws back at the yield point and the action reverts
|
|
6883
|
+
* like any other failure).
|
|
6884
|
+
* - Semantics are quiescence, not flight identity: if another refresh (or
|
|
6885
|
+
* any invalidation) supersedes this one mid-flight, the promise waits for
|
|
6886
|
+
* — and delivers — whatever finally lands.
|
|
6887
|
+
* - Inside an action, truth landing into the held transaction is STAGED;
|
|
6888
|
+
* the promise still settles then (matching `resolve()`/`until()`, #2930)
|
|
6889
|
+
* and delivers the staged value — the caller's own optimistic override is
|
|
6890
|
+
* never the delivered value.
|
|
6891
|
+
* - The re-ask itself stays verdict-quiet exactly as before: `isPending`
|
|
6892
|
+
* does not flip for a bare refresh (pair with `affects()` for a visible
|
|
6893
|
+
* pending window).
|
|
6894
|
+
*
|
|
6895
|
+
* @example
|
|
6896
|
+
* ```ts
|
|
6897
|
+
* const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
|
|
6898
|
+
*
|
|
6899
|
+
* // Fire-and-forget re-fetch
|
|
6900
|
+
* <button onClick={() => refresh(user)}>Reload</button>;
|
|
6901
|
+
*
|
|
6902
|
+
* // Imperative settle point
|
|
6903
|
+
* const fresh = await refresh(user);
|
|
6904
|
+
* ```
|
|
6905
|
+
*/
|
|
6906
|
+
function refresh(target) {
|
|
6907
|
+
const node = target?.[$REFRESH];
|
|
6908
|
+
if (!node) {
|
|
6909
|
+
{
|
|
6910
|
+
const message =
|
|
6911
|
+
"[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
|
|
6912
|
+
"Pass the original source target, not a wrapper function or derived property read.";
|
|
6913
|
+
emitDiagnostic({
|
|
6914
|
+
code: "INVALID_REFRESH_TARGET",
|
|
6915
|
+
kind: "write",
|
|
6916
|
+
severity: "error",
|
|
6917
|
+
message
|
|
6918
|
+
});
|
|
6919
|
+
throw new Error(message);
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
// Mark now, watch on a microtask. The waiter is resolve()'s machinery with
|
|
6923
|
+
// two extra reader bits, but it must NOT compute at call time (effects
|
|
6924
|
+
// recompute eagerly on creation): same-tick refreshes coalesce into ONE
|
|
6925
|
+
// re-ask only because every mark lands before anything pulls, and eager
|
|
6926
|
+
// per-call pulls turned three refreshes into three fetches. Deferred, the
|
|
6927
|
+
// waiter's first read sees the coalesced state: FRESH_READ pulls the node
|
|
6928
|
+
// through recompute if it is still dirty (self-deduping — a clean node
|
|
6929
|
+
// no-ops, so N waiters cost one pull; this also closes the race where a
|
|
6930
|
+
// waiter reads the PRE-re-ask value as settled and delivers stale), after
|
|
6931
|
+
// which the read either parks on the re-ask's pending window (async — the
|
|
6932
|
+
// settle walk re-runs it on every landing, equal-value and
|
|
6933
|
+
// staged-under-hold included, and a rejection arrives through the effect's
|
|
6934
|
+
// error channel) or serves the sync answer. AUTHORITATIVE_READ keeps an
|
|
6935
|
+
// action's own optimistic override out of the delivered value. resolve()'s
|
|
6936
|
+
// own eager compute is untouched: created after a refresh it still settles
|
|
6937
|
+
// stale-while-revalidate (#2930) — its contract is "first settled value",
|
|
6938
|
+
// not "next quiescent state".
|
|
6939
|
+
markRefresh(node);
|
|
6940
|
+
const promise = new Promise((res, rej) => {
|
|
6941
|
+
queueMicrotask(() => {
|
|
6942
|
+
// No createRoot: the microtask has no ambient owner, so the effect is
|
|
6943
|
+
// naturally detached, and settle disposes the node directly — the root
|
|
6944
|
+
// added ~560B of otherwise-shakeable machinery for nothing but the
|
|
6945
|
+
// dev-mode NO_OWNER_EFFECT warning, so dev keeps a root husk purely to
|
|
6946
|
+
// stay quiet. The waiter swaps in its microtask queue during its own
|
|
6947
|
+
// first compute (before the initial apply enqueue), replacing the
|
|
6948
|
+
// root-owner plumbing.
|
|
6949
|
+
// Typed as the effect node, not Owner: the capture runs inside the
|
|
6950
|
+
// effect's own compute, where the ambient owner IS the effect —
|
|
6951
|
+
// exactly what dispose() takes.
|
|
6952
|
+
let waiter = null;
|
|
6953
|
+
const make = () =>
|
|
6954
|
+
effect(
|
|
6955
|
+
() => {
|
|
6956
|
+
if (waiter === null) {
|
|
6957
|
+
waiter = getOwner();
|
|
6958
|
+
const queue = new MicrotaskQueue();
|
|
6959
|
+
queue._parent = waiter._queue;
|
|
6960
|
+
waiter._queue = queue;
|
|
6961
|
+
}
|
|
6962
|
+
return read(node);
|
|
6963
|
+
},
|
|
6964
|
+
value => {
|
|
6965
|
+
res(typeof target === "function" ? value : target);
|
|
6966
|
+
dispose(waiter);
|
|
6967
|
+
},
|
|
6968
|
+
err => {
|
|
6969
|
+
rej(err);
|
|
6970
|
+
dispose(waiter);
|
|
6971
|
+
},
|
|
6972
|
+
{
|
|
6973
|
+
user: true,
|
|
6974
|
+
_extraConfig: CONFIG_DIRECT_COMMIT | CONFIG_AUTHORITATIVE_READ | CONFIG_FRESH_READ
|
|
6975
|
+
}
|
|
6976
|
+
);
|
|
6977
|
+
createRoot(make);
|
|
6978
|
+
});
|
|
6979
|
+
});
|
|
6980
|
+
// Fire-and-forget refresh must not turn a failed refetch into an unhandled
|
|
6981
|
+
// rejection; awaiting callers attach their own handlers to `promise`.
|
|
6982
|
+
promise.catch(() => {});
|
|
6983
|
+
return promise;
|
|
6984
|
+
}
|
|
6985
|
+
/**
|
|
6986
|
+
* Awaits a reactive predicate and resolves the first time it settles *truthy*,
|
|
6987
|
+
* with that (narrowed) value. Falsy results and pending async reads both mean
|
|
6988
|
+
* "not yet": the subscription stays live and re-evaluates as sources change.
|
|
6989
|
+
* If the predicate settles with an error — a throw, or an async source that
|
|
6990
|
+
* rejects — the promise rejects with it, as do timeout and abort.
|
|
6991
|
+
*
|
|
6992
|
+
* Where {@link resolve} answers "what is this value" (first settled value,
|
|
6993
|
+
* whatever it is), `until` answers "when does the world confirm this
|
|
6994
|
+
* condition". The difference matters inside an `action()`: `yield until(...)`
|
|
6995
|
+
* holds the action's transaction — and any optimistic state riding it — open
|
|
6996
|
+
* until the condition is independently true.
|
|
6997
|
+
*
|
|
6998
|
+
* To make that sound, `until`'s predicate reads the AUTHORITATIVE view — and
|
|
6999
|
+
* this is the one read-semantics difference from `resolve`, which reads the
|
|
7000
|
+
* normal (transaction's own) view where overrides are visible:
|
|
7001
|
+
*
|
|
7002
|
+
* - **Optimistic overrides are invisible** to the predicate. Your own
|
|
7003
|
+
* tentative write can never satisfy your own ack, even on the
|
|
7004
|
+
* single-primitive shape where the optimistic store IS the live-fed store.
|
|
7005
|
+
* (Derived computeds serve their normal cached values — express the
|
|
7006
|
+
* condition over sources of truth, not derived views of the overlay.)
|
|
7007
|
+
* - **Everything else reads normally, including uncommitted transition-staged
|
|
7008
|
+
* data.** Real data is real wherever it currently lives. This is
|
|
7009
|
+
* load-bearing, not a loophole: truth that arrives *into* the open
|
|
7010
|
+
* transaction (a `refresh()` this action issued, an entangled landing)
|
|
7011
|
+
* stages and cannot commit until the hold releases — a predicate that
|
|
7012
|
+
* refused staged reads would deadlock on the very data it is waiting for.
|
|
7013
|
+
*
|
|
7014
|
+
* This is the acknowledgment mechanism for mutations confirmed on a live data
|
|
7015
|
+
* channel (sockets, subscriptions, live queries) rather than by the mutation's
|
|
7016
|
+
* own response: correlate by a client-generated id or version in the predicate,
|
|
7017
|
+
* and let truth arrive however it arrives — push, refetch, or another tab.
|
|
7018
|
+
*
|
|
7019
|
+
* Failure composes with action semantics: a rejection is thrown back into the
|
|
7020
|
+
* generator at the `yield` point — catchable there, or the action fails and
|
|
7021
|
+
* its optimistic state reverts.
|
|
7022
|
+
*
|
|
7023
|
+
* Must be called *outside* a tracking scope.
|
|
7024
|
+
*
|
|
7025
|
+
* @example
|
|
7026
|
+
* ```ts
|
|
7027
|
+
* const send = action(async function* (text: string) {
|
|
7028
|
+
* const clientId = crypto.randomUUID();
|
|
7029
|
+
* setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
|
|
7030
|
+
* await socket.send({ clientId, text }); // fire-and-forget transport
|
|
7031
|
+
* // Hold until the live source echoes the write (authoritative view —
|
|
7032
|
+
* // the optimistic row above cannot satisfy this):
|
|
7033
|
+
* yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
|
|
7034
|
+
* });
|
|
7035
|
+
* ```
|
|
7036
|
+
*
|
|
7037
|
+
* @param fn a reactive predicate over authoritative state
|
|
7038
|
+
* @param options optional `timeout` (ms) and abort `signal`
|
|
7039
|
+
*/
|
|
7040
|
+
function until(fn, options) {
|
|
7041
|
+
if (getObserver()) {
|
|
7042
|
+
throw new Error(
|
|
7043
|
+
"Cannot call until inside a reactive scope; await it from an action or another imperative scope."
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
7046
|
+
// Late-bind the wakeup hook for the A17-silent ack paths (pay-for-use:
|
|
7047
|
+
// apps that never call until() never retain it).
|
|
7048
|
+
installAuthoritativeRead();
|
|
7049
|
+
// Flip-entanglement (#3164 follow-up): the transaction this until() holds
|
|
7050
|
+
// open (the action's, when yielded from one). The predicate is the user's
|
|
7051
|
+
// declaration of what confirms it — when a foreign transition's staged
|
|
7052
|
+
// write flips it truthy, that transition merges here and reveals at the
|
|
7053
|
+
// joint settle instead of painting the confirmation under live optimism.
|
|
7054
|
+
const awaiting = activeTransition;
|
|
7055
|
+
return new Promise((res, rej) => {
|
|
7056
|
+
const signal = options?.signal;
|
|
7057
|
+
if (signal?.aborted) return rej(signal.reason);
|
|
7058
|
+
createRoot(dispose => {
|
|
7059
|
+
// Same delivery contract as resolve() (#2930): effect applies ride a
|
|
7060
|
+
// microtask so the promise can settle while the transaction the caller
|
|
7061
|
+
// yielded it into is still open — that transaction being open is the
|
|
7062
|
+
// entire point of the hold.
|
|
7063
|
+
const owner = getOwner();
|
|
7064
|
+
const queue = new MicrotaskQueue();
|
|
7065
|
+
queue._parent = owner._queue;
|
|
7066
|
+
owner._queue = queue;
|
|
7067
|
+
let timer;
|
|
7068
|
+
let onAbort;
|
|
7069
|
+
const settle = fire => {
|
|
7070
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
7071
|
+
if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
|
|
7072
|
+
fire();
|
|
7073
|
+
dispose();
|
|
7074
|
+
};
|
|
7075
|
+
effect(
|
|
7076
|
+
awaiting === null
|
|
7077
|
+
? fn
|
|
7078
|
+
: () => {
|
|
7079
|
+
const value = fn();
|
|
7080
|
+
// Runs inside the compute (pure phase): the confirming
|
|
7081
|
+
// transition's stamps are live and its commit hasn't run, so
|
|
7082
|
+
// the merge lands before any reveal. Falsy evaluations skip —
|
|
7083
|
+
// non-flipping updates were never named as the confirmation.
|
|
7084
|
+
if (value) entangleConfirmingTransitions(getObserver(), awaiting);
|
|
7085
|
+
return value;
|
|
7086
|
+
},
|
|
7087
|
+
value => {
|
|
7088
|
+
// Falsy is "not yet": keep the subscription live and wait for the
|
|
7089
|
+
// next evaluation. Only a truthy settled value resolves.
|
|
7090
|
+
if (value) settle(() => res(value));
|
|
7091
|
+
},
|
|
7092
|
+
err => settle(() => rej(err)),
|
|
7093
|
+
// AUTHORITATIVE_READ: overrides invisible to the predicate.
|
|
7094
|
+
// DIRECT_COMMIT: truth that stages into the held transaction (a
|
|
7095
|
+
// refresh the action issued) must flow through to the microtask
|
|
7096
|
+
// apply — a staged effect value would deadlock the hold on data
|
|
7097
|
+
// the hold itself is keeping uncommitted.
|
|
7098
|
+
{ user: true, _extraConfig: CONFIG_AUTHORITATIVE_READ | CONFIG_DIRECT_COMMIT }
|
|
7099
|
+
);
|
|
7100
|
+
if (options?.timeout !== undefined)
|
|
7101
|
+
timer = setTimeout(() => settle(() => rej(new TimeoutError())), options.timeout);
|
|
7102
|
+
if (signal !== undefined) {
|
|
7103
|
+
onAbort = () => settle(() => rej(signal.reason));
|
|
7104
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7105
|
+
}
|
|
7106
|
+
});
|
|
7107
|
+
});
|
|
7108
|
+
}
|
|
6094
7109
|
function createOptimistic(first, second) {
|
|
6095
7110
|
// Install before the node exists: only engine-installed programs can carry
|
|
6096
7111
|
// an _overrideValue slot (same runtime-install pattern as
|
|
@@ -7060,8 +8075,29 @@ function materializePB(target) {
|
|
|
7060
8075
|
target.ovl = false;
|
|
7061
8076
|
}
|
|
7062
8077
|
function ensurePB(target) {
|
|
7063
|
-
if (activeTransition !== null) foldBatches.set(target, activeTransition);
|
|
7064
8078
|
let pb = target.pb;
|
|
8079
|
+
// Truth-staged backing hand-off (#3164 fold): a TENTATIVE draft opening on
|
|
8080
|
+
// a target whose pending backing is truth-staged (a landing folded into a
|
|
8081
|
+
// retaining transaction — it carries a foldBatches stamp) must not share
|
|
8082
|
+
// the container. Tentative writes would pollute staged truth, and the
|
|
8083
|
+
// tentative discard (notifyOptimisticWrites nulls pb) would destroy the
|
|
8084
|
+
// landing. Park the staged backing and open a fresh draft seeded from the
|
|
8085
|
+
// optimistic view below; the tentative discard restores it. The
|
|
8086
|
+
// tentativePBs guard scopes this to draft OPEN: the draft's own backing
|
|
8087
|
+
// (foldBatches-stamped by its first write when an action's transition is
|
|
8088
|
+
// ambient) must not be parked by its own later writes.
|
|
8089
|
+
if (
|
|
8090
|
+
pb !== null &&
|
|
8091
|
+
!tentativePBs.has(pb) &&
|
|
8092
|
+
target.fam?.opt === true &&
|
|
8093
|
+
!projectionWriteActive &&
|
|
8094
|
+
!getWriteOverride() &&
|
|
8095
|
+
foldBatches.has(target)
|
|
8096
|
+
) {
|
|
8097
|
+
stagedTruthPB.set(target, pb);
|
|
8098
|
+
pb = target.pb = null;
|
|
8099
|
+
}
|
|
8100
|
+
if (activeTransition !== null) foldBatches.set(target, activeTransition);
|
|
7065
8101
|
if (pb === null) {
|
|
7066
8102
|
// Prototype-chain overlay (#3044): plain-data non-array containers
|
|
7067
8103
|
// outside projection/optimistic families open drafts in O(1) — own keys
|
|
@@ -7083,6 +8119,7 @@ function ensurePB(target) {
|
|
|
7083
8119
|
// seed from committed truth — seeding overrides there would fold a lane
|
|
7084
8120
|
// value into the committed home ("authority wins at reveal" would break).
|
|
7085
8121
|
if (target.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
|
|
8122
|
+
tentativePBs.add(pb);
|
|
7086
8123
|
const nodes = target.n;
|
|
7087
8124
|
if (nodes !== null) {
|
|
7088
8125
|
for (const key of Reflect.ownKeys(nodes)) {
|
|
@@ -7200,6 +8237,17 @@ function queueFold(target) {
|
|
|
7200
8237
|
* Refreshed on every write; resolved through currentTransition at drain
|
|
7201
8238
|
* (transitions merge — same rule as heldMaskView). */
|
|
7202
8239
|
const foldBatches = new WeakMap();
|
|
8240
|
+
/** Parked truth-staged pending backings (#3164 fold): a tentative draft that
|
|
8241
|
+
* opens while a folded landing's backing is live moves the staged container
|
|
8242
|
+
* here (see ensurePB); the tentative discard in notifyOptimisticWrites
|
|
8243
|
+
* restores it in place of the usual null. */
|
|
8244
|
+
const stagedTruthPB = new WeakMap();
|
|
8245
|
+
/** Backings opened by TENTATIVE drafts (optimistic user setters): ensurePB's
|
|
8246
|
+
* truth-park must not fire against the draft's own container on its second
|
|
8247
|
+
* and later writes (the first write stamps foldBatches whenever an action's
|
|
8248
|
+
* transition is ambient). Entries die with their draft — tentative backings
|
|
8249
|
+
* are consumed at setter exit. */
|
|
8250
|
+
const tentativePBs = new WeakSet();
|
|
7203
8251
|
/** Committed-time privatization for parent-chain slot updates (path copying). */
|
|
7204
8252
|
function privatizeCommitted(target) {
|
|
7205
8253
|
if (ownedRaw.has(target.v)) return;
|
|
@@ -7533,7 +8581,13 @@ function notifyWrites(t) {
|
|
|
7533
8581
|
// IMMEDIATE — landed truth shows to untracked readers even while a
|
|
7534
8582
|
// downstream consumer's own async still holds the effect-level reveal
|
|
7535
8583
|
// (spec-async "verdicts never inherit consumers' in-flight state").
|
|
7536
|
-
|
|
8584
|
+
// EXCEPT under an active transaction (#3164 fold): a landing riding a
|
|
8585
|
+
// retaining transaction (the optimistic module's aroundWrite binds it)
|
|
8586
|
+
// stages instead — ensurePB stamped foldBatches, so the backing commits
|
|
8587
|
+
// with the transaction and the reveal is atomic at settle. The pinned
|
|
8588
|
+
// immediate-commit contract is stated over the no-transaction microtask
|
|
8589
|
+
// posture, which `activeTransition === null` is exactly.
|
|
8590
|
+
if (t.fam !== null && t.pb !== null && getWriteOverride() && activeTransition === null) {
|
|
7537
8591
|
// Landed truth (post-await write-override): immediately visible to every
|
|
7538
8592
|
// reader — any staged held view is superseded.
|
|
7539
8593
|
if (t.ht !== null) t.ht = t.hv = null;
|
|
@@ -7786,26 +8840,56 @@ function readSource(target) {
|
|
|
7786
8840
|
const hv = heldMaskView(target);
|
|
7787
8841
|
if (hv !== null) return hv;
|
|
7788
8842
|
}
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
8843
|
+
return pendingBackingVisible(target, false) ? target.pb : target.v;
|
|
8844
|
+
}
|
|
8845
|
+
/** The single pb-vs-committed visibility decision (#3147), shared by per-key
|
|
8846
|
+
* backing reads (readSource) and deep()/snapshot composition (snapshotWalk)
|
|
8847
|
+
* so the two reader families can never disagree about a HELD landing.
|
|
8848
|
+
*
|
|
8849
|
+
* Signal-parity visibility (core read(): owner-context reads serve
|
|
8850
|
+
* _pendingValue, context-free reads serve committed — effects recompute
|
|
8851
|
+
* BEFORE commitPendingNodes in the flush, so the pending view must be
|
|
8852
|
+
* servable). Drafts (setter window OR projection write-override) and
|
|
8853
|
+
* owner-context reads see the pending backing; context-free reads see
|
|
8854
|
+
* committed. Node reads apply the same rule, so all homes agree.
|
|
8855
|
+
*
|
|
8856
|
+
* `speculative` is deep()/snapshot's posture: an untrack/deep PEEK that sees
|
|
8857
|
+
* ordinary pending staging regardless of owner context (the documented
|
|
8858
|
+
* divergence from context-free per-key reads) — but never through a hold:
|
|
8859
|
+
* held truth stays masked exactly as it is for per-key readers. */
|
|
8860
|
+
function pendingBackingVisible(target, speculative) {
|
|
8861
|
+
return (
|
|
7796
8862
|
target.pb !== null &&
|
|
7797
8863
|
(inDraft(target) ||
|
|
7798
8864
|
getWriteOverride() ||
|
|
7799
|
-
|
|
8865
|
+
// Owner-context (and speculative-peek) readers see the pending
|
|
8866
|
+
// backing — EXCEPT held truth on an optimistic family (#3164 fold):
|
|
8867
|
+
// a live pb on an opt family outside the draft/write-override windows
|
|
8868
|
+
// is a staged landing (tentative drafts never outlive their setter),
|
|
8869
|
+
// and only the authoritative postures and latest() see it (the
|
|
8870
|
+
// backing-level twin of core read()'s A17-for-held-truth arm;
|
|
8871
|
+
// ordinary readers keep committed until the transaction's reveal).
|
|
8872
|
+
((speculative || inOwnerContext()) && !heldTruthMasked(target)) ||
|
|
7800
8873
|
// A projection's pending backing is authoritative-elect: serve it to
|
|
7801
8874
|
// context-free readers too UNLESS a transition is holding the node
|
|
7802
8875
|
// commits (downstream async hold — stale committed is the contract)
|
|
7803
8876
|
// or the reader is a CHILDREN_FORBIDDEN scope, which never observes
|
|
7804
8877
|
// its own unsettled write (#3082, signal parity per #3006).
|
|
7805
|
-
(target.fam !== null && !foldHeld(target) && !inForbiddenScope()))
|
|
7806
|
-
)
|
|
7807
|
-
|
|
7808
|
-
|
|
8878
|
+
(target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
|
|
8879
|
+
);
|
|
8880
|
+
}
|
|
8881
|
+
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
8882
|
+
* stamped by a live transition that retains optimism — is masked from
|
|
8883
|
+
* ordinary readers (they keep committed until the transaction's reveal);
|
|
8884
|
+
* the authoritative postures and latest() tunnel through. Un-stamped
|
|
8885
|
+
* backings and optimism-free transitions keep ordinary mid-batch/
|
|
8886
|
+
* speculation visibility. */
|
|
8887
|
+
function heldTruthMasked(target) {
|
|
8888
|
+
if (target.fam?.opt !== true || latestReadActive || authoritativeServe()) return false;
|
|
8889
|
+
const fb = foldBatches.get(target);
|
|
8890
|
+
// opt families are only created by createOptimisticStore, whose module
|
|
8891
|
+
// install populates optHooks — the assertion holds by construction.
|
|
8892
|
+
return fb !== undefined && optHooks.retainsOptimism(fb);
|
|
7809
8893
|
}
|
|
7810
8894
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
7811
8895
|
// Allocation-free own-accessor probe (replaces eager descriptor scans — the
|
|
@@ -7838,6 +8922,26 @@ function runAuthoritative(fn) {
|
|
|
7838
8922
|
function hasActiveOverride(node) {
|
|
7839
8923
|
return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
|
|
7840
8924
|
}
|
|
8925
|
+
/** The reading computation is until()'s authoritative-view predicate — same
|
|
8926
|
+
* source of truth as core read()'s A17 carve-out (`context`, which persists
|
|
8927
|
+
* under untrack). optimisticView()'s composition gate consults exactly this:
|
|
8928
|
+
* write-side machinery (patch emission, tentative re-application) must keep
|
|
8929
|
+
* composing even when it runs inside an authoritative-write bracket. */
|
|
8930
|
+
function authoritativeRead() {
|
|
8931
|
+
const c = context;
|
|
8932
|
+
return c !== null && (c._config & CONFIG_AUTHORITATIVE_READ) !== 0;
|
|
8933
|
+
}
|
|
8934
|
+
/** Serve-side authoritative gate: until()'s predicate PLUS truth authors —
|
|
8935
|
+
* the projection derive's draft (wrapDraft trap brackets, runAuthoritative;
|
|
8936
|
+
* the same posture pair ensurePB classifies drafts by). A source computing
|
|
8937
|
+
* the next truth must never read its callers' tentative overlays: a derive
|
|
8938
|
+
* continuation's `store.push` computing its index from an action's
|
|
8939
|
+
* optimistic row landed truth in the wrong slot and corrupted committed
|
|
8940
|
+
* state (#3108). Trap-level overlay serves gate on this so values, length,
|
|
8941
|
+
* membership, and keys leave the authoritative view together. */
|
|
8942
|
+
function authoritativeServe() {
|
|
8943
|
+
return projectionWriteActive || getWriteOverride() || authoritativeRead();
|
|
8944
|
+
}
|
|
7841
8945
|
/** Context-aware node view for reads outside tracking: active override >
|
|
7842
8946
|
* held pending (owner context) > the BACKING value. Committed truth lives in
|
|
7843
8947
|
* the backing (single-home rule, O6) — node `_value` is never served here,
|
|
@@ -7848,11 +8952,24 @@ function hasActiveOverride(node) {
|
|
|
7848
8952
|
function nodeValue(node, backing) {
|
|
7849
8953
|
// latest() sees the in-flight parked value like an owner-context reader
|
|
7850
8954
|
// does (#3075) — signal/memo parity for store-node-backed keys.
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
8955
|
+
// Authoritative-view reads (until()'s predicate) skip the override arm
|
|
8956
|
+
// only: staged pending values are authoritative, overrides are the
|
|
8957
|
+
// caller's optimism.
|
|
8958
|
+
const v =
|
|
8959
|
+
!authoritativeServe() && hasActiveOverride(node)
|
|
8960
|
+
? unwrapOverride(node._x?._overrideValue)
|
|
8961
|
+
: node._pendingValue !== NOT_PENDING &&
|
|
8962
|
+
(latestReadActive ||
|
|
8963
|
+
// Owner-context pending visibility — except HELD truth (#3164,
|
|
8964
|
+
// see CONFIG_HELD_TRUTH: fold-staged or entangle-stolen
|
|
8965
|
+
// confirming truth), which only authoritative/latest readers
|
|
8966
|
+
// see (core read()'s A17-for-held-truth twin; ordinary readers
|
|
8967
|
+
// keep committed until the transaction's reveal — latest() is
|
|
8968
|
+
// exempted by the leading arm above).
|
|
8969
|
+
((inOwnerContext() || authoritativeServe()) &&
|
|
8970
|
+
!(node._config & CONFIG_HELD_TRUTH && !authoritativeServe())))
|
|
8971
|
+
? node._pendingValue
|
|
8972
|
+
: backing;
|
|
7856
8973
|
return v === FORCE ? backing : v;
|
|
7857
8974
|
}
|
|
7858
8975
|
/** Serve an own data key: node-first when a node exists (pending visibility,
|
|
@@ -7877,13 +8994,18 @@ function serveDataKey(target, key, backingValue, src, node) {
|
|
|
7877
8994
|
read(getNode(target, key, backingValue));
|
|
7878
8995
|
}
|
|
7879
8996
|
}
|
|
7880
|
-
|
|
8997
|
+
// Truth authors read the backing's own length — an optimistic row from
|
|
8998
|
+
// the caller's transaction must not shift where the author's next write
|
|
8999
|
+
// lands (#3108).
|
|
9000
|
+
return (authoritativeServe() ? src : optHooks.optimisticView(target, src)).length;
|
|
7881
9001
|
}
|
|
7882
9002
|
if (inDraft(target)) {
|
|
7883
9003
|
// Optimistic drafts before their first write have no pending backing yet;
|
|
7884
9004
|
// reads must still see the live optimistic view (compose, not clobber —
|
|
7885
9005
|
// #2951). Once ensurePB runs, the seeded clone carries the view.
|
|
7886
|
-
|
|
9006
|
+
// AUTHORITATIVE drafts (projection derive) never overlay — ensurePB's
|
|
9007
|
+
// seeding rule, applied to the read side (#3108).
|
|
9008
|
+
if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
|
|
7887
9009
|
const node = target.n?.[key];
|
|
7888
9010
|
if (node !== undefined && hasActiveOverride(node))
|
|
7889
9011
|
v = unwrapOverride(node._x?._overrideValue);
|
|
@@ -8092,7 +9214,15 @@ const traps = {
|
|
|
8092
9214
|
if (target.s) return serveShallow(target, key, nv);
|
|
8093
9215
|
return isWrappable(nv) ? draftServe(target, wrapNext(nv, target, key)) : nv;
|
|
8094
9216
|
}
|
|
8095
|
-
} else if (
|
|
9217
|
+
} else if (
|
|
9218
|
+
v === undefined &&
|
|
9219
|
+
inDraft(target) &&
|
|
9220
|
+
target.fam?.opt &&
|
|
9221
|
+
target.pb === null &&
|
|
9222
|
+
// AUTHORITATIVE drafts (landing folds) never seed from overrides —
|
|
9223
|
+
// the caller's optimism is not truth (has-trap twin below).
|
|
9224
|
+
!authoritativeServe()
|
|
9225
|
+
) {
|
|
8096
9226
|
const node = target.n?.[key];
|
|
8097
9227
|
if (node !== undefined && hasActiveOverride(node))
|
|
8098
9228
|
v = unwrapOverride(node._x?._overrideValue);
|
|
@@ -8119,14 +9249,16 @@ const traps = {
|
|
|
8119
9249
|
if (!inDraft(target)) {
|
|
8120
9250
|
if (getObserver() !== null) {
|
|
8121
9251
|
const node = getHasNode(target, key, present);
|
|
9252
|
+
// Authoritative-view readers get the right answer for free: core read()
|
|
9253
|
+
// skips the override arm for them, so nv is authoritative presence.
|
|
8122
9254
|
const nv = read(node);
|
|
8123
9255
|
if (hasActiveOverride(node)) present = !!nv;
|
|
8124
|
-
} else {
|
|
9256
|
+
} else if (!authoritativeServe()) {
|
|
8125
9257
|
const node = target.h?.[key];
|
|
8126
9258
|
if (node !== undefined && hasActiveOverride(node))
|
|
8127
9259
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
8128
9260
|
}
|
|
8129
|
-
} else if (target.fam?.opt && target.pb === null) {
|
|
9261
|
+
} else if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
|
|
8130
9262
|
const node = target.h?.[key];
|
|
8131
9263
|
if (node !== undefined && hasActiveOverride(node))
|
|
8132
9264
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
@@ -8152,8 +9284,14 @@ const traps = {
|
|
|
8152
9284
|
// Optimistic membership overlay: presence-node overrides add/remove keys
|
|
8153
9285
|
// (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
|
|
8154
9286
|
// Draft reads before the first write overlay too (pb, once created, is
|
|
8155
|
-
// seeded with the view).
|
|
8156
|
-
|
|
9287
|
+
// seeded with the view). Authoritative-view reads (until()'s predicate,
|
|
9288
|
+
// truth-author drafts) skip the overlay.
|
|
9289
|
+
if (
|
|
9290
|
+
!authoritativeServe() &&
|
|
9291
|
+
target.fam?.opt &&
|
|
9292
|
+
target.h !== null &&
|
|
9293
|
+
(!inDraft(target) || target.pb === null)
|
|
9294
|
+
) {
|
|
8157
9295
|
let set = null;
|
|
8158
9296
|
for (const key of Reflect.ownKeys(target.h)) {
|
|
8159
9297
|
const node = target.h[key];
|
|
@@ -8175,7 +9313,7 @@ const traps = {
|
|
|
8175
9313
|
if (target.del !== null && target.del.has(key)) return undefined;
|
|
8176
9314
|
if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
|
|
8177
9315
|
}
|
|
8178
|
-
if (target.fam?.opt && !inDraft(target)) {
|
|
9316
|
+
if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
|
|
8179
9317
|
const node = target.h?.[key];
|
|
8180
9318
|
if (node !== undefined && hasActiveOverride(node)) {
|
|
8181
9319
|
if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
|
|
@@ -8466,10 +9604,15 @@ function snapshotWalk(value, seen, fam) {
|
|
|
8466
9604
|
if (t === undefined) break;
|
|
8467
9605
|
if (t.fam !== null) fam = t.fam;
|
|
8468
9606
|
if (t.fam?.opt === true) (optOwners ??= []).push(t);
|
|
9607
|
+
// The shared visibility decision (#3147): the speculative peek serves
|
|
9608
|
+
// pending staging, but a HELD landing is masked to committed exactly as
|
|
9609
|
+
// it is for per-key readers — the two families must answer alike while
|
|
9610
|
+
// a transaction holds store landings.
|
|
9611
|
+
const usePB = pendingBackingVisible(t, true);
|
|
8469
9612
|
// Snapshot runs mid-flush (tracked memos execute before commit), so a
|
|
8470
9613
|
// pending prototype overlay must present as a REAL merged container.
|
|
8471
|
-
if (t.ovl) materializePB(t);
|
|
8472
|
-
const backing = t.pb
|
|
9614
|
+
if (usePB && t.ovl) materializePB(t);
|
|
9615
|
+
const backing = usePB ? t.pb : t.v;
|
|
8473
9616
|
if (backing === src) break;
|
|
8474
9617
|
src = backing;
|
|
8475
9618
|
}
|
|
@@ -9138,7 +10281,8 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
9138
10281
|
* driven from inside an enclosing authoritative-write scope (next-store
|
|
9139
10282
|
* optimistic derives), and a hard `false` would clobber it mid-derive.
|
|
9140
10283
|
*/
|
|
9141
|
-
function wrapDraft(inner, isActive,
|
|
10284
|
+
function wrapDraft(inner, isActive, aroundWrite) {
|
|
10285
|
+
const write = op => (aroundWrite ? aroundWrite(op) : op());
|
|
9142
10286
|
const traps = {
|
|
9143
10287
|
get(_, prop) {
|
|
9144
10288
|
let value;
|
|
@@ -9153,7 +10297,7 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9153
10297
|
}
|
|
9154
10298
|
if (prop === $TARGET) return value;
|
|
9155
10299
|
return typeof value === "object" && value !== null
|
|
9156
|
-
? wrapDraft(value, isActive,
|
|
10300
|
+
? wrapDraft(value, isActive, aroundWrite)
|
|
9157
10301
|
: value;
|
|
9158
10302
|
},
|
|
9159
10303
|
has(_, prop) {
|
|
@@ -9175,8 +10319,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9175
10319
|
setWriteOverride(true);
|
|
9176
10320
|
setProjectionWriteActive(true);
|
|
9177
10321
|
try {
|
|
9178
|
-
|
|
9179
|
-
|
|
10322
|
+
write(() => {
|
|
10323
|
+
inner[prop] = value;
|
|
10324
|
+
});
|
|
9180
10325
|
} finally {
|
|
9181
10326
|
setWriteOverride(false);
|
|
9182
10327
|
setProjectionWriteActive(was);
|
|
@@ -9189,8 +10334,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9189
10334
|
setWriteOverride(true);
|
|
9190
10335
|
setProjectionWriteActive(true);
|
|
9191
10336
|
try {
|
|
9192
|
-
|
|
9193
|
-
|
|
10337
|
+
write(() => {
|
|
10338
|
+
delete inner[prop];
|
|
10339
|
+
});
|
|
9194
10340
|
} finally {
|
|
9195
10341
|
setWriteOverride(false);
|
|
9196
10342
|
setProjectionWriteActive(was);
|
|
@@ -9231,8 +10377,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9231
10377
|
setWriteOverride(true);
|
|
9232
10378
|
setProjectionWriteActive(true);
|
|
9233
10379
|
try {
|
|
9234
|
-
|
|
9235
|
-
|
|
10380
|
+
write(() => {
|
|
10381
|
+
Reflect.defineProperty(inner, prop, desc);
|
|
10382
|
+
});
|
|
9236
10383
|
} finally {
|
|
9237
10384
|
setWriteOverride(false);
|
|
9238
10385
|
setProjectionWriteActive(was);
|
|
@@ -9284,7 +10431,7 @@ function createStoreDerivedNext(fn, seed, options) {
|
|
|
9284
10431
|
}
|
|
9285
10432
|
];
|
|
9286
10433
|
}
|
|
9287
|
-
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit,
|
|
10434
|
+
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
|
|
9288
10435
|
const owner = getOwner();
|
|
9289
10436
|
let settled = false;
|
|
9290
10437
|
let result;
|
|
@@ -9298,7 +10445,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
9298
10445
|
const draft = wrapDraft(
|
|
9299
10446
|
wrappedStore,
|
|
9300
10447
|
() => !settled || owner._x?._inFlight === result,
|
|
9301
|
-
|
|
10448
|
+
aroundDraftWrite
|
|
9302
10449
|
);
|
|
9303
10450
|
storeSetterNext(
|
|
9304
10451
|
draft,
|
|
@@ -9313,7 +10460,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
9313
10460
|
if (v === s || v === undefined) return;
|
|
9314
10461
|
const write = () =>
|
|
9315
10462
|
storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
|
|
9316
|
-
wrapCommit ? wrapCommit(write) : write();
|
|
10463
|
+
wrapCommit ? wrapCommit(write, v) : write();
|
|
9317
10464
|
};
|
|
9318
10465
|
const sync = handleAsync(owner, result, commit);
|
|
9319
10466
|
if (!owner._loading) commit(sync);
|
|
@@ -9884,12 +11031,31 @@ function armRowHooks() {
|
|
|
9884
11031
|
* armed presence nodes (the §6 overlay), so structural optimism reverts with
|
|
9885
11032
|
* the same per-transaction granularity (FINDING-2's fix by construction).
|
|
9886
11033
|
*
|
|
9887
|
-
* Derived form = an optimistic projection
|
|
9888
|
-
*
|
|
9889
|
-
*
|
|
9890
|
-
*
|
|
11034
|
+
* Derived form = an optimistic projection. Landings follow the fold rule
|
|
11035
|
+
* (#3164, RUL-2 as re-ruled): while a transaction retains optimistic edits
|
|
11036
|
+
* on the family, truth that lands STAGES into that transaction — a keyed
|
|
11037
|
+
* identity-preserving walk written through the ordinary staged setter
|
|
11038
|
+
* channel — and reveals atomically at settle, exactly like a signal landing
|
|
11039
|
+
* under an active override (asyncWrite's held branch). Optimistic edits are
|
|
11040
|
+
* never consumed by landings; they live exactly as long as their transaction
|
|
11041
|
+
* and die by engine-native revert. With no retainer, landings commit
|
|
11042
|
+
* immediately under projectionWriteActive (authoritative, silently beneath
|
|
11043
|
+
* any bare-write overrides — those ride the flight's own transition, #2951).
|
|
11044
|
+
* Authoritative readers (until()'s predicate) tunnel into staged truth via
|
|
11045
|
+
* the node read path's pending-value arm. The transitionBlocked store-half
|
|
11046
|
+
* (#2951) is installed here for next-shaped targets, chaining the
|
|
9891
11047
|
* legacy/engine checks.
|
|
9892
11048
|
*/
|
|
11049
|
+
/** #3164 fold: a stamped truth is HELD (masked from ordinary readers until
|
|
11050
|
+
* the reveal) only while its transition is live AND retaining optimism —
|
|
11051
|
+
* overrides are what make partial-coverage composition a tear. A plain
|
|
11052
|
+
* async transition carries no overrides, so downstream computes must see
|
|
11053
|
+
* staged values to converge (normal speculation). Resolves merges first:
|
|
11054
|
+
* merge unions optimistic nodes/stores into the target. */
|
|
11055
|
+
function transitionHoldsOptimism(transition) {
|
|
11056
|
+
const t = currentTransition(transition);
|
|
11057
|
+
return t._done !== true && (t._optimisticNodes.length !== 0 || t._optimisticStores.size !== 0);
|
|
11058
|
+
}
|
|
9893
11059
|
let blockedInstalled = false;
|
|
9894
11060
|
function installNextBlockedHalf() {
|
|
9895
11061
|
if (blockedInstalled) return;
|
|
@@ -9897,7 +11063,12 @@ function installNextBlockedHalf() {
|
|
|
9897
11063
|
// Late-bind the optimistic machinery into the plain store/reconcile paths
|
|
9898
11064
|
// (all call sites are fam?.opt-gated, so this always runs first) and the
|
|
9899
11065
|
// affects witness's view resolver.
|
|
9900
|
-
setOptHooks({
|
|
11066
|
+
setOptHooks({
|
|
11067
|
+
notifyOptimisticWrites,
|
|
11068
|
+
optimisticView,
|
|
11069
|
+
applyTentative,
|
|
11070
|
+
retainsOptimism: transitionHoldsOptimism
|
|
11071
|
+
});
|
|
9901
11072
|
setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw));
|
|
9902
11073
|
// Scheduler flush tails call _clearOptimisticStores whenever tracked
|
|
9903
11074
|
// stores exist; next has no layer to clear — reverts are engine-native —
|
|
@@ -9918,6 +11089,13 @@ function installNextBlockedHalf() {
|
|
|
9918
11089
|
// row identity against the post-revert view (resolved from the
|
|
9919
11090
|
// target at drain — overrides are gone by then).
|
|
9920
11091
|
if (ot.pc !== null && ot.pc.ro !== null) rowHooks.emitRowOpsOptimistic(ot, null, null);
|
|
11092
|
+
// Keyset resync (classic channel twin): the keyset node's own
|
|
11093
|
+
// revert can compare EQUAL (a landing's bump matched the
|
|
11094
|
+
// tentative bump) while the arrangement underneath changed —
|
|
11095
|
+
// mapArray/ownKeys subscribers must re-read the post-revert
|
|
11096
|
+
// view. Authoritative bump: never re-arm the node we are
|
|
11097
|
+
// clearing.
|
|
11098
|
+
if (ot.k !== null) runAuthoritative(() => setSignal(ot.k, v => v + 1));
|
|
9921
11099
|
}
|
|
9922
11100
|
}
|
|
9923
11101
|
}
|
|
@@ -9928,14 +11106,21 @@ function installNextBlockedHalf() {
|
|
|
9928
11106
|
GlobalQueue._transitionBlocked = transition => {
|
|
9929
11107
|
for (const store of transition._optimisticStores) {
|
|
9930
11108
|
const t = store?.[$TARGET];
|
|
9931
|
-
const
|
|
11109
|
+
const fam = t?.fam;
|
|
11110
|
+
const fw = fam?.node;
|
|
9932
11111
|
// The hold exists to keep optimistic state alive until the store's own
|
|
9933
11112
|
// truth lands (#2951). Once the family carries NO live overrides (a
|
|
9934
11113
|
// landing consumed them, or they never existed), a pending firewall is
|
|
9935
11114
|
// no reason to park the transaction — blocking then leaks it forever
|
|
9936
11115
|
// when the in-flight question is never answered (undisposed fixtures).
|
|
9937
|
-
if (fw
|
|
9938
|
-
|
|
11116
|
+
if (fw == null || !(fw._statusFlags & STATUS_PENDING)) continue;
|
|
11117
|
+
// Ownership is declared (#3146): only the flight's OWN transaction
|
|
11118
|
+
// parks on the flight (the #2951 anchor routed the bare write there).
|
|
11119
|
+
// A transaction that merely brushed the store never waits for truth
|
|
11120
|
+
// it does not carry.
|
|
11121
|
+
const ft = fam.ft != null ? liveTransition(fam.ft) : null;
|
|
11122
|
+
if (ft !== null && ft !== currentTransition(transition)) continue;
|
|
11123
|
+
if (familyHasLiveOverrides(fam)) return true;
|
|
9939
11124
|
}
|
|
9940
11125
|
return chained(transition);
|
|
9941
11126
|
};
|
|
@@ -9978,39 +11163,304 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
9978
11163
|
};
|
|
9979
11164
|
const store = wrapNext(initialValue, null, null, fam);
|
|
9980
11165
|
fam.px = store;
|
|
11166
|
+
// Same key resolution the projection channels use ("id" default) — replay's
|
|
11167
|
+
// satisfaction rule reads it off the family.
|
|
11168
|
+
const keyOption = options?.key === undefined ? "id" : options.key;
|
|
11169
|
+
fam.key =
|
|
11170
|
+
typeof keyOption === "function"
|
|
11171
|
+
? keyOption
|
|
11172
|
+
: keyOption === null
|
|
11173
|
+
? null
|
|
11174
|
+
: row => (isWrappable(row) ? row[keyOption] : undefined);
|
|
9981
11175
|
if (fam.shallow) {
|
|
9982
11176
|
store[$TARGET].s = true;
|
|
9983
11177
|
markRawIngest(initialValue);
|
|
9984
11178
|
}
|
|
9985
11179
|
if (derived) {
|
|
9986
11180
|
const fn = first;
|
|
9987
|
-
//
|
|
9988
|
-
//
|
|
9989
|
-
//
|
|
9990
|
-
//
|
|
9991
|
-
|
|
9992
|
-
|
|
11181
|
+
// #3146: an async settle event belongs to the flight's OWN transaction.
|
|
11182
|
+
// A live declared one re-enters (a merge if the generic settle path
|
|
11183
|
+
// already entered a graph-stamped stranger — the landing supersedes any
|
|
11184
|
+
// recompute deriving from that stranger's world); a dead one renews (per
|
|
11185
|
+
// A18(1) each arrival reveals on its own schedule, so per-yield
|
|
11186
|
+
// transactions die with their commit and the next settle event opens the
|
|
11187
|
+
// flight's next one — still declared, never anonymous). An UNDECLARED
|
|
11188
|
+
// flight (loading window) keeps the ambient reveal (#2933: the loading
|
|
11189
|
+
// rail is transaction-invisible).
|
|
11190
|
+
const enterFlightTransition = () => {
|
|
11191
|
+
const declared = fam.ft;
|
|
11192
|
+
if (declared == null) return;
|
|
11193
|
+
let ft = liveTransition(declared);
|
|
11194
|
+
if (ft === null) fam.ft = ft = createTransition();
|
|
11195
|
+
fam.node._transition = ft;
|
|
11196
|
+
globalQueue.initTransition(ft);
|
|
11197
|
+
};
|
|
11198
|
+
// Landing router (#3164 fold ruling): while a transaction retains
|
|
11199
|
+
// optimistic edits on this family, truth landings stage INTO it and
|
|
11200
|
+
// reveal atomically at settle; with no retainer they commit immediately
|
|
11201
|
+
// under the authoritative posture (async commits land outside the
|
|
11202
|
+
// computed's sync body, so the posture is re-applied here) — inside the
|
|
11203
|
+
// flight-owned transaction (#3146). Sync commits (the derive's body,
|
|
11204
|
+
// owner is the firewall itself) reveal with their own recompute's flush.
|
|
11205
|
+
const wrapCommit = (write, value) => {
|
|
11206
|
+
const txn = retainingTransition(fam);
|
|
11207
|
+
if (txn !== null) return void stageLanding(fam, txn, value);
|
|
11208
|
+
if (getOwner() !== fam.node) enterFlightTransition();
|
|
9993
11209
|
runAuthoritative(write);
|
|
9994
|
-
|
|
11210
|
+
};
|
|
11211
|
+
// Draft writes (the derive mutating its draft, sync body and post-await
|
|
11212
|
+
// continuations alike) are the same truth channel per-operation: bind
|
|
11213
|
+
// each op to the retaining transaction so its node writes stage and its
|
|
11214
|
+
// backing fold defers (ensurePB stamps foldBatches with the swapped-in
|
|
11215
|
+
// batch; the write-override eager-commit branch in notifyWrites yields
|
|
11216
|
+
// to any active transaction).
|
|
11217
|
+
const aroundDraftWrite = op => {
|
|
11218
|
+
const txn = retainingTransition(fam);
|
|
11219
|
+
if (txn === null) op();
|
|
11220
|
+
else runFolded(txn, op);
|
|
11221
|
+
};
|
|
11222
|
+
// Flight declaration (#3146): a recompute that registered a truth-flight
|
|
11223
|
+
// OWNS its transaction. The ask's transaction is recorded on the family
|
|
11224
|
+
// (created by the flight's own pending throw when none was ambient, the
|
|
11225
|
+
// causing write's/refresh's when one was — graph-driven causality) and
|
|
11226
|
+
// the firewall is stamped so every settle path resolves the flight's
|
|
11227
|
+
// transaction by construction, not by whatever last brushed the node.
|
|
11228
|
+
// When the ask took none (a dead stale stamp — the previous flight's,
|
|
11229
|
+
// cleared nowhere — bare-returns the pre-throw entry), the flight opens
|
|
11230
|
+
// its own here, same activation point as the pre-throw's creation: the
|
|
11231
|
+
// ambient batch (the causing write, same-tick bare optimism) adopts into
|
|
11232
|
+
// it exactly as it would have there, and the pending notification that
|
|
11233
|
+
// follows this unwind registers observers against it. The flight
|
|
11234
|
+
// also registers as its own async reporter: the transaction lives
|
|
11235
|
+
// exactly as long as the question is unanswered, observed or not — the
|
|
11236
|
+
// #2951 refetch-hold no longer depends on a tracked observer having
|
|
11237
|
+
// happened to register one. Loading-window flights declare nothing
|
|
11238
|
+
// (#2933: the loading rail is transaction-invisible); a sync run clears
|
|
11239
|
+
// the declaration.
|
|
11240
|
+
const declareFlight = self => {
|
|
11241
|
+
if (self._x?._inFlight == null) {
|
|
11242
|
+
if (!self._loading) fam.ft = null;
|
|
11243
|
+
return;
|
|
11244
|
+
}
|
|
11245
|
+
if (self._loading) return;
|
|
11246
|
+
let txn = activeTransition;
|
|
11247
|
+
if (txn === null) globalQueue.initTransition((txn = createTransition()));
|
|
11248
|
+
fam.ft = txn;
|
|
11249
|
+
self._transition = txn;
|
|
11250
|
+
let reporters = txn._asyncReporters.get(self);
|
|
11251
|
+
if (reporters === undefined) txn._asyncReporters.set(self, (reporters = new Set()));
|
|
11252
|
+
reporters.add(self);
|
|
9995
11253
|
};
|
|
9996
11254
|
let nodeOptions;
|
|
9997
11255
|
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
9998
11256
|
if (options?.name) nodeOptions = { ...nodeOptions, name: options.name };
|
|
9999
11257
|
const node = computed(() => {
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
11258
|
+
const self = getOwner();
|
|
11259
|
+
try {
|
|
11260
|
+
runAuthoritative(() =>
|
|
11261
|
+
runProjectionComputedNext(
|
|
11262
|
+
store,
|
|
11263
|
+
fn,
|
|
11264
|
+
options?.key === undefined ? "id" : options.key,
|
|
11265
|
+
wrapCommit,
|
|
11266
|
+
aroundDraftWrite
|
|
11267
|
+
)
|
|
11268
|
+
);
|
|
11269
|
+
} finally {
|
|
11270
|
+
declareFlight(self);
|
|
11271
|
+
}
|
|
10009
11272
|
}, nodeOptions);
|
|
10010
11273
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
10011
11274
|
fam.node = node;
|
|
10012
11275
|
}
|
|
10013
|
-
return [
|
|
11276
|
+
return [
|
|
11277
|
+
store,
|
|
11278
|
+
fn => {
|
|
11279
|
+
// Retention ledger (#3164): record the owning transaction so landings
|
|
11280
|
+
// know to fold. Captured at entry — the action machinery has the
|
|
11281
|
+
// transaction ambient while user code runs; a bare write with no
|
|
11282
|
+
// transaction retains nothing (it rides the flight's own transition
|
|
11283
|
+
// per #2951 and dies with it).
|
|
11284
|
+
const txn = activeTransition;
|
|
11285
|
+
storeSetterNext(store, fn);
|
|
11286
|
+
if (txn !== null) (fam.rt ??= new Set()).add(txn);
|
|
11287
|
+
}
|
|
11288
|
+
];
|
|
11289
|
+
}
|
|
11290
|
+
/** Resolve a retained transition through its merge chain (`_done` holds the
|
|
11291
|
+
* merge target while merged, `true` once settled). Null = dead. */
|
|
11292
|
+
function liveTransition(txn) {
|
|
11293
|
+
while (typeof txn._done === "object") txn = txn._done;
|
|
11294
|
+
return txn._done === true ? null : txn;
|
|
11295
|
+
}
|
|
11296
|
+
/** The transaction truth landings fold into: the first live member of the
|
|
11297
|
+
* family's retention ledger (dead members prune here). Multiple live
|
|
11298
|
+
* retainers entangle through their shared family writes and settle
|
|
11299
|
+
* together, so folding into the first reaches all of them. */
|
|
11300
|
+
function retainingTransition(fam) {
|
|
11301
|
+
const rt = fam.rt;
|
|
11302
|
+
if (rt === undefined || rt.size === 0) return null;
|
|
11303
|
+
let live = null;
|
|
11304
|
+
for (const txn of rt) {
|
|
11305
|
+
const resolved = liveTransition(txn);
|
|
11306
|
+
if (resolved === null) rt.delete(txn);
|
|
11307
|
+
else live ??= resolved;
|
|
11308
|
+
}
|
|
11309
|
+
return live;
|
|
11310
|
+
}
|
|
11311
|
+
/** Fold a landing into the retaining transaction (#3164): the landed value
|
|
11312
|
+
* is written through the ORDINARY staged setter channel — node writes park
|
|
11313
|
+
* as `_pendingValue` registered with the transaction's batch (speculation
|
|
11314
|
+
* and until()'s authoritative tunnel see them; live view stays coherent),
|
|
11315
|
+
* and the backing fold defers via the foldBatches stamp — under the
|
|
11316
|
+
* authoritative posture, so armed nodes take the engine bypass and no
|
|
11317
|
+
* override is created. The engine's own commit machinery reveals everything
|
|
11318
|
+
* atomically when the transaction settles (transitions never abort: failed
|
|
11319
|
+
* actions still commit — only optimistic overrides revert). */
|
|
11320
|
+
function stageLanding(fam, txn, incoming) {
|
|
11321
|
+
runFolded(txn, () =>
|
|
11322
|
+
runAuthoritative(() =>
|
|
11323
|
+
storeSetterNext(
|
|
11324
|
+
fam.px,
|
|
11325
|
+
draft => {
|
|
11326
|
+
stagedApply(draft, unwrapValue(incoming), fam.key ?? null);
|
|
11327
|
+
},
|
|
11328
|
+
false
|
|
11329
|
+
)
|
|
11330
|
+
)
|
|
11331
|
+
);
|
|
11332
|
+
}
|
|
11333
|
+
/** Run a fold write inside the retaining transaction's batch, then
|
|
11334
|
+
* transition-stamp its staged nodes NOW (parity with the parked-transition
|
|
11335
|
+
* flush path's reassignPendingTransition): the stamp is what routes stale
|
|
11336
|
+
* (render) readers to the committed value — core read's cross-transaction
|
|
11337
|
+
* guard — and what makes foldHeld defer the backing for context-free
|
|
11338
|
+
* readers. A microtask staging never crosses that flush path, so without
|
|
11339
|
+
* the stamp a render effect's speculative recompute would compose staged
|
|
11340
|
+
* truth with live overrides — the #3164 tear, one window later. Armed
|
|
11341
|
+
* nodes additionally raise CONFIG_HELD_TRUTH: their staged value is
|
|
11342
|
+
* confirming truth masked from ordinary readers until the reveal (plain
|
|
11343
|
+
* staged nodes stay visible — normal speculation; override-covered nodes
|
|
11344
|
+
* stay unarmed — the override is their display and its revert their
|
|
11345
|
+
* notification, A17). */
|
|
11346
|
+
function runFolded(txn, op) {
|
|
11347
|
+
runAsTransitionBatch(txn, op);
|
|
11348
|
+
const pending = txn._pendingNodes;
|
|
11349
|
+
for (let i = 0; i < pending.length; i++) {
|
|
11350
|
+
const node = pending[i];
|
|
11351
|
+
node._transition = txn;
|
|
11352
|
+
if (node._config & CONFIG_OPTIMISTIC && !hasActiveOverride(node))
|
|
11353
|
+
node._config |= CONFIG_HELD_TRUTH;
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
/** Keyed identity-preserving deep merge through live draft proxies — the
|
|
11357
|
+
* staged twin of the adoption walk. Reads see the pending backing (staged
|
|
11358
|
+
* view), so consecutive landings during one hold compose; key-matched rows
|
|
11359
|
+
* keep their raw (and so their proxy) in the slot with only changed leaves
|
|
11360
|
+
* written; unmatched rows land wholesale. Runs inside stageLanding's
|
|
11361
|
+
* authoritative bracket: drafts seed from committed truth, never overlays. */
|
|
11362
|
+
function stagedApply(cur, incoming, keyFn) {
|
|
11363
|
+
const curArr = Array.isArray(cur);
|
|
11364
|
+
if (curArr && Array.isArray(incoming)) {
|
|
11365
|
+
const len = incoming.length;
|
|
11366
|
+
if (keyFn !== null) {
|
|
11367
|
+
// Occurrence-aware key queues (parity with the adoption window):
|
|
11368
|
+
// duplicate keys match per occurrence, each current row consumed once.
|
|
11369
|
+
let byKey = null;
|
|
11370
|
+
const curLen = cur.length;
|
|
11371
|
+
for (let j = 0; j < curLen; j++) {
|
|
11372
|
+
const raw = unwrapValue(cur[j]);
|
|
11373
|
+
if (!isWrappable(raw)) continue;
|
|
11374
|
+
const k = keyFn(raw);
|
|
11375
|
+
if (k === undefined) continue;
|
|
11376
|
+
const q = (byKey ??= new Map()).get(k);
|
|
11377
|
+
if (q === undefined) byKey.set(k, [raw]);
|
|
11378
|
+
else q.push(raw);
|
|
11379
|
+
}
|
|
11380
|
+
// Echo adoption: an optimistic structural add whose key the landing
|
|
11381
|
+
// confirms must keep its raw (and so its proxy — list drivers keep the
|
|
11382
|
+
// DOM row). Tentative rows never reach committed truth (they live in
|
|
11383
|
+
// node overrides), so key-match the draft target's active override
|
|
11384
|
+
// rows as a secondary pool. Committed rows queued first own their
|
|
11385
|
+
// keys; overlay rows only extend coverage. Adopted raws enter staged
|
|
11386
|
+
// truth; at settle the override reverts and the reveal re-seats the
|
|
11387
|
+
// same raw.
|
|
11388
|
+
const overlayNodes = cur[$TARGET]?.n;
|
|
11389
|
+
if (overlayNodes != null) {
|
|
11390
|
+
for (const ok of Reflect.ownKeys(overlayNodes)) {
|
|
11391
|
+
const node = overlayNodes[ok];
|
|
11392
|
+
if (!hasActiveOverride(node)) continue;
|
|
11393
|
+
const raw = unwrapValue(unwrapOverride(node._x._overrideValue));
|
|
11394
|
+
if (!isWrappable(raw)) continue;
|
|
11395
|
+
const k = keyFn(raw);
|
|
11396
|
+
if (k === undefined) continue;
|
|
11397
|
+
const q = (byKey ??= new Map()).get(k);
|
|
11398
|
+
if (q === undefined) byKey.set(k, [raw]);
|
|
11399
|
+
else q.push(raw);
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
for (let i = 0; i < len; i++) {
|
|
11403
|
+
const nv = incoming[i];
|
|
11404
|
+
let matched;
|
|
11405
|
+
if (isWrappable(nv) && byKey !== null) {
|
|
11406
|
+
const nk = keyFn(nv);
|
|
11407
|
+
if (nk !== undefined) {
|
|
11408
|
+
for (const [k, q] of byKey) {
|
|
11409
|
+
if (!sameKey(k, nk)) continue;
|
|
11410
|
+
matched = q.shift();
|
|
11411
|
+
if (q.length === 0) byKey.delete(k);
|
|
11412
|
+
break;
|
|
11413
|
+
}
|
|
11414
|
+
}
|
|
11415
|
+
}
|
|
11416
|
+
if (matched !== undefined) {
|
|
11417
|
+
if (unwrapValue(cur[i]) !== matched) cur[i] = matched;
|
|
11418
|
+
stagedApply(cur[i], nv, keyFn);
|
|
11419
|
+
} else {
|
|
11420
|
+
const pv = unwrapValue(cur[i]);
|
|
11421
|
+
if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) cur[i] = nv;
|
|
11422
|
+
}
|
|
11423
|
+
}
|
|
11424
|
+
} else {
|
|
11425
|
+
for (let i = 0; i < len; i++) {
|
|
11426
|
+
const nv = incoming[i];
|
|
11427
|
+
const pv = unwrapValue(cur[i]);
|
|
11428
|
+
if (pv === nv) continue;
|
|
11429
|
+
if (isWrappable(nv) && isWrappable(pv) && Array.isArray(nv) === Array.isArray(pv))
|
|
11430
|
+
stagedApply(cur[i], nv, keyFn);
|
|
11431
|
+
else if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) cur[i] = nv;
|
|
11432
|
+
}
|
|
11433
|
+
}
|
|
11434
|
+
if (cur.length !== len) cur.length = len;
|
|
11435
|
+
return;
|
|
11436
|
+
}
|
|
11437
|
+
// Object merge; also the degenerate root-kind-change shape (arrays accept
|
|
11438
|
+
// keyed writes/deletes, so a wholesale restatement still lands staged).
|
|
11439
|
+
for (const k of Reflect.ownKeys(incoming)) {
|
|
11440
|
+
if (curArr && k === "length") continue;
|
|
11441
|
+
const nv = incoming[k];
|
|
11442
|
+
const pv = unwrapValue(cur[k]);
|
|
11443
|
+
if (pv === nv) continue;
|
|
11444
|
+
if (isWrappable(nv) && isWrappable(pv) && Array.isArray(nv) === Array.isArray(pv)) {
|
|
11445
|
+
// Different-keyed entities never merge (tentative-channel parity):
|
|
11446
|
+
// the incoming object replaces the slot wholesale.
|
|
11447
|
+
if (keyFn !== null) {
|
|
11448
|
+
const pk = keyFn(pv);
|
|
11449
|
+
const nk = keyFn(nv);
|
|
11450
|
+
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) {
|
|
11451
|
+
cur[k] = nv;
|
|
11452
|
+
continue;
|
|
11453
|
+
}
|
|
11454
|
+
}
|
|
11455
|
+
stagedApply(cur[k], nv, keyFn);
|
|
11456
|
+
} else if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) {
|
|
11457
|
+
cur[k] = nv;
|
|
11458
|
+
}
|
|
11459
|
+
}
|
|
11460
|
+
for (const k of Reflect.ownKeys(cur)) {
|
|
11461
|
+
if ((curArr && k === "length") || k in incoming) continue;
|
|
11462
|
+
delete cur[k];
|
|
11463
|
+
}
|
|
10014
11464
|
}
|
|
10015
11465
|
// ---- optimistic-only store machinery (moved from next/store.ts /
|
|
10016
11466
|
// next/reconcile.ts so plain-store bundles tree-shake it) ----
|
|
@@ -10019,13 +11469,18 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
10019
11469
|
* for exactly the changed keys. Visible-view diffing keeps no-op writes from
|
|
10020
11470
|
* entangling lanes (RUL-10 / opt R38). */
|
|
10021
11471
|
function notifyOptimisticWrites(t, pb) {
|
|
10022
|
-
// A bare write while the store's own truth is in flight rides
|
|
10023
|
-
// transaction (#2951
|
|
10024
|
-
//
|
|
11472
|
+
// A bare write while the store's own truth is in flight rides the FLIGHT'S
|
|
11473
|
+
// OWN transaction (#2951 via the #3146 declaration): entangle it so the
|
|
11474
|
+
// override survives until the refetch settles instead of flash-reverting
|
|
10025
11475
|
// at plain flush end. The blocked-check store-half keeps that transaction
|
|
10026
|
-
// from settling while the firewall is pending.
|
|
10027
|
-
|
|
10028
|
-
|
|
11476
|
+
// from settling while the firewall is pending. Declared ownership replaces
|
|
11477
|
+
// the old circumstantial route through the firewall's `_transition` stamp,
|
|
11478
|
+
// which was whatever last brushed the node.
|
|
11479
|
+
const declared = t.fam?.ft;
|
|
11480
|
+
if (declared != null) {
|
|
11481
|
+
const ft = liveTransition(declared);
|
|
11482
|
+
if (ft !== null) globalQueue.initTransition(ft);
|
|
11483
|
+
}
|
|
10029
11484
|
const old = t.v;
|
|
10030
11485
|
// Patch channel (override-application site): the draft IS the intended
|
|
10031
11486
|
// visible state; prev is the view before these overrides apply. Bypasses
|
|
@@ -10093,96 +11548,22 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
10093
11548
|
// (structural ones already ride the key-set bump above).
|
|
10094
11549
|
bumpDeep(t);
|
|
10095
11550
|
// Discard the draft — committed raw is untouched (revert target by
|
|
10096
|
-
// construction)
|
|
10097
|
-
//
|
|
10098
|
-
|
|
11551
|
+
// construction) — restoring any truth-staged backing this draft displaced
|
|
11552
|
+
// (#3164 fold: ensurePB parked it so tentative writes could not pollute
|
|
11553
|
+
// staged truth). Register the root store for the scheduler's settle hooks.
|
|
11554
|
+
t.pb = stagedTruthPB.get(t) ?? null;
|
|
11555
|
+
if (t.pb !== null) stagedTruthPB.delete(t);
|
|
10099
11556
|
(t.fam.overlaid ??= new Set()).add(t);
|
|
10100
11557
|
GlobalQueue._trackOptimisticStore?.(t.fam.px ?? t.px);
|
|
10101
11558
|
}
|
|
10102
|
-
/**
|
|
10103
|
-
* Landing consumption (RUL-2): fresh authoritative data supersedes every
|
|
10104
|
-
* tentative override in the family. Mirrors legacy clearProjectionOverride —
|
|
10105
|
-
* drop the override, clear lane/ownership, notify subscribers whose visible
|
|
10106
|
-
* value changes (reversion effects go to regular queues via the projection
|
|
10107
|
-
* write posture the caller holds).
|
|
10108
|
-
*/
|
|
10109
|
-
function consumeOverridesNext(fam) {
|
|
10110
|
-
const overlaid = fam.overlaid;
|
|
10111
|
-
if (overlaid === undefined || overlaid.size === 0) return;
|
|
10112
|
-
runAuthoritative(() => {
|
|
10113
|
-
for (const t of overlaid) {
|
|
10114
|
-
const drop = (node, committed) => {
|
|
10115
|
-
if (!hasActiveOverride(node)) return;
|
|
10116
|
-
const prev = unwrapOverride(node._x?._overrideValue);
|
|
10117
|
-
// Full legacy reset (clearOptimisticOverride parity): the landing is
|
|
10118
|
-
// authoritative NOW — fold committed into the node directly instead
|
|
10119
|
-
// of riding a transaction's commit (whose queues may be stashed with
|
|
10120
|
-
// the transaction parked; the wake would strand until it settles).
|
|
10121
|
-
ext(node)._overrideValue = NOT_PENDING;
|
|
10122
|
-
node._config |= CONFIG_OPTIMISTIC;
|
|
10123
|
-
const nx = node._x;
|
|
10124
|
-
if (nx) {
|
|
10125
|
-
nx._overrideOwner = null;
|
|
10126
|
-
nx._optimisticLane = undefined;
|
|
10127
|
-
}
|
|
10128
|
-
node._pendingValue = NOT_PENDING;
|
|
10129
|
-
node._value = committed;
|
|
10130
|
-
if (!node._equals || !node._equals(prev, committed)) {
|
|
10131
|
-
insertSubs(node, true);
|
|
10132
|
-
schedule();
|
|
10133
|
-
}
|
|
10134
|
-
};
|
|
10135
|
-
// Landing consumes STRUCTURAL optimism only (legacy layer parity):
|
|
10136
|
-
// membership edits, array length, and the value overrides written WITH
|
|
10137
|
-
// them (a key carrying an active presence override is an add/delete —
|
|
10138
|
-
// classified BEFORE the adoption may have made the key exist in landed
|
|
10139
|
-
// data). A pure value override on a key the landing carries stays with
|
|
10140
|
-
// its owning transaction (rapid-toggle contract: a live action's edit
|
|
10141
|
-
// of an existing entity rides on top of landed truth).
|
|
10142
|
-
const isArr = Array.isArray(t.v);
|
|
10143
|
-
const has = t.h;
|
|
10144
|
-
let structuralKeys = null;
|
|
10145
|
-
if (has !== null) {
|
|
10146
|
-
for (const key of Reflect.ownKeys(has)) {
|
|
10147
|
-
if (hasActiveOverride(has[key])) (structuralKeys ??= new Set()).add(key);
|
|
10148
|
-
}
|
|
10149
|
-
}
|
|
10150
|
-
const nodes = t.n;
|
|
10151
|
-
if (nodes !== null) {
|
|
10152
|
-
for (const key of Reflect.ownKeys(nodes)) {
|
|
10153
|
-
const structural =
|
|
10154
|
-
structuralKeys?.has(key) || !(key in t.v) || (isArr && key === "length");
|
|
10155
|
-
if (!structural) continue;
|
|
10156
|
-
drop(nodes[key], isArr && key === "length" ? t.v.length : t.v[key]);
|
|
10157
|
-
}
|
|
10158
|
-
}
|
|
10159
|
-
if (has !== null) {
|
|
10160
|
-
for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
|
|
10161
|
-
}
|
|
10162
|
-
if (t.k !== null && hasActiveOverride(t.k)) {
|
|
10163
|
-
ext(t.k)._overrideValue = NOT_PENDING;
|
|
10164
|
-
t.k._config |= CONFIG_OPTIMISTIC;
|
|
10165
|
-
const kx = t.k._x;
|
|
10166
|
-
if (kx) {
|
|
10167
|
-
kx._overrideOwner = null;
|
|
10168
|
-
kx._optimisticLane = undefined;
|
|
10169
|
-
}
|
|
10170
|
-
insertSubs(t.k, true);
|
|
10171
|
-
schedule();
|
|
10172
|
-
}
|
|
10173
|
-
// Patch channel (override-consumption site): visible truth flipped to
|
|
10174
|
-
// committed for the consumed keys — force a re-apply from the live
|
|
10175
|
-
// view so the DOM leaves the override state.
|
|
10176
|
-
if (t.pc !== null && t.pc.p !== null) patchHooks.emitPatchOptimistic(t, null, null);
|
|
10177
|
-
}
|
|
10178
|
-
overlaid.clear();
|
|
10179
|
-
});
|
|
10180
|
-
}
|
|
10181
11559
|
/** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
|
|
10182
11560
|
* view, lane values included; a fresh copy per call during pending windows —
|
|
10183
|
-
* RUL-12). Returns `src` untouched when no override is active on `t`.
|
|
11561
|
+
* RUL-12). Returns `src` untouched when no override is active on `t`.
|
|
11562
|
+
* Authoritative-view reads (until()'s predicate) skip composition entirely:
|
|
11563
|
+
* the predicate observes authoritative truth, never the caller's tentative
|
|
11564
|
+
* overlay. (Write-side emission callers never run under such a compute.) */
|
|
10184
11565
|
function optimisticView(t, src) {
|
|
10185
|
-
if (t.fam?.opt !== true) return src;
|
|
11566
|
+
if (t.fam?.opt !== true || authoritativeRead()) return src;
|
|
10186
11567
|
let out = null;
|
|
10187
11568
|
const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
|
|
10188
11569
|
const nodes = t.n;
|
|
@@ -11506,7 +12887,10 @@ function flattenArray(children, results = [], options) {
|
|
|
11506
12887
|
} while (typeof child === "function" && !child.length);
|
|
11507
12888
|
}
|
|
11508
12889
|
if (Array.isArray(child)) {
|
|
11509
|
-
|
|
12890
|
+
// OR, don't overwrite: an accessor already pushed under doNotUnwrap
|
|
12891
|
+
// still needs the resolving wrapper even when a later sibling
|
|
12892
|
+
// fragment contains no functions (#3133).
|
|
12893
|
+
needsUnwrap = flattenArray(child, results, options) || needsUnwrap;
|
|
11510
12894
|
} else if (
|
|
11511
12895
|
options?.skipNonRendered &&
|
|
11512
12896
|
(child == null || child === true || child === false || child === "")
|
|
@@ -11534,6 +12918,7 @@ export {
|
|
|
11534
12918
|
NoOwnerError,
|
|
11535
12919
|
NotReadyError,
|
|
11536
12920
|
SUPPORTS_PROXY,
|
|
12921
|
+
TimeoutError,
|
|
11537
12922
|
action,
|
|
11538
12923
|
affects,
|
|
11539
12924
|
clearSnapshots,
|
|
@@ -11592,5 +12977,6 @@ export {
|
|
|
11592
12977
|
storeHasOptimisticFamily,
|
|
11593
12978
|
storeIsShallow,
|
|
11594
12979
|
storePath,
|
|
12980
|
+
until,
|
|
11595
12981
|
untrack
|
|
11596
12982
|
};
|