@solidjs/signals 2.0.0-rc.14 → 2.0.0-rc.15

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 (66) hide show
  1. package/dist/dev-shared.js +248 -189
  2. package/dist/dev.attribution.js +124 -41
  3. package/dist/dev.js +553 -322
  4. package/dist/observe/affects.js +34 -31
  5. package/dist/observe/boundaries.js +166 -132
  6. package/dist/observe/core/async.js +90 -83
  7. package/dist/observe/core/attribution-queries.js +1 -1
  8. package/dist/observe/core/attribution.js +281 -197
  9. package/dist/observe/core/constants.js +7 -2
  10. package/dist/observe/core/context.js +3 -3
  11. package/dist/observe/core/core.js +277 -245
  12. package/dist/observe/core/dev.js +3 -3
  13. package/dist/observe/core/effect.js +21 -21
  14. package/dist/observe/core/external.js +2 -2
  15. package/dist/observe/core/graph.js +25 -25
  16. package/dist/observe/core/heap.js +9 -9
  17. package/dist/observe/core/lanes.js +206 -160
  18. package/dist/observe/core/owner.js +27 -27
  19. package/dist/observe/core/scheduler.js +92 -86
  20. package/dist/observe/core/verdict.js +66 -54
  21. package/dist/observe/map.js +35 -35
  22. package/dist/observe/reveal.js +24 -24
  23. package/dist/observe/signals.js +38 -28
  24. package/dist/observe/store/affects.js +75 -68
  25. package/dist/observe/store/hooks.js +13 -0
  26. package/dist/observe/store/index.js +0 -2
  27. package/dist/observe/store/optimistic.js +67 -34
  28. package/dist/observe/store/projection.js +37 -31
  29. package/dist/observe/store/reconcile.js +32 -30
  30. package/dist/observe/store/store.js +128 -85
  31. package/dist/prod/affects.js +34 -31
  32. package/dist/prod/boundaries.js +143 -109
  33. package/dist/prod/core/async.js +64 -67
  34. package/dist/prod/core/constants.js +7 -2
  35. package/dist/prod/core/context.js +3 -3
  36. package/dist/prod/core/core.js +302 -270
  37. package/dist/prod/core/effect.js +38 -38
  38. package/dist/prod/core/external.js +4 -4
  39. package/dist/prod/core/graph.js +27 -27
  40. package/dist/prod/core/heap.js +30 -30
  41. package/dist/prod/core/lanes.js +257 -211
  42. package/dist/prod/core/owner.js +39 -39
  43. package/dist/prod/core/scheduler.js +125 -119
  44. package/dist/prod/core/verdict.js +70 -58
  45. package/dist/prod/map.js +73 -73
  46. package/dist/prod/reveal.js +38 -38
  47. package/dist/prod/signals.js +30 -20
  48. package/dist/prod/store/affects.js +75 -68
  49. package/dist/prod/store/hooks.js +13 -0
  50. package/dist/prod/store/index.js +0 -2
  51. package/dist/prod/store/optimistic.js +114 -81
  52. package/dist/prod/store/projection.js +30 -24
  53. package/dist/prod/store/reconcile.js +32 -30
  54. package/dist/prod/store/store.js +136 -93
  55. package/dist/types/boundaries.d.ts +4 -0
  56. package/dist/types/core/attribution.d.ts +6 -2
  57. package/dist/types/core/constants.d.ts +5 -0
  58. package/dist/types/core/core.d.ts +33 -3
  59. package/dist/types/core/lanes.d.ts +10 -0
  60. package/dist/types/core/scheduler.d.ts +12 -6
  61. package/dist/types/core/types.d.ts +0 -6
  62. package/dist/types/store/affects.d.ts +11 -0
  63. package/dist/types/store/hooks.d.ts +48 -0
  64. package/dist/types/store/index.d.ts +0 -1
  65. package/dist/types/store/store.d.ts +3 -38
  66. package/package.json +1 -1
package/dist/dev.js CHANGED
@@ -10,27 +10,27 @@ import {
10
10
  b as CONFIG_VERDICT,
11
11
  c as REACTIVE_PROBE_UNANSWERED,
12
12
  d as blocked,
13
- e as enqueueSub,
13
+ e as REACTIVE_LANE_DIRTY,
14
+ f as enqueueSub,
14
15
  r as resolveTx,
15
- f as CONFIG_AUTHORITATIVE,
16
+ g as CONFIG_AUTHORITATIVE,
16
17
  t as txOf,
17
- g as tracking,
18
- h as CONFIG_CHILDREN_FORBIDDEN,
18
+ h as tracking,
19
+ i as CONFIG_CHILDREN_FORBIDDEN,
19
20
  p as passLane,
20
21
  S as STATUS_PENDING,
21
22
  E as EFFECT_RENDER,
22
- i as sameLane,
23
- j as CONFIG_HELD,
24
- k as flushTransaction,
25
- l as STATUS_UNINITIALIZED,
26
- m as NotReadyError,
27
- o as REACTIVE_SCREEN_READ,
28
- u as REACTIVE_FRAME_READ,
29
- v as REACTIVE_LANE_READ,
30
- w as ext,
31
- x as REACTIVE_RECOMPUTING_DEPS,
32
- y as queuePendingNode,
33
- z as REACTIVE_LANE_DIRTY,
23
+ j as sameLane,
24
+ k as CONFIG_HELD,
25
+ l as flushTransaction,
26
+ m as STATUS_UNINITIALIZED,
27
+ o as NotReadyError,
28
+ u as REACTIVE_SCREEN_READ,
29
+ v as REACTIVE_FRAME_READ,
30
+ w as REACTIVE_LANE_READ,
31
+ x as ext,
32
+ y as REACTIVE_RECOMPUTING_DEPS,
33
+ z as queuePendingNode,
34
34
  A as list,
35
35
  B as clock,
36
36
  D as insertSubs,
@@ -43,119 +43,123 @@ import {
43
43
  M as ownFlights,
44
44
  O as setPassLane,
45
45
  P as REACTIVE_JOINED,
46
- Q as stagedRead,
47
- T as attrHooks,
48
- U as holdNode,
49
- V as joinFuture,
50
- W as CONFIG_SLOT_NODE,
51
- X as commitPendingNode,
52
- Y as REACTIVE_IN_HEAP,
53
- Z as REACTIVE_DIRTY,
54
- _ as REACTIVE_CHECK,
55
- $ as CONFIG_INPUTS_PUBLISHED,
56
- a0 as setVerdict,
57
- a1 as context,
58
- a2 as REACTIVE_PROBED,
59
- a3 as globalQueue,
60
- a4 as staleReader,
61
- a5 as REACTIVE_STAGED_READ,
62
- a6 as verdict,
63
- a7 as link,
64
- a8 as pullComputed,
65
- a9 as strictRead,
66
- aa as warnStrictReadUntracked,
67
- ab as markLateLinker,
68
- ac as spectating,
69
- ad as STATUS_ERROR,
70
- ae as unflushedValue,
71
- af as stagedReaders,
72
- ag as enterAction,
73
- ah as setEffectCallback,
74
- ai as exitAction,
75
- aj as isThenable,
76
- ak as actionDepth,
77
- al as flush,
78
- am as setQuestion,
79
- an as emitDiagnostic,
80
- ao as enterCallback,
81
- ap as exitCallback,
82
- aq as getOwner,
83
- ar as inEffectCallback,
84
- as as signal,
85
- at as setSignal,
86
- au as cleanup,
87
- av as read,
88
- aw as NoOwnerError,
89
- ax as ContextNotFoundError,
90
- ay as createEffectNode,
91
- az as recompute,
92
- aA as EFFECT_USER,
93
- aB as reportDiagnostic,
94
- aC as computed,
95
- aD as staleValues,
96
- aE as CONFIG_AUTO_DISPOSE,
97
- aF as EFFECT_TRACKED,
98
- aG as unwrapStatusError,
99
- aH as haltReactivity,
100
- aI as setStrictRead,
101
- aJ as StatusError,
102
- aK as trimStaleDeps,
103
- aL as setTrackedQueueCallback,
104
- aM as setEffectStatusNotify,
105
- aN as _hitUnhandledAsync,
106
- aO as resetUnhandledAsync,
107
- aP as ROOT_ERROR_HOOK,
108
- aQ as registerGraph,
109
- aR as runWithOwner,
110
- aS as setMemo,
111
- aT as $REFRESH,
112
- aU as markRefresh,
113
- aV as createRoot,
114
- aW as getObserver,
115
- aX as dispose,
116
- aY as TimeoutError,
117
- aZ as untrack,
118
- a_ as dirtyQueue,
119
- a$ as CONFIG_REDERIVE,
120
- b0 as passTx,
121
- b1 as CONFIG_FRESH_READ,
122
- b2 as createOwner,
123
- b3 as REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE,
124
- b4 as ownedScopeWriteMessage,
125
- b5 as DEV$1,
126
- b6 as isEqual,
127
- b7 as CONFIG_MANUAL_WRITE,
128
- b8 as notePromotedWrite,
129
- b9 as slotSignal,
130
- ba as stagedScreen,
131
- bb as CONFIG_PROMOTED,
132
- bc as setSlotUnobserved,
133
- bd as joinPassTx,
134
- be as asyncTailFlights,
135
- bf as untrackDepth,
136
- bg as checkPostAwaitRead,
137
- bh as insertIntoHeap,
138
- bi as isDisposed,
139
- bj as handleAsync,
140
- bk as addPendingSource,
141
- bl as propagateStatus,
142
- bm as forEachDependent,
143
- bn as settlePendingSource,
144
- bo as notifyEpoch,
145
- bp as SUPPORTS_PROXY,
146
- bq as REACTIVE_ZOMBIE,
147
- br as OBSERVE$1
46
+ Q as REACTIVE_STAGED_READ,
47
+ T as stagedRead,
48
+ U as CONFIG_SLOT_NODE,
49
+ V as attrHooks,
50
+ W as holdNode,
51
+ X as joinFuture,
52
+ Y as commitPendingNode,
53
+ Z as REACTIVE_IN_HEAP,
54
+ _ as REACTIVE_DIRTY,
55
+ $ as REACTIVE_CHECK,
56
+ a0 as CONFIG_INPUTS_PUBLISHED,
57
+ a1 as setVerdict,
58
+ a2 as context,
59
+ a3 as REACTIVE_PROBED,
60
+ a4 as bornInto,
61
+ a5 as globalQueue,
62
+ a6 as staleReader,
63
+ a7 as verdict,
64
+ a8 as link,
65
+ a9 as pullComputed,
66
+ aa as strictRead,
67
+ ab as warnStrictReadUntracked,
68
+ ac as joinPass,
69
+ ad as markLateLinker,
70
+ ae as spectating,
71
+ af as STATUS_ERROR,
72
+ ag as unflushedValue,
73
+ ah as stagedReaders,
74
+ ai as enterAction,
75
+ aj as setEffectCallback,
76
+ ak as exitAction,
77
+ al as isThenable,
78
+ am as actionDepth,
79
+ an as flush,
80
+ ao as setQuestion,
81
+ ap as emitDiagnostic,
82
+ aq as enterCallback,
83
+ ar as exitCallback,
84
+ as as getOwner,
85
+ at as inEffectCallback,
86
+ au as signal,
87
+ av as setSignal,
88
+ aw as cleanup,
89
+ ax as read,
90
+ ay as NoOwnerError,
91
+ az as ContextNotFoundError,
92
+ aA as createEffectNode,
93
+ aB as recompute,
94
+ aC as EFFECT_USER,
95
+ aD as reportDiagnostic,
96
+ aE as computed,
97
+ aF as staleValues,
98
+ aG as CONFIG_AUTO_DISPOSE,
99
+ aH as EFFECT_TRACKED,
100
+ aI as unwrapStatusError,
101
+ aJ as haltReactivity,
102
+ aK as setStrictRead,
103
+ aL as StatusError,
104
+ aM as trimStaleDeps,
105
+ aN as setTrackedQueueCallback,
106
+ aO as setEffectStatusNotify,
107
+ aP as _hitUnhandledAsync,
108
+ aQ as resetUnhandledAsync,
109
+ aR as ROOT_ERROR_HOOK,
110
+ aS as registerGraph,
111
+ aT as runWithOwner,
112
+ aU as setMemo,
113
+ aV as $REFRESH,
114
+ aW as markRefresh,
115
+ aX as createRoot,
116
+ aY as getObserver,
117
+ aZ as dispose,
118
+ a_ as TimeoutError,
119
+ a$ as untrack,
120
+ b0 as dirtyQueue,
121
+ b1 as CONFIG_REDERIVE,
122
+ b2 as passTx,
123
+ b3 as CONFIG_FRESH_READ,
124
+ b4 as createOwner,
125
+ b5 as REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE,
126
+ b6 as ownedScopeWriteMessage,
127
+ b7 as DEV$1,
128
+ b8 as isEqual,
129
+ b9 as CONFIG_MANUAL_WRITE,
130
+ ba as snapshotCaptureActive,
131
+ bb as captureWriteSnapshot,
132
+ bc as notePromotedWrite,
133
+ bd as slotSignal,
134
+ be as stagedScreen,
135
+ bf as CONFIG_PROMOTED,
136
+ bg as setSlotUnobserved,
137
+ bh as joinPassTx,
138
+ bi as asyncTailFlights,
139
+ bj as untrackDepth,
140
+ bk as checkPostAwaitRead,
141
+ bl as insertIntoHeap,
142
+ bm as isDisposed,
143
+ bn as handleAsync,
144
+ bo as addPendingSource,
145
+ bp as propagateStatus,
146
+ bq as forEachDependent,
147
+ br as settlePendingSource,
148
+ bs as notifyEpoch,
149
+ bt as SUPPORTS_PROXY,
150
+ bu as REACTIVE_ZOMBIE,
151
+ bv as OBSERVE$1
148
152
  } from "./dev-shared.js";
149
153
  export {
150
- bs as clearSnapshots,
151
- bt as enforceLoadingBoundary,
152
- bu as getNextChildId,
153
- bv as markSnapshotScope,
154
- bw as peekNextChildId,
155
- bx as releaseSnapshotScope,
156
- by as resetErrorHalt,
157
- bz as setConsoleFooter,
158
- bA as setSnapshotCapture
154
+ bw as clearSnapshots,
155
+ bx as enforceLoadingBoundary,
156
+ by as getNextChildId,
157
+ bz as markSnapshotScope,
158
+ bA as peekNextChildId,
159
+ bB as releaseSnapshotScope,
160
+ bC as resetErrorHalt,
161
+ bD as setConsoleFooter,
162
+ bE as setSnapshotCapture
159
163
  } from "./dev-shared.js";
160
164
 
161
165
  // Lanes (plan sec. 28 replay, 2026-10-02; maintainer rulings 2026-10-01/02). A lane
@@ -190,6 +194,8 @@ export {
190
194
  // carries none of this.
191
195
  /** Live lanes (parented). Scanned at every seam while non-empty. */
192
196
  const lanes = [];
197
+ /** A pass left a shown slot the equality gate can no longer see (#3892). */
198
+ const LEFT_SHOWN = 1 << 20;
193
199
  function newLane(parent) {
194
200
  const l = newTransaction(true, parent);
195
201
  lanes.push(l);
@@ -326,8 +332,10 @@ function enterLane(l, c) {
326
332
  // The pass read a held write before its first lane read (it joined the
327
333
  // frame's future as mainline would): lane work sees the screen — the
328
334
  // seam re-derives it on the committed world next round (`stagedReaders`,
329
- // the park's repair), as it does a lane pass that read a staging.
330
- if (c._flags & REACTIVE_JOINED) stagedRead(c);
335
+ // the park's repair), as it does a lane pass that read a staging. Not a
336
+ // pass whose held reads were all of nodes born held (no staged read,
337
+ // `stagedRead`): it has no committed world to re-derive on (#3969).
338
+ if (c._flags & REACTIVE_JOINED && c._flags & REACTIVE_STAGED_READ) stagedRead(c);
331
339
  return;
332
340
  }
333
341
  if (sameLane(p, l)) return setPassLane(l);
@@ -402,17 +410,22 @@ function verdictLane(t) {
402
410
  * propagation (async.ts `propagateStatus`), the node gone pending. */
403
411
  function laneStage(el, l, create, errored) {
404
412
  if (!(create || errored || el._flags & REACTIVE_LANE_READ || el._config & CONFIG_GUESS)) {
405
- if (el._x !== null) {
406
- if (el._x._transaction?._lane) el._x._transaction = null;
407
- el._x._lane = NOT_PENDING;
408
- }
409
- el._config &= -2305;
413
+ const x = el._x;
414
+ if (x !== null) {
415
+ const tx = x._transaction;
416
+ // Cleared before equality, so a return to the committed value notifies
417
+ // nobody. The seam wakes it unless the frame is still held (#3892).
418
+ if (tx?._shown && !blocked(resolveTx(tx._parent))) el._flags |= LEFT_SHOWN;
419
+ if (tx?._lane) x._transaction = null;
420
+ x._lane = NOT_PENDING;
421
+ }
422
+ el._config &= -1050881;
410
423
  return false;
411
424
  }
412
425
  if (!el._type) {
413
426
  if (!(el._config & CONFIG_OVERRIDE)) {
414
427
  el._pendingValue = NOT_PENDING;
415
- el._config = (el._config & ~CONFIG_HELD) | CONFIG_OVERRIDE;
428
+ el._config = (el._config & -1048833) | CONFIG_OVERRIDE;
416
429
  ext(el);
417
430
  }
418
431
  // A written guess whose own pass is the lane's work — its source is a
@@ -464,6 +477,11 @@ function supersede(n, value, changed) {
464
477
  // lane opened (two actions guessing one slot entangle, A34 (1)) — the
465
478
  // truth is held by the transaction that lands, not the merged-away one.
466
479
  const parent = resolveTx(l._parent ?? l);
480
+ // A store row answered by a new object the store would reconcile into
481
+ // the guessed one unchanged is an echo, not a correction (#3898): the
482
+ // server's object never matches the guess by reference.
483
+ if (changed && n._config & CONFIG_SLOT_NODE && GlobalQueue._echoes?.(n._x._lane, value))
484
+ changed = false;
467
485
  if (changed) {
468
486
  // Observe: a displayed guess is being replaced by a differing truth — a
469
487
  // landing's (superseded), or the value it covered (reverted: the body
@@ -476,17 +494,46 @@ function supersede(n, value, changed) {
476
494
  if (l._shown) n._value = n._x._lane;
477
495
  n._config &= -10241;
478
496
  n._x._lane = NOT_PENDING;
479
- n._pendingValue = value;
480
497
  holdNode(n, parent);
481
- joinFuture(parent);
482
498
  if (n._subs !== null) insertSubs(n);
483
499
  } else {
484
- n._pendingValue = value;
485
500
  n._config |= CONFIG_HELD;
486
- joinFuture(parent);
487
- for (let s = n._subs; s !== null; s = s._nextSub)
501
+ // A confirmation is silent (A17); only truth readers are told.
502
+ GlobalQueue._confirmed?.(l);
503
+ }
504
+ n._pendingValue = value;
505
+ joinFuture(parent);
506
+ }
507
+ /** Truth readers (`until`, `refresh`; CONFIG_AUTHORITATIVE) — pay-for-use:
508
+ * the first one installs these on GlobalQueue (signals.ts `watch`).
509
+ *
510
+ * A truth reader's read of a lane node: the lane's value when it is the
511
+ * truth (#3943), else the truth staged beneath a guess, else the committed
512
+ * value. */
513
+ function truthRead(el) {
514
+ return truth(el)
515
+ ? laneValueOf(el)
516
+ : el._config & CONFIG_GUESS && el._pendingValue !== NOT_PENDING
517
+ ? el._pendingValue
518
+ : el._value;
519
+ }
520
+ /** A guess confirmed: its lane's truth readers re-read — of the lane's
521
+ * derivations too, which may be the truth now (#3943). */
522
+ function truthConfirmed(l) {
523
+ for (let i = 0; i < l._nodes.length; i++)
524
+ for (let s = l._nodes[i]._subs; s !== null; s = s._nextSub)
488
525
  if (s._sub._config & CONFIG_AUTHORITATIVE) enqueueSub(s._sub);
489
- }
526
+ }
527
+ /** A lane node's value is the truth when every guess beneath it is
528
+ * confirmed — its source staged a value equal to the guess — and nothing
529
+ * beneath it is held in the parent: same inputs, same value. */
530
+ function truth(n) {
531
+ if (n._config & CONFIG_GUESS)
532
+ return n._pendingValue !== NOT_PENDING && n._equals && n._equals(n._x._lane, n._pendingValue);
533
+ if (n._pendingValue !== NOT_PENDING) return;
534
+ if (n._config & CONFIG_OVERRIDE)
535
+ for (let d = n._deps; d; d = d._nextDep) if (!truth(d._dep)) return;
536
+ return 1;
490
537
  }
491
538
  /** A lane's end, two ways. `into === null`: the parent landed — the lane's
492
539
  * values are the truth now: a guess lands the truth staged beneath it or
@@ -586,7 +633,7 @@ function dissolveLane(l, into, except) {
586
633
  } else {
587
634
  if (x._pendingFirstChild !== null || x._pendingDisposal !== null)
588
635
  GlobalQueue._dispose(n, false, true);
589
- n._config &= -385;
636
+ n._config &= -1048961;
590
637
  }
591
638
  }
592
639
  }
@@ -621,8 +668,7 @@ function dissolveLane(l, into, except) {
621
668
  * member throws like any. */
622
669
  function laneRead(c, el) {
623
670
  const guess = el._config & CONFIG_GUESS;
624
- if (c !== null && c._config & CONFIG_AUTHORITATIVE)
625
- return guess && el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
671
+ if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth(el);
626
672
  if (c === null) return laneValueOf(el);
627
673
  const l = txOf(el);
628
674
  const status = el._statusFlags;
@@ -734,6 +780,10 @@ function laneSeam(l, leaked) {
734
780
  reruns(l);
735
781
  for (let i = 0; i < l._nodes.length; i++) {
736
782
  const n = l._nodes[i];
783
+ if (n._flags & LEFT_SHOWN) {
784
+ n._flags ^= LEFT_SHOWN;
785
+ insertSubs(n);
786
+ }
737
787
  const x = n._x;
738
788
  if (x._transaction !== l) continue;
739
789
  if (!(n._config & CONFIG_GUESS) && !n._type && n._pendingValue !== NOT_PENDING) {
@@ -886,6 +936,13 @@ GlobalQueue._laneSeams = leaks => {
886
936
  : !blocked(l._parent)))
887
937
  )
888
938
  continue;
939
+ // A leaked effect re-derives seated in its lane from its first read: it
940
+ // carries no lane value (`laneStage`), so without this its pass starts
941
+ // as mainline, joins the parked frame on its first held read and reads
942
+ // the staging, then re-enters the lane at its guess read and leaks
943
+ // again — every round (#4014). A computed is already seated by its
944
+ // override, and a written guess's own pass stays the frame's.
945
+ if (l != null && l._lane && r._type) r._flags |= REACTIVE_LANE_DIRTY;
889
946
  enqueueSub(r);
890
947
  if (l != null && l._lane && leaked.indexOf(l) === -1) leaked.push(l);
891
948
  }
@@ -957,9 +1014,12 @@ function route(c, t) {
957
1014
  * every flush (fuzzer F12). True when the reader is served the committed
958
1015
  * value — not over a flight committed beneath inputs already on screen (A15
959
1016
  * reveal corollary, #3305): that value would tear against them, and the
960
- * reveal observes the flight. */
1017
+ * reveal observes the flight. Nor for a reader born into the flight's
1018
+ * transaction (`bornInto`, #4010): it has no screen — it waits on the flight
1019
+ * with the rest of its frame. */
961
1020
  function observeFlight(c, el) {
962
- if (el._config & CONFIG_INPUTS_PUBLISHED) return false;
1021
+ if (el._config & CONFIG_INPUTS_PUBLISHED || (el._config & CONFIG_HELD && bornInto(c, txOf(el))))
1022
+ return false;
963
1023
  if (globalQueue._running) {
964
1024
  joinFuture(null);
965
1025
  if (!(el._config & CONFIG_HELD)) queuePendingNode(el);
@@ -983,9 +1043,10 @@ let probeFound = false;
983
1043
  * reader re-derives there. `latest` shows the proposal that lands — a
984
1044
  * rewrite or a landing reaches its reader through the link it holds — so a
985
1045
  * re-run would only repeat the frame. */
986
- function verdictRead(c, t, rerun = probing) {
1046
+ function verdictRead(c, t, plain) {
987
1047
  route(c, t);
988
- if (rerun) staleReader(c, t);
1048
+ if (plain) staleReader(c, t);
1049
+ else if (probing && t._reruns[t._reruns.length - 1] !== c) t._reruns.push(c);
989
1050
  }
990
1051
  /** A verdict read of a node staged or in flight but not held yet: whether it
991
1052
  * will be is the seam's.
@@ -1068,11 +1129,14 @@ function pendingVerdict(c, el) {
1068
1129
  }
1069
1130
  }
1070
1131
  /** The pass entering a verdict window is a verdict reader (CONFIG_VERDICT):
1071
- * a dependency going pending re-derives it, the verdict having changed. */
1132
+ * a dependency going pending re-derives it, the verdict having changed. Not
1133
+ * an untracked window: that is a snapshot, like any untracked read (#3946 —
1134
+ * a router's link claim probing inside the computation that creates the
1135
+ * anchor). A throw still reaches the pass through `untrack`. */
1072
1136
  function markVerdictReader(window) {
1073
1137
  let c = context;
1074
1138
  if (c?._root) c = c._parentComputed;
1075
- if (c !== null) {
1139
+ if (c !== null && tracking) {
1076
1140
  c._config |= CONFIG_VERDICT;
1077
1141
  c._flags |= REACTIVE_PROBED;
1078
1142
  // Observe tier: which windows this pass entered (1 `isPending`, 2
@@ -1154,7 +1218,8 @@ function heldLatest(el, c) {
1154
1218
  * - an errored derivation throws for every reader (#2897). */
1155
1219
  function verdictValue(el, c) {
1156
1220
  const owner = el;
1157
- const tracked = c !== null && tracking && !(c._config & CONFIG_CHILDREN_FORBIDDEN);
1221
+ const owned = c !== null && !(c._config & CONFIG_CHILDREN_FORBIDDEN);
1222
+ const tracked = owned && tracking;
1158
1223
  if (c !== null && tracking) {
1159
1224
  link(el, c);
1160
1225
  if (owner._fn !== undefined) {
@@ -1233,9 +1298,14 @@ function verdictValue(el, c) {
1233
1298
  }
1234
1299
  return el._value;
1235
1300
  }
1236
- // Born into the future: its staging is its only value.
1237
- if (el._pendingValue !== NOT_PENDING && !(owner._statusFlags & STATUS_PENDING))
1301
+ // Born into the future: its staging is its only value. It has no
1302
+ // committed value, so no verdict: `isPending` reads it as its read does —
1303
+ // the owner joins the hold (held with it, or caught by a boundary that
1304
+ // has not shown content) and answers for the commit, where it is final.
1305
+ if (el._pendingValue !== NOT_PENDING && !(owner._statusFlags & STATUS_PENDING)) {
1306
+ if (probing && owned) joinPass(c, el);
1238
1307
  return el._pendingValue;
1308
+ }
1239
1309
  } else if (el._pendingValue !== NOT_PENDING) {
1240
1310
  if (owner._statusFlags & STATUS_PENDING && !uninitialized)
1241
1311
  pendingVerdict(tracked ? c : null, owner);
@@ -2309,6 +2379,10 @@ function createReaction(effectFn, options) {
2309
2379
  * joined it (L2, born held — A29) and delivers at that commit: the
2310
2380
  * committed view, never a foreign hold's unrevealed frame. */
2311
2381
  function watch(fn, onValue, onError, config = 0, inPass, own = flushTransaction) {
2382
+ if (config & CONFIG_AUTHORITATIVE) {
2383
+ GlobalQueue._truth = truthRead;
2384
+ GlobalQueue._confirmed = truthConfirmed;
2385
+ }
2312
2386
  // `config` is on the node for its first pass too (the pass runs inside
2313
2387
  // `computed()`): `until`'s predicate is authoritative from its first read,
2314
2388
  // or an already-displayed guess would satisfy it in a later slice. (REDERIVE
@@ -2446,17 +2520,24 @@ function refresh(target) {
2446
2520
  const own = flushTransaction;
2447
2521
  const promise = new Promise((res, rej) => {
2448
2522
  queueMicrotask(() => {
2449
- // No createRoot: the microtask has no ambient owner, so the watcher is
2450
- // naturally detached, and settle disposes the node directly (dev keeps
2451
- // a root husk purely to stay quiet about the missing owner). The
2452
- // capture runs inside the watcher's own pass, where the ambient owner
2453
- // IS the watcher — exactly what dispose() takes.
2523
+ // The root is load-bearing: an ownerless computed is auto-disposing,
2524
+ // and the settle walk releases an unobserved one instead of re-running
2525
+ // it, so the waiter would never see the refetch land (#3888). Settle
2526
+ // disposes the waiter directly; the capture runs inside the watcher's
2527
+ // own pass, where the ambient owner IS the watcher — exactly what
2528
+ // dispose() takes.
2454
2529
  let waiter = null;
2455
2530
  const make = () =>
2456
2531
  watch(
2457
2532
  () => {
2458
2533
  if (waiter === null) waiter = getOwner();
2459
- return read(node);
2534
+ // Authoritative: the truth beneath a guess (`until`'s view). A
2535
+ // guessed node's own re-ask is not answered by it (#3895): park
2536
+ // on that flight, or reject with its error.
2537
+ const v = read(node);
2538
+ if (node._config & CONFIG_GUESS && node._statusFlags & (STATUS_PENDING | STATUS_ERROR))
2539
+ throw node._x?._error;
2540
+ return v;
2460
2541
  },
2461
2542
  value => {
2462
2543
  res(typeof target === "function" ? value : target);
@@ -2466,7 +2547,7 @@ function refresh(target) {
2466
2547
  rej(err);
2467
2548
  dispose(waiter);
2468
2549
  },
2469
- CONFIG_FRESH_READ,
2550
+ CONFIG_FRESH_READ | CONFIG_AUTHORITATIVE,
2470
2551
  undefined,
2471
2552
  own
2472
2553
  );
@@ -3343,6 +3424,15 @@ function compare(key, a, b) {
3343
3424
  return key ? key(a) === key(b) : true;
3344
3425
  }
3345
3426
 
3427
+ let optHooks = null;
3428
+ function installOptHooks(hooks) {
3429
+ optHooks = hooks;
3430
+ }
3431
+ let affectsHooks = null;
3432
+ function installAffectsHooks(hooks) {
3433
+ affectsHooks = hooks;
3434
+ }
3435
+
3346
3436
  /**
3347
3437
  * Store — plain stores on L2 (plan sec. 31, S1).
3348
3438
  *
@@ -3373,14 +3463,6 @@ function compare(key, a, b) {
3373
3463
  * setter (`wk`); overlay drafts for large plain records; no new named
3374
3464
  * fields on array targets; zero allocation for adopted-but-unread objects.
3375
3465
  */
3376
- let optHooks = null;
3377
- function installOptHooks(hooks) {
3378
- optHooks = hooks;
3379
- }
3380
- let affectsHooks = null;
3381
- function installAffectsHooks(hooks) {
3382
- affectsHooks = hooks;
3383
- }
3384
3466
  /** Key of the deep-witness slot node (never a user key). */
3385
3467
  const $DEEP = Symbol("STORE_DEEP");
3386
3468
  // ---------------------------------------------------------------------------
@@ -3570,7 +3652,7 @@ function releaseSlot(node) {
3570
3652
  function noteNode(target, node, key) {
3571
3653
  if (target.fam !== null) target.fam.live.add(target);
3572
3654
  markDescendants(target);
3573
- if (affectsHooks !== null) affectsHooks.born(target, node, key);
3655
+ if (affectsHooks !== null) affectsHooks._born(target, node, key);
3574
3656
  }
3575
3657
  // Shared slot-node release handler: registered once; the core sweep
3576
3658
  // dispatches CONFIG_SLOT_NODE nodes here (graph.ts) instead of holding a
@@ -3746,6 +3828,7 @@ function notifyContainer(k, pb) {
3746
3828
  if (k._config & CONFIG_OVERRIDE) setSignal(k, pb);
3747
3829
  else {
3748
3830
  if (k._config & CONFIG_HELD) joinFuture(txOf(k));
3831
+ if (snapshotCaptureActive) captureWriteSnapshot(k, k._value);
3749
3832
  insertSubs(k);
3750
3833
  schedule();
3751
3834
  }
@@ -4118,6 +4201,10 @@ GlobalQueue._storeCommit = () => {
4118
4201
  drainFolds();
4119
4202
  optHooks?.sweep();
4120
4203
  };
4204
+ /** The store half of `affects()` (store/affects.ts) makes its marks' nodes
4205
+ * and walks records through these: it never imports the engine. */
4206
+ GlobalQueue._storeNode = getNode;
4207
+ GlobalQueue._storeWrappable = isWrappable;
4121
4208
  /** A projection's creation run commits directly (a memo's first value is
4122
4209
  * its `_value`, not a staging): the draft still writes a clone of the seed,
4123
4210
  * nothing is staged on the nodes, and the setter's exit folds the touched
@@ -4144,7 +4231,10 @@ function foldTarget(t, old) {
4144
4231
  // the adopted object (shared ownership — never cloned).
4145
4232
  t.pb = null;
4146
4233
  t.wk = null;
4147
- } else if (t.ovl && (t.v !== old || !overlayRebuilds(t, pb))) {
4234
+ } else if (
4235
+ t.ovl &&
4236
+ (t.v !== old || (!overlayRebuilds(t, pb) && k?._x?._snapshotValue !== old))
4237
+ ) {
4148
4238
  // Overlay flatten (#3044): the backing keeps its identity, so the
4149
4239
  // `t.v === old` gate below skips path copying (the parent slot
4150
4240
  // already points here) and the adopted-notify (setter
@@ -4355,16 +4445,19 @@ function notifyWrites(t) {
4355
4445
  ) {
4356
4446
  // Accessor keys: the node is linked for shape-change notification,
4357
4447
  // its value is never served (the getter runs with the proxy
4358
- // receiver on read) — FORCE wakes the readers.
4448
+ // receiver on read) — FORCE wakes the readers. A data descriptor
4449
+ // falls through so the cleared-acc hot read serves it (#3949);
4450
+ // an own getter's descriptor has no `value`, so the compare below
4451
+ // would otherwise keep the getter's result (or FORCE) for `undefined`.
4359
4452
  node.acc = isOwnAccessor(pb, key);
4360
4453
  const od = Object.getOwnPropertyDescriptor(old, key);
4361
4454
  const nd = Object.getOwnPropertyDescriptor(pb, key);
4362
- if ((od && (od.get || od.set)) || (nd && (nd.get || nd.set))) {
4455
+ if (!nd || nd.get || nd.set) {
4363
4456
  if (od?.get !== nd?.get || od?.set !== nd?.set || od?.value !== nd?.value)
4364
4457
  setSignal(node, () => FORCE);
4365
4458
  continue;
4366
4459
  }
4367
- if (!isEqual(od?.value, nd?.value)) setSignal(node, () => nd?.value);
4460
+ if (od?.get || od?.set || !isEqual(od?.value, nd.value)) setSignal(node, () => nd.value);
4368
4461
  continue;
4369
4462
  }
4370
4463
  const nv = t.del !== null && t.del.has(key) ? undefined : pb[key];
@@ -4462,14 +4555,15 @@ function notifyKeyDiff(node, key, old, neu, probe = true) {
4462
4555
  node.acc = isOwnAccessor(neu, key);
4463
4556
  const od = Object.getOwnPropertyDescriptor(old, key);
4464
4557
  const nd = Object.getOwnPropertyDescriptor(neu, key);
4465
- if ((od && (od.get || od.set)) || (nd && (nd.get || nd.set))) {
4558
+ // Data descriptors fall through, as in notifyWrites (#3949).
4559
+ if (!nd || nd.get || nd.set) {
4466
4560
  if (od?.get !== nd?.get || od?.set !== nd?.set || od?.value !== nd?.value)
4467
4561
  setSignal(node, () => FORCE);
4468
4562
  return;
4469
4563
  }
4470
4564
  const ov = od?.value;
4471
4565
  const nv = nd?.value;
4472
- if (!isEqual(ov, nv) && !targetsEqual(ov, nv))
4566
+ if (od?.get || od?.set || (!isEqual(ov, nv) && !targetsEqual(ov, nv)))
4473
4567
  setSignal(node, typeof nv === "function" ? () => nv : nv);
4474
4568
  } else {
4475
4569
  const ov = old[key];
@@ -4710,7 +4804,67 @@ function readSource(target, key, shape = false) {
4710
4804
  /** Did the HELD staging (the container node's, not a mainline layer above
4711
4805
  * it) change `key`? */
4712
4806
  function heldKeyChanged(target, key, shape = true) {
4713
- return changedBetween(target, target.k._value, target.k._pendingValue, key, shape);
4807
+ return (
4808
+ changedBetween(target, target.k._value, target.k._pendingValue, key, shape) &&
4809
+ !adoptChild(target, key)
4810
+ );
4811
+ }
4812
+ /** The values of a held frame, built once per frame: neither the committed
4813
+ * frame nor the held staging changes while the hold stands (a draft writes a
4814
+ * clone), so the set stays true for every first read of the hold. */
4815
+ const frameRaws = new WeakMap();
4816
+ function rawsOf(frame) {
4817
+ let raws = frameRaws.get(frame);
4818
+ if (raws === undefined) frameRaws.set(frame, (raws = new Set(Object.values(frame))));
4819
+ return raws;
4820
+ }
4821
+ /** The same row at a slot, as the adoption pairs rows (reconcile.ts): two
4822
+ * objects whose keys agree — positionally for an unkeyed family. */
4823
+ function sameRow$1(pv, nv, keyFn) {
4824
+ return (
4825
+ pv !== null &&
4826
+ typeof pv === "object" &&
4827
+ nv !== null &&
4828
+ typeof nv === "object" &&
4829
+ (keyFn === null || sameKey(keyFn(pv), keyFn(nv)))
4830
+ );
4831
+ }
4832
+ /** A key of a held adoption whose old and new values are the same row that
4833
+ * nothing had proxied when the adoption ran, so `descend` never paired
4834
+ * them: paired now, as it would have been (#3712) — one target, the old
4835
+ * raw its committed frame, the new raw its staging, held by the same
4836
+ * transaction. Both raws resolve to it, so the slot reads as unchanged and
4837
+ * the row's own leaves decide: `id` the same in both frames holds no one,
4838
+ * `version` holds. A record's keys pair by name; an array's by the family's
4839
+ * key — without a family the pairing is not known here and the hold stands. */
4840
+ function adoptChild(target, key) {
4841
+ const k = target.k;
4842
+ const old = asBacking(k._value);
4843
+ const ov = old[key];
4844
+ const nv = k._pendingValue[key];
4845
+ const keyFn = Array.isArray(old) ? target.fam?.key : null;
4846
+ if (
4847
+ target.s ||
4848
+ keyFn === undefined ||
4849
+ !sameRow$1(ov, nv, keyFn) ||
4850
+ !isWrappable(ov) ||
4851
+ !isWrappable(nv) ||
4852
+ Array.isArray(ov) !== Array.isArray(nv) ||
4853
+ lookupTarget(ov, target.fam) !== undefined ||
4854
+ lookupTarget(nv, target.fam) !== undefined ||
4855
+ // A raw in both frames (the same objects swapped or reordered) is its
4856
+ // own row: pairing it with another would alias the two after the commit.
4857
+ rawsOf(old).has(nv) ||
4858
+ rawsOf(k._pendingValue).has(ov)
4859
+ )
4860
+ return false;
4861
+ // A raw-marked value wraps to itself, a proxy to its own target: neither
4862
+ // is a fresh target around `ov`.
4863
+ const ct = wrap(ov, target, key)[$TARGET];
4864
+ if (ct === undefined || ct.v !== ov) return false;
4865
+ adoptPB(ct, nv, false);
4866
+ holdNode(getContainerNode(ct), txOf(k));
4867
+ return true;
4714
4868
  }
4715
4869
  /** A family target's unheld staging, read with no pass (a handler). */
4716
4870
  function familyAhead(target) {
@@ -4839,11 +4993,13 @@ function pullFamily(target) {
4839
4993
  // One that already observes the flight (linked by the pass that saw it
4840
4994
  // go up — the frame's hold, `blocked`) keeps observing it: a re-run for
4841
4995
  // another reason (a guess it read, the seam) that dropped the link would
4842
- // release the hold with the flight still up.
4996
+ // release the hold with the flight still up. An errored derive has no
4997
+ // landing to learn of: the error is what the frame shows.
4843
4998
  const c = context;
4844
4999
  if (
4845
5000
  c !== null &&
4846
5001
  c._type === EFFECT_RENDER &&
5002
+ !(fw._statusFlags & STATUS_ERROR) &&
4847
5003
  fw._config & CONFIG_HELD &&
4848
5004
  flushTransaction !== txOf(fw) &&
4849
5005
  !linkedTo(c, fw)
@@ -4955,7 +5111,7 @@ const traps = {
4955
5111
  // (The witness before the pull: a mark on an uninitialized derived
4956
5112
  // store is witnessed, then the pull throws — loading, declared pending.)
4957
5113
  if (verdict !== null && affectsHooks !== null && getObserver() === null)
4958
- affectsHooks.witness(target, key);
5114
+ affectsHooks._witness(target, key);
4959
5115
  if (target.fam !== null) pullFamily(target);
4960
5116
  // Hot inline case: existing PLAIN node (non-accessor) read outside any
4961
5117
  // draft — the dbmon/uibench effect re-read shape. Core's `read()` serves
@@ -5102,7 +5258,7 @@ const traps = {
5102
5258
  // (The witness before the pull: a mark on an uninitialized derived
5103
5259
  // store is witnessed, then the pull throws — loading, declared pending.)
5104
5260
  if (verdict !== null && affectsHooks !== null && getObserver() === null)
5105
- affectsHooks.witness(target, key);
5261
+ affectsHooks._witness(target, key);
5106
5262
  if (target.fam !== null) pullFamily(target);
5107
5263
  const src = readSource(target, key);
5108
5264
  // A tracked reader's presence node answers (born from the two frames;
@@ -5127,7 +5283,7 @@ const traps = {
5127
5283
  },
5128
5284
  ownKeys(target) {
5129
5285
  if (verdict !== null && affectsHooks !== null && getObserver() === null)
5130
- affectsHooks.witness(target, undefined);
5286
+ affectsHooks._witness(target, undefined);
5131
5287
  if (target.fam !== null) pullFamily(target);
5132
5288
  return visibleKeys(target, enumerationSource(target));
5133
5289
  },
@@ -5135,7 +5291,7 @@ const traps = {
5135
5291
  if (key === $OWNER || key === $RECORD) return undefined;
5136
5292
  const obs = getObserver();
5137
5293
  if (verdict !== null && affectsHooks !== null && obs === null)
5138
- affectsHooks.witness(target, key);
5294
+ affectsHooks._witness(target, key);
5139
5295
  if (target.fam !== null) pullFamily(target);
5140
5296
  // An enumerator (spread, Object.entries) already holds the container
5141
5297
  // node and reads its frame; a descriptor read on its own tracks
@@ -5662,127 +5818,6 @@ const storePath = /* @__PURE__ */ Object.assign(
5662
5818
  { DELETE }
5663
5819
  );
5664
5820
 
5665
- /**
5666
- * Store — the store half of `affects()` (plan sec. 29, plan sec. 37; A24 (4), #2882,
5667
- * #2893, #2904).
5668
- *
5669
- * A mark on a store is a mark on nodes (affects.ts: a count on the node, a
5670
- * dependency walk at probe time): `affects(record, key)` marks the slot's
5671
- * leaf; `affects(record)` marks a carrier (the record's `$AFFECTS` leaf)
5672
- * and every live node in the record's subtree — the edges existing readers
5673
- * subscribed through. Coverage is by RAW IDENTITY, not read path (#2882: a
5674
- * captured row proxy, a derived store sharing the source's raw, never read
5675
- * through the declaration's proxy): the identities reachable at the
5676
- * declaration are the mark's `scope`, so a node born inside the window on
5677
- * a covered record inherits the mark (released with the carrier's last),
5678
- * and an untracked probe through a record with no nodes is witnessed into
5679
- * the verdict. A mark declared on a draft (inside a setter) or over an
5680
- * optimistic family walks the view the writer sees.
5681
- *
5682
- * Installed by `store/index.ts`: a program with stores carries it; one
5683
- * without pays nothing (affects.ts asks `GlobalQueue._storeMarks`).
5684
- */
5685
- /** The keyless mark's carrier key: a leaf of the record no read serves. */
5686
- const $AFFECTS = Symbol("STORE_AFFECTS");
5687
- const scopes = new Map();
5688
- /** The nodes an `affects(store[, key])` declaration marks (affects.ts
5689
- * registers each). */
5690
- function storeMarks(t, key) {
5691
- if (key === undefined) {
5692
- const carrier = getNode(t, $AFFECTS);
5693
- let entry = scopes.get(carrier);
5694
- if (entry === undefined) scopes.set(carrier, (entry = { scope: new Set(), inherited: [] }));
5695
- const found = [carrier];
5696
- walk(t.px, entry, found, t.fam, new Set());
5697
- return found;
5698
- }
5699
- const node = t.n?.[key] ?? getNode(t, key);
5700
- let entry = scopes.get(node);
5701
- if (entry === undefined) scopes.set(node, (entry = { scope: new Set(), inherited: [], key }));
5702
- entry.scope.add(t.v);
5703
- if (t.pb !== null) entry.scope.add(t.pb);
5704
- return [node];
5705
- }
5706
- /** The identities reachable from `value` into the scope (the draft's and
5707
- * the committed backing both, when a draft is open; an optimistic family's
5708
- * view), every live node under each record into `found`. Untracked by
5709
- * construction: raws, never traps. */
5710
- function walk(value, entry, found, fam, visited) {
5711
- if (!isWrappable(value)) return;
5712
- const t = value[$TARGET] ?? lookupTarget(value, fam);
5713
- let raw = t !== undefined ? (t.pb ?? t.v) : value;
5714
- if (visited.has(raw)) return;
5715
- visited.add(raw);
5716
- entry.scope.add(raw);
5717
- if (t !== undefined) {
5718
- if (t.pb !== null) entry.scope.add(t.v);
5719
- // The writer's view: the tick's own unflushed guesses are in motion too.
5720
- if (t.fam?.opt === true) raw = optHooks.view(t, raw, true);
5721
- const nodes = t.n;
5722
- if (nodes !== null)
5723
- for (const k of Reflect.ownKeys(nodes))
5724
- // Another mark's carrier is its own channel: counting it here would
5725
- // extend that scope's lifetime to this declaration's.
5726
- if (k !== $AFFECTS) found.push(nodes[k]);
5727
- const has = t.h;
5728
- if (has !== null) for (const k of Reflect.ownKeys(has)) found.push(has[k]);
5729
- if (t.k !== null) found.push(t.k);
5730
- if (t.dk !== null) found.push(t.dk);
5731
- fam = t.fam ?? fam;
5732
- }
5733
- for (const k of Reflect.ownKeys(raw)) {
5734
- if (k === $OWNER) continue;
5735
- const d = Object.getOwnPropertyDescriptor(raw, k);
5736
- if (d === undefined || d.get !== undefined) continue;
5737
- walk(d.value, entry, found, fam, visited);
5738
- }
5739
- }
5740
- /** A live scope covering `t`'s identity (keyed: for `key`). Chained
5741
- * backings (§7b): a view's backing is another store's proxy — marks cover
5742
- * the BASE raw, so every identity along the chain is checked. */
5743
- function covering(t, key, skip) {
5744
- for (const [carrier, entry] of scopes) {
5745
- if (carrier === skip || (entry.key !== undefined && entry.key !== key)) continue;
5746
- for (let r = t.v; ; ) {
5747
- if (entry.scope.has(r)) return carrier;
5748
- const inner = r?.[$TARGET];
5749
- if (inner === undefined) break;
5750
- const backing = inner.pb ?? inner.v;
5751
- if (backing === r) break;
5752
- r = backing;
5753
- }
5754
- }
5755
- return null;
5756
- }
5757
- installAffectsHooks({
5758
- // A node born on a record a live mark covers inherits it (released with
5759
- // the carrier's last registration).
5760
- born(t, node, key) {
5761
- if (key === $AFFECTS) return;
5762
- const carrier = covering(t, key, node);
5763
- if (carrier === null) return;
5764
- GlobalQueue._mark(node);
5765
- scopes.get(carrier).inherited.push(node);
5766
- },
5767
- // An untracked probe (`isPending(() => s.x)` with no observer) reading a
5768
- // record a live mark covers: no node carries the mark for it — the
5769
- // verdict is told directly.
5770
- witness(t, key) {
5771
- const own = t.n?.[$AFFECTS];
5772
- if ((own !== undefined && own._x !== null && own._x._marks !== 0) || covering(t, key) !== null)
5773
- GlobalQueue._witnessMark();
5774
- }
5775
- });
5776
- GlobalQueue._storeMarks = storeMarks;
5777
- /** The carrier's last mark released (affects.ts): its scope dies, and the
5778
- * nodes that inherited the mark release theirs. */
5779
- GlobalQueue._releaseMarkScope = carrier => {
5780
- const entry = scopes.get(carrier);
5781
- if (entry === undefined) return;
5782
- scopes.delete(carrier);
5783
- GlobalQueue._releaseMarks(entry.inherited);
5784
- };
5785
-
5786
5821
  /**
5787
5822
  * Store — reconcile: the immutable-diff adoption channel (INTERNALS §3).
5788
5823
  *
@@ -5874,17 +5909,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
5874
5909
  for (const end = Math.min(plen, nlen); i < end; i++) {
5875
5910
  const nv = nextRows[i];
5876
5911
  const pvRaw = prevRows[i];
5877
- if (
5878
- pvRaw !== nv &&
5879
- !(
5880
- pvRaw !== null &&
5881
- typeof pvRaw === "object" &&
5882
- nv !== null &&
5883
- typeof nv === "object" &&
5884
- sameKey(keyFn(pvRaw), keyFn(nv))
5885
- )
5886
- )
5887
- break; // misaligned: fall to the keyed remainder below
5912
+ if (pvRaw !== nv && !sameRow$1(pvRaw, nv, keyFn)) break; // misaligned: fall to the keyed remainder below
5888
5913
  if (
5889
5914
  (pvRaw !== nv || (nv !== null && typeof nv === "object" && isOwned(nv))) &&
5890
5915
  nv !== null &&
@@ -6296,7 +6321,9 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
6296
6321
  // A flight went up (NotReady out of the pass — the derive's own, with
6297
6322
  // the derive as its source): the leaves' readers learn it from here —
6298
6323
  // they do not subscribe to the derive.
6299
- if (e instanceof NotReadyError && !first) wakeFamily(fam, e);
6324
+ // A synchronous error the same way (#3956: a rerun that finds the
6325
+ // entry failed throws): every live reader re-derives and meets it.
6326
+ if (!first) e instanceof NotReadyError ? wakeFamily(fam, e) : errorFamily(fam);
6300
6327
  throw e;
6301
6328
  }
6302
6329
  // A pass that returned with a flight up (an iterator's sync first yield):
@@ -6313,11 +6340,20 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
6313
6340
  return owner;
6314
6341
  }
6315
6342
  function createProjectionInternal(fn, seed, options) {
6343
+ const keyOption = options?.key === undefined ? "id" : options.key;
6316
6344
  const fam = {
6317
6345
  map: new WeakMap(),
6318
6346
  node: null,
6319
6347
  live: new Set(),
6320
- shallow: !!options?.shallow
6348
+ shallow: !!options?.shallow,
6349
+ // The row key, for a child paired after the adoption (store.ts
6350
+ // `adoptChild`, #3712) as the adoption pairs them.
6351
+ key:
6352
+ typeof keyOption === "function"
6353
+ ? keyOption
6354
+ : keyOption === null
6355
+ ? null
6356
+ : row => (isWrappable(row) ? row[keyOption] : undefined)
6321
6357
  };
6322
6358
  const store = wrap(seed, null, null, fam);
6323
6359
  if (fam.shallow) {
@@ -6332,7 +6368,7 @@ function createProjectionInternal(fn, seed, options) {
6332
6368
  }
6333
6369
  const node = computed(() => {
6334
6370
  if (!fam.node) fam.node = getOwner();
6335
- runProjectionComputed(store, fn, options?.key === undefined ? "id" : options.key);
6371
+ runProjectionComputed(store, fn, keyOption);
6336
6372
  }, nodeOptions);
6337
6373
  node._config &= ~CONFIG_AUTO_DISPOSE;
6338
6374
  fam.node = node;
@@ -6494,8 +6530,38 @@ function rowSame(a, b, keyFn) {
6494
6530
  if (keyFn === null || !isWrappable(a) || !isWrappable(b)) return false;
6495
6531
  const ka = keyFn(unwrapValue(a));
6496
6532
  const kb = keyFn(unwrapValue(b));
6533
+ // Keyless rows are identified by position — reconcile's fallback when the
6534
+ // key is missing. The caller already paired these by index, so two keyless
6535
+ // rows are the same row and the landing confirms the arrangement instead of
6536
+ // dissolving it. Dissolving held an external echo under the action, and a
6537
+ // mainline `until` then timed out on a source that already contained it
6538
+ // (#3898). Keyed rows still match only on the key.
6539
+ if (ka === undefined && kb === undefined) return true;
6497
6540
  return ka !== undefined && sameKey(ka, kb);
6498
6541
  }
6542
+ /** Whether reconcile would write anything into `a` from `b`: wrappable
6543
+ * pairs descend, leaves compare as its writes do. A key mismatch is a
6544
+ * replacement, which a content difference already says. Plain data — the
6545
+ * server's echo of a guessed row is a new object with the same contents. */
6546
+ function echoes(a, b) {
6547
+ if (a === b) return true;
6548
+ if (!isWrappable(a) || !isWrappable(b)) return false;
6549
+ a = unwrapValue(a);
6550
+ b = unwrapValue(b);
6551
+ const ak = Object.keys(a);
6552
+ if (Array.isArray(a) !== Array.isArray(b) || ak.length !== Object.keys(b).length) return false;
6553
+ for (const k of ak) if (!hasOwn.call(b, k) || !echoes(a[k], b[k])) return false;
6554
+ return true;
6555
+ }
6556
+ // A cycle (a row that references itself) is not plain data: no match, so
6557
+ // the landing stays a correction.
6558
+ GlobalQueue._echoes = (a, b) => {
6559
+ try {
6560
+ return echoes(a, b);
6561
+ } catch {
6562
+ return false;
6563
+ }
6564
+ };
6499
6565
  function arrangementEqual(a, b, keyFn) {
6500
6566
  if (Array.isArray(a) !== Array.isArray(b)) return false;
6501
6567
  if (Array.isArray(a)) {
@@ -7934,6 +8000,137 @@ function deep(value) {
7934
8000
  return deep$1(value);
7935
8001
  }
7936
8002
 
8003
+ /**
8004
+ * Store — the store half of `affects()` (plan sec. 29, plan sec. 37; A24 (4), #2882,
8005
+ * #2893, #2904).
8006
+ *
8007
+ * A mark on a store is a mark on nodes (affects.ts: a count on the node, a
8008
+ * dependency walk at probe time): `affects(record, key)` marks the slot's
8009
+ * leaf; `affects(record)` marks a carrier (the record's `$AFFECTS` leaf)
8010
+ * and every live node in the record's subtree — the edges existing readers
8011
+ * subscribed through. Coverage is by RAW IDENTITY, not read path (#2882: a
8012
+ * captured row proxy, a derived store sharing the source's raw, never read
8013
+ * through the declaration's proxy): the identities reachable at the
8014
+ * declaration are the mark's `scope`, so a node born inside the window on
8015
+ * a covered record inherits the mark (released with the carrier's last),
8016
+ * and an untracked probe through a record with no nodes is witnessed into
8017
+ * the verdict. A mark declared on a draft (inside a setter) or over an
8018
+ * optimistic family walks the view the writer sees.
8019
+ *
8020
+ * Imported by `affects()`, which installs it on its first store target
8021
+ * (#3891, after #3912): a program with stores that never declares a mark
8022
+ * carries none of it. It never imports store.ts — the engine's top level —
8023
+ * so a program declaring marks without stores carries this module alone:
8024
+ * the store's nodes and its wrappability test come through `GlobalQueue`
8025
+ * (`_storeNode`, `_storeWrappable`), and node birth and the untracked traps
8026
+ * reach back through `affectsHooks` (hooks.ts).
8027
+ */
8028
+ /** The keyless mark's carrier key: a leaf of the record no read serves. */
8029
+ const $AFFECTS = Symbol("STORE_AFFECTS");
8030
+ const scopes = new Map();
8031
+ function scopeOf(carrier, key) {
8032
+ let entry = scopes.get(carrier);
8033
+ if (entry === undefined)
8034
+ scopes.set(carrier, (entry = { _scope: new Set(), _inherited: [], _key: key }));
8035
+ return entry;
8036
+ }
8037
+ /** The nodes an `affects(store[, key])` declaration marks (affects.ts
8038
+ * registers each). */
8039
+ function storeMarks(t, key) {
8040
+ const node = GlobalQueue._storeNode(t, key === undefined ? $AFFECTS : key);
8041
+ const entry = scopeOf(node, key);
8042
+ if (key === undefined) {
8043
+ const found = [node];
8044
+ walk(t.px, entry, found, t.fam, new Set());
8045
+ return found;
8046
+ }
8047
+ entry._scope.add(t.v);
8048
+ if (t.pb !== null) entry._scope.add(t.pb);
8049
+ return [node];
8050
+ }
8051
+ /** The identities reachable from `value` into the scope (the draft's and
8052
+ * the committed backing both, when a draft is open; an optimistic family's
8053
+ * view), every live node under each record into `found`. Untracked by
8054
+ * construction: raws, never traps. */
8055
+ function walk(value, entry, found, fam, visited) {
8056
+ if (!GlobalQueue._storeWrappable(value)) return;
8057
+ const t = value[$TARGET] ?? lookupTarget(value, fam);
8058
+ let raw = t !== undefined ? (t.pb ?? t.v) : value;
8059
+ if (visited.has(raw)) return;
8060
+ visited.add(raw);
8061
+ entry._scope.add(raw);
8062
+ if (t !== undefined) {
8063
+ if (t.pb !== null) entry._scope.add(t.v);
8064
+ // The writer's view: the tick's own unflushed guesses are in motion too.
8065
+ if (t.fam?.opt === true) raw = optHooks.view(t, raw, true);
8066
+ const nodes = t.n;
8067
+ if (nodes !== null)
8068
+ for (const k of Reflect.ownKeys(nodes))
8069
+ // Another mark's carrier is its own channel: counting it here would
8070
+ // extend that scope's lifetime to this declaration's.
8071
+ if (k !== $AFFECTS) found.push(nodes[k]);
8072
+ const has = t.h;
8073
+ if (has !== null) for (const k of Reflect.ownKeys(has)) found.push(has[k]);
8074
+ if (t.k !== null) found.push(t.k);
8075
+ if (t.dk !== null) found.push(t.dk);
8076
+ fam = t.fam ?? fam;
8077
+ }
8078
+ for (const k of Reflect.ownKeys(raw))
8079
+ if (k !== $OWNER) {
8080
+ const d = Object.getOwnPropertyDescriptor(raw, k);
8081
+ if (d.get === undefined) walk(d.value, entry, found, fam, visited);
8082
+ }
8083
+ }
8084
+ /** A live scope covering `t`'s identity (keyed: for `key`). Chained
8085
+ * backings (§7b): a view's backing is another store's proxy — marks cover
8086
+ * the BASE raw, so every identity along the chain is checked. */
8087
+ function covering(t, key, skip) {
8088
+ for (const [carrier, entry] of scopes) {
8089
+ if (carrier === skip || (entry._key !== undefined && entry._key !== key)) continue;
8090
+ for (let r = t.v; ; ) {
8091
+ if (entry._scope.has(r)) return carrier;
8092
+ const inner = r?.[$TARGET];
8093
+ if (inner === undefined) break;
8094
+ const backing = inner.pb ?? inner.v;
8095
+ if (backing === r) break;
8096
+ r = backing;
8097
+ }
8098
+ }
8099
+ return null;
8100
+ }
8101
+ /** The store's side: node birth and the untracked traps call it (store.ts,
8102
+ * through `affectsHooks`) — with no live mark too: an empty scope map is
8103
+ * no work. */
8104
+ const hooks = {
8105
+ // A node born on a record a live mark covers inherits it (released with
8106
+ // the carrier's last registration).
8107
+ _born(t, node, key) {
8108
+ if (scopes.size === 0 || key === $AFFECTS) return;
8109
+ const carrier = covering(t, key, node);
8110
+ if (!carrier) return;
8111
+ GlobalQueue._mark(node);
8112
+ scopes.get(carrier)._inherited.push(node);
8113
+ },
8114
+ // An untracked probe (`isPending(() => s.x)` with no observer) reading a
8115
+ // record a live mark covers: no node carries the mark for it — the
8116
+ // verdict is told directly.
8117
+ _witness(t, key) {
8118
+ if (t.n?.[$AFFECTS]?._x?._marks || covering(t, key)) GlobalQueue._witnessMark();
8119
+ }
8120
+ };
8121
+ /** `affects()` calls it before a store-targeted declaration (idempotent). */
8122
+ function installStoreAffects() {
8123
+ installAffectsHooks(hooks);
8124
+ }
8125
+ /** The carrier's last mark released (affects.ts): its scope dies, and the
8126
+ * nodes that inherited the mark release theirs. */
8127
+ function releaseMarkScope(carrier) {
8128
+ const entry = scopes.get(carrier);
8129
+ if (entry === undefined) return;
8130
+ scopes.delete(carrier);
8131
+ GlobalQueue._releaseMarks(entry._inherited);
8132
+ }
8133
+
7937
8134
  // `affects()` — the declaration verb of the pending model (A24 (4), ruled
7938
8135
  // 2026-07-13; the #2893 audit corollaries; rebuilt on L2 2026-10-02 as ruled:
7939
8136
  // transaction-inert).
@@ -8001,7 +8198,7 @@ function release$1(nodes) {
8001
8198
  repoll(n);
8002
8199
  // A store mark's carrier: its scope (and the marks the nodes born in
8003
8200
  // its window inherited) goes with it.
8004
- GlobalQueue._releaseMarkScope?.(n);
8201
+ releaseMarkScope(n);
8005
8202
  }
8006
8203
  }
8007
8204
  nodes.length = 0;
@@ -8032,7 +8229,8 @@ function affects(target, key) {
8032
8229
  // record's carrier and every live node under it.
8033
8230
  const t = target?.[$TARGET];
8034
8231
  if (t !== undefined) {
8035
- const nodes = GlobalQueue._storeMarks(t, key);
8232
+ installStoreAffects();
8233
+ const nodes = storeMarks(t, key);
8036
8234
  for (let i = 0; i < nodes.length; i++) register(nodes[i]);
8037
8235
  schedule();
8038
8236
  return;
@@ -8354,8 +8552,11 @@ function prune(b, pass) {
8354
8552
  // Landed, held: the content is that transaction's — the output's
8355
8553
  // pass enters it and the reveal lands with the reader's run. The
8356
8554
  // seam, past its park decision, waits for the commit instead. (A
8357
- // lane's reader reveals with the lane, this round.)
8358
- if (!pass) continue;
8555
+ // lane's reader reveals with the lane, this round.) A boundary that
8556
+ // has not shown content catches it instead: the fallback stays until
8557
+ // the reader is committed (#3540: what a fresh boundary catches holds
8558
+ // nothing outside it).
8559
+ if (!pass || !b._initialized) continue;
8359
8560
  joinFuture(txOf(r));
8360
8561
  }
8361
8562
  b._readers.delete(r);
@@ -8541,7 +8742,8 @@ function createBoundary(type, fn, show, onFn) {
8541
8742
  _gated: false,
8542
8743
  _collapsed: false,
8543
8744
  _error: type === STATUS_ERROR ? signal(undefined, ERROR_SIGNAL) : null,
8544
- _show: show
8745
+ _show: show,
8746
+ _fb: null
8545
8747
  };
8546
8748
  const controller = type === STATUS_PENDING ? owner._context[REVEAL] : null;
8547
8749
  if (getOwner())
@@ -8682,6 +8884,20 @@ function createBoundary(type, fn, show, onFn) {
8682
8884
  if (b._fallback && b._type & STATUS_PENDING && attrHooks !== null)
8683
8885
  attrHooks.boundaryFallback(b, b._tree, false);
8684
8886
  b._fallback = false;
8887
+ // The fallback dies when the content that replaces it is on screen —
8888
+ // with this flush's commits when the pass is the frame's, at the
8889
+ // landing when the frame parks (the run is held with it, as the old
8890
+ // children of a held pass are) — not now: a held swap keeps the
8891
+ // fallback on screen, live, until it commits. A pass back to the
8892
+ // fallback before then finds it still built and keeps it.
8893
+ const fb = b._fb;
8894
+ if (fb !== null)
8895
+ globalQueue.enqueue(EFFECT_RENDER, () => {
8896
+ if (!b._fallback && b._fb === fb) {
8897
+ dispose(fb);
8898
+ b._fb = null;
8899
+ }
8900
+ });
8685
8901
  release(b);
8686
8902
  // Shown: the slot leaves its reveal order (a later re-arm gates nobody
8687
8903
  // else; the frontier moves on).
@@ -8724,7 +8940,22 @@ function fallback(b) {
8724
8940
  if (!globalQueue._running) schedule();
8725
8941
  }
8726
8942
  b._fallback = true;
8727
- return b._show(b);
8943
+ // Built in its own scope, like `Show`'s function child, once per show
8944
+ // (D-39: the fallback is the display until a new answer replaces the
8945
+ // error): a re-derive of the output while it shows — a second reader
8946
+ // caught, the same error propagated again through a sibling's re-run, a
8947
+ // retry pending after `reset()`, a later error — keeps the built
8948
+ // fallback, its DOM and focus with it (#3991). A later error reaches it
8949
+ // through the error accessor; only a fallback reading the accessor at its
8950
+ // top level is rebuilt by it. Created beside the output — in its owner,
8951
+ // outside the boundary, as the output is: its errors and runs are the
8952
+ // enclosing boundary's — with the output's id, so what it builds gets the
8953
+ // ids a build under the output would; disposed once content is on screen.
8954
+ if (b._fb === null) {
8955
+ const out = getOwner();
8956
+ b._fb = runWithOwner(out._parent, () => computed(() => b._show(b), { id: out.id }));
8957
+ }
8958
+ return read(b._fb);
8728
8959
  }
8729
8960
  const ERROR_SIGNAL = { ownedWrite: true, _noSnapshot: true };
8730
8961
  /**