@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.
Files changed (57) hide show
  1. package/dist/dev.js +1618 -232
  2. package/dist/node.cjs +2262 -1199
  3. package/dist/prod/boundaries.js +4 -1
  4. package/dist/prod/core/async.js +152 -101
  5. package/dist/prod/core/constants.js +55 -1
  6. package/dist/prod/core/core.js +305 -234
  7. package/dist/prod/core/effect.js +28 -28
  8. package/dist/prod/core/error.js +13 -1
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +27 -27
  11. package/dist/prod/core/heap.js +30 -30
  12. package/dist/prod/core/lanes.js +32 -32
  13. package/dist/prod/core/optimistic.js +57 -57
  14. package/dist/prod/core/owner.js +34 -34
  15. package/dist/prod/core/scheduler.js +346 -152
  16. package/dist/prod/core/verdict.js +122 -65
  17. package/dist/prod/index.js +3 -3
  18. package/dist/prod/map.js +106 -106
  19. package/dist/prod/signals.js +321 -26
  20. package/dist/prod/store/next/optimistic.js +363 -151
  21. package/dist/prod/store/next/patch.js +6 -6
  22. package/dist/prod/store/next/projection.js +25 -21
  23. package/dist/prod/store/next/store.js +223 -113
  24. package/dist/prod/store/store.js +2 -2
  25. package/dist/types/core/async.d.ts +2 -0
  26. package/dist/types/core/attribution-hooks.d.ts +11 -0
  27. package/dist/types/core/attribution.d.ts +57 -4
  28. package/dist/types/core/constants.d.ts +54 -0
  29. package/dist/types/core/core.d.ts +19 -20
  30. package/dist/types/core/dev.d.ts +8 -2
  31. package/dist/types/core/error.d.ts +9 -0
  32. package/dist/types/core/index.d.ts +2 -2
  33. package/dist/types/core/scheduler.d.ts +39 -0
  34. package/dist/types/core/types.d.ts +12 -0
  35. package/dist/types/index.d.ts +3 -3
  36. package/dist/types/signals.d.ts +108 -11
  37. package/dist/types/store/next/optimistic.d.ts +13 -10
  38. package/dist/types/store/next/projection.d.ts +1 -1
  39. package/dist/types/store/next/store.d.ts +22 -0
  40. package/dist/types/store/next/target.d.ts +25 -0
  41. package/dist/types-cjs/core/async.d.cts +2 -0
  42. package/dist/types-cjs/core/attribution-hooks.d.cts +11 -0
  43. package/dist/types-cjs/core/attribution.d.cts +57 -4
  44. package/dist/types-cjs/core/constants.d.cts +54 -0
  45. package/dist/types-cjs/core/core.d.cts +19 -20
  46. package/dist/types-cjs/core/dev.d.cts +8 -2
  47. package/dist/types-cjs/core/error.d.cts +9 -0
  48. package/dist/types-cjs/core/index.d.cts +2 -2
  49. package/dist/types-cjs/core/scheduler.d.cts +39 -0
  50. package/dist/types-cjs/core/types.d.cts +12 -0
  51. package/dist/types-cjs/index.d.cts +3 -3
  52. package/dist/types-cjs/signals.d.cts +108 -11
  53. package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
  54. package/dist/types-cjs/store/next/projection.d.cts +1 -1
  55. package/dist/types-cjs/store/next/store.d.cts +22 -0
  56. package/dist/types-cjs/store/next/target.d.cts +25 -0
  57. 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
- const rootCause = event.causes.map(c => `"${c.name}" (${c.kind})`).join(", ");
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
- `[HOT_SCOPE_RERUNS] ${event.nodeKind} "${event.nodeName}" re-ran ${node._devWinCount} times ` +
383
- `in ${Math.max(1, now - node._devWinStart)}msa hot signal is likely leaking into this ` +
384
- `scope. Latest cause: ${rootCause || "(untracked pull)"}`;
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: "HOT_SCOPE_RERUNS",
493
+ code: "HOT_SCOPE_FANOUT",
387
494
  kind: "perf",
388
495
  severity: "warn",
389
496
  message,
390
- nodeName: event.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) transition = currentTransition(transition);
1593
- if (transition && transition === activeTransition) return;
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
- commitPendingNode(pendingNodes[i]);
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) throw new Error("Potential Infinite Loop Detected.");
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
- if (!el._x?._pendingSources?.delete(source)) return false;
2573
- if (el._x?._pendingSources.size === 0) if (el._x !== null) el._x._pendingSources = undefined;
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
- el._x?._pendingSources?.clear();
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 !== null && updateCompanions(node);
3223
+ updateCompanions?.(node);
2713
3224
  } else {
2714
3225
  node._statusFlags &= ~STATUS_PENDING;
2715
3226
  if (!errored) setPendingError(node);
2716
- updateCompanions !== null && updateCompanions(node);
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 !== null && GlobalQueue._syncCompanions(el, value);
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 !== null && GlobalQueue._syncCompanions(el, value);
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
- if (el._pendingValue === NOT_PENDING) el._loading = false;
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 !== null && GlobalQueue._updatePendingSignal(el);
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) el._x._inFlight = 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
- (isEffect && (activeTransition !== el._transition || activeTransition === null)) ||
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
- if (el._flags & REACTIVE_RECOMPUTING_DEPS) return;
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 != null ? options.equals : isEqual,
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 x = el._x;
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 != null ? options.equals : isEqual,
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
- return unwrapOverride(el._x?._overrideValue);
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
- throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
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(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
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
- * Invalidates one reactive source, forcing it to re-execute even if its inputs
4394
- * haven't changed.
4395
- *
4396
- * Pass either a Solid-created accessor or a projected store created from
4397
- * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
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 refresh(target) {
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
- return true;
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
- prepareComputed(pendingComputed, true);
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 (c && ownerStatus & STATUS_PENDING && ownerStatus & STATUS_UNINITIALIZED) {
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
- { user: true }
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
- if (t.fam !== null && t.pb !== null && getWriteOverride()) {
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
- // Signal-parity visibility (core read(): owner-context reads serve
7790
- // _pendingValue, context-free reads serve committed — effects recompute
7791
- // BEFORE commitPendingNodes in the flush, so the pending view must be
7792
- // servable). Drafts (setter window OR projection write-override) and
7793
- // owner-context reads see the pending backing; context-free reads see
7794
- // committed. Node reads apply the same rule, so both homes agree.
7795
- if (
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
- inOwnerContext() ||
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
- return target.pb;
7808
- return target.v;
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
- const v = hasActiveOverride(node)
7852
- ? unwrapOverride(node._x?._overrideValue)
7853
- : node._pendingValue !== NOT_PENDING && (latestReadActive || inOwnerContext())
7854
- ? node._pendingValue
7855
- : backing;
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
- return optHooks.optimisticView(target, src).length;
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
- if (target.fam?.opt && target.pb === null) {
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 (v === undefined && inDraft(target) && target.fam?.opt && target.pb === null) {
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
- if (target.fam?.opt && target.h !== null && (!inDraft(target) || target.pb === null)) {
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 ?? t.v;
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, onDraftWrite) {
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, onDraftWrite)
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
- inner[prop] = value;
9179
- onDraftWrite?.();
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
- delete inner[prop];
9193
- onDraftWrite?.();
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
- Reflect.defineProperty(inner, prop, desc);
9235
- onDraftWrite?.();
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, onDraftWrite) {
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
- onDraftWrite
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: the derive's recompute and its
9888
- * async commits run under projectionWriteActive (authoritative landings
9889
- * commit silently beneath any active overrides). The transitionBlocked
9890
- * store-half (#2951) is installed here for next-shaped targets, chaining the
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({ notifyOptimisticWrites, optimisticView, applyTentative });
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 fw = t?.fam?.node;
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 != null && fw._statusFlags & STATUS_PENDING && familyHasLiveOverrides(t.fam))
9938
- return true;
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
- // Async commits land outside the computed's sync body — re-apply the
9988
- // authoritative-write posture there too. Landings consume the family's
9989
- // tentative overrides (RUL-2: visible landed truth replaces optimism)
9990
- // both the reconcile-channel commit and per-op post-await draft writes.
9991
- const consume = () => consumeOverridesNext(fam);
9992
- const wrapCommit = write => {
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
- consume();
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
- runAuthoritative(() =>
10001
- runProjectionComputedNext(
10002
- store,
10003
- fn,
10004
- options?.key === undefined ? "id" : options.key,
10005
- wrapCommit,
10006
- consume
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 [store, fn => storeSetterNext(store, fn)];
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 THAT
10023
- // transaction (#2951, legacy parity): entangle the firewall's transition so
10024
- // the override survives until the refetch settles instead of flash-reverting
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
- const fw = t.fam?.node;
10028
- if (fw?._transition) globalQueue.initTransition(fw._transition);
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). Register the root store for the scheduler's settle hooks
10097
- // and the target for landing consumption (RUL-2).
10098
- t.pb = null;
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
- needsUnwrap = flattenArray(child, results, options);
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
  };