@solidjs/signals 2.0.0-rc.5 → 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 +475 -72
- package/dist/node.cjs +851 -662
- package/dist/prod/core/async.js +41 -19
- package/dist/prod/core/core.js +58 -62
- package/dist/prod/core/effect.js +6 -6
- package/dist/prod/core/heap.js +1 -1
- package/dist/prod/core/lanes.js +4 -4
- package/dist/prod/core/optimistic.js +16 -16
- package/dist/prod/core/owner.js +4 -4
- package/dist/prod/core/scheduler.js +45 -32
- package/dist/prod/core/verdict.js +23 -19
- package/dist/prod/signals.js +68 -1
- package/dist/prod/store/next/optimistic.js +137 -64
- package/dist/prod/store/next/projection.js +2 -2
- package/dist/prod/store/next/store.js +122 -106
- package/dist/types/core/attribution-hooks.d.ts +11 -0
- package/dist/types/core/attribution.d.ts +48 -0
- package/dist/types/core/dev.d.ts +8 -2
- package/dist/types/core/scheduler.d.ts +5 -0
- package/dist/types/signals.d.ts +0 -11
- package/dist/types/store/next/target.d.ts +10 -0
- package/dist/types-cjs/core/attribution-hooks.d.cts +11 -0
- package/dist/types-cjs/core/attribution.d.cts +48 -0
- package/dist/types-cjs/core/dev.d.cts +8 -2
- package/dist/types-cjs/core/scheduler.d.cts +5 -0
- package/dist/types-cjs/signals.d.cts +0 -11
- package/dist/types-cjs/store/next/target.d.cts +10 -0
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -232,7 +232,8 @@ const defaultOptions = {
|
|
|
232
232
|
wideDeps: 30,
|
|
233
233
|
hotTime: { budgetMs: 8, windowMs: 1000 },
|
|
234
234
|
unstableMemos: 4,
|
|
235
|
-
wideWrites: 250
|
|
235
|
+
wideWrites: 250,
|
|
236
|
+
waterfalls: { minFlightMs: 50 }
|
|
236
237
|
};
|
|
237
238
|
let options = { ...defaultOptions };
|
|
238
239
|
const listeners = new Set();
|
|
@@ -425,10 +426,13 @@ function checkDepWidth(el) {
|
|
|
425
426
|
});
|
|
426
427
|
console.warn(message);
|
|
427
428
|
}
|
|
429
|
+
const hotCauses = new Map();
|
|
430
|
+
const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
428
431
|
/**
|
|
429
432
|
* Hot-scope warning — flags a scope that re-ran more than `count` times
|
|
430
433
|
* inside one `windowMs` window. Warned once per window, with the most recent
|
|
431
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).
|
|
432
436
|
*/
|
|
433
437
|
function checkHotRuns(el, event) {
|
|
434
438
|
const cfg = options.hotRuns;
|
|
@@ -443,22 +447,55 @@ function checkHotRuns(el, event) {
|
|
|
443
447
|
node._devWinCount = (node._devWinCount ?? 0) + 1;
|
|
444
448
|
if (node._devHotWarned || node._devWinCount < cfg.count) return;
|
|
445
449
|
node._devHotWarned = true;
|
|
446
|
-
|
|
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;
|
|
447
487
|
const message =
|
|
448
|
-
`[
|
|
449
|
-
|
|
450
|
-
`
|
|
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.`;
|
|
451
492
|
emitDiagnostic({
|
|
452
|
-
code: "
|
|
493
|
+
code: "HOT_SCOPE_FANOUT",
|
|
453
494
|
kind: "perf",
|
|
454
495
|
severity: "warn",
|
|
455
496
|
message,
|
|
456
|
-
nodeName:
|
|
457
|
-
data: {
|
|
458
|
-
runs: node._devWinCount,
|
|
459
|
-
windowMs: cfg.windowMs,
|
|
460
|
-
causes: event.causes.map(c => c.name)
|
|
461
|
-
}
|
|
497
|
+
nodeName: causeKey,
|
|
498
|
+
data: { cause: causeKey, scopes: window.scopes, runs: window.runs, windowMs: cfg.windowMs }
|
|
462
499
|
});
|
|
463
500
|
console.warn(message);
|
|
464
501
|
}
|
|
@@ -637,6 +674,120 @@ function checkUnstableOutput(el, prevValue, newValue) {
|
|
|
637
674
|
});
|
|
638
675
|
console.warn(message);
|
|
639
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
|
+
}
|
|
640
791
|
// The engine's implementation of the core's dev hook points. Installed by
|
|
641
792
|
// enable(), uninstalled by disable() — while uninstalled the core pays one
|
|
642
793
|
// null check per site and nothing else.
|
|
@@ -710,6 +861,9 @@ const engineHooks = {
|
|
|
710
861
|
refreshed(el) {
|
|
711
862
|
stampWrite(el, "refresh");
|
|
712
863
|
},
|
|
864
|
+
flightStart(el, flight) {
|
|
865
|
+
trackFlightStart(el, flight);
|
|
866
|
+
},
|
|
713
867
|
asyncStart(el) {
|
|
714
868
|
asyncStartSeq = el._devChange?.seq ?? 0;
|
|
715
869
|
asyncStartTime = el._time;
|
|
@@ -726,6 +880,9 @@ const engineHooks = {
|
|
|
726
880
|
const committed =
|
|
727
881
|
el._value !== asyncStartValue || el._time !== asyncStartTime || el._pendingValue === value;
|
|
728
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);
|
|
729
886
|
return;
|
|
730
887
|
}
|
|
731
888
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
@@ -734,6 +891,7 @@ const engineHooks = {
|
|
|
734
891
|
const change = el._devChange;
|
|
735
892
|
if (change !== undefined && change.seq > asyncStartSeq && change.kind === "write")
|
|
736
893
|
stampWrite(el, "async", NO_VALUES, value);
|
|
894
|
+
finalizeFlight(el);
|
|
737
895
|
}
|
|
738
896
|
};
|
|
739
897
|
const attribution = {
|
|
@@ -742,6 +900,8 @@ const attribution = {
|
|
|
742
900
|
frames.length = 0;
|
|
743
901
|
scopeCosts.clear();
|
|
744
902
|
writeCosts.clear();
|
|
903
|
+
waterfallLog = [];
|
|
904
|
+
hotCauses.clear();
|
|
745
905
|
setAttributionHooks(engineHooks);
|
|
746
906
|
},
|
|
747
907
|
disable() {
|
|
@@ -750,6 +910,8 @@ const attribution = {
|
|
|
750
910
|
frames.length = 0;
|
|
751
911
|
scopeCosts.clear();
|
|
752
912
|
writeCosts.clear();
|
|
913
|
+
waterfallLog = [];
|
|
914
|
+
hotCauses.clear();
|
|
753
915
|
setAttributionHooks(null);
|
|
754
916
|
},
|
|
755
917
|
subscribe(listener) {
|
|
@@ -775,6 +937,15 @@ const attribution = {
|
|
|
775
937
|
writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
|
|
776
938
|
};
|
|
777
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
|
+
},
|
|
778
949
|
format: formatRerun
|
|
779
950
|
};
|
|
780
951
|
|
|
@@ -1920,6 +2091,12 @@ function commitPendingNode(n) {
|
|
|
1920
2091
|
n._pendingValue = NOT_PENDING;
|
|
1921
2092
|
// Set _modified for effects, but not for tracked effects (they handle their own scheduling)
|
|
1922
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;
|
|
1923
2100
|
}
|
|
1924
2101
|
// The committed hold is the first observable answer for a loading-window
|
|
1925
2102
|
// node — the window closes here, not at compute time (#2990). Unconditional
|
|
@@ -2187,6 +2364,13 @@ function transitionComplete(transition) {
|
|
|
2187
2364
|
done && (transition._done = true);
|
|
2188
2365
|
return done;
|
|
2189
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
|
+
}
|
|
2190
2374
|
function currentTransition(transition) {
|
|
2191
2375
|
while (transition._done && typeof transition._done === "object") transition = transition._done;
|
|
2192
2376
|
return transition;
|
|
@@ -2851,12 +3035,14 @@ function addPendingSource(el, source) {
|
|
|
2851
3035
|
return true;
|
|
2852
3036
|
}
|
|
2853
3037
|
function removePendingSource(el, source) {
|
|
2854
|
-
|
|
2855
|
-
if (
|
|
3038
|
+
const sources = el._x?._pendingSources;
|
|
3039
|
+
if (!sources?.delete(source)) return false;
|
|
3040
|
+
if (!sources.size) el._x._pendingSources = undefined;
|
|
2856
3041
|
return true;
|
|
2857
3042
|
}
|
|
2858
3043
|
function clearPendingSources(el) {
|
|
2859
|
-
|
|
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.
|
|
2860
3046
|
if (el._x !== null) el._x._pendingSources = undefined;
|
|
2861
3047
|
}
|
|
2862
3048
|
// A rejection-pending only resolves through the settle sweep over the
|
|
@@ -2973,6 +3159,49 @@ function settleErroredDependents(el, error) {
|
|
|
2973
3159
|
if (scheduled) schedule();
|
|
2974
3160
|
}
|
|
2975
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);
|
|
2976
3205
|
let scheduled = false;
|
|
2977
3206
|
let released;
|
|
2978
3207
|
const visited = new Set();
|
|
@@ -3069,6 +3298,12 @@ function handleAsync(el, result, setter) {
|
|
|
3069
3298
|
// fired _flightTeardown. A future non-recompute registration path must
|
|
3070
3299
|
// release it here before overwriting _inFlight.
|
|
3071
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);
|
|
3072
3307
|
let syncValue;
|
|
3073
3308
|
// Settle-time transition re-entry. The loading rail is invisible to
|
|
3074
3309
|
// transactions (#2933): a boundary-caught first load never registers as an
|
|
@@ -3127,6 +3362,10 @@ function handleAsync(el, result, setter) {
|
|
|
3127
3362
|
}
|
|
3128
3363
|
settleTransition();
|
|
3129
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);
|
|
3130
3369
|
el._time = clock;
|
|
3131
3370
|
// A real error settles derivatively-pending dependents (notifyStatus
|
|
3132
3371
|
// cleared their pending sources), so stranded lazy ones release here —
|
|
@@ -3141,8 +3380,16 @@ function handleAsync(el, result, setter) {
|
|
|
3141
3380
|
if (el._flags & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
|
|
3142
3381
|
settleTransition();
|
|
3143
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;
|
|
3144
3390
|
trimStaleDeps(el);
|
|
3145
3391
|
clearStatus(el);
|
|
3392
|
+
if (wasReask) el._x._reask = true;
|
|
3146
3393
|
const lane = resolveLane(el);
|
|
3147
3394
|
if (lane) lane._pendingAsync.delete(el);
|
|
3148
3395
|
// Attribution hook: lets the engine snapshot state before the landing
|
|
@@ -3236,8 +3483,12 @@ function handleAsync(el, result, setter) {
|
|
|
3236
3483
|
// (`_pendingValue` set above or inside setSignal) is not — the verdict's
|
|
3237
3484
|
// held-value branch is window-gated, and commitPendingNode closes the
|
|
3238
3485
|
// window when the hold commits, so no one-frame isPending pulse can leak
|
|
3239
|
-
// to live observers between the landing and its commit (#2990).
|
|
3240
|
-
|
|
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
|
+
}
|
|
3241
3492
|
settlePendingSource(el);
|
|
3242
3493
|
schedule();
|
|
3243
3494
|
flush();
|
|
@@ -3701,8 +3952,7 @@ function recompute(el, create = false) {
|
|
|
3701
3952
|
// settles synchronously, those dependents settle HERE — asyncWrite's
|
|
3702
3953
|
// settlePendingSource walk never runs for a landing that was preempted
|
|
3703
3954
|
// (#3181).
|
|
3704
|
-
const wasPendingSource =
|
|
3705
|
-
(el._statusFlags & STATUS_PENDING) !== 0 && el._x?._pendingSources?.has(el) === true;
|
|
3955
|
+
const wasPendingSource = el._x?._pendingSources?.has(el);
|
|
3706
3956
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
3707
3957
|
// the recompute wipes _flags below.
|
|
3708
3958
|
const hadReask = (el._flags & REACTIVE_REASK) !== 0;
|
|
@@ -3816,6 +4066,9 @@ function recompute(el, create = false) {
|
|
|
3816
4066
|
undefined,
|
|
3817
4067
|
notReady ? el._x?._optimisticLane : undefined
|
|
3818
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);
|
|
3819
4072
|
if (reaskChanged) GlobalQueue._repollVerdicts(el);
|
|
3820
4073
|
}
|
|
3821
4074
|
} finally {
|
|
@@ -3948,17 +4201,11 @@ function recompute(el, create = false) {
|
|
|
3948
4201
|
// (el._x?._error re-set), so this only runs on a genuinely clean recovery.
|
|
3949
4202
|
if (outgoingError !== undefined && !valueChanged && !el._x?._error)
|
|
3950
4203
|
settleErroredDependents(el, outgoingError);
|
|
3951
|
-
//
|
|
3952
|
-
//
|
|
3953
|
-
//
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
// it re-throws its cached NotReadyError forever, and every reader that
|
|
3957
|
-
// suspended through that memo re-parks on the dead source), but the walk
|
|
3958
|
-
// runs on the changed shape too, exactly as the landing path does —
|
|
3959
|
-
// insertSubs notifies value SUBSCRIBERS, not pending REGISTRANTS, and
|
|
3960
|
-
// the two sets only partially overlap.
|
|
3961
|
-
if (wasPendingSource && !(el._statusFlags & STATUS_PENDING)) settlePendingSource(el);
|
|
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);
|
|
3962
4209
|
}
|
|
3963
4210
|
// Attribution hook: fired before the lane restore so `currentOptimisticLane`
|
|
3964
4211
|
// still reflects THIS run's posture. The facts distinguish an overlay
|
|
@@ -5467,7 +5714,11 @@ function computePendingState(el) {
|
|
|
5467
5714
|
) {
|
|
5468
5715
|
if (hasActiveOverride$1(el))
|
|
5469
5716
|
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._x?._overrideValue));
|
|
5470
|
-
|
|
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;
|
|
5471
5722
|
}
|
|
5472
5723
|
return newQuestionInFlight(comp);
|
|
5473
5724
|
}
|
|
@@ -6316,6 +6567,74 @@ function createSignal(first, second) {
|
|
|
6316
6567
|
function createMemo(compute, options) {
|
|
6317
6568
|
return accessor(computed(compute, options));
|
|
6318
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
|
+
*/
|
|
6319
6638
|
function createEffect(compute, effectFn, options) {
|
|
6320
6639
|
if (effectFn === undefined) {
|
|
6321
6640
|
const message =
|
|
@@ -8521,33 +8840,43 @@ function readSource(target) {
|
|
|
8521
8840
|
const hv = heldMaskView(target);
|
|
8522
8841
|
if (hv !== null) return hv;
|
|
8523
8842
|
}
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
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 (
|
|
8531
8862
|
target.pb !== null &&
|
|
8532
8863
|
(inDraft(target) ||
|
|
8533
8864
|
getWriteOverride() ||
|
|
8534
|
-
// Owner-context readers see the pending
|
|
8535
|
-
// on an optimistic family (#3164 fold):
|
|
8536
|
-
// outside the draft/write-override windows
|
|
8537
|
-
// (tentative drafts never outlive their setter),
|
|
8538
|
-
// authoritative postures and latest() see it (the
|
|
8539
|
-
// of core read()'s A17-for-held-truth arm;
|
|
8540
|
-
// committed until the transaction's reveal).
|
|
8541
|
-
(inOwnerContext() && !heldTruthMasked(target)) ||
|
|
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)) ||
|
|
8542
8873
|
// A projection's pending backing is authoritative-elect: serve it to
|
|
8543
8874
|
// context-free readers too UNLESS a transition is holding the node
|
|
8544
8875
|
// commits (downstream async hold — stale committed is the contract)
|
|
8545
8876
|
// or the reader is a CHILDREN_FORBIDDEN scope, which never observes
|
|
8546
8877
|
// its own unsettled write (#3082, signal parity per #3006).
|
|
8547
8878
|
(target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
|
|
8548
|
-
)
|
|
8549
|
-
return target.pb;
|
|
8550
|
-
return target.v;
|
|
8879
|
+
);
|
|
8551
8880
|
}
|
|
8552
8881
|
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
8553
8882
|
* stamped by a live transition that retains optimism — is masked from
|
|
@@ -9275,10 +9604,15 @@ function snapshotWalk(value, seen, fam) {
|
|
|
9275
9604
|
if (t === undefined) break;
|
|
9276
9605
|
if (t.fam !== null) fam = t.fam;
|
|
9277
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);
|
|
9278
9612
|
// Snapshot runs mid-flush (tracked memos execute before commit), so a
|
|
9279
9613
|
// pending prototype overlay must present as a REAL merged container.
|
|
9280
|
-
if (t.ovl) materializePB(t);
|
|
9281
|
-
const backing = t.pb
|
|
9614
|
+
if (usePB && t.ovl) materializePB(t);
|
|
9615
|
+
const backing = usePB ? t.pb : t.v;
|
|
9282
9616
|
if (backing === src) break;
|
|
9283
9617
|
src = backing;
|
|
9284
9618
|
}
|
|
@@ -10772,14 +11106,21 @@ function installNextBlockedHalf() {
|
|
|
10772
11106
|
GlobalQueue._transitionBlocked = transition => {
|
|
10773
11107
|
for (const store of transition._optimisticStores) {
|
|
10774
11108
|
const t = store?.[$TARGET];
|
|
10775
|
-
const
|
|
11109
|
+
const fam = t?.fam;
|
|
11110
|
+
const fw = fam?.node;
|
|
10776
11111
|
// The hold exists to keep optimistic state alive until the store's own
|
|
10777
11112
|
// truth lands (#2951). Once the family carries NO live overrides (a
|
|
10778
11113
|
// landing consumed them, or they never existed), a pending firewall is
|
|
10779
11114
|
// no reason to park the transaction — blocking then leaks it forever
|
|
10780
11115
|
// when the in-flight question is never answered (undisposed fixtures).
|
|
10781
|
-
if (fw
|
|
10782
|
-
|
|
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;
|
|
10783
11124
|
}
|
|
10784
11125
|
return chained(transition);
|
|
10785
11126
|
};
|
|
@@ -10837,15 +11178,35 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
10837
11178
|
}
|
|
10838
11179
|
if (derived) {
|
|
10839
11180
|
const fn = first;
|
|
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
|
+
};
|
|
10840
11198
|
// Landing router (#3164 fold ruling): while a transaction retains
|
|
10841
11199
|
// optimistic edits on this family, truth landings stage INTO it and
|
|
10842
11200
|
// reveal atomically at settle; with no retainer they commit immediately
|
|
10843
11201
|
// under the authoritative posture (async commits land outside the
|
|
10844
|
-
// computed's sync body, so the posture is re-applied here)
|
|
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.
|
|
10845
11205
|
const wrapCommit = (write, value) => {
|
|
10846
11206
|
const txn = retainingTransition(fam);
|
|
10847
|
-
if (txn
|
|
10848
|
-
|
|
11207
|
+
if (txn !== null) return void stageLanding(fam, txn, value);
|
|
11208
|
+
if (getOwner() !== fam.node) enterFlightTransition();
|
|
11209
|
+
runAuthoritative(write);
|
|
10849
11210
|
};
|
|
10850
11211
|
// Draft writes (the derive mutating its draft, sync body and post-await
|
|
10851
11212
|
// continuations alike) are the same truth channel per-operation: bind
|
|
@@ -10858,19 +11219,56 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
10858
11219
|
if (txn === null) op();
|
|
10859
11220
|
else runFolded(txn, op);
|
|
10860
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);
|
|
11253
|
+
};
|
|
10861
11254
|
let nodeOptions;
|
|
10862
11255
|
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
10863
11256
|
if (options?.name) nodeOptions = { ...nodeOptions, name: options.name };
|
|
10864
11257
|
const node = computed(() => {
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
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
|
+
}
|
|
10874
11272
|
}, nodeOptions);
|
|
10875
11273
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
10876
11274
|
fam.node = node;
|
|
@@ -11071,13 +11469,18 @@ function stagedApply(cur, incoming, keyFn) {
|
|
|
11071
11469
|
* for exactly the changed keys. Visible-view diffing keeps no-op writes from
|
|
11072
11470
|
* entangling lanes (RUL-10 / opt R38). */
|
|
11073
11471
|
function notifyOptimisticWrites(t, pb) {
|
|
11074
|
-
// A bare write while the store's own truth is in flight rides
|
|
11075
|
-
// transaction (#2951
|
|
11076
|
-
//
|
|
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
|
|
11077
11475
|
// at plain flush end. The blocked-check store-half keeps that transaction
|
|
11078
|
-
// from settling while the firewall is pending.
|
|
11079
|
-
|
|
11080
|
-
|
|
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
|
+
}
|
|
11081
11484
|
const old = t.v;
|
|
11082
11485
|
// Patch channel (override-application site): the draft IS the intended
|
|
11083
11486
|
// visible state; prev is the view before these overrides apply. Bypasses
|