@solidjs/signals 2.0.0-rc.8 → 2.0.0-rc.9

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 (90) hide show
  1. package/dist/dev-shared.js +1399 -285
  2. package/dist/dev.attribution.js +459 -307
  3. package/dist/dev.js +1864 -435
  4. package/dist/observe/affects.js +3 -1
  5. package/dist/observe/attribution.js +7 -1
  6. package/dist/observe/boundaries.js +209 -154
  7. package/dist/observe/core/action.js +18 -8
  8. package/dist/observe/core/async.js +220 -110
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +282 -0
  11. package/dist/observe/core/attribution-hooks.js +23 -1
  12. package/dist/observe/core/attribution-queries.js +28 -0
  13. package/dist/observe/core/attribution.js +262 -485
  14. package/dist/observe/core/constants.js +34 -1
  15. package/dist/observe/core/context.js +3 -3
  16. package/dist/observe/core/core.js +867 -367
  17. package/dist/observe/core/dev.js +78 -17
  18. package/dist/observe/core/effect.js +67 -51
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/external.js +4 -4
  21. package/dist/observe/core/graph.js +37 -37
  22. package/dist/observe/core/heap.js +45 -45
  23. package/dist/observe/core/invariants.js +2 -0
  24. package/dist/observe/core/lanes.js +86 -49
  25. package/dist/observe/core/optimistic.js +242 -95
  26. package/dist/observe/core/owner.js +98 -84
  27. package/dist/observe/core/scheduler.js +543 -305
  28. package/dist/observe/core/verdict.js +247 -129
  29. package/dist/observe/index.js +7 -3
  30. package/dist/observe/map.js +126 -124
  31. package/dist/observe/signals.js +33 -17
  32. package/dist/observe/store/index.js +2 -0
  33. package/dist/observe/store/next/optimistic.js +141 -132
  34. package/dist/observe/store/next/projection.js +34 -21
  35. package/dist/observe/store/next/reconcile.js +58 -56
  36. package/dist/observe/store/next/store.js +260 -146
  37. package/dist/observe/store/store.js +5 -3
  38. package/dist/observe/store/utils.js +948 -135
  39. package/dist/prod/attribution.js +26 -17
  40. package/dist/prod/boundaries.js +128 -76
  41. package/dist/prod/core/action.js +16 -8
  42. package/dist/prod/core/async.js +251 -143
  43. package/dist/prod/core/constants.js +34 -1
  44. package/dist/prod/core/context.js +3 -3
  45. package/dist/prod/core/core.js +843 -347
  46. package/dist/prod/core/dev.js +17 -1
  47. package/dist/prod/core/effect.js +48 -34
  48. package/dist/prod/core/error-hooks.js +71 -0
  49. package/dist/prod/core/external.js +4 -4
  50. package/dist/prod/core/graph.js +37 -37
  51. package/dist/prod/core/heap.js +45 -45
  52. package/dist/prod/core/lanes.js +90 -53
  53. package/dist/prod/core/optimistic.js +247 -100
  54. package/dist/prod/core/owner.js +57 -45
  55. package/dist/prod/core/scheduler.js +543 -305
  56. package/dist/prod/core/verdict.js +245 -127
  57. package/dist/prod/index.js +7 -3
  58. package/dist/prod/map.js +112 -112
  59. package/dist/prod/signals.js +28 -12
  60. package/dist/prod/store/next/optimistic.js +135 -128
  61. package/dist/prod/store/next/projection.js +31 -20
  62. package/dist/prod/store/next/store.js +325 -252
  63. package/dist/prod/store/store.js +2 -2
  64. package/dist/prod/store/utils.js +946 -135
  65. package/dist/types/attribution.d.ts +7 -2
  66. package/dist/types/attribution.prod.d.ts +10 -1
  67. package/dist/types/boundaries.d.ts +10 -1
  68. package/dist/types/core/action.d.ts +12 -5
  69. package/dist/types/core/attribution-costs.d.ts +35 -0
  70. package/dist/types/core/attribution-feedback.d.ts +133 -0
  71. package/dist/types/core/attribution-hooks.d.ts +28 -3
  72. package/dist/types/core/attribution-queries.d.ts +10 -0
  73. package/dist/types/core/attribution.d.ts +51 -172
  74. package/dist/types/core/constants.d.ts +33 -0
  75. package/dist/types/core/core.d.ts +186 -5
  76. package/dist/types/core/dev.d.ts +129 -9
  77. package/dist/types/core/error-hooks.d.ts +71 -0
  78. package/dist/types/core/index.d.ts +3 -1
  79. package/dist/types/core/invariants.d.ts +4 -0
  80. package/dist/types/core/lanes.d.ts +31 -4
  81. package/dist/types/core/scheduler.d.ts +95 -2
  82. package/dist/types/core/types.d.ts +3 -0
  83. package/dist/types/index.d.ts +2 -2
  84. package/dist/types/signals.d.ts +8 -0
  85. package/dist/types/store/index.d.ts +2 -1
  86. package/dist/types/store/next/optimistic.d.ts +1 -1
  87. package/dist/types/store/next/store.d.ts +6 -5
  88. package/dist/types/store/next/target.d.ts +1 -1
  89. package/dist/types/store/utils.d.ts +177 -6
  90. package/package.json +1 -1
package/dist/dev.js CHANGED
@@ -24,126 +24,147 @@ import {
24
24
  m as insertSubs,
25
25
  n as schedule,
26
26
  p as STATUS_PENDING,
27
- q as CONFIG_OVERRIDE_SUPERSEDED,
28
- t as hasActiveOverride$1,
29
- v as NotReadyError,
30
- w as activeLanes,
31
- x as signalLanes,
32
- y as laneHeld,
27
+ q as CONFIG_DERIVED_OVERRIDE,
28
+ t as CONFIG_OPTIMISTIC,
29
+ v as CONFIG_OVERRIDE_SUPERSEDED,
30
+ w as hasActiveOverride,
31
+ x as NotReadyError,
32
+ y as activeLanes,
33
+ z as signalLanes,
34
+ A as laneHeld,
33
35
  E as EFFECT_RENDER,
34
- z as CONFIG_AUTHORITATIVE_OBSERVED,
35
- A as findLane,
36
- B as stale,
37
- D as queuePendingNode,
38
- F as latestReadActive,
39
- H as REACTIVE_MANUAL_WRITE,
40
- I as currentOptimisticLane,
41
- J as resolveLane,
42
- K as REACTIVE_OPTIMISTIC_DIRTY,
43
- L as assignOrMergeLane,
44
- M as LANE_RUN,
45
- P as attrHooks,
46
- Q as EFFECT_USER,
47
- T as REACTIVE_DISPOSED,
48
- U as insertIntoHeap,
49
- V as queueFor,
50
- W as REACTIVE_ZOMBIE,
51
- X as markHeap,
52
- Y as pendingCheckActive,
53
- Z as setPendingCheckActive,
54
- _ as prepareComputed,
55
- $ as context,
56
- a0 as tracking,
57
- a1 as link,
58
- a2 as currentTransition,
59
- a3 as enqueueSub,
60
- a4 as setLatestReadActive,
61
- a5 as setContextInternal,
62
- a6 as optimisticComputed,
63
- a7 as CONFIG_HAS_COMPANIONS,
64
- a8 as CONFIG_CHILD_COMPANIONS,
65
- a9 as setStrictRead,
66
- aa as strictRead,
67
- ab as optimisticSignal,
68
- ac as activeAffectsMarks,
69
- ad as STATUS_ERROR,
70
- ae as REACTIVE_RECOMPUTING_DEPS,
71
- af as setEffectStatusNotify,
72
- ag as unwrapStatusError,
73
- ah as haltReactivity,
74
- ai as StatusError,
75
- aj as createEffectNode,
76
- ak as recompute,
77
- al as reportDiagnostic,
78
- am as emitDiagnostic,
79
- an as computed,
80
- ao as staleValues,
81
- ap as CONFIG_AUTO_DISPOSE,
82
- aq as CONFIG_CHILDREN_FORBIDDEN,
83
- ar as EFFECT_TRACKED,
84
- as as setEffectCallback,
85
- at as setTrackedQueueCallback,
86
- au as _hitUnhandledAsync,
87
- av as resetUnhandledAsync,
88
- aw as setOrigin,
89
- ax as isThenable,
90
- ay as actionStepDepth,
91
- az as flush,
92
- aA as enterActionStep,
93
- aB as exitActionStep,
94
- aC as registerGraph,
95
- aD as runWithOwner,
96
- aE as setMemo,
97
- aF as $REFRESH,
98
- aG as installAuthoritativeRead,
99
- aH as markRefresh,
100
- aI as createRoot,
101
- aJ as getObserver,
102
- aK as dispose,
103
- aL as CONFIG_DIRECT_COMMIT,
104
- aM as CONFIG_AUTHORITATIVE_READ,
105
- aN as entangleConfirmingTransitions,
106
- aO as TimeoutError,
107
- aP as untrack,
108
- aQ as Queue,
109
- aR as CONFIG_FRESH_READ,
110
- aS as forEachDependent,
111
- aT as statusNotifierOf,
112
- aU as shiftAffectsMarks,
113
- aV as SUPPORTS_PROXY,
114
- aW as setSlotUnobserved,
115
- aX as devGuardStoreSetterWrite,
116
- aY as projectionWriteActive,
117
- aZ as setProjectionWriteActive,
118
- a_ as DEV$1,
119
- a$ as isEqual,
120
- b0 as readNodeFast,
121
- b1 as READ_SLOW,
122
- b2 as throwPendingUntrackedRead,
123
- b3 as warnStrictReadUntracked,
124
- b4 as unlinkFirewallChild,
125
- b5 as CONFIG_OWNED_WRITE,
126
- b6 as CONFIG_OPTIMISTIC,
127
- b7 as slotSignal,
128
- b8 as CONFIG_HELD_TRUTH,
129
- b9 as setStoreCommitHook,
130
- ba as suppressComputedRecompute,
131
- bb as handleAsync,
132
- bc as createTransition,
133
- bd as runAsTransitionBatch,
134
- be as createOwner,
135
- bf as OBSERVE$1
36
+ B as CONFIG_AUTHORITATIVE_OBSERVED,
37
+ D as findLane,
38
+ F as sourceObserved,
39
+ H as resolveLane,
40
+ I as stale,
41
+ J as readsHeldCommitted,
42
+ K as enterStagedRead,
43
+ L as currentTransition,
44
+ M as queuePendingNode,
45
+ P as latestReadActive,
46
+ Q as REACTIVE_MANUAL_WRITE,
47
+ T as currentOptimisticLane,
48
+ U as REACTIVE_OPTIMISTIC_DIRTY,
49
+ V as assignOrMergeLane,
50
+ W as LANE_RUN,
51
+ X as attrHooks,
52
+ Y as EFFECT_USER,
53
+ Z as REACTIVE_DISPOSED,
54
+ _ as dispose,
55
+ $ as insertIntoHeap,
56
+ a0 as queueFor,
57
+ a1 as visibleOverride,
58
+ a2 as unflushedValue,
59
+ a3 as context,
60
+ a4 as REACTIVE_RECOMPUTING_DEPS,
61
+ a5 as markLateLinker,
62
+ a6 as prepareComputed,
63
+ a7 as tracking,
64
+ a8 as link,
65
+ a9 as enqueueSub,
66
+ aa as setLatestReadActive,
67
+ ab as setContextInternal,
68
+ ac as optimisticComputed,
69
+ ad as CONFIG_HAS_COMPANIONS,
70
+ ae as unflushed,
71
+ af as CONFIG_ADOPTED_UNFLUSHED,
72
+ ag as CONFIG_CHILD_COMPANIONS,
73
+ ah as runAsTransitionBatch,
74
+ ai as unflushedCompanions,
75
+ aj as setPendingCheckActive,
76
+ ak as setStrictRead,
77
+ al as strictRead,
78
+ am as optimisticSignal,
79
+ an as activeAffectsMarks,
80
+ ao as pendingCheckActive,
81
+ ap as STATUS_ERROR,
82
+ aq as setEffectStatusNotify,
83
+ ar as unwrapStatusError,
84
+ as as haltReactivity,
85
+ at as StatusError,
86
+ au as trimStaleDeps,
87
+ av as createEffectNode,
88
+ aw as recompute,
89
+ ax as reportDiagnostic,
90
+ ay as emitDiagnostic,
91
+ az as computed,
92
+ aA as staleValues,
93
+ aB as CONFIG_AUTO_DISPOSE,
94
+ aC as CONFIG_CHILDREN_FORBIDDEN,
95
+ aD as EFFECT_TRACKED,
96
+ aE as setEffectCallback,
97
+ aF as setTrackedQueueCallback,
98
+ aG as _hitUnhandledAsync,
99
+ aH as resetUnhandledAsync,
100
+ aI as setOrigin,
101
+ aJ as isThenable,
102
+ aK as actionStepDepth,
103
+ aL as flush,
104
+ aM as enterActionStep,
105
+ aN as exitActionStep,
106
+ aO as ROOT_ERROR_HOOK,
107
+ aP as registerGraph,
108
+ aQ as runWithOwner,
109
+ aR as setMemo,
110
+ aS as dirtyQueue,
111
+ aT as $REFRESH,
112
+ aU as installAuthoritativeRead,
113
+ aV as markRefresh,
114
+ aW as createRoot,
115
+ aX as getObserver,
116
+ aY as CONFIG_DIRECT_COMMIT,
117
+ aZ as CONFIG_AUTHORITATIVE_READ,
118
+ a_ as entangleConfirmingTransitions,
119
+ a$ as TimeoutError,
120
+ b0 as untrack,
121
+ b1 as Queue,
122
+ b2 as CONFIG_FRESH_READ,
123
+ b3 as forEachDependent,
124
+ b4 as statusNotifierOf,
125
+ b5 as shiftAffectsMarks,
126
+ b6 as SUPPORTS_PROXY,
127
+ b7 as setSlotUnobserved,
128
+ b8 as devGuardStoreSetterWrite,
129
+ b9 as devGuardStoreSetterResult,
130
+ ba as projectionWriteActive,
131
+ bb as setProjectionWriteActive,
132
+ bc as DEV$1,
133
+ bd as isEqual,
134
+ be as readNodeFast,
135
+ bf as READ_SLOW,
136
+ bg as throwPendingUntrackedRead,
137
+ bh as warnStrictReadUntracked,
138
+ bi as unlinkFirewallChild,
139
+ bj as CONFIG_OWNED_WRITE,
140
+ bk as ownsHold,
141
+ bl as deferSlotRelease,
142
+ bm as slotSignal,
143
+ bn as serve,
144
+ bo as recordStaleReplay,
145
+ bp as setStoreCommitHook,
146
+ bq as suppressComputedRecompute,
147
+ br as handleAsync,
148
+ bs as createTransition,
149
+ bt as CONFIG_HELD_TRUTH,
150
+ bu as createOwner,
151
+ bv as spectate,
152
+ bw as transitions,
153
+ bx as wakeParked,
154
+ by as REACTIVE_ZOMBIE,
155
+ bz as OBSERVE$1
136
156
  } from "./dev-shared.js";
137
157
  export {
138
- bg as clearSnapshots,
139
- bh as enforceLoadingBoundary,
140
- bi as getNextChildId,
141
- bj as isDisposed,
142
- bk as markSnapshotScope,
143
- bl as peekNextChildId,
144
- bm as releaseSnapshotScope,
145
- bn as resetErrorHalt,
146
- bo as setSnapshotCapture
158
+ bA as clearSnapshots,
159
+ bB as enforceLoadingBoundary,
160
+ bC as getNextChildId,
161
+ bD as isDisposed,
162
+ bE as markSnapshotScope,
163
+ bF as ownerPath,
164
+ bG as peekNextChildId,
165
+ bH as releaseSnapshotScope,
166
+ bI as resetErrorHalt,
167
+ bJ as setSnapshotCapture
147
168
  } from "./dev-shared.js";
148
169
 
149
170
  let externalSourceConfig = null;
@@ -341,7 +362,7 @@ function optimisticWrite(el, v) {
341
362
  ext(el)._optimisticLane = lane;
342
363
  // A fresh override re-masks: whatever truth is staged, this write is the
343
364
  // value for the graph again until the source answers it (#3331).
344
- el._config = (el._config | CONFIG_HAS_LANE) & ~CONFIG_OVERRIDE_SUPERSEDED;
365
+ el._config = (el._config | CONFIG_HAS_LANE) & -8912897;
345
366
  // Literal undefined must not land raw: the slot doubles as the optimistic
346
367
  // brand, and erasing it makes the write invisible and routes follow-up
347
368
  // writes off the optimistic path into permanent commits (#2898).
@@ -356,16 +377,72 @@ function optimisticWrite(el, v) {
356
377
  schedule();
357
378
  return v;
358
379
  }
380
+ /**
381
+ * Lanes stage (#3479): a lane pass's publish for a memo. An optimistic
382
+ * derivation is an override — the speculative result lives in the override
383
+ * slot, `_value` stays the committed truth. The whole optimistic frame is then
384
+ * in one place: the lane's readers and untracked reads see it (A17), a render
385
+ * effect off the held lane sees the committed frame whole (readsHeldCommitted,
386
+ * #3460) — the source's shadow AND its derivations — where a speculative
387
+ * `_value` beside a committed shadow tore it. The node joins the
388
+ * transaction's optimistic nodes on its first speculative publish; the revert
389
+ * drops the override and re-derives it from the truth (a derived override has
390
+ * no truth of its own — see resolveOptimisticNodes, endOptimism).
391
+ */
392
+ function laneOverride(el, value, lane) {
393
+ // The wake-only channel (#3009, see recomputeLane): a plain write to a
394
+ // latest()-tracked source rides a companion-sourced lane with no
395
+ // transaction on either side only to wake the verdict companions. Nothing
396
+ // is speculative — the pass commits directly, as any plain write does.
397
+ lane = findLane(lane);
398
+ if (!lane._transition && !activeTransition && lane._source._x?._parentSource !== undefined) {
399
+ el._value = value;
400
+ return;
401
+ }
402
+ if (!hasActiveOverride(el)) {
403
+ // It reverts with the lane's transaction (a landing runs outside any
404
+ // flush, where the ambient batch would revert it at its own end); an
405
+ // orphan lane's falls to the batch, adopted with it (initTransition) as a
406
+ // write's is. No `_overrideOwner`: a derived override is a plain member,
407
+ // its transaction its lane's (resolveTransition), and it merges lanes
408
+ // through itself as any shared reader does (assignOrMergeLane). No
409
+ // provenance stamp either: not an intent, any truth supersedes it.
410
+ (lane._transition
411
+ ? currentTransition(lane._transition)
412
+ : globalQueue._batch
413
+ )._optimisticNodes.push(el);
414
+ }
415
+ // A lane pass's output is a derivation — also over a WRITTEN guess it
416
+ // corrects (a `createOptimistic(fn)` re-derived from fresh upstream data):
417
+ // the guess is gone, the slot holds fn's answer, and the revert promotes it
418
+ // rather than dropping to a stale `_value` and re-asking downstream (the
419
+ // next user write re-arms the guess: optimisticWrite clears the bit). No
420
+ // `_overrideTime` stamp: that marks a user WRITE unflushed until the flush
421
+ // that carries it (A28) and shields it from same-tick supersession — a pass's
422
+ // result is neither (a pulled ownerless memo publishes outside any flush).
423
+ // A fresh lane frame ends a supersession in force: the pass just dropped
424
+ // the staged truth it pointed at (recompute, INV-11 corollary) — left set,
425
+ // the flag served a `_value` never committed (fuzzer latest-1 #2481).
426
+ el._config = (el._config | CONFIG_DERIVED_OVERRIDE) & ~CONFIG_OVERRIDE_SUPERSEDED;
427
+ el._x._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
428
+ }
359
429
  /**
360
430
  * transitionComplete's override blockage: a settling transition stays open
361
431
  * while one of its optimistic nodes holds an active override that is still
362
- * pending on real (non-affects-sentinel) async.
432
+ * pending on real (non-affects-sentinel) async. A derived override's flight is
433
+ * the lane's own work, never authoritative — it does not hold the settle.
434
+ * Neither is a companion's (#3494): the `latest()` shadow backfilled under the
435
+ * owner's transaction (A28 (3)) is an observation of the flight, and a
436
+ * mainline `latest(details)` after the flight's last reader unmounted held the
437
+ * released write until the orphaned request landed.
363
438
  */
364
439
  function transitionBlocked(transition) {
365
440
  for (let i = 0; i < transition._optimisticNodes.length; i++) {
366
441
  const node = transition._optimisticNodes[i];
367
442
  if (
368
- hasActiveOverride$1(node) &&
443
+ !(node._config & CONFIG_DERIVED_OVERRIDE) &&
444
+ node._x?._parentSource === undefined &&
445
+ hasActiveOverride(node) &&
369
446
  "_statusFlags" in node &&
370
447
  node._statusFlags & STATUS_PENDING &&
371
448
  node._x?._error instanceof NotReadyError
@@ -388,14 +465,31 @@ function resolveOptimisticNodes(nodes) {
388
465
  // elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
389
466
  if (!(node._statusFlags & STATUS_PENDING)) node._statusFlags &= ~STATUS_UNINITIALIZED;
390
467
  const prevOverride = node._x?._overrideValue;
391
- ext(node)._overrideValue = NOT_PENDING;
468
+ // A derived override (lanes stage, #3479) has no truth of its own: the
469
+ // slot disarms — the memo is plain again — and the override PROMOTES to
470
+ // `_value`. Not superseded, nothing it derives from told it otherwise
471
+ // (a source override that reverts to a differing truth dirties it just
472
+ // above — sources join this list before their derivations — and its
473
+ // recompute then replaces the promotion), so by the graph's invariant
474
+ // the override IS what a recompute from the truth yields. Re-deriving
475
+ // instead re-asked an async member's flight and held the transaction on
476
+ // it — a waterfall after the reveal.
477
+ const derived = node._config & CONFIG_DERIVED_OVERRIDE;
478
+ ext(node)._overrideValue =
479
+ derived && !(node._config & CONFIG_OPTIMISTIC) ? undefined : NOT_PENDING;
392
480
  // A superseded override's subscribers already re-derived from the truth
393
481
  // when it arrived (#3331) — the drop changes nothing they read. Everyone
394
482
  // else learns of the correction here: this drop IS their notification.
395
483
  const superseded = (node._config & CONFIG_OVERRIDE_SUPERSEDED) !== 0;
396
- node._config &= ~CONFIG_OVERRIDE_SUPERSEDED;
397
- if (!superseded && prevOverride !== NOT_PENDING && node._value !== unwrapOverride(prevOverride))
398
- insertSubs(node, true);
484
+ node._config &= -8912897;
485
+ if (
486
+ !superseded &&
487
+ prevOverride !== NOT_PENDING &&
488
+ node._value !== unwrapOverride(prevOverride)
489
+ ) {
490
+ if (derived) node._value = unwrapOverride(prevOverride);
491
+ else insertSubs(node, true);
492
+ }
399
493
  node._transition = null;
400
494
  if (node._x !== null) node._x._overrideOwner = null;
401
495
  }
@@ -476,6 +570,77 @@ function supersedeOverride(el, value) {
476
570
  if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
477
571
  insertSubs(el);
478
572
  }
573
+ /**
574
+ * The flush's pre-verdict step once the action bodies have ended (#3427).
575
+ * The bodies were the optimism's justification; with them over, the
576
+ * overrides still in force revert at the settle — unless the transaction is
577
+ * still waiting on AUTHORITATIVE work: an override node's own source in
578
+ * flight (`transitionBlocked` — that answer supersedes or confirms on
579
+ * arrival), or a held flight that does not derive from an override (a plain
580
+ * write's load the action asked for). Through that window the optimistic
581
+ * world stands: a co-written "saving" flag stays rendered until the page it
582
+ * covers lands (A17 — the optimistic world is one).
583
+ *
584
+ * The flights that DO derive from an override — routed through a live lane —
585
+ * are obsolete: their input is the guess that is about to revert, and nobody
586
+ * will read their answer. With nothing authoritative left, each override's
587
+ * truth is already here (the staged value an A17-silent landing left, else
588
+ * the committed value) and supersedes it now, exactly as an arriving
589
+ * differing truth does (A18, #3331): the graph re-derives from it as this
590
+ * transaction's held work — a lane-derived memo re-asks with the truth, its
591
+ * other changed inputs included — and the transaction settles when THAT
592
+ * lands. Before this the settle first waited for the obsolete flight,
593
+ * revealed the obsolete optimistic frame when it landed, and only then
594
+ * started the correction — a waterfall with a flash in the middle. Returns
595
+ * whether it superseded anything (the caller re-runs the heap).
596
+ *
597
+ * The optimistic world is one, so it ends early only when all of it can. An
598
+ * optimistic STORE edit cannot yet: its truth is the base layer under an
599
+ * overlay that `_clearOptimisticStores` folds off at settlement, with no
600
+ * tracked/displayed split — superseding its tracking signals alone would
601
+ * re-derive readers against a still-displayed overlay (and a memo reading
602
+ * both a signal and the store would ask a mixed question). A transaction
603
+ * holding one keeps the settle-then-revert order throughout. Companions
604
+ * (`_parentSource` set) are optimistic nodes too — a verdict written through
605
+ * the optimistic path so it flushes ahead of the hold — but they answer for
606
+ * their owner and snap at settlement (`_snapCompanions`), not here.
607
+ */
608
+ function endOptimism(transition) {
609
+ if (
610
+ !transition._acted ||
611
+ transition._actions.length ||
612
+ !transition._optimisticNodes.length ||
613
+ transition._optimisticStores.size ||
614
+ transitionBlocked(transition)
615
+ )
616
+ return false;
617
+ for (const source of transition._asyncReporters.keys())
618
+ if (
619
+ sourceObserved(transition, source, transition) &&
620
+ source._x?._pendingSources?.has(source) &&
621
+ !resolveLane(source)
622
+ )
623
+ return false;
624
+ let superseded = false;
625
+ for (const node of transition._optimisticNodes) {
626
+ if (
627
+ !hasActiveOverride(node) ||
628
+ node._x._parentSource ||
629
+ // A derived override re-derives when its source's is superseded.
630
+ node._config & (CONFIG_OVERRIDE_SUPERSEDED | CONFIG_DERIVED_OVERRIDE) ||
631
+ node._statusFlags & STATUS_UNINITIALIZED
632
+ )
633
+ continue;
634
+ const truth = node._pendingValue !== NOT_PENDING ? node._pendingValue : node._value;
635
+ if (!node._equals || !node._equals(truth, unwrapOverride(node._x._overrideValue))) {
636
+ supersedeOverride(node, truth);
637
+ // Judged by the mark, not the call: a provenance refusal (an older
638
+ // action's window is still open) leaves the node for a later pass.
639
+ if (node._config & CONFIG_OVERRIDE_SUPERSEDED) superseded = true;
640
+ }
641
+ }
642
+ return superseded;
643
+ }
479
644
  /** read()'s value for a tracked reader of a superseded node: the truth —
480
645
  * staged, or already committed (a mainline landing commits at the head of
481
646
  * its flush, ahead of the heap run, and the override drops only at the
@@ -483,9 +648,35 @@ function supersedeOverride(el, value) {
483
648
  * has left) — or the displayed override for a stale (render) reader of some
484
649
  * OTHER transaction, the same visibility a foreign transaction's staged
485
650
  * write has. */
486
- function supersededRead(el) {
487
- if (stale && el._transition && activeTransition !== el._transition)
488
- return unwrapOverride(el._x?._overrideValue);
651
+ /**
652
+ * A tracked read of an active override (read()'s override arm). Lanes mirror
653
+ * transitions (#3460): a render effect OFF the override's held lane — re-run
654
+ * by a sync write, or mounted mid-hold — sees the committed value, as a stale
655
+ * reader of a held transaction does, and publishes now; the lane's release
656
+ * re-runs it (readsHeldCommitted). The lane defers the override's own readers'
657
+ * runs, so the committed value is what is on screen — the override is the
658
+ * visible value only once the lane has revealed (or, demoted at body-end,
659
+ * A18). Otherwise the override displays, unless the node's own source
660
+ * answered with a DIFFERENT value (A18 supersession, #3331): the optimism is
661
+ * over for the graph — a tracked reader sees the staged truth — while the
662
+ * override remains the DISPLAYED value for untracked reads (and for a stale
663
+ * reader of some other transaction).
664
+ */
665
+ function overrideRead(el, c) {
666
+ if (stale && readsHeldCommitted(el, c)) return el._value;
667
+ if (!(el._config & CONFIG_OVERRIDE_SUPERSEDED)) return unwrapOverride(el._x?._overrideValue);
668
+ // The owning transaction: `_overrideOwner` (#2912), not the stamp — an
669
+ // override written directly inside an action never passes the adoption
670
+ // loop that stamps `_transition`, and a body-end supersession (#3427)
671
+ // stages nothing that would queue it. Without the owner a stale reader of
672
+ // a body-ended node read the committed truth beside a display still
673
+ // showing the override.
674
+ const owner = resolveTransition(el);
675
+ if (stale && owner && activeTransition !== owner) return unwrapOverride(el._x?._overrideValue);
676
+ // A superseded read is a staged read (A29) whether the truth is staged or
677
+ // already committed: the pass that derives from it derives from the
678
+ // owning transaction's world (the override is still displayed by it).
679
+ enterStagedRead(el, owner);
489
680
  return el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
490
681
  }
491
682
  /**
@@ -561,11 +752,16 @@ function laneSuspends(owner) {
561
752
  // this is only reachable under a lane, which implies the engine.
562
753
  if (owner._statusFlags & STATUS_UNINITIALIZED) return true;
563
754
  // Per-lane suspension: only throw if in same lane as pending async
564
- // AND the node doesn't have an active override (overrides are the visible value,
565
- // downstream in the lane should read the override, not throw)
755
+ // AND the node doesn't have an active WRITTEN override (overrides are the
756
+ // visible value, downstream in the lane should read the override, not
757
+ // throw). A derived override (#3479) is a previous speculative answer, not
758
+ // an intent: the re-ask pending behind it suspends like any lane async.
566
759
  const pendingLane = owner._x?._optimisticLane;
567
760
  if (!pendingLane) return false;
568
- return findLane(pendingLane) === findLane(currentOptimisticLane) && !hasActiveOverride$1(owner);
761
+ return (
762
+ findLane(pendingLane) === findLane(currentOptimisticLane) &&
763
+ (!hasActiveOverride(owner) || (owner._config & CONFIG_DERIVED_OVERRIDE) !== 0)
764
+ );
569
765
  }
570
766
  /**
571
767
  * read()'s reveal carve-out asks whether a pending node is routed through a
@@ -722,7 +918,9 @@ function installOptimisticEngine() {
722
918
  GlobalQueue._cleanupLanes = cleanupCompletedLanes;
723
919
  GlobalQueue._runLaneEffects = runLaneEffects;
724
920
  GlobalQueue._supersedeOverride = supersedeOverride;
725
- GlobalQueue._supersededRead = supersededRead;
921
+ GlobalQueue._endOptimism = endOptimism;
922
+ GlobalQueue._overrideRead = overrideRead;
923
+ GlobalQueue._laneOverride = laneOverride;
726
924
  GlobalQueue._landOnOverride = landOnOverride;
727
925
  GlobalQueue._gatedRead = gatedRead;
728
926
  GlobalQueue._laneSuspends = laneSuspends;
@@ -760,9 +958,10 @@ const suppressedProbes = new Map();
760
958
  * the one-load gate at the call sites). The snap then iterates exactly the
761
959
  * children someone asked verdicts of — O(companions) — never the full
762
960
  * `_child` chain, which carries one node per materialized leaf (the
763
- * O(all-leaves-ever-read)-per-update pathology). Entries are permanent like
764
- * the companions themselves; a store with no leaf-level isPending()/latest()
765
- * reads never allocates the set or pays the walk. */
961
+ * O(all-leaves-ever-read)-per-update pathology). Entries live as long as the
962
+ * store addresses the leaf the unobserved sweep's `unlinkFirewallChild`
963
+ * drops them (#3503); a store with no leaf-level isPending()/latest() reads
964
+ * never allocates the set or pays the walk. */
766
965
  function markFirewallChildCompanions(el) {
767
966
  const fw = el._firewall;
768
967
  if (!fw) return;
@@ -778,10 +977,53 @@ function getPendingSignal(el) {
778
977
  el._config |= CONFIG_HAS_COMPANIONS;
779
978
  markFirewallChildCompanions(el);
780
979
  ext(ps)._parentSource = el;
781
- if (computePendingState(el)) setSignal(ps, true);
980
+ if (computePendingState(el)) backfillCompanion(el, ps, true);
981
+ joinUnflushedResync(el);
782
982
  }
783
983
  return ps;
784
984
  }
985
+ /**
986
+ * A lazily created companion's first write mirrors state the owner already
987
+ * carries — a held write, a pending verdict. The companion is created wherever
988
+ * the first latest()/isPending() read happens to run, but the write belongs
989
+ * to whatever HOLDS that state: written in the ambient window, the override
990
+ * would register in the ambient batch and revert when that flush's round ends
991
+ * — the shadow re-derived from the committed view, the verdict flipped false —
992
+ * while the owner's hold was still on (#3336: A and B differing only in
993
+ * whether a companion existed before the hold). A companion created lazily
994
+ * answers as if it had always existed: its backfill is registered with the
995
+ * owner's transaction and lives and reverts with it. A hold with no
996
+ * transaction (a pending async, a same-flush staged write) is ambient and the
997
+ * write stays ambient. (A28 (3), lifted from #3337.)
998
+ */
999
+ function backfillCompanion(el, companion, value) {
1000
+ const transition = el._transition;
1001
+ if (transition) runAsTransitionBatch(transition, () => setSignal(companion, value));
1002
+ else setSignal(companion, value);
1003
+ }
1004
+ /** A28: a companion created while its source carries an UNFLUSHED write joins
1005
+ * the flush-start re-sync like a companion that existed at the write —
1006
+ * syncCompanions only reaches companions that exist at write time. Without
1007
+ * this the flush brings it current by a plain recompute instead of the
1008
+ * optimistic write: its readers are then staged under whatever transaction
1009
+ * the round entered (a memo over latest() of a held source was held with the
1010
+ * source, and its untracked reads answered the previous value until the hold
1011
+ * committed) rather than direct-committed as the optimistic view they are. */
1012
+ function joinUnflushedResync(el) {
1013
+ if (unflushed(el)) unflushedCompanions.push(el);
1014
+ }
1015
+ /** The staged value the verdict channels answer for (A28): while a node
1016
+ * carries an unflushed write its `_pendingValue` is not yet part of any
1017
+ * flushed world, so the channels answer for the value the last flush left
1018
+ * staged (a held node's stash) or for nothing (NOT_PENDING). */
1019
+ function flushedStaged(el) {
1020
+ if (!unflushed(el)) return el._pendingValue;
1021
+ // Ambient, or adopted before any flush (CONFIG_ADOPTED_UNFLUSHED): nothing
1022
+ // a flush carried is staged for it. A held rewrite: the stash.
1023
+ return el._transition === null || el._config & CONFIG_ADOPTED_UNFLUSHED
1024
+ ? NOT_PENDING
1025
+ : el._x._flushedStaged;
1026
+ }
785
1027
  function collectPendingSources(el) {
786
1028
  if (!pendingProbe) return;
787
1029
  pendingProbe.sources.add(el);
@@ -869,7 +1111,8 @@ function computePendingState(el) {
869
1111
  const parent = parentNode._firewall || parentNode;
870
1112
  return newQuestionInFlight(parent);
871
1113
  }
872
- if (firewall && el._pendingValue !== NOT_PENDING && !hasActiveOverride$1(el)) {
1114
+ const staged = flushedStaged(el);
1115
+ if (firewall && staged !== NOT_PENDING && !hasActiveOverride(el)) {
873
1116
  return (
874
1117
  !!(firewall._flags & REACTIVE_MANUAL_WRITE) ||
875
1118
  (!firewall._x?._inFlight && !(firewall._statusFlags & STATUS_PENDING)) ||
@@ -880,18 +1123,41 @@ function computePendingState(el) {
880
1123
  // the window's own landing in flight to its commit — verdict-quiet like the
881
1124
  // rest of the window (the UNINITIALIZED check suppresses exactly this frame
882
1125
  // for windowless first loads; born-committed nodes need their own gate, #2990).
1126
+ // A18 (d) for a body-end supersession (#3427): the truth at hand is the
1127
+ // COMMITTED value — nothing staged — yet the display still shows the
1128
+ // override; pending iff they differ, as for a staged arrival below.
883
1129
  if (
884
- el._pendingValue !== NOT_PENDING &&
885
- !(comp._statusFlags & STATUS_UNINITIALIZED) &&
886
- !comp._loading
887
- ) {
888
- if (hasActiveOverride$1(el))
889
- return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._x?._overrideValue));
1130
+ el._config & CONFIG_OVERRIDE_SUPERSEDED &&
1131
+ el._pendingValue === NOT_PENDING &&
1132
+ visibleOverride(el)
1133
+ )
1134
+ return !el._equals || !el._equals(el._value, unwrapOverride(el._x?._overrideValue));
1135
+ // A28 (2): an unflushed write is not yet observable — the verdict answers
1136
+ // for the flushed staged value.
1137
+ if (staged !== NOT_PENDING && !comp._loading) {
1138
+ // A18 (d): under a displayed override the observable value is the
1139
+ // override, so the verdict is "the arrived truth differs from it" —
1140
+ // even before the node's first commit. The UNINITIALIZED suppression
1141
+ // below is A19 exception (1), "no observable value exists to be
1142
+ // non-final"; an override is one (a node whose first landing was held
1143
+ // by a reveal it never got to commit, then superseded under its
1144
+ // override, read false here).
1145
+ if (visibleOverride(el))
1146
+ return !el._equals || !el._equals(staged, unwrapOverride(el._x?._overrideValue));
890
1147
  // A quiet re-ask's held landing still answers the same question: the
891
1148
  // classification survives the landing (asyncWrite) and dies with the
892
1149
  // commit (commitPendingNode) — verdict-quiet through the reveal, like
893
1150
  // the loading window above (#3178).
894
- if (!comp._x?._reask) return true;
1151
+ // A staged value equal to the committed one is no proposal (A34, #3494): the
1152
+ // observable value IS final (A19). The coalesced `setShow(false);
1153
+ // setShow(true)` read pending through the flush that carried it — and,
1154
+ // stamped into a hold that flush opened, until the hold settled.
1155
+ if (
1156
+ !(comp._statusFlags & STATUS_UNINITIALIZED) &&
1157
+ !comp._x?._reask &&
1158
+ (!el._equals || !el._equals(el._value, staged))
1159
+ )
1160
+ return true;
895
1161
  }
896
1162
  return newQuestionInFlight(comp);
897
1163
  }
@@ -982,6 +1248,18 @@ function snapCompanionsToState(owner) {
982
1248
  }
983
1249
  const shadow = owner._x?._latestValueComputed;
984
1250
  if (shadow && !(shadow._flags & REACTIVE_DISPOSED)) {
1251
+ // A leaf whose firewall is disposed (the projection's teardown snaps its
1252
+ // companion-bearing leaves): the shadow's compute reads through a
1253
+ // disposed, possibly still-pending projection and would sit
1254
+ // NotReady/uninitialized forever — never derived, its backfilled override
1255
+ // dropped at the settle — against a leaf whose committed value differs
1256
+ // (INV-4 at the next quiescence; spec O5). It dies with its source;
1257
+ // getLatestValueComputed treats a disposed shadow as absent, so a later
1258
+ // read recreates it from the committed view.
1259
+ if (owner._firewall?._flags & REACTIVE_DISPOSED) {
1260
+ dispose(shadow);
1261
+ return;
1262
+ }
985
1263
  if (
986
1264
  (shadow._x?._overrideValue === undefined || shadow._x?._overrideValue === NOT_PENDING) &&
987
1265
  shadow._pendingValue === NOT_PENDING &&
@@ -1012,7 +1290,12 @@ function getLatestValueComputed(el) {
1012
1290
  setPendingCheckActive(false);
1013
1291
  const prevContext = context;
1014
1292
  setContextInternal(null); // Detach from owner so it isn't disposed with effects
1015
- lvc = optimisticComputed(() => read(el));
1293
+ GlobalQueue._verdictPull = true;
1294
+ try {
1295
+ lvc = optimisticComputed(() => read(el), { ownedWrite: true });
1296
+ } finally {
1297
+ GlobalQueue._verdictPull = false;
1298
+ }
1016
1299
  ext(el)._latestValueComputed = lvc;
1017
1300
  el._config |= CONFIG_HAS_COMPANIONS;
1018
1301
  markFirewallChildCompanions(el);
@@ -1021,8 +1304,9 @@ function getLatestValueComputed(el) {
1021
1304
  // created lazily, possibly after the write was processed — syncCompanions
1022
1305
  // only pushes into companions that already exist, so the first latest()
1023
1306
  // read inside a held transition showed the committed value (#3041).
1024
- if (el._pendingValue !== NOT_PENDING && !hasActiveOverride$1(el))
1025
- setSignal(lvc, el._pendingValue);
1307
+ const staged = flushedStaged(el);
1308
+ if (staged !== NOT_PENDING && !hasActiveOverride(el)) backfillCompanion(el, lvc, staged);
1309
+ joinUnflushedResync(el);
1026
1310
  setContextInternal(prevContext);
1027
1311
  setPendingCheckActive(prevCheck);
1028
1312
  setLatestReadActive(prevPending);
@@ -1030,67 +1314,84 @@ function getLatestValueComputed(el) {
1030
1314
  return lvc;
1031
1315
  }
1032
1316
  /** The latest()-mode read path, installed as GlobalQueue._latestRead. */
1317
+ /** A7: the source has no visible value yet — judged on the OWNER, as read()
1318
+ * does: a store leaf behind a projection's firewall is a plain signal whose
1319
+ * `_value` is the seed (A25: a draft, never a value), and read() routes a
1320
+ * latest() read here before its own firewall/status logic. An override
1321
+ * displays a value even before the first commit (A17). */
1322
+ function uninitializedSource(el) {
1323
+ const owner = el._firewall || el;
1324
+ return !!(owner._statusFlags & STATUS_UNINITIALIZED) && !hasActiveOverride(el);
1325
+ }
1033
1326
  function latestRead(el) {
1327
+ // A leaf of a DISPOSED projection has no flushed world left to mirror: a
1328
+ // shadow created for it now would read through the dead firewall, sit
1329
+ // NotReady/uninitialized forever, and no teardown would ever retire it (the
1330
+ // firewall's already ran — spec O5). Serve the committed value; create
1331
+ // nothing. (A read of a disposed node freezes at its last commit, #3024.)
1332
+ if (el._firewall?._flags & REACTIVE_DISPOSED) return el._value;
1034
1333
  const pendingComputed = getLatestValueComputed(el);
1035
1334
  const prevPending = latestReadActive;
1036
1335
  setLatestReadActive(false);
1037
- const visibleValue =
1038
- el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING
1039
- ? unwrapOverride(el._x?._overrideValue)
1040
- : el._value;
1336
+ const visibleValue = visibleOverride(el) ? unwrapOverride(el._x?._overrideValue) : el._value;
1337
+ // A28: an unflushed write is not the staged value latest() serves. The
1338
+ // shadow was written at the source's write to mirror it (A8) — consult it
1339
+ // only once a flush has carried the write.
1340
+ const u = unflushedValue(el);
1341
+ if (u !== NOT_PENDING) {
1342
+ // The reader derived from the flushed world because of an unflushed
1343
+ // write: it runs again in the flush that carries it (late linker) —
1344
+ // a pull may have cleared the mark the write's walk set.
1345
+ if (context !== null && context._flags & REACTIVE_RECOMPUTING_DEPS) markLateLinker(context);
1346
+ // Link the reader to the shadow so the flush that carries the write
1347
+ // updates it (the shadow itself already mirrors the write, A8).
1348
+ try {
1349
+ read(pendingComputed);
1350
+ } catch {
1351
+ /* the flushed value answers */
1352
+ } finally {
1353
+ setLatestReadActive(prevPending);
1354
+ }
1355
+ // An ambient write: the visible value (override or committed). A rewrite
1356
+ // of a held node: the staged value the last flush left.
1357
+ return el._transition === null ? visibleValue : u;
1358
+ }
1041
1359
  let value;
1042
1360
  try {
1043
- // An untracked latest() read has no reading context, so read() never
1044
- // performs its mid-tick pull a plain write queued between two latest()
1045
- // calls left a still-subscribed shadow at its previous speculative value
1046
- // until the flush (#2922). Mirror the tracked-read pull here: mark the
1047
- // queued staleness through the graph, then bring the shadow up to date.
1048
- const queue = queueFor(pendingComputed);
1049
- if (
1050
- pendingComputed._height >= queue._min &&
1051
- !(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
1052
- ) {
1053
- markHeap(queue);
1054
- // Suspend probe collection during the pull (mirrors pendingCheckRead's
1055
- // prepare): a probe through latest() answers for the SHADOW — the
1056
- // read() dispatch collects it deliberately, so the verdict reflects
1057
- // async still in flight for the latest view, not the parent's held
1058
- // write. A stale shadow recomputing HERE ran its `read(parent)` with
1059
- // the probe still live and collected the parent too, so the verdict
1060
- // depended on whether anything had pulled the shadow current earlier
1061
- // in the tick (#3104: reading latest(m) flipped a later
1062
- // latest(() => isPending(x)) from true to false).
1063
- const prevCheck = pendingCheckActive;
1064
- setPendingCheckActive(false);
1065
- try {
1066
- prepareComputed(pendingComputed, true);
1067
- } finally {
1068
- setPendingCheckActive(prevCheck);
1069
- }
1070
- }
1361
+ // No mid-tick pull: writes become visible at flush (A28), so before the
1362
+ // flush the shadow is exactly as current as the flushed world — the read
1363
+ // below serves it as is. (#2922's pull, which brought the shadow current
1364
+ // against the unflushed write, is superseded: `flush()` first to read
1365
+ // your own write.)
1071
1366
  value = read(pendingComputed);
1072
1367
  } catch (e) {
1073
- if (e instanceof NotReadyError && (!context || !(el._statusFlags & STATUS_UNINITIALIZED)))
1074
- return visibleValue;
1368
+ // A NotReady from the shadow of an INITIALIZED source means the shadow
1369
+ // is mid-flight: serve the visible (committed / override) value. An
1370
+ // uninitialized source has no visible value — latest() throws in every
1371
+ // scope rather than fabricate `undefined` for a `T` that excludes it
1372
+ // (A7; the unowned scope used to return undefined here).
1373
+ if (e instanceof NotReadyError && !uninitializedSource(el)) return visibleValue;
1075
1374
  throw e;
1076
1375
  } finally {
1077
1376
  setLatestReadActive(prevPending);
1078
1377
  }
1079
- if (pendingComputed._statusFlags & STATUS_PENDING) return visibleValue;
1080
- if (stale && currentOptimisticLane && pendingComputed._x?._optimisticLane) {
1081
- const pcLane = findLane(pendingComputed._x?._optimisticLane);
1082
- const curLane = findLane(currentOptimisticLane);
1083
- if (pcLane !== curLane && laneHeld(pcLane)) {
1084
- return visibleValue;
1085
- }
1378
+ if (pendingComputed._statusFlags & STATUS_PENDING) {
1379
+ if (uninitializedSource(el)) throw new NotReadyError(el);
1380
+ return visibleValue;
1086
1381
  }
1382
+ // A render effect off the shadow's HELD lane sees the committed value and
1383
+ // re-runs at the release (#3460; lanes mirror transitions — see
1384
+ // readsHeldCommitted). Was: only a reader under ANOTHER lane; a mainline
1385
+ // reader, mounted or re-run by a sync write mid-hold, showed the
1386
+ // speculative value beside the lane's deferred readers.
1387
+ if (stale && context !== null && readsHeldCommitted(pendingComputed, context)) return el._value;
1087
1388
  // A shadow recomputed by the pull above (not at creation) holds its fresh
1088
1389
  // speculative value in _pendingValue; a contextless read() only surfaces
1089
1390
  // _value. Overrides stay authoritative (A17), and stale readers keep the
1090
1391
  // other transition's committed view, matching read()'s own selection.
1091
1392
  if (
1092
1393
  pendingComputed._pendingValue !== NOT_PENDING &&
1093
- !hasActiveOverride$1(pendingComputed) &&
1394
+ !hasActiveOverride(pendingComputed) &&
1094
1395
  !(stale && pendingComputed._transition && activeTransition !== pendingComputed._transition)
1095
1396
  )
1096
1397
  return pendingComputed._pendingValue;
@@ -1115,7 +1416,14 @@ function latestShadowWithInitializedParent(owner) {
1115
1416
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
1116
1417
  function pendingCheckRead(el, c, owner, firewall) {
1117
1418
  setPendingCheckActive(false);
1118
- if (typeof el._fn === "function") prepareComputed(el, true);
1419
+ if (typeof el._fn === "function") {
1420
+ GlobalQueue._verdictPull = true;
1421
+ try {
1422
+ prepareComputed(el, true);
1423
+ } finally {
1424
+ GlobalQueue._verdictPull = false;
1425
+ }
1426
+ }
1119
1427
  const ownerStatus = owner._statusFlags;
1120
1428
  if (
1121
1429
  c &&
@@ -1162,10 +1470,15 @@ function heldAwaitingAsync(el) {
1162
1470
  // action (#2831: a reader that saw the new value must not also see
1163
1471
  // pending); still-computing answers are covered by the reporter scan.
1164
1472
  if (t._actions.length && !el._fn) return true;
1165
- // A node not yet stamped with a transition only qualifies through the
1166
- // action check above; the reporter scan below is for transition-held
1167
- // writes whose source async is still computing.
1168
- if (!et) return false;
1473
+ // The reporter scan runs for an unstamped node too (#3457): a node staged
1474
+ // AFTER the transaction opened is pushed straight into the transaction's
1475
+ // batch (queuePendingNode, once initTransition adopted it) and only gets
1476
+ // its `_transition` stamp when the flush stashes the hold, but its staged
1477
+ // value is already the transaction's, and `t` resolved to that very
1478
+ // transaction above. Gating on the stamp let a memo whose recompute read
1479
+ // a sync memo's fresh staged value mid-flush pair "not pending" with it
1480
+ // (A10) while the transaction's async source was still computing, so a
1481
+ // memo-wrapped isPending() read false where a direct probe read true.
1169
1482
  for (const [source, reporters] of t._asyncReporters) {
1170
1483
  if (
1171
1484
  reporters.size &&
@@ -1297,7 +1610,11 @@ function effect(compute, effect, error, options) {
1297
1610
  options
1298
1611
  );
1299
1612
  recompute(node, true);
1613
+ // A first pass that derived from a live transaction's staged world was
1614
+ // staged into that transaction (recompute: born held); the transaction's
1615
+ // commit replays this effect. Its first run is not this creation's (A29).
1300
1616
  !options?.defer &&
1617
+ node._pendingValue === NOT_PENDING &&
1301
1618
  (node._type === EFFECT_USER || options?.schedule
1302
1619
  ? node._queue.enqueue(node._type, runEffect.bind(null, node))
1303
1620
  : runEffect(node, LANE_RUN));
@@ -1430,6 +1747,9 @@ function runEffect(node, type) {
1430
1747
  }
1431
1748
  return;
1432
1749
  }
1750
+ // Captured before the callback: its own throw errors the node below, but
1751
+ // the compute pass that produced `_value` was clean, so its tail still goes.
1752
+ const cleanPass = node._x?._error == null;
1433
1753
  let prevStrictRead = false;
1434
1754
  {
1435
1755
  prevStrictRead = setStrictRead("an effect callback");
@@ -1462,6 +1782,12 @@ function runEffect(node, type) {
1462
1782
  }
1463
1783
  node._prevValue = node._value;
1464
1784
  node._modified = false;
1785
+ // The run applied: this is the frame now, so the dependency tail the
1786
+ // compute pass left linked goes (A30, #3438 — `recompute` defers an
1787
+ // effect's trim while a run is owed; the twin of `commitPendingNode`'s
1788
+ // trim for a staged pass). An errored compute kept its full list with
1789
+ // `_depsTail` marking where it stopped; leave it, as the commit does.
1790
+ if (cleanPass) trimStaleDeps(node);
1465
1791
  }
1466
1792
  // Outside the try (see the rule in attribution-hooks.ts). Reached whether or
1467
1793
  // not the callback threw — a throw that escapes the catch above halts.
@@ -1587,15 +1913,22 @@ function restoreTransition(seq, transition, fn) {
1587
1913
  * `yield` is the transaction-safe suspension point: the action waits for a
1588
1914
  * yielded promise and re-enters the transaction before running the code after
1589
1915
  * it. A plain `await` does NOT — the runtime has no hook into an async
1590
- * generator's internal await continuations, so writes to fresh signals
1591
- * between an `await` and the next `yield` escape the transaction and commit
1592
- * immediately. `await` is still the ergonomic choice for typed results; just
1593
- * put a bare `yield` before any writes that follow it:
1916
+ * generator's internal await continuations, so code between an `await` and
1917
+ * the next `yield` runs OUTSIDE the transaction: writes to fresh signals
1918
+ * commit immediately, and anything that creates a reader there `until()`,
1919
+ * `latest()`, a memo or effect, a mount is created mainline, where a read of
1920
+ * this action's held state makes it born held (A29): staged with the
1921
+ * transaction and replayed at its commit. For `until()` that commit is the
1922
+ * settle its own promise holds open (#3482). `await` is still the ergonomic
1923
+ * choice for typed results; just put a bare `yield` before any write or
1924
+ * reader creation that follows it — including the expression of the next
1925
+ * `yield`, which is evaluated before the step re-enters:
1594
1926
  *
1595
1927
  * ```ts
1596
1928
  * const saved = await api.createTodo(text); // typed result
1597
- * yield; // re-enter the transaction before writing
1929
+ * yield; // re-enter the transaction before writing or reading
1598
1930
  * setTodos(t => { ... });
1931
+ * yield until(() => todos.some(t => t.id === saved.id));
1599
1932
  * ```
1600
1933
  *
1601
1934
  * (For the same reason, don't call `flush()` inside an action body — it
@@ -1658,6 +1991,7 @@ function action(genFn) {
1658
1991
  globalQueue.initTransition();
1659
1992
  let ctx = activeTransition;
1660
1993
  ctx._actions.push(it);
1994
+ ctx._acted = true;
1661
1995
  const done = (v, e, failed = false) => {
1662
1996
  ctx = currentTransition(ctx);
1663
1997
  const i = ctx._actions.indexOf(it);
@@ -1731,6 +2065,73 @@ function action(genFn) {
1731
2065
  };
1732
2066
  }
1733
2067
 
2068
+ let ambientHook;
2069
+ const reported = new WeakSet();
2070
+ /**
2071
+ * Registers the ambient client error hook — the one call a browser `init()`
2072
+ * makes to see every failure a boundary renders a fallback for, in
2073
+ * production. (Uncaught errors reach `reportError` / `window.onerror`.)
2074
+ *
2075
+ * ```ts
2076
+ * configureClientErrors({
2077
+ * onError: (error, { ownerPath }) =>
2078
+ * Sentry.captureException(error, {
2079
+ * mechanism: { type: "solid.error_boundary", handled: true }
2080
+ * })
2081
+ * });
2082
+ * ```
2083
+ */
2084
+ function configureClientErrors(config) {
2085
+ if (config && config.onError !== undefined && typeof config.onError !== "function") {
2086
+ throw new TypeError(`Invalid onError: expected a function, received ${typeof config.onError}.`);
2087
+ }
2088
+ ambientHook = config ? config.onError : undefined;
2089
+ }
2090
+ /** The nearest root's hook above `owner` (parked under `ROOT_ERROR_HOOK`), else the ambient one. */
2091
+ function hookFor(owner) {
2092
+ for (let o = owner; o; o = o._parent) {
2093
+ const hook = o[ROOT_ERROR_HOOK];
2094
+ if (hook !== undefined) return hook;
2095
+ }
2096
+ return ambientHook;
2097
+ }
2098
+ /** Component labels up the owner chain, root first — `_name` where the runtime keeps it. */
2099
+ function labels(owner) {
2100
+ const path = [];
2101
+ for (let o = owner; o; o = o._parent) {
2102
+ const name = o._name;
2103
+ if (typeof name === "string" && name.length) path.push(name);
2104
+ }
2105
+ return path.length ? path.reverse() : undefined;
2106
+ }
2107
+ /**
2108
+ * Tells the client error hook about `error`, caught by the boundary whose
2109
+ * owner is `owner`, thrown by `thrower` (the computation the engine's status
2110
+ * wrapper named; unknown for a value that never crossed one) — once per
2111
+ * error object. A throwing hook is reported on the console and otherwise
2112
+ * ignored — a monitor must never take the app down.
2113
+ * @internal
2114
+ */
2115
+ function reportClientError(error, owner, thrower) {
2116
+ const isObject = error !== null && (typeof error === "object" || typeof error === "function");
2117
+ if (isObject) {
2118
+ if (reported.has(error)) return;
2119
+ reported.add(error);
2120
+ }
2121
+ const hook = hookFor(owner);
2122
+ if (hook === undefined) return;
2123
+ const context = {};
2124
+ const boundary = labels(owner);
2125
+ const path = labels(thrower) ?? boundary;
2126
+ if (path !== undefined) context.ownerPath = path;
2127
+ if (boundary !== undefined) context.boundaryPath = boundary;
2128
+ try {
2129
+ hook(error, context);
2130
+ } catch (hookError) {
2131
+ console.error(hookError);
2132
+ }
2133
+ }
2134
+
1734
2135
  /**
1735
2136
  * Low-level reactive-cleanup primitive. Registers a callback that runs when
1736
2137
  * the surrounding owner is disposed.
@@ -2290,12 +2691,20 @@ function refresh(target) {
2290
2691
  *
2291
2692
  * Must be called *outside* a tracking scope.
2292
2693
  *
2694
+ * Inside an action, call it from a step: after an `await`, put a bare `yield`
2695
+ * before `yield until(...)`. The runtime cannot hook an async generator's
2696
+ * `await` continuation, so the `until(...)` expression — which CREATES the
2697
+ * predicate's reader — would otherwise run outside the transaction; created
2698
+ * there it is born held (A29) and replays only at the commit its own promise
2699
+ * holds open (#3482). See {@link action}.
2700
+ *
2293
2701
  * @example
2294
2702
  * ```ts
2295
2703
  * const send = action(async function* (text: string) {
2296
2704
  * const clientId = crypto.randomUUID();
2297
2705
  * setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
2298
2706
  * await socket.send({ clientId, text }); // fire-and-forget transport
2707
+ * yield; // re-enter the transaction after the await
2299
2708
  * // Hold until the live source echoes the write (authoritative view —
2300
2709
  * // the optimistic row above cannot satisfy this):
2301
2710
  * yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
@@ -2486,7 +2895,15 @@ function onSettled(callback) {
2486
2895
  const owner = getOwner();
2487
2896
  owner && !(owner._config & CONFIG_CHILDREN_FORBIDDEN)
2488
2897
  ? trackedEffect(() => untrack(callback), { name: "onSettled" })
2489
- : globalQueue.enqueue(EFFECT_USER, () => {
2898
+ : globalQueue.enqueue(EFFECT_USER, function fire() {
2899
+ // Settled means derived. A settle that reverts optimism (or replays
2900
+ // gated reads) only enqueues the affected subscribers; the pass after
2901
+ // the commit re-derives them. Fired in the commit pass, the callback
2902
+ // read the optimistic source already reverted beside a sync memo of it
2903
+ // still holding the optimistic value — reads do not pull (#3411). Fall
2904
+ // to the next pass while the heap has work; `run` swapped the queue,
2905
+ // so this lands there, and `enqueue` keeps the drain alive.
2906
+ if (dirtyQueue._max >= dirtyQueue._min) return globalQueue.enqueue(EFFECT_USER, fire);
2490
2907
  // Unowned, out-of-band fire (no owner, or a children-forbidden one this
2491
2908
  // one-shot must not bind to): a returned cleanup has no lifecycle to
2492
2909
  // attach to. Reject it in dev; in production the return is simply
@@ -3011,44 +3428,825 @@ function affects(target, key) {
3011
3428
  function trueFn() {
3012
3429
  return true;
3013
3430
  }
3014
- const propTraps = {
3015
- get(_, property, receiver) {
3431
+ /** @internal What a source ENTRY is, decided once when the view is built
3432
+ * (`merge()` learns it while flattening; `omit()` from its argument) and
3433
+ * carried beside the entry — `MergeView.kinds[i]`, `OmitView.kind` — so no
3434
+ * read has to ask. Asking is the cost: any brand check on a Proxy is a trap
3435
+ * (`instanceof` is a `getPrototypeOf` trap, ~20 ns on a store — as much as
3436
+ * the read itself), and a merge over a store did two per read. */
3437
+ const SOURCE_PLAIN = 0; // a plain object: own keys fixed, data is data
3438
+ const SOURCE_OMIT = 1; // an `OmitView` record (only as a merge entry)
3439
+ const SOURCE_PROXY = 2; // a store or foreign proxy: everything is a trap
3440
+ const SOURCE_MEMO = 3; // a function source (merge's memo): swaps objects
3441
+ const SOURCE_MERGE = 4; // a `MergeView` record (only as an omit's source)
3442
+ const EMPTY = Object.freeze({});
3443
+ // The object behind a LEAF entry (any kind but OMIT): a memo is read
3444
+ // (tracked, as merge's own reads are) and a nullish result has no keys.
3445
+ function leafOf(s, kind) {
3446
+ return kind === SOURCE_MEMO ? ((s = s()) == null ? EMPTY : s) : s;
3447
+ }
3448
+ const $SOURCES = Symbol("MERGE_SOURCE");
3449
+ const $OMIT = Symbol("OMIT_VIEW");
3450
+ // The MergeView behind a merge proxy. `$SOURCES` answers the array; the
3451
+ // record itself is reached through this symbol so the table can live on it.
3452
+ const $VIEW = Symbol("MERGE_VIEW");
3453
+ /** @internal The record behind an `omit()` proxy: `source` with `hidden`
3454
+ * keys removed. It is the proxy's TARGET, so the shared handler reads it as
3455
+ * plain fields — no per-instance closures — and it is what props consumers
3456
+ * walk directly (`merge`, `spread`, `ssrElement`): a view never materializes
3457
+ * a copy, and a consumer that knows the record never goes through its traps
3458
+ * (a `getOwnPropertyDescriptor` trap per key allocates a descriptor and a
3459
+ * getter, so enumerating a proxy costs more than the copy it was avoiding).
3460
+ * `hidden` is a key list or a predicate (`omit(props, k => k[0] === "$")`).
3461
+ *
3462
+ * An omit over a `merge()` holds the merge's RECORD (`MergeView`, kind
3463
+ * `SOURCE_MERGE`) — never its proxy, so no read hops through a trap — and is
3464
+ * one record however many leaves the merge has. A consumer walks it as ONE
3465
+ * filtered entry (`sourceKeys` / `sourceGet` recurse into the merge's
3466
+ * sources by function call), and a later `merge()` over it carries the
3467
+ * record as one entry instead of copying its leaves: on a component chain of
3468
+ * defaults + omit + spread (`merge(omit(merge(omit(props))))`) the layers
3469
+ * nest as records, each a few fields, where a flatten to leaf views built a
3470
+ * view and a combined key list per leaf per layer — the largest allocation
3471
+ * of a Kobalte-shaped render. A merge leaf may be merge's memo for a
3472
+ * function source; it is resolved on access. */
3473
+ class OmitView {
3474
+ source;
3475
+ kind;
3476
+ hidden;
3477
+ /** see `MergeView.table` */
3478
+ table = 0;
3479
+ /** see `tableOwnKeys` / `tableDescriptor` */
3480
+ keys = undefined;
3481
+ descs = undefined;
3482
+ constructor(
3483
+ source,
3484
+ /** of `source` — PLAIN, PROXY (a store or a foreign proxy), MEMO, or
3485
+ * MERGE (a `MergeView` record); never OMIT, a view over a view folds
3486
+ * into one. */
3487
+ kind,
3488
+ hidden
3489
+ ) {
3490
+ this.source = source;
3491
+ this.kind = kind;
3492
+ this.hidden = hidden;
3493
+ }
3494
+ }
3495
+ function isHidden(view, key) {
3496
+ const h = view.hidden;
3497
+ return typeof h === "function" ? h(key) : h.includes(key);
3498
+ }
3499
+ // Both filters as one. Two key lists stay a key list (one `includes`, no
3500
+ // closure); a predicate on either side needs a closure. An omit over a
3501
+ // merge builds one combined list per leaf, per component layer, so the
3502
+ // copy's form matters in every tier: `concat` runs the species/spreadable
3503
+ // protocol (2–3× the cost of a copy once optimized), a hand loop is 2–4×
3504
+ // `concat` in the interpreter and baseline tiers (a bytecode per element
3505
+ // against one builtin), and a presized `new Array(n)` is holey, which takes
3506
+ // `includes` off its fast path. `slice` + `push` of the (short) second list
3507
+ // is within a third of the best form in every tier, and packed.
3508
+ function combineHidden(a, b) {
3509
+ if (typeof a !== "function" && typeof b !== "function") {
3510
+ const out = a.slice();
3511
+ for (let i = 0; i < b.length; i++) out.push(b[i]);
3512
+ return out;
3513
+ }
3514
+ return key =>
3515
+ (typeof a === "function" ? a(key) : a.includes(key)) ||
3516
+ (typeof b === "function" ? b(key) : b.includes(key));
3517
+ }
3518
+ // The object a view filters (see `leafOf`).
3519
+ function viewSource(view) {
3520
+ return leafOf(view.source, view.kind);
3521
+ }
3522
+ // Whether a `$PROXY`-marked object is one of OUR views rather than a store
3523
+ // (or a foreign proxy). Asked through `$TARGET`, which a store's `get` trap
3524
+ // answers on its symbol fast path and the view traps answer first thing —
3525
+ // so the question never reaches a store's generic read path (firewall gate,
3526
+ // tracked key read), which is what any unknown symbol (`$SOURCES`, `$OMIT`)
3527
+ // would take. Call only after `$PROXY in o` is known true.
3528
+ function isView(o) {
3529
+ return o[$TARGET] === undefined && (o[$OMIT] !== undefined || o[$VIEW] !== undefined);
3530
+ }
3531
+ /** @internal The `OmitView` behind an `omit()` proxy, or undefined. */
3532
+ function omitView(o) {
3533
+ return o != null && $PROXY in o && o[$TARGET] === undefined ? o[$OMIT] : undefined;
3534
+ }
3535
+ // A props SOURCE ENTRY is a plain object, a proxy (store, merge, omit — the
3536
+ // last two are normally unwrapped first: `mergeView` / `omitView`), a memo,
3537
+ // or an `OmitView` record, and travels with its kind. These three answer for
3538
+ // an entry what `Object.keys` / `in` / `[]` answer for an object, so every
3539
+ // consumer walks entries with one code path, an `OmitView` is filtered
3540
+ // rather than materialized, and nothing is asked of a proxy but the read.
3541
+ // Own keys of a leaf: a proxy answers through ONE `ownKeys` trap (a store's
3542
+ // keeps the key set tracked; `Object.keys` on a proxy would add a descriptor
3543
+ // trap per key); a plain object its enumerable string keys. What a memo
3544
+ // holds is only known once read.
3545
+ function leafKeys(leaf, kind) {
3546
+ if (kind === SOURCE_PLAIN) return Object.keys(leaf);
3547
+ if (kind === SOURCE_PROXY || leaf[$PROXY] === leaf) return Reflect.ownKeys(leaf);
3548
+ return Object.keys(leaf);
3549
+ }
3550
+ /** @internal Own string keys of a source entry — every consumer skips
3551
+ * symbols itself. */
3552
+ function sourceKeys(s, kind) {
3553
+ if (kind === SOURCE_OMIT) {
3554
+ if (s.kind === SOURCE_MERGE) return mergeKeysOf(s.source, false, s);
3555
+ const keys = leafKeys(viewSource(s), s.kind);
3556
+ const out = [];
3557
+ for (let i = 0; i < keys.length; i++) if (!isHidden(s, keys[i])) out.push(keys[i]);
3558
+ return out;
3559
+ }
3560
+ return leafKeys(leafOf(s, kind), kind);
3561
+ }
3562
+ /** @internal `key in entry`. */
3563
+ function sourceHas(s, kind, key) {
3564
+ if (kind === SOURCE_OMIT) {
3565
+ if (isHidden(s, key)) return false;
3566
+ return s.kind === SOURCE_MERGE ? mergeHas(s.source, key) : key in viewSource(s);
3567
+ }
3568
+ return key in leafOf(s, kind);
3569
+ }
3570
+ /** @internal `entry[key]` — the source's getter runs once, here. */
3571
+ function sourceGet(s, kind, key) {
3572
+ if (kind === SOURCE_OMIT) {
3573
+ if (isHidden(s, key)) return undefined;
3574
+ return s.kind === SOURCE_MERGE ? mergeGet(s.source, key) : viewSource(s)[key];
3575
+ }
3576
+ return leafOf(s, kind)[key];
3577
+ }
3578
+ // An entry whose own key set is fixed: a plain object, or a view over one —
3579
+ // an omit of a plain object, or of a merge whose entries all are. Not a
3580
+ // store (its key set is a tracked signal), not a merge memo source (it
3581
+ // swaps whole objects), and not any proxy that declares itself with
3582
+ // `$PROXY in s` — a frames slot proxy answers `has` for every key and lists
3583
+ // none, so only the `in` walk is right for it.
3584
+ function entryHasStaticKeys(s, kind) {
3585
+ if (kind === SOURCE_PLAIN) return true;
3586
+ if (kind !== SOURCE_OMIT) return false;
3587
+ if (s.kind === SOURCE_PLAIN) return true;
3588
+ return s.kind === SOURCE_MERGE && mergeHasStaticKeys(s.source);
3589
+ }
3590
+ function mergeHasStaticKeys(view) {
3591
+ const f = view.sources,
3592
+ k = view.kinds;
3593
+ for (let i = 0; i < f.length; i++) if (!entryHasStaticKeys(f[i], k[i])) return false;
3594
+ return true;
3595
+ }
3596
+ /** @internal Whether the own key set of a props object cannot change
3597
+ * reactively: a plain object, or a merge/omit view over plain objects only.
3598
+ * A consumer may then decide from `Object.getOwnPropertyDescriptor` once —
3599
+ * "no `children` key" or "a data `children`" holds for the object's lifetime,
3600
+ * so no tracking scope is needed for it (#3388). For a store, or a view with
3601
+ * a store or memo leaf, keys can appear later and the reactive path is the
3602
+ * only correct one. */
3603
+ function hasStaticKeys(o) {
3604
+ if (!($PROXY in o)) return true;
3605
+ if (o[$TARGET] !== undefined) return false;
3606
+ const merged = o[$VIEW];
3607
+ if (merged !== undefined) return mergeHasStaticKeys(merged);
3608
+ const view = o[$OMIT];
3609
+ return view !== undefined && entryHasStaticKeys(view, SOURCE_OMIT);
3610
+ }
3611
+ /**
3612
+ * Whether `o[key]` can never change for the lifetime of `o`: the key is a
3613
+ * data property of a plain object, or is absent from an object whose key set
3614
+ * is fixed. A getter, a key on a store, a memo-backed `merge()` source, or
3615
+ * any key of an object whose keys can appear later (a store) is not static.
3616
+ *
3617
+ * Looks through `merge()`/`omit()` views to the leaf that owns the key. Any
3618
+ * object will do, but props are the case it exists for: the compiler encodes
3619
+ * a literal at the call site (`as="button"`) as a data property and an
3620
+ * expression (`as={isLink() ? "a" : "button"}`) as a getter, so a component
3621
+ * library reads the caller's own static/dynamic classification of a prop at
3622
+ * runtime — identically on server and client, the compiled shape being the
3623
+ * same on both — and can take a no-computation path for the literal:
3624
+ *
3625
+ * ```tsx
3626
+ * const Tag = dynamic(() => props.as, { static: isStatic(props, "as") });
3627
+ * ```
3628
+ *
3629
+ * One descriptor lookup; no read of the value, nothing tracked.
3630
+ */
3631
+ function isStatic(o, key) {
3632
+ if ($PROXY in o) {
3633
+ // A store answers its descriptor trap with a value; through a view the
3634
+ // descriptor is truthful (see `sourceDescriptor`). A foreign proxy is
3635
+ // opaque: nothing about it is known to be fixed.
3636
+ if (viewOf(o) === undefined) return false;
3637
+ const desc = Reflect.getOwnPropertyDescriptor(o, key);
3638
+ return desc === undefined ? hasStaticKeys(o) : desc.get === undefined;
3639
+ }
3640
+ const desc = Reflect.getOwnPropertyDescriptor(o, key);
3641
+ return desc === undefined || (desc.get === undefined && desc.set === undefined);
3642
+ }
3643
+ function accessorDescriptor(get, enumerable = true) {
3644
+ return { configurable: true, enumerable, get, set: trueFn };
3645
+ }
3646
+ /** The descriptor a consumer should see for `key` on an entry —
3647
+ * the view proxies answer `getOwnPropertyDescriptor` with it, so it tells the
3648
+ * truth through any depth of merge/omit layers.
3649
+ *
3650
+ * A DATA descriptor means "nothing reactive can hide behind this value": the
3651
+ * key is a data property of a plain-object leaf — the compiler's own
3652
+ * encoding of a static attribute. Everything else is an accessor: a getter
3653
+ * on a leaf, a key on a store proxy (its "data" is a signal), or a key on a
3654
+ * merge memo source (the whole object is reactive). That is what lets a
3655
+ * consumer skip a reactive node for a static prop at the bottom of a
3656
+ * component chain, and it is why the store case must NOT forward the store's
3657
+ * own descriptor, which reports a value.
3658
+ *
3659
+ * `configurable: true` always — the target has no such property, and the
3660
+ * Proxy invariants forbid reporting a non-configurable one. */
3661
+ // `present` says the caller has already established `key in s` (a trap's
3662
+ // shadowing walk did), so a store is not asked a second time.
3663
+ function sourceDescriptor(s, kind, key, present = false) {
3664
+ if (kind === SOURCE_OMIT) {
3665
+ if (isHidden(s, key)) return undefined;
3666
+ return sourceDescriptor(s.source, s.kind, key, present);
3667
+ }
3668
+ // An omit's merge record: the descriptor of the last entry that has the
3669
+ // key, as the merge proxy's own trap answers (see `mergeDescriptor`).
3670
+ if (kind === SOURCE_MERGE) return mergeDescriptor(s, key);
3671
+ // A memo source (`merge(() => …)`) is reactive wholesale: whatever shape
3672
+ // the memo's current object has, the key is an accessor here.
3673
+ if (kind === SOURCE_MEMO) {
3674
+ return present || key in leafOf(s, kind)
3675
+ ? accessorDescriptor(() => leafOf(s, kind)[key])
3676
+ : undefined;
3677
+ }
3678
+ if (kind === SOURCE_PROXY) {
3679
+ // Another view (an omit's source may be a merge proxy) already answers
3680
+ // truthfully. A store's reported "data" is a signal, and a foreign proxy
3681
+ // (frames slot props) has no own descriptors: for both, existence is
3682
+ // `in` and the kind is accessor — one trap, and never the store's
3683
+ // descriptor path.
3684
+ if (isView(s)) return Reflect.getOwnPropertyDescriptor(s, key);
3685
+ return present || key in s ? accessorDescriptor(() => s[key]) : undefined;
3686
+ }
3687
+ const desc = Reflect.getOwnPropertyDescriptor(s, key);
3688
+ if (desc === undefined) return undefined;
3689
+ if (desc.get !== undefined || desc.set !== undefined)
3690
+ return accessorDescriptor(() => s[key], desc.enumerable);
3691
+ // The proxy target has no such key, so the descriptor must be configurable;
3692
+ // Reflect's is a fresh object, so a configurable one is handed out as is.
3693
+ if (desc.configurable) return desc;
3694
+ return { configurable: true, enumerable: desc.enumerable, writable: true, value: desc.value };
3695
+ }
3696
+ // Own ENUMERABLE keys, symbols included, of an entry — the user-facing key
3697
+ // set (`Object.keys(merged)`), where enumerability matters (#2769).
3698
+ function sourceEnumerableKeys(s, kind) {
3699
+ {
3700
+ if (s.kind === SOURCE_MERGE) return mergeEnumerableKeys(s.source, s);
3701
+ const keys = ownEnumerableKeys(viewSource(s));
3702
+ const out = [];
3703
+ for (let i = 0; i < keys.length; i++) if (!isHidden(s, keys[i])) out.push(keys[i]);
3704
+ return out;
3705
+ }
3706
+ }
3707
+ // The target of a merge() proxy: the flattened sources, read by one shared
3708
+ // handler — like OmitView, no per-instance closures. `sources` is what
3709
+ // `$SOURCES` answers.
3710
+ /** @internal */
3711
+ class MergeView {
3712
+ sources;
3713
+ kinds;
3714
+ /** key → the plain leaf that owns it (later sources win), built by an
3715
+ * enumeration or once the reads have paid for it (see `resolvedTable`)
3716
+ * when every leaf has static keys; `null` when one doesn't. Until then
3717
+ * the slot counts the per-key trap reads so far. One slot rather than a
3718
+ * counter field of its own: a view is built per source per component
3719
+ * layer, and each field initializer is a measurable share of a
3720
+ * constructor that small in the lower JIT tiers. */
3721
+ table = 0;
3722
+ /** see `tableOwnKeys` / `tableDescriptor` */
3723
+ keys = undefined;
3724
+ descs = undefined;
3725
+ constructor(
3726
+ sources,
3727
+ /** `kinds[i]` is what `sources[i]` is (see `SourceKind`). */
3728
+ kinds
3729
+ ) {
3730
+ this.sources = sources;
3731
+ this.kinds = kinds;
3732
+ }
3733
+ }
3734
+ /** @internal The `MergeView` behind a `merge()` proxy — its flattened
3735
+ * `sources` with their `kinds` — or undefined. */
3736
+ function mergeView(o) {
3737
+ return o != null && $PROXY in o && o[$TARGET] === undefined ? o[$VIEW] : undefined;
3738
+ }
3739
+ /** @internal The record behind a merge() or omit() proxy — a `MergeView`
3740
+ * (flattened `sources` with their `kinds`) or an `OmitView` — or undefined
3741
+ * for anything else (a plain object, a store, a foreign proxy). Two fast
3742
+ * traps on a store, none on a plain object. */
3743
+ function viewOf(o) {
3744
+ if (o == null || !($PROXY in o) || o[$TARGET] !== undefined) return undefined;
3745
+ const merged = o[$VIEW];
3746
+ return merged !== undefined ? merged : o[$OMIT];
3747
+ }
3748
+ /** @internal The resolved key table of a merge/omit view — every own key of
3749
+ * the view mapped to the plain object that owns it, in merged order (a key
3750
+ * at the position of the last source that carries it, see `tableSet`) — or
3751
+ * undefined when it has none: a leaf is a store or a memo source, whose
3752
+ * keys can change, or the object is not a view at all.
3753
+ *
3754
+ * This is the flat object the eager copy used to build, made lazily and
3755
+ * without copying: one pass over the leaves' own keys, then every
3756
+ * `get`/`has`/descriptor is one lookup plus one read of the owning leaf, and
3757
+ * a consumer (`spread` rerunning its effect) walks the table instead of
3758
+ * re-deriving shadowing from the leaves each time. Own keys only, as the
3759
+ * copy's were: a plain source's key set is fixed once merged (keys added to
3760
+ * it later are not seen — the copy didn't see them either).
3761
+ *
3762
+ * It is built by an ENUMERATION — the `ownKeys` trap, or a consumer asking
3763
+ * for it here — or once per-key reads have paid for it (`READS_FOR_TABLE`),
3764
+ * not on the first read. A per-key read has a direct answer (a walk of the
3765
+ * sources, last to first, one `in` each) whose cost is the source count,
3766
+ * while the table's is every key of every leaf, so the walk wins until a
3767
+ * view has been read about as many times as it has keys. On the server it
3768
+ * never is: a component reads its props a few times, the element enumerates
3769
+ * them once through its own source walk, and the view is gone — building on
3770
+ * first read there cost a component chain a table per layer (profiled on
3771
+ * the Kobalte-shaped chain: a third of SSR time in the table code and its
3772
+ * garbage). On the
3773
+ * client a view read on every reactive rerun crosses the threshold in its
3774
+ * first few updates and is one lookup per read from then on, as before.
3775
+ * Once built — by a `spread`, `Object.keys`, `{...props}`, or the count —
3776
+ * every trap uses it. */
3777
+ function resolvedTable(o) {
3778
+ if (o == null || !($PROXY in o) || o[$TARGET] !== undefined) return undefined;
3779
+ const view = o[$OMIT];
3780
+ if (view !== undefined) return omitTable(view);
3781
+ const merged = o[$VIEW];
3782
+ return merged === undefined ? undefined : mergeTable(merged);
3783
+ }
3784
+ function mergeTable(view) {
3785
+ let table = view.table;
3786
+ if (typeof table !== "object") {
3787
+ // a read count: not decided yet
3788
+ const f = view.sources,
3789
+ k = view.kinds;
3790
+ for (let i = 0; i < f.length; i++) {
3791
+ if (!entryHasStaticKeys(f[i], k[i])) {
3792
+ view.table = null;
3793
+ return undefined;
3794
+ }
3795
+ }
3796
+ table = new Map();
3797
+ collectTable(table, view, undefined);
3798
+ view.table = table;
3799
+ }
3800
+ return table === null ? undefined : table;
3801
+ }
3802
+ // One pass over a merge record's leaves into `table`, through the nested
3803
+ // omit-over-merge entries — the filters enclosing the current leaf are the
3804
+ // `filters` stack — so a component chain builds ONE table at the view that
3805
+ // asked, not one per layer. A key an entry hides that an EARLIER entry owned
3806
+ // must stay: a filter applies to its own entry's contribution, not to the
3807
+ // merge, which is exactly what the stack expresses. Every entry has static
3808
+ // keys (the caller checked), so a leaf's own keys are the truth.
3809
+ function collectTable(table, view, filters) {
3810
+ const f = view.sources,
3811
+ k = view.kinds;
3812
+ for (let i = 0; i < f.length; i++) {
3813
+ const leaf = f[i];
3814
+ if (k[i] === SOURCE_OMIT) {
3815
+ if (leaf.kind === SOURCE_MERGE) {
3816
+ if (filters === undefined) filters = [leaf];
3817
+ else filters.push(leaf);
3818
+ collectTable(table, leaf.source, filters);
3819
+ filters.pop();
3820
+ continue;
3821
+ }
3822
+ const src = leaf.source;
3823
+ const keys = Reflect.ownKeys(src);
3824
+ for (let j = 0; j < keys.length; j++) {
3825
+ const key = keys[j];
3826
+ if (!isHidden(leaf, key) && !hiddenByAny(filters, key)) tableSet(table, key, src);
3827
+ }
3828
+ } else {
3829
+ const keys = Reflect.ownKeys(leaf);
3830
+ for (let j = 0; j < keys.length; j++) {
3831
+ const key = keys[j];
3832
+ if (!hiddenByAny(filters, key)) tableSet(table, key, leaf);
3833
+ }
3834
+ }
3835
+ }
3836
+ }
3837
+ function hiddenByAny(filters, key) {
3838
+ if (filters !== undefined)
3839
+ for (let i = filters.length - 1; i >= 0; i--) if (isHidden(filters[i], key)) return true;
3840
+ return false;
3841
+ }
3842
+ // Key order is the merged one — every key at the position of the LAST source
3843
+ // that carries it — the order `ssrElement`'s array form serializes in and the
3844
+ // eager copy enumerated in, so a spread through a view and a spread over the
3845
+ // sources emit the same attribute order.
3846
+ function tableSet(table, key, leaf) {
3847
+ if (table.has(key)) table.delete(key);
3848
+ table.set(key, leaf);
3849
+ }
3850
+ // An omit view's table: its source's (a merge's table, or a plain object's
3851
+ // own keys) minus the hidden keys. Cached on the record.
3852
+ function omitTable(view) {
3853
+ let table = view.table;
3854
+ if (typeof table !== "object") {
3855
+ const src = view.source;
3856
+ if (view.kind === SOURCE_MERGE) {
3857
+ // One pass over the merge's leaves with this filter on the stack —
3858
+ // the merge record builds no table of its own for it.
3859
+ if (!mergeHasStaticKeys(src)) {
3860
+ view.table = null;
3861
+ return undefined;
3862
+ }
3863
+ table = new Map();
3864
+ collectTable(table, src, [view]);
3865
+ } else if (view.kind === SOURCE_PLAIN) {
3866
+ table = new Map();
3867
+ const keys = Reflect.ownKeys(src);
3868
+ for (let j = 0; j < keys.length; j++) {
3869
+ const key = keys[j];
3870
+ if (!isHidden(view, key)) table.set(key, src);
3871
+ }
3872
+ } else {
3873
+ // a store or a memo: keys can change, no table
3874
+ view.table = null;
3875
+ return undefined;
3876
+ }
3877
+ view.table = table;
3878
+ }
3879
+ return table === null ? undefined : table;
3880
+ }
3881
+ // The user-facing key set of a resolved table: its keys that are enumerable
3882
+ // on the leaf that owns them (`Object.keys(merged)`, #2769). Fixed, like the
3883
+ // table, so it is built once per view: an `ownKeys` trap may hand back the
3884
+ // same array every time (the engine copies it).
3885
+ const propertyIsEnumerable$1 = Object.prototype.propertyIsEnumerable;
3886
+ function tableOwnKeys(view, table) {
3887
+ let keys = view.keys;
3888
+ if (keys === undefined) {
3889
+ keys = view.keys = [];
3890
+ for (const [key, leaf] of table) if (propertyIsEnumerable$1.call(leaf, key)) keys.push(key);
3891
+ }
3892
+ return keys;
3893
+ }
3894
+ // The descriptor for a table key — its owning leaf's, truthful (see
3895
+ // `sourceDescriptor`) — with the shape cached per key so an enumeration
3896
+ // (`for…in`, `Object.keys`, `{...props}`: a descriptor trap per key, on
3897
+ // every pass) does not re-read the leaf's descriptor and re-allocate a
3898
+ // getter each time. An accessor reads live, so its descriptor is reused as
3899
+ // is; a data descriptor is rebuilt with the current value.
3900
+ function tableDescriptor(view, table, key) {
3901
+ const leaf = table.get(key);
3902
+ if (leaf === undefined) return undefined;
3903
+ let descs = view.descs;
3904
+ if (descs === undefined) descs = view.descs = new Map();
3905
+ let cached = descs.get(key);
3906
+ if (cached === undefined) {
3907
+ cached = sourceDescriptor(leaf, SOURCE_PLAIN, key);
3908
+ if (cached === undefined) return undefined;
3909
+ descs.set(key, cached);
3910
+ return cached;
3911
+ }
3912
+ if (cached.get !== undefined) return cached;
3913
+ return {
3914
+ configurable: true,
3915
+ enumerable: cached.enumerable,
3916
+ writable: cached.writable,
3917
+ value: leaf[key]
3918
+ };
3919
+ }
3920
+ // Per-key trap reads a view takes before building its table, from the
3921
+ // break-even: a build is ~60 ns per key (an `ownKeys` share, a `has`, a
3922
+ // `set`), a walk ~20 ns per source (an `in`, a hidden-list check), and a
3923
+ // leaf carries about five keys — so the table has paid for itself after
3924
+ // ~15 reads. Measured on the Kobalte-shaped chain: a walk is 2× a lookup at
3925
+ // depth 1 and up to 10× for a first-source key at depth 7 (a walk through
3926
+ // seven nested layers, a hidden-list check at each), so a view read on
3927
+ // every update wants the table; a view read a handful of times (every
3928
+ // server-side view) never wants it.
3929
+ const READS_FOR_TABLE = 16;
3930
+ // The table for a per-key trap: the one a view HAS (built by an enumeration
3931
+ // or an earlier read, see `resolvedTable`), or the one this read pays for,
3932
+ // or none — a walk answers. One per view type, so a trap pays no type check.
3933
+ // A settled slot is an object (the Map, or `null`); a number is the count.
3934
+ function mergeReadTable(view) {
3935
+ const table = view.table;
3936
+ if (typeof table === "object") return table === null ? undefined : table;
3937
+ if (table + 1 < READS_FOR_TABLE) {
3938
+ view.table = table + 1;
3939
+ return undefined;
3940
+ }
3941
+ return mergeTable(view);
3942
+ }
3943
+ // Only for an omit over a merge (`kind === SOURCE_MERGE`; the caller checks,
3944
+ // inline — a call is not free in every tier): an omit over one object reads
3945
+ // it directly — a list check and a property read, nothing a table would
3946
+ // shorten.
3947
+ function omitReadTable(view) {
3948
+ const table = view.table;
3949
+ if (typeof table === "object") return table === null ? undefined : table;
3950
+ if (table + 1 < READS_FOR_TABLE) {
3951
+ view.table = table + 1;
3952
+ return undefined;
3953
+ }
3954
+ return omitTable(view);
3955
+ }
3956
+ // The table a record HAS — built already by an enumeration or a trap's read
3957
+ // count — or undefined. What a nested walk asks: a record reached through an
3958
+ // outer view's entry counts no reads of its own (the outer view decides for
3959
+ // the whole tree, and its table build then builds the inner ones), so the
3960
+ // inner merges of a component chain build nothing on the server where the
3961
+ // leaves are read a few times each.
3962
+ function tableOf(view) {
3963
+ const table = view.table;
3964
+ return typeof table === "object" && table !== null ? table : undefined;
3965
+ }
3966
+ // "no entry has the key" — distinct from an entry that holds `undefined`.
3967
+ const MISSING = Symbol();
3968
+ // The read: the value of the last entry that has the key, or MISSING. ONE
3969
+ // walk — a nested omit-over-merge entry answers presence and value together,
3970
+ // so a chain of layers is walked once per read, not once per layer per
3971
+ // level.
3972
+ function mergeLookup(view, property) {
3973
+ const table = tableOf(view);
3974
+ if (table !== undefined) {
3975
+ const leaf = table.get(property);
3976
+ return leaf === undefined ? MISSING : leaf[property];
3977
+ }
3978
+ const f = view.sources,
3979
+ k = view.kinds;
3980
+ for (let i = f.length - 1; i >= 0; i--) {
3981
+ const kind = k[i];
3982
+ if (kind === SOURCE_OMIT) {
3983
+ const v = f[i];
3984
+ if (isHidden(v, property)) continue;
3985
+ if (v.kind === SOURCE_MERGE) {
3986
+ const value = mergeLookup(v.source, property);
3987
+ if (value !== MISSING) return value;
3988
+ continue;
3989
+ }
3990
+ const s = viewSource(v);
3991
+ if (property in s) return s[property];
3992
+ } else {
3993
+ const s = leafOf(f[i], kind);
3994
+ if (property in s) return s[property];
3995
+ }
3996
+ }
3997
+ return MISSING;
3998
+ }
3999
+ function mergeGet(view, property) {
4000
+ const value = mergeLookup(view, property);
4001
+ return value === MISSING ? undefined : value;
4002
+ }
4003
+ // `key in merge`, on the record.
4004
+ function mergeHas(view, property) {
4005
+ const table = tableOf(view);
4006
+ if (table !== undefined) return table.has(property);
4007
+ const f = view.sources,
4008
+ k = view.kinds;
4009
+ for (let i = f.length - 1; i >= 0; i--) if (sourceHas(f[i], k[i], property)) return true;
4010
+ return false;
4011
+ }
4012
+ // The proxy's `getOwnPropertyDescriptor`, on the record.
4013
+ function mergeDescriptor(view, property) {
4014
+ const table = tableOf(view);
4015
+ if (table !== undefined) return tableDescriptor(view, table, property);
4016
+ const f = view.sources,
4017
+ k = view.kinds;
4018
+ for (let i = f.length - 1; i >= 0; i--) {
4019
+ if (!sourceHas(f[i], k[i], property)) continue;
4020
+ // `in` also answers for inherited keys, which have no own descriptor.
4021
+ return (
4022
+ sourceDescriptor(f[i], k[i], property, true) ??
4023
+ accessorDescriptor(() => mergeGet(view, property))
4024
+ );
4025
+ }
4026
+ return undefined;
4027
+ }
4028
+ // Own keys of a merge record in merged order — every key at the position
4029
+ // of the LAST entry that carries it, the order the table keeps and
4030
+ // `ssrElement` serializes in. `enumerable` selects the user-facing set
4031
+ // (`Object.keys`, #2769) over every own string key (a consumer's walk);
4032
+ // `filter` is the omit this record is read through, applied as the keys
4033
+ // are gathered so an omit over a merge builds ONE list per layer. A list
4034
+ // with `indexOf` rather than a Set: a props object has a dozen keys, and a
4035
+ // Set's hash store was 2 KB per row on the Kobalte-shaped chain.
4036
+ function mergeKeysOf(view, enumerable, filter) {
4037
+ const out = [];
4038
+ collectKeys(view, filter === undefined ? undefined : [filter], enumerable, out, null);
4039
+ return out;
4040
+ }
4041
+ // One pass over a merge record's leaves — through its nested omit-over-merge
4042
+ // entries, `filters` the omits enclosing the current leaf (see
4043
+ // `collectTable`) — appending each leaf's keys to `keys` in merged order
4044
+ // (a key already listed moves to the end: later wins) and, when `owners` is
4045
+ // given, the object that owns the key at the same index. A consumer that
4046
+ // reads every key once (`ssrElement`) then reads `owners[i][keys[i]]`: no
4047
+ // `in` walk per key, no table. A memo leaf is resolved once here.
4048
+ function collectKeys(view, filters, enumerable, keys, owners) {
4049
+ const f = view.sources,
4050
+ k = view.kinds;
4051
+ for (let i = 0; i < f.length; i++) {
4052
+ let leaf = f[i],
4053
+ kind = k[i];
4054
+ let filter;
4055
+ if (kind === SOURCE_OMIT) {
4056
+ if (leaf.kind === SOURCE_MERGE) {
4057
+ if (filters === undefined) filters = [leaf];
4058
+ else filters.push(leaf);
4059
+ collectKeys(leaf.source, filters, enumerable, keys, owners);
4060
+ filters.pop();
4061
+ continue;
4062
+ }
4063
+ filter = leaf;
4064
+ kind = leaf.kind;
4065
+ leaf = leaf.source;
4066
+ }
4067
+ leaf = leafOf(leaf, kind);
4068
+ const ks = enumerable ? ownEnumerableKeys(leaf) : leafKeys(leaf, kind);
4069
+ for (let j = 0; j < ks.length; j++) {
4070
+ const key = ks[j];
4071
+ if (filter !== undefined && isHidden(filter, key)) continue;
4072
+ if (hiddenByAny(filters, key)) continue;
4073
+ addKey(keys, owners, key, leaf);
4074
+ }
4075
+ }
4076
+ }
4077
+ // Append `key` owned by `owner`, moving an earlier listing to the end: later
4078
+ // wins, and the position is the last owner's (the merged order).
4079
+ function addKey(keys, owners, key, owner) {
4080
+ const at = keys.indexOf(key);
4081
+ if (at !== -1) {
4082
+ keys.splice(at, 1);
4083
+ if (owners !== null) owners.splice(at, 1);
4084
+ }
4085
+ keys.push(key);
4086
+ if (owners !== null) owners.push(owner);
4087
+ }
4088
+ /** @internal Every own string key of a props SOURCE — a plain object, a
4089
+ * store or foreign proxy, or a merge/omit view — appended to `keys` in
4090
+ * merged order with the object that owns each at the same index of
4091
+ * `owners`: a key already listed (by this source or an earlier one) moves
4092
+ * to the end, so several sources collected in turn give the order and the
4093
+ * winners a merge of them would. A consumer that reads each key once
4094
+ * (`ssrElement`) then reads `owners[i][keys[i]]` — the owner's getter runs
4095
+ * there, once — and asks nothing else of a view: no table, no `in` walk per
4096
+ * key through the merge/omit layers, no key list per leaf. One pass,
4097
+ * however deep the layers nest. Symbols are listed; the consumer skips
4098
+ * them. */
4099
+ function sourceOwners(s, keys, owners) {
4100
+ if ($PROXY in s) {
4101
+ const view = viewOf(s);
4102
+ if (view === undefined) {
4103
+ // a store: one `ownKeys` trap, reads through `[]`
4104
+ const ks = Reflect.ownKeys(s);
4105
+ for (let i = 0; i < ks.length; i++) addKey(keys, owners, ks[i], s);
4106
+ return;
4107
+ }
4108
+ if (view instanceof OmitView) {
4109
+ if (view.kind === SOURCE_MERGE) return collectKeys(view.source, [view], false, keys, owners);
4110
+ const leaf = viewSource(view);
4111
+ const ks = leafKeys(leaf, view.kind);
4112
+ for (let i = 0; i < ks.length; i++)
4113
+ if (!isHidden(view, ks[i])) addKey(keys, owners, ks[i], leaf);
4114
+ return;
4115
+ }
4116
+ return collectKeys(view, undefined, false, keys, owners);
4117
+ }
4118
+ const ks = Object.keys(s);
4119
+ for (let i = 0; i < ks.length; i++) addKey(keys, owners, ks[i], s);
4120
+ }
4121
+ // The user-facing key set of a merge record, read through `filter` if given.
4122
+ function mergeEnumerableKeys(view, filter) {
4123
+ const table = mergeTable(view);
4124
+ if (table === undefined) return mergeKeysOf(view, true, filter);
4125
+ const keys = tableOwnKeys(view, table);
4126
+ if (filter === undefined) return keys;
4127
+ const out = [];
4128
+ for (let i = 0; i < keys.length; i++) if (!isHidden(filter, keys[i])) out.push(keys[i]);
4129
+ return out;
4130
+ }
4131
+ const mergeTraps = {
4132
+ get(view, property, receiver) {
3016
4133
  if (property === $PROXY) return receiver;
3017
- return _.get(property);
4134
+ if (property === $TARGET || property === $OMIT) return undefined;
4135
+ if (property === $SOURCES) return view.sources;
4136
+ if (property === $VIEW) return view;
4137
+ // A trap read counts toward the table (see `mergeReadTable`); the walk
4138
+ // itself is the record's.
4139
+ const table = mergeReadTable(view);
4140
+ if (table !== undefined) {
4141
+ const leaf = table.get(property);
4142
+ return leaf === undefined ? undefined : leaf[property];
4143
+ }
4144
+ return mergeGet(view, property);
3018
4145
  },
3019
- has(_, property) {
4146
+ has(view, property) {
3020
4147
  if (property === $PROXY) return true;
3021
- return _.has(property);
4148
+ if (property === $TARGET || property === $OMIT || property === $SOURCES || property === $VIEW)
4149
+ return false;
4150
+ const table = mergeReadTable(view);
4151
+ if (table !== undefined) return table.has(property);
4152
+ return mergeHas(view, property);
3022
4153
  },
3023
4154
  set: trueFn,
3024
4155
  deleteProperty: trueFn,
3025
- getOwnPropertyDescriptor(_, property) {
3026
- return {
3027
- configurable: true,
3028
- enumerable: true,
3029
- get() {
3030
- return _.get(property);
3031
- },
3032
- set: trueFn,
3033
- deleteProperty: trueFn
3034
- };
4156
+ getOwnPropertyDescriptor(view, property) {
4157
+ if (
4158
+ property === $PROXY ||
4159
+ property === $TARGET ||
4160
+ property === $OMIT ||
4161
+ property === $SOURCES ||
4162
+ property === $VIEW
4163
+ )
4164
+ return undefined;
4165
+ const table = mergeReadTable(view);
4166
+ if (table !== undefined) return tableDescriptor(view, table, property);
4167
+ return mergeDescriptor(view, property);
3035
4168
  },
3036
- ownKeys(_) {
3037
- return _.keys();
4169
+ ownKeys(view) {
4170
+ return mergeEnumerableKeys(view);
3038
4171
  }
3039
4172
  };
3040
- function resolveSource(s) {
3041
- return !(s = typeof s === "function" ? s() : s) ? {} : s;
3042
- }
3043
- const $SOURCES = Symbol("MERGE_SOURCE");
3044
- /** @internal The flattened sources behind a `merge()` PROXY, or undefined.
3045
- * Only the proxy form: its writes are no-ops, so the sources are the whole
3046
- * truth. merge()'s plain-object form also records `$SOURCES` (so nested
3047
- * merges flatten), but it is a real object callers may mutate afterwards
3048
- * (html's tagged templates assign props after spreading) — those own writes
3049
- * live on the object, not in the sources, so it must be read directly. */
4173
+ // An omit view reads its source directly — a hidden-key check and one
4174
+ // property read; over a merge record, the merge's own walk by function call,
4175
+ // never a trap. Once an enumeration or the read count has built its table
4176
+ // (over a merge with plain leaves only: the merge's, filtered) every trap
4177
+ // answers from that instead.
4178
+ const omitTraps = {
4179
+ get(view, property, receiver) {
4180
+ if (property === $PROXY) return receiver;
4181
+ // $VIEW is the underlying merge's record, UNFILTERED: never forwarded,
4182
+ // and $SOURCES never answers the merge's own sources, which would hand
4183
+ // a re-merge the unfiltered objects and leak the omitted keys (#3014).
4184
+ // A consumer reaches the record through $OMIT and walks it as ONE
4185
+ // filtered entry.
4186
+ if (property === $TARGET || property === $VIEW || property === $SOURCES) return undefined;
4187
+ if (property === $OMIT) return view;
4188
+ if (view.kind === SOURCE_MERGE) {
4189
+ const table = omitReadTable(view);
4190
+ if (table !== undefined) {
4191
+ const leaf = table.get(property);
4192
+ return leaf === undefined ? undefined : leaf[property];
4193
+ }
4194
+ if (isHidden(view, property)) return undefined;
4195
+ return mergeGet(view.source, property);
4196
+ }
4197
+ if (isHidden(view, property)) return undefined;
4198
+ return viewSource(view)[property];
4199
+ },
4200
+ has(view, property) {
4201
+ if (property === $PROXY) return true;
4202
+ if (property === $TARGET || property === $VIEW || property === $SOURCES || property === $OMIT)
4203
+ return false;
4204
+ if (view.kind === SOURCE_MERGE) {
4205
+ const table = omitReadTable(view);
4206
+ if (table !== undefined) return table.has(property);
4207
+ if (isHidden(view, property)) return false;
4208
+ return mergeHas(view.source, property);
4209
+ }
4210
+ if (isHidden(view, property)) return false;
4211
+ return property in viewSource(view);
4212
+ },
4213
+ set: trueFn,
4214
+ deleteProperty: trueFn,
4215
+ getOwnPropertyDescriptor(view, property) {
4216
+ if (
4217
+ property === $PROXY ||
4218
+ property === $TARGET ||
4219
+ property === $VIEW ||
4220
+ property === $OMIT ||
4221
+ property === $SOURCES
4222
+ )
4223
+ return undefined;
4224
+ if (view.kind === SOURCE_MERGE) {
4225
+ const table = omitReadTable(view);
4226
+ if (table !== undefined) return tableDescriptor(view, table, property);
4227
+ }
4228
+ return sourceDescriptor(view, SOURCE_OMIT, property);
4229
+ },
4230
+ ownKeys(view) {
4231
+ if (view.kind === SOURCE_MERGE) {
4232
+ const table = omitTable(view);
4233
+ if (table !== undefined) return tableOwnKeys(view, table);
4234
+ // No table (a store or memo leaf): the merge's own key set, filtered.
4235
+ return sourceEnumerableKeys(view);
4236
+ }
4237
+ const keys = Reflect.ownKeys(viewSource(view));
4238
+ const out = [];
4239
+ for (let i = 0; i < keys.length; i++) if (!isHidden(view, keys[i])) out.push(keys[i]);
4240
+ return out;
4241
+ }
4242
+ };
4243
+ /** @internal The flattened sources behind a `merge()` proxy, or undefined.
4244
+ * A merge's writes are no-ops, so its sources are the whole truth. A COPY of
4245
+ * a merge (`{...merged}`, a descriptor copy) is a plain object that carries
4246
+ * no sources — `ownKeys` never answers $SOURCES — so what is on the copy is
4247
+ * the truth there and every consumer reads it directly (#3384). */
3050
4248
  function mergeSources(o) {
3051
- return o != null && o[$PROXY] === o ? o[$SOURCES] : undefined;
4249
+ return o != null && $PROXY in o && o[$TARGET] === undefined ? o[$SOURCES] : undefined;
3052
4250
  }
3053
4251
  /**
3054
4252
  * Merges multiple props-like objects into a single proxy that *preserves
@@ -3058,6 +4256,12 @@ function mergeSources(o) {
3058
4256
  * Function arguments are treated as memo-backed sources — useful for passing
3059
4257
  * derived defaults whose computation should track reactively.
3060
4258
  *
4259
+ * The result is a live VIEW of its sources, never a copy: creating it costs
4260
+ * nothing per key, every read goes to the source that owns the key (a getter
4261
+ * runs there, a data property is read live), and writing to it is a no-op.
4262
+ * A single non-function source is returned as is. To own a mutable object,
4263
+ * copy it: `{ ...merged }` snapshots the current values.
4264
+ *
3061
4265
  * Use this in component bodies to merge defaults / overrides without losing
3062
4266
  * Solid's per-property tracking.
3063
4267
  *
@@ -3072,44 +4276,69 @@ function mergeSources(o) {
3072
4276
  */
3073
4277
  function merge(...sources) {
3074
4278
  if (sources.length === 1 && typeof sources[0] !== "function") return sources[0];
3075
- let proxy = false;
3076
4279
  const flattened = [];
4280
+ const kinds = [];
4281
+ // The one non-falsy source, if there is exactly one: it IS the merge.
4282
+ let only = undefined;
4283
+ let count = 0;
3077
4284
  for (let i = 0; i < sources.length; i++) {
3078
4285
  const s = sources[i];
3079
- proxy = proxy || (!!s && $PROXY in s);
3080
- const childSources = !!s && s[$SOURCES];
3081
- if (childSources) {
3082
- for (let i = 0; i < childSources.length; i++) flattened.push(childSources[i]);
3083
- } else flattened.push(typeof s === "function" ? ((proxy = true), createMemo(s)) : s);
3084
- }
3085
- if (SUPPORTS_PROXY && proxy) {
3086
- return new Proxy(
3087
- {
3088
- get(property) {
3089
- if (property === $SOURCES) return flattened;
3090
- for (let i = flattened.length - 1; i >= 0; i--) {
3091
- const s = resolveSource(flattened[i]);
3092
- if (property in s) return s[property];
3093
- }
3094
- },
3095
- has(property) {
3096
- for (let i = flattened.length - 1; i >= 0; i--) {
3097
- if (property in resolveSource(flattened[i])) return true;
3098
- }
3099
- return false;
3100
- },
3101
- keys() {
3102
- const keys = new Set();
3103
- for (let i = 0; i < flattened.length; i++) {
3104
- const sourceKeys = ownEnumerableKeys(resolveSource(flattened[i]));
3105
- for (let j = 0; j < sourceKeys.length; j++) keys.add(sourceKeys[j]);
4286
+ if (!s) continue;
4287
+ count++;
4288
+ only = s;
4289
+ if (typeof s === "function") {
4290
+ flattened.push(createMemo(s));
4291
+ kinds.push(SOURCE_MEMO);
4292
+ continue;
4293
+ }
4294
+ if ($PROXY in s) {
4295
+ // A store (`$TARGET`) is a leaf as it is. A merge() proxy is flattened
4296
+ // through: its writes are no-ops, so its sources are exactly what it
4297
+ // reads. An omit() proxy joins as its view record ONE entry, its
4298
+ // filter travelling with it, whether it is over a plain object or a
4299
+ // whole merge (never the merge's own sources, which would leak the
4300
+ // omitted keys, #3014). A consumer's walk recurses into the record.
4301
+ if (s[$TARGET] === undefined) {
4302
+ const child = s[$VIEW];
4303
+ if (child !== undefined) {
4304
+ for (let j = 0; j < child.sources.length; j++) {
4305
+ flattened.push(child.sources[j]);
4306
+ kinds.push(child.kinds[j]);
3106
4307
  }
3107
- return [...keys];
4308
+ continue;
3108
4309
  }
3109
- },
3110
- propTraps
3111
- );
3112
- }
4310
+ const view = s[$OMIT];
4311
+ if (view !== undefined) {
4312
+ flattened.push(view);
4313
+ kinds.push(SOURCE_OMIT);
4314
+ continue;
4315
+ }
4316
+ }
4317
+ flattened.push(s);
4318
+ kinds.push(SOURCE_PROXY);
4319
+ continue;
4320
+ }
4321
+ flattened.push(s);
4322
+ kinds.push(SOURCE_PLAIN);
4323
+ }
4324
+ if (SUPPORTS_PROXY) {
4325
+ if (count === 1 && typeof only !== "function") return only;
4326
+ // Always a view, never a copy. Building a plain object here costs a
4327
+ // descriptor read, a bound getter and a defineProperty per key per
4328
+ // layer, and component libraries stack several layers per element
4329
+ // (defaults → omit → call-site statics → …), so the copies dominated
4330
+ // their render cost while every consumer that matters — `spread`,
4331
+ // `ssrElement`, a nested merge — reads the flattened sources directly
4332
+ // anyway (#3448). The view is O(1) to create and reads through to the
4333
+ // sources, so a data property on a source is read live, like a getter.
4334
+ // Writes to the result are no-ops (a consumer that needs its own object
4335
+ // copies: `{...merged}`, which the traps answer truthfully). Copies of
4336
+ // the result never carry $SOURCES (#3384): `ownKeys` answers only the
4337
+ // sources' keys.
4338
+ return new Proxy(new MergeView(flattened, kinds), mergeTraps);
4339
+ }
4340
+ // No Proxy: an eager descriptor copy, semantics as close to the view as a
4341
+ // plain object allows (getters stay live; data properties are snapshots).
3113
4342
  const defined = Object.create(null);
3114
4343
  let nonTargetKey = false;
3115
4344
  let lastIndex = flattened.length - 1;
@@ -3145,62 +4374,50 @@ function merge(...sources) {
3145
4374
  if (desc.get) Object.defineProperty(target, key, desc);
3146
4375
  else target[key] = desc.value;
3147
4376
  }
3148
- target[$SOURCES] = flattened;
3149
4377
  return target;
3150
4378
  }
3151
- /**
3152
- * Returns a reactive proxy of `props` with the listed keys hidden. Tracking
3153
- * on the remaining keys is preserved.
3154
- *
3155
- * Use it to forward "rest" props to a child element while pulling out the
3156
- * keys your component handles itself — the equivalent of `splitProps(p, ["a","b"])[1]`.
3157
- *
3158
- * @example
3159
- * ```tsx
3160
- * function Input(props: { label: string; value: string; onInput: (v: string) => void } & JSX.HTMLAttributes<HTMLInputElement>) {
3161
- * const rest = omit(props, "label", "value", "onInput");
3162
- *
3163
- * return (
3164
- * <label>
3165
- * {props.label}
3166
- * <input
3167
- * {...rest}
3168
- * value={props.value}
3169
- * onInput={e => props.onInput(e.currentTarget.value)}
3170
- * />
3171
- * </label>
3172
- * );
3173
- * }
3174
- * ```
3175
- */
3176
4379
  function omit(props, ...keys) {
3177
- if (SUPPORTS_PROXY && $PROXY in props) {
3178
- return new Proxy(
3179
- {
3180
- get(property) {
3181
- // $SOURCES must not tunnel through the filter: merge() flattens
3182
- // whatever answers it, so forwarding would hand a re-merge the
3183
- // UNFILTERED sources of an underlying merge proxy and the omitted
3184
- // keys leak back in (#3014 — the SSR element-spread path re-merges
3185
- // static attributes with the rest object). Opaque here: merge
3186
- // composes omit proxies through their traps instead.
3187
- return property === $SOURCES || keys.includes(property) ? undefined : props[property];
3188
- },
3189
- has(property) {
3190
- return property !== $SOURCES && !keys.includes(property) && property in props;
3191
- },
3192
- keys() {
3193
- return ownEnumerableKeys(props).filter(k => !keys.includes(k));
4380
+ let hidden = keys.length === 1 && typeof keys[0] === "function" ? keys[0] : keys;
4381
+ if (SUPPORTS_PROXY) {
4382
+ // A view over a view folds: one record, both filters, the original
4383
+ // source — so a consumer walks the real object however deep the omits go.
4384
+ // Over a merge() proxy the source is the merge's RECORD (see OmitView):
4385
+ // one record whatever the leaf count, read by function call.
4386
+ let source = props;
4387
+ let kind = SOURCE_PLAIN;
4388
+ if (typeof props === "function") kind = SOURCE_MEMO;
4389
+ else if ($PROXY in props) {
4390
+ kind = SOURCE_PROXY;
4391
+ if (props[$TARGET] === undefined) {
4392
+ const inner = props[$OMIT];
4393
+ if (inner !== undefined) {
4394
+ source = inner.source;
4395
+ kind = inner.kind;
4396
+ hidden = combineHidden(inner.hidden, hidden);
4397
+ } else {
4398
+ const merged = props[$VIEW];
4399
+ if (merged !== undefined) {
4400
+ source = merged;
4401
+ kind = SOURCE_MERGE;
4402
+ }
3194
4403
  }
3195
- },
3196
- propTraps
3197
- );
4404
+ }
4405
+ }
4406
+ return new Proxy(new OmitView(source, kind, hidden), omitTraps);
3198
4407
  }
3199
4408
  const result = {};
3200
4409
  const propNames = Object.getOwnPropertyNames(props);
3201
- const blocked = keys.length > 4 && propNames.length > keys.length ? new Set(keys) : undefined;
4410
+ const isHiddenKey =
4411
+ typeof hidden === "function"
4412
+ ? hidden
4413
+ : hidden.length > 4 && propNames.length > hidden.length
4414
+ ? (
4415
+ blocked => key =>
4416
+ blocked.has(key)
4417
+ )(new Set(hidden))
4418
+ : key => hidden.includes(key);
3202
4419
  for (const propName of propNames) {
3203
- if (blocked ? !blocked.has(propName) : !keys.includes(propName)) {
4420
+ if (!isHiddenKey(propName)) {
3204
4421
  const desc = Object.getOwnPropertyDescriptor(props, propName);
3205
4422
  !desc.get && !desc.set && desc.enumerable && desc.writable && desc.configurable
3206
4423
  ? (result[propName] = desc.value)
@@ -3355,6 +4572,11 @@ const slotNodeEquals = function (a, b) {
3355
4572
  setSlotUnobserved(node => {
3356
4573
  // A live affects() mark keeps the node addressable (sweep parity).
3357
4574
  if (node._x?._affectsCount) return;
4575
+ // An active override or a staged write is state only the node holds (an
4576
+ // optimistic signal keeps its override whether or not anything reads it —
4577
+ // store parity, posture-store-parity S7): defer the release to the flush
4578
+ // that resolves it (the scheduler's transient-node sweep).
4579
+ if (hasActiveOverride(node) || node._pendingValue !== NOT_PENDING) return deferSlotRelease(node);
3358
4580
  const t = node._host;
3359
4581
  const key = node._key;
3360
4582
  if (t.n && t.n[key] === node) {
@@ -3387,11 +4609,16 @@ function getNode(
3387
4609
  // (stageHeldKey). Its held-adoption notification (stageHeldAdoptions)
3388
4610
  // ran before the node existed and the drain has nothing left to say.
3389
4611
  // Without this the node was born from whichever view its first reader
3390
- // saw and never learned the other. (The held-FOLD twin a setter's
3391
- // write to an unobserved key landing only in the pending backing — is
3392
- // #3336's, and lands with #3337.)
3393
- const held = heldAdoptionTransition(target);
4612
+ // saw and never learned the other. Two kinds of hold, one rule (#3336):
4613
+ // - a held FOLD (pb): a setter's write to an unobserved key landed only
4614
+ // in the pending backing; committed is `v[key]`, staged is `pb[key]`
4615
+ // (undefined for a deleted key);
4616
+ // - a held ADOPTION (ht): the adopted value already swapped into `v`;
4617
+ // committed is the held view `hv[key]`, staged is `v[key]`.
4618
+ const fold = heldFoldTransition(target);
4619
+ let held = heldAdoptionTransition(target);
3394
4620
  if (held !== null) current = target.hv[key];
4621
+ else if ((held = fold) !== null) current = target.v[key];
3395
4622
  // Create-floor diet: slotSignal bakes the whole node into one literal —
3396
4623
  // no options object, no equals/unobserved closures, no NodeExtension,
3397
4624
  // no post-construction expandos (acc + the wrap cache px/pxv are
@@ -3418,7 +4645,10 @@ function getNode(
3418
4645
  // "signal". Gated on the engine being installed — node creation is
3419
4646
  // the hottest store path, and the disabled cost must stay one null
3420
4647
  // check (nodes created before enable() stay generically named).
3421
- if (attrHooks !== null) created._name = "store." + String(key);
4648
+ if (attrHooks !== null) {
4649
+ created._name = "store." + String(key);
4650
+ stampNodeOwner(created, target);
4651
+ }
3422
4652
  // Optimistic families: arm the override slot — setSignal routes armed
3423
4653
  // nodes through the core engine (lanes, ownership, reverts all native).
3424
4654
  if (target.fam?.opt) {
@@ -3428,7 +4658,16 @@ function getNode(
3428
4658
  // A node born inside a live mark's identity scope inherits the mark
3429
4659
  // (the declaration walk could only cover nodes existing then).
3430
4660
  if (key !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(created, target.v, key);
3431
- if (held !== null) stageHeldKey(created, target.v[key], held);
4661
+ if (held !== null)
4662
+ stageHeldKey(
4663
+ created,
4664
+ fold !== null
4665
+ ? target.del !== null && target.del.has(key)
4666
+ ? undefined
4667
+ : target.pb[key]
4668
+ : target.v[key],
4669
+ held
4670
+ );
3432
4671
  nodes[key] = node;
3433
4672
  target.nc++;
3434
4673
  markDescendants(target);
@@ -3456,6 +4695,48 @@ function heldAdoptionTransition(target) {
3456
4695
  * it walks no subscriber (the node has none yet). Transition-stamped now, as
3457
4696
  * `runFolded` does — no parked-flush pass will stamp it.
3458
4697
  */
4698
+ /** The live transaction holding `target`'s pending backing (the #3089
4699
+ * write-time stamp, resolved through merges), else null. */
4700
+ function liveFoldTransition(target) {
4701
+ if (target.pb === null) return null;
4702
+ const fb = foldBatches.get(target);
4703
+ if (fb === undefined) return null;
4704
+ const txn = currentTransition(fb);
4705
+ return txn._done === false ? txn : null;
4706
+ }
4707
+ /** liveFoldTransition for node materialization (stageHeldKey). Inside the
4708
+ * draft the backing is the setter's working copy, not a flushed hold — its
4709
+ * nodes take their writes at setter exit (notifyWrites). Optimistic families
4710
+ * hold at the backing: tentative writes are node overrides over a discarded
4711
+ * clone, and a truth-staged landing is masked from ordinary readers by
4712
+ * heldTruthMasked — neither is a plain staged write to mirror. Chained
4713
+ * backings serve the inner store's live value, never a node value. */
4714
+ function heldFoldTransition(target) {
4715
+ if (target.ch || target.fam?.opt === true || inDraft(target)) return null;
4716
+ return liveFoldTransition(target);
4717
+ }
4718
+ /**
4719
+ * Rule 1 at the backing, the hold half (core serve()'s stale-of-foreign and
4720
+ * A29 arms, for a container instead of a node): given the transaction
4721
+ * holding what a reader `c` (a pass; callers serve context-free readers the
4722
+ * committed container themselves) is about to be served, is the STAGED
4723
+ * container its to see? Its own hold, or none — yes. A foreign hold — a
4724
+ * stale pass (render effect) keeps the committed frame and is recorded for
4725
+ * replay at the hold's commit (recordStaleReplay, A15 / A26); a deriving
4726
+ * pass takes the staged world and enters the transaction (enterStagedRead,
4727
+ * A29). Shared by both hold kinds: a setter's fold (committed `v`, staged
4728
+ * `pb`, the write-time stamp) and an adoption under a transaction
4729
+ * (committed = the held view `hv`, staged = the adopted `v`, #3074).
4730
+ */
4731
+ function holdVisible(txn, c) {
4732
+ if (txn === null || ownsHold(txn)) return true;
4733
+ if (stale) {
4734
+ recordStaleReplay(txn, c);
4735
+ return false;
4736
+ }
4737
+ enterStagedRead(null, txn);
4738
+ return true;
4739
+ }
3459
4740
  function stageHeldKey(node, nv, txn) {
3460
4741
  if (slotNodeEquals.call(node, node._value, nv)) return;
3461
4742
  node._pendingValue = nv;
@@ -3467,6 +4748,27 @@ function sameLogicalSlot(target, a, b) {
3467
4748
  const at = lookupTarget$1(a, target.fam);
3468
4749
  return at !== undefined && at === lookupTarget$1(b, target.fam);
3469
4750
  }
4751
+ /**
4752
+ * Observe-tier: the owner each store root was created under. The proxy
4753
+ * cannot carry `_owner` itself (`registerGraph`'s stamp is swallowed by the
4754
+ * set trap outside a draft), so the root target keys it here and the store's
4755
+ * nodes copy it into `_owner` as they are created — an `OBSERVE.exclude`d
4756
+ * panel's store nodes are then excluded subjects, exactly like its signals.
4757
+ * Both ends are gated with the naming on the engine being installed: node
4758
+ * creation is the hottest store path, store creation is next, and the
4759
+ * disabled cost of each stays one null check. A store created before
4760
+ * `enable()` therefore has no recorded owner and its nodes are never excluded
4761
+ * subjects — the same boundary the naming draws; a panel enables first.
4762
+ */
4763
+ const storeOwners = new WeakMap();
4764
+ function storeRootOwner(target) {
4765
+ let root = target;
4766
+ while (root.u !== null) root = root.u;
4767
+ return storeOwners.get(root);
4768
+ }
4769
+ function stampNodeOwner(created, target) {
4770
+ created._owner = storeRootOwner(target) ?? null;
4771
+ }
3470
4772
  function getHasNode(target, key, present) {
3471
4773
  const nodes = (target.h ??= Object.create(null));
3472
4774
  let node = nodes[key];
@@ -3477,6 +4779,13 @@ function getHasNode(target, key, present) {
3477
4779
  equals: isEqual,
3478
4780
  unobserved() {
3479
4781
  if (created._x?._affectsCount) return;
4782
+ // The structural twin of the value slot's rule (setSlotUnobserved,
4783
+ // S7): an optimistic add/delete lives on this presence node as its
4784
+ // override — releasing it with the override on would let `in`,
4785
+ // `Object.keys` and descriptors fall back to committed structure
4786
+ // while the action is live. Defer to the flush that resolves it.
4787
+ if (hasActiveOverride(created) || created._pendingValue !== NOT_PENDING)
4788
+ return deferSlotRelease(created);
3480
4789
  if (target.h && target.h[key] === created) {
3481
4790
  delete target.h[key];
3482
4791
  unlinkFirewallChild(created);
@@ -3486,6 +4795,7 @@ function getHasNode(target, key, present) {
3486
4795
  target.fam?.node ?? undefined
3487
4796
  ));
3488
4797
  created._config |= CONFIG_OWNED_WRITE;
4798
+ if (attrHooks !== null) stampNodeOwner(created, target);
3489
4799
  if (target.fam?.opt) {
3490
4800
  ext(created)._overrideValue = NOT_PENDING;
3491
4801
  created._config |= CONFIG_OPTIMISTIC;
@@ -3513,6 +4823,7 @@ function getKeySetNode(target) {
3513
4823
  target.fam?.node ?? undefined
3514
4824
  ));
3515
4825
  created._config |= CONFIG_OWNED_WRITE;
4826
+ if (attrHooks !== null) stampNodeOwner(created, target);
3516
4827
  if (target.fam?.opt) {
3517
4828
  ext(created)._overrideValue = NOT_PENDING;
3518
4829
  created._config |= CONFIG_OPTIMISTIC;
@@ -3539,6 +4850,7 @@ function getDeepNode(target) {
3539
4850
  target.fam?.node ?? undefined
3540
4851
  ));
3541
4852
  created._config |= CONFIG_OWNED_WRITE;
4853
+ if (attrHooks !== null) stampNodeOwner(created, target);
3542
4854
  if (target.fam?.opt) {
3543
4855
  ext(created)._overrideValue = NOT_PENDING;
3544
4856
  created._config |= CONFIG_OPTIMISTIC;
@@ -4078,6 +5390,7 @@ function storePath$1(t) {
4078
5390
  function reportReplacedContainers(t, old, pb, writtenKeys) {
4079
5391
  const keys = writtenKeys ?? Reflect.ownKeys(pb);
4080
5392
  const isArray = Array.isArray(pb);
5393
+ const owner = storeRootOwner(t);
4081
5394
  for (const key of keys) {
4082
5395
  if ((isArray && key === "length") || key === $OWNER) continue;
4083
5396
  if (t.del !== null && t.del.has(key)) continue;
@@ -4115,7 +5428,8 @@ function reportReplacedContainers(t, old, pb, writtenKeys) {
4115
5428
  isArr,
4116
5429
  total,
4117
5430
  unchanged,
4118
- isArr ? ov.length : Object.keys(ov).length
5431
+ isArr ? ov.length : Object.keys(ov).length,
5432
+ owner
4119
5433
  );
4120
5434
  }
4121
5435
  }
@@ -4528,22 +5842,12 @@ const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
4528
5842
  * computed (#2687 — untracked reads inside mapArray Roots see in-flight
4529
5843
  * values mid-flush). CHILDREN_FORBIDDEN execution scopes (createTrackedEffect
4530
5844
  * / onSettled callbacks) get COMMITTED visibility (#3006), same as core. */
4531
- function inOwnerContext() {
5845
+ /** Core read()'s reader: the current computation, a root reading as its
5846
+ * parent computed (`context` persists under untrack — an untracked read
5847
+ * inside an effect is still that effect's read). */
5848
+ function readerContext() {
4532
5849
  const c = getOwner();
4533
- if (c === null) return false;
4534
- const eff = c._root ? c._parentComputed : c;
4535
- return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
4536
- }
4537
- /** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
4538
- * callbacks). Distinct from context-free: these scopes get committed
4539
- * visibility even against a projection's authoritative-elect pending
4540
- * backing (#3082) — parity with signals, where core read() serves
4541
- * committed to them regardless of staged writes. */
4542
- function inForbiddenScope() {
4543
- const c = getOwner();
4544
- if (c === null) return false;
4545
- const eff = c._root ? c._parentComputed : c;
4546
- return eff != null && !!(eff._config & CONFIG_CHILDREN_FORBIDDEN);
5850
+ return c === null ? null : c._root ? (c._parentComputed ?? null) : c;
4547
5851
  }
4548
5852
  /** A pending fold is transition-held when any written node's parked value is
4549
5853
  * stamped by a live transition (a plain batch parking — the lazy-recompute
@@ -4563,19 +5867,23 @@ function foldHeld(target) {
4563
5867
  return false;
4564
5868
  }
4565
5869
  function readSource(target) {
4566
- // Held view first (#3074): an adoption staged under a live hold serves the
4567
- // pre-hold committed backing to committed-visibility readers. Speculative
4568
- // readers — drafts, write-override, owner-context computeds recomputing
4569
- // inside the transaction, and latest() reads see the adopted backing.
4570
- if (
4571
- target.ht !== null &&
4572
- !latestReadActive &&
4573
- !inDraft(target) &&
4574
- !getWriteOverride() &&
4575
- !inOwnerContext()
4576
- ) {
5870
+ // Adoption hold first (#3074): an adoption staged under a live transaction
5871
+ // (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
5872
+ // committed-visibility readers — context-free and children-forbidden ones,
5873
+ // and stale passes off a foreign hold. Drafts, write-override and latest()
5874
+ // see the adopted backing.
5875
+ const ht = target.ht;
5876
+ if (ht !== null && !latestReadActive && !inDraft(target) && !getWriteOverride()) {
4577
5877
  const hv = heldMaskView(target);
4578
- if (hv !== null) return hv;
5878
+ if (hv !== null) {
5879
+ const c = readerContext();
5880
+ if (
5881
+ c === null ||
5882
+ c._config & CONFIG_CHILDREN_FORBIDDEN ||
5883
+ !holdVisible(ht === PLAIN_HOLD ? null : currentTransition(ht), c)
5884
+ )
5885
+ return hv;
5886
+ }
4579
5887
  }
4580
5888
  return pendingBackingVisible(target, false) ? target.pb : target.v;
4581
5889
  }
@@ -4595,25 +5903,28 @@ function readSource(target) {
4595
5903
  * divergence from context-free per-key reads) — but never through a hold:
4596
5904
  * held truth stays masked exactly as it is for per-key readers. */
4597
5905
  function pendingBackingVisible(target, speculative) {
4598
- return (
4599
- target.pb !== null &&
4600
- (inDraft(target) ||
4601
- getWriteOverride() ||
4602
- // Owner-context (and speculative-peek) readers see the pending
4603
- // backing EXCEPT held truth on an optimistic family (#3164 fold):
4604
- // a live pb on an opt family outside the draft/write-override windows
4605
- // is a staged landing (tentative drafts never outlive their setter),
4606
- // and only the authoritative postures and latest() see it (the
4607
- // backing-level twin of core read()'s A17-for-held-truth arm;
4608
- // ordinary readers keep committed until the transaction's reveal).
4609
- ((speculative || inOwnerContext()) && !heldTruthMasked(target)) ||
4610
- // A projection's pending backing is authoritative-elect: serve it to
4611
- // context-free readers too UNLESS a transition is holding the node
4612
- // commits (downstream async hold stale committed is the contract)
4613
- // or the reader is a CHILDREN_FORBIDDEN scope, which never observes
4614
- // its own unsettled write (#3082, signal parity per #3006).
4615
- (target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
4616
- );
5906
+ if (target.pb === null) return false;
5907
+ // The writer's own channels compose on the pending backing regardless.
5908
+ if (inDraft(target) || getWriteOverride()) return true;
5909
+ // HELD truth on an optimistic family (#3164 fold) is masked from ordinary
5910
+ // readers until the transaction's reveal (the backing-level twin of core
5911
+ // serve()'s CONFIG_HELD_TRUTH arm; authoritative postures and latest()
5912
+ // tunnel through inside heldTruthMasked).
5913
+ if (heldTruthMasked(target)) return false;
5914
+ const c = readerContext();
5915
+ if (c === null || c._config & CONFIG_CHILDREN_FORBIDDEN) {
5916
+ // No pass, or a children-forbidden one: the committed frame (A32)
5917
+ // except the speculative peek (deep()/snapshot()), which sees ordinary
5918
+ // pending staging but never through a live foreign hold, and a
5919
+ // projection's pending backing, authoritative-elect for context-free
5920
+ // readers UNLESS a transition holds the node commits (downstream async
5921
+ // hold stale committed is the contract; the write-time stamp covers
5922
+ // keys with no node, #3336) or the scope is children-forbidden (#3082).
5923
+ const txn = liveFoldTransition(target);
5924
+ if (speculative) return txn === null || ownsHold(txn);
5925
+ return target.fam !== null && c === null && !foldHeld(target) && txn === null;
5926
+ }
5927
+ return holdVisible(liveFoldTransition(target), c);
4617
5928
  }
4618
5929
  /** #3164 fold: HELD truth on an optimistic family — a pending backing
4619
5930
  * stamped by a live transition that retains optimism — is masked from
@@ -4655,11 +5966,6 @@ function runAuthoritative(fn) {
4655
5966
  setProjectionWriteActive(was);
4656
5967
  }
4657
5968
  }
4658
- /** Active optimistic override on an armed node (armed slot idles at
4659
- * NOT_PENDING; undefined = unarmed plain node). */
4660
- function hasActiveOverride(node) {
4661
- return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
4662
- }
4663
5969
  /** The reading computation is until()'s authoritative-view predicate — same
4664
5970
  * source of truth as core read()'s A17 carve-out (`context`, which persists
4665
5971
  * under untrack). optimisticView()'s composition gate consults exactly this:
@@ -4688,26 +5994,23 @@ function authoritativeServe() {
4688
5994
  * FORCE sentinels never surface (they only bump subscribers of accessor
4689
5995
  * keys, which are served by the trap, not the node). */
4690
5996
  function nodeValue(node, backing) {
4691
- // latest() sees the in-flight parked value like an owner-context reader
4692
- // does (#3075) signal/memo parity for store-node-backed keys.
4693
- // Authoritative-view reads (until()'s predicate) skip the override arm
4694
- // only: staged pending values are authoritative, overrides are the
4695
- // caller's optimism.
4696
- const v =
4697
- !authoritativeServe() && hasActiveOverride(node)
5997
+ // Store-only tunnels, ahead of Rule 1: truth authors (authoritativeServe
5998
+ // the projection derive's draft, the write-override continuation) see
5999
+ // staged truth and never an override; latest() reaching this untracked
6000
+ // path for a store key sees the in-flight parked value like an
6001
+ // owner-context reader does (#3075), the visible override first.
6002
+ let v;
6003
+ if (authoritativeServe()) v = node._pendingValue !== NOT_PENDING ? node._pendingValue : backing;
6004
+ else if (latestReadActive)
6005
+ v = visibleOverride(node)
4698
6006
  ? unwrapOverride(node._x?._overrideValue)
4699
- : node._pendingValue !== NOT_PENDING &&
4700
- (latestReadActive ||
4701
- // Owner-context pending visibility — except HELD truth (#3164,
4702
- // see CONFIG_HELD_TRUTH: fold-staged or entangle-stolen
4703
- // confirming truth), which only authoritative/latest readers
4704
- // see (core read()'s A17-for-held-truth twin; ordinary readers
4705
- // keep committed until the transaction's reveal — latest() is
4706
- // exempted by the leading arm above).
4707
- ((inOwnerContext() || authoritativeServe()) &&
4708
- !(node._config & CONFIG_HELD_TRUTH && !authoritativeServe())))
6007
+ : node._pendingValue !== NOT_PENDING
4709
6008
  ? node._pendingValue
4710
6009
  : backing;
6010
+ // Otherwise the one slow selection core read() uses (serve): override,
6011
+ // lane gate, A28, readerSeesCommitted / A29 — with the BACKING as the
6012
+ // committed value (single-home rule, O6).
6013
+ else v = serve(node, readerContext(), node._firewall || node, backing);
4711
6014
  return v === FORCE ? backing : v;
4712
6015
  }
4713
6016
  /** §7b: a chained target's child found as a RAW — from its pending backing
@@ -4759,7 +6062,8 @@ function serveDataKey(target, key, backingValue, src, node, accKnown = -1) {
4759
6062
  // Truth authors read the backing's own length — an optimistic row from
4760
6063
  // the caller's transaction must not shift where the author's next write
4761
6064
  // lands (#3108).
4762
- return (authoritativeServe() ? src : optHooks.optimisticView(target, src)).length;
6065
+ return (authoritativeServe() ? src : optHooks.optimisticView(target, src, inDraft(target)))
6066
+ .length;
4763
6067
  }
4764
6068
  if (inDraft(target)) {
4765
6069
  // Optimistic drafts before their first write have no pending backing yet;
@@ -4773,23 +6077,23 @@ function serveDataKey(target, key, backingValue, src, node, accKnown = -1) {
4773
6077
  v = unwrapOverride(node._x?._overrideValue);
4774
6078
  }
4775
6079
  } else {
4776
- if (node !== undefined) {
4777
- // §7b: a lane value on the outer node SHADOWS read-through an active
4778
- // override pierces the chained gate; otherwise chained backings always
4779
- // serve the live inner value.
4780
- if (getObserver() !== null) {
4781
- // read()'s plain-signal fast path hoisted over the call (legacy trap
4782
- // parity): READ_SLOW = a global read window or non-plain node.
4783
- let nv = readNodeFast(node);
4784
- if (nv === READ_SLOW) nv = read(node);
4785
- if (!chained || hasActiveOverride(node)) v = nv === FORCE ? backingValue : nv;
4786
- } else if (!chained || hasActiveOverride(node)) {
4787
- v = nodeValue(node, backingValue);
4788
- }
4789
- } else if (getObserver() !== null) {
4790
- // First tracked read: create + link, and let the wrap-cache branch
4791
- // below populate px/pxv so read #2 skips wrapNext (slice 2).
4792
- read((node = getNode(target, key, backingValue, accKnown)));
6080
+ // §7b: a lane value on the outer node SHADOWS read-through — an active
6081
+ // override pierces the chained gate; otherwise chained backings always
6082
+ // serve the live inner value.
6083
+ if (getObserver() !== null) {
6084
+ // First tracked read: create + link (the wrap-cache branch below
6085
+ // populates px/pxv so read #2 skips wrapNext, slice 2). The value is
6086
+ // served THROUGH the node from this read on a node born under a held
6087
+ // fold carries the hold (getNode, #3336), and the backing it was read
6088
+ // from does not.
6089
+ if (node === undefined) node = getNode(target, key, backingValue, accKnown);
6090
+ // read()'s plain-signal fast path hoisted over the call (legacy trap
6091
+ // parity): READ_SLOW = a global read window or non-plain node.
6092
+ let nv = readNodeFast(node);
6093
+ if (nv === READ_SLOW) nv = read(node);
6094
+ if (!chained || hasActiveOverride(node)) v = nv === FORCE ? backingValue : nv;
6095
+ } else if (node !== undefined && (!chained || hasActiveOverride(node))) {
6096
+ v = nodeValue(node, backingValue);
4793
6097
  }
4794
6098
  }
4795
6099
  // Shallow stores serve data raw; store-proxy slots get boundary wrappers.
@@ -5012,7 +6316,7 @@ const traps = {
5012
6316
  !authoritativeServe()
5013
6317
  ) {
5014
6318
  const node = target.n?.[key];
5015
- if (node !== undefined && hasActiveOverride(node))
6319
+ if (node !== undefined && visibleOverride(node))
5016
6320
  v = unwrapOverride(node._x?._overrideValue);
5017
6321
  }
5018
6322
  if (target.s) return serveShallow(target, key, v);
@@ -5044,7 +6348,7 @@ const traps = {
5044
6348
  if (hasActiveOverride(node)) present = !!nv;
5045
6349
  } else if (!authoritativeServe()) {
5046
6350
  const node = target.h?.[key];
5047
- if (node !== undefined && hasActiveOverride(node))
6351
+ if (node !== undefined && visibleOverride(node))
5048
6352
  present = !!unwrapOverride(node._x?._overrideValue);
5049
6353
  }
5050
6354
  } else if (target.fam?.opt && draftSeesOverrides(target) && !authoritativeServe()) {
@@ -5062,7 +6366,25 @@ const traps = {
5062
6366
  },
5063
6367
  getOwnPropertyDescriptor(target, key) {
5064
6368
  if (key === $OWNER) return undefined;
5065
- const desc = visibleDescriptor(target, readSource(target), key);
6369
+ // A descriptor read is a PRESENCE read: it subscribes to the key's
6370
+ // presence node and witnesses affects()/isPending() exactly as `in` does
6371
+ // (structural oracle, 2026-09-17 — the trap read no node before, so a
6372
+ // render effect inspecting a key through getOwnPropertyDescriptor never
6373
+ // re-ran for an optimistic add or delete, and an isPending() probe over
6374
+ // it witnessed nothing). The value it reports rides the value node's
6375
+ // view through visibleDescriptor.
6376
+ if (pendingCheckActive) witnessAffectsMark(target, key);
6377
+ if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
6378
+ const src = readSource(target);
6379
+ const desc = visibleDescriptor(target, src, key);
6380
+ if (!inDraft(target) && getObserver() !== null) {
6381
+ // The node is born from the source's presence (as `has` births it),
6382
+ // not the override-adjusted answer.
6383
+ let present = key in src;
6384
+ if (present && target.del !== null && src === target.pb && target.del.has(key))
6385
+ present = false;
6386
+ read(getHasNode(target, key, present));
6387
+ }
5066
6388
  if (desc === undefined) return undefined;
5067
6389
  // Array targets carry a real non-configurable `length` the proxy
5068
6390
  // invariant forces us to report faithfully; everything else reports
@@ -5176,8 +6498,10 @@ const traps = {
5176
6498
  };
5177
6499
  /** Low-level setter primitive: opens write mode on a next proxy, runs `fn`,
5178
6500
  * emits write-time notifications at outermost exit, applies returned
5179
- * replacements as adoptions. `guard=false` skips the owned-scope dev guard —
5180
- * projection recomputes legitimately write from inside their computed. */
6501
+ * replacements as adoptions. `guard=false` skips the dev guards (owned-scope
6502
+ * write, thenable result) — projection recomputes legitimately write from
6503
+ * inside their computed, and their async derive is handled by the recompute,
6504
+ * not returned through here. */
5181
6505
  function storeSetterNext(proxy, fn, guard = true) {
5182
6506
  if (guard) devGuardStoreSetterWrite();
5183
6507
  const target = proxy[$TARGET];
@@ -5203,6 +6527,9 @@ function storeSetterNext(proxy, fn, guard = true) {
5203
6527
  for (const t of touched) notifyWrites(t);
5204
6528
  }
5205
6529
  }
6530
+ // After the sync writes have notified (they were real, like an effect's
6531
+ // side effects before its invalid-cleanup throw) and before adoption.
6532
+ if (guard) devGuardStoreSetterResult(result);
5206
6533
  if (result !== undefined && result !== proxy && isWrappable(result)) {
5207
6534
  // Returned replacement: on an optimistic family (outside authoritative
5208
6535
  // writes) the replacement is itself an optimistic edit — diff it against
@@ -5236,7 +6563,18 @@ function createStoreNext(initialValue, shallow = false) {
5236
6563
  proxy[$TARGET].s = true;
5237
6564
  markRawIngest(initialValue);
5238
6565
  }
5239
- registerGraph(proxy, getOwner());
6566
+ {
6567
+ const owner = getOwner();
6568
+ // Dev-tier graph registration (owner signal lists, onGraph); the
6569
+ // `_owner` write itself never reaches the proxy, see storeOwners.
6570
+ registerGraph(proxy, owner);
6571
+ // Only once the engine is installed, like the node stamping it feeds: a
6572
+ // WeakMap.set per fresh store is a growing ephemeron table (~+35% on the
6573
+ // 2000-store create+commit shape, CodSpeed −11.7% on #3380's first cut)
6574
+ // and, disabled, buys nothing — a store created before enable() has no
6575
+ // excluded owner to inherit either way.
6576
+ if (attrHooks !== null) storeOwners.set(proxy[$TARGET], owner);
6577
+ }
5240
6578
  const setter = fn => storeSetterNext(proxy, fn);
5241
6579
  return [proxy, setter];
5242
6580
  }
@@ -5313,9 +6651,10 @@ function visibleKeys(target, src) {
5313
6651
  (!inDraft(target) || draftSeesOverrides(target))
5314
6652
  ) {
5315
6653
  let set = null;
6654
+ const draft = inDraft(target);
5316
6655
  for (const key of Reflect.ownKeys(target.h)) {
5317
6656
  const node = target.h[key];
5318
- if (!hasActiveOverride(node)) continue;
6657
+ if (!(draft ? hasActiveOverride(node) : visibleOverride(node))) continue;
5319
6658
  set ??= new Set(keys);
5320
6659
  if (unwrapOverride(node._x?._overrideValue)) set.add(key);
5321
6660
  else set.delete(key);
@@ -5337,7 +6676,7 @@ function visibleDescriptor(target, src, key) {
5337
6676
  }
5338
6677
  if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
5339
6678
  const node = target.h?.[key];
5340
- if (node !== undefined && hasActiveOverride(node)) {
6679
+ if (node !== undefined && visibleOverride(node)) {
5341
6680
  if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
5342
6681
  if (desc === undefined) {
5343
6682
  const vn = target.n?.[key];
@@ -5978,7 +7317,7 @@ function descend(pv, nv, keyFn, fam, proj = false) {
5978
7317
  * driven from inside an enclosing authoritative-write scope (next-store
5979
7318
  * optimistic derives), and a hard `false` would clobber it mid-derive.
5980
7319
  */
5981
- function wrapDraft(inner, isActive, aroundWrite) {
7320
+ function wrapDraft(inner, isActive, aroundWrite, shallow) {
5982
7321
  const write = op => (aroundWrite ? aroundWrite(op) : op());
5983
7322
  const traps = {
5984
7323
  get(_, prop) {
@@ -5992,8 +7331,9 @@ function wrapDraft(inner, isActive, aroundWrite) {
5992
7331
  setWriteOverride(false);
5993
7332
  setProjectionWriteActive(was);
5994
7333
  }
5995
- if (prop === $TARGET) return value;
5996
- return typeof value === "object" && value !== null
7334
+ // A shallow store's leaves are raw by contract (#3498): no draft proxy
7335
+ // over them, so identity holds and a frozen leaf is never trapped.
7336
+ return !shallow && typeof value === "object" && value !== null && prop !== $TARGET
5997
7337
  ? wrapDraft(value, isActive, aroundWrite)
5998
7338
  : value;
5999
7339
  },
@@ -6128,21 +7468,27 @@ function createStoreDerivedNext(fn, seed, options) {
6128
7468
  }
6129
7469
  ];
6130
7470
  }
7471
+ // A detached copy of projection state: the root container alone for a shallow
7472
+ // store — its leaves are raw references by contract and stay so (#3498) — the
7473
+ // whole tree otherwise.
7474
+ function cloneState(v, shallow) {
7475
+ return shallow ? (Array.isArray(v) ? v.slice() : { ...v }) : JSON.parse(JSON.stringify(v));
7476
+ }
6131
7477
  function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
6132
7478
  const owner = getOwner();
7479
+ const target = wrappedStore[$TARGET];
6133
7480
  let settled = false;
6134
7481
  let result;
6135
7482
  // Open loading window (seedLoadingValue): the observable store IS commit #0
6136
7483
  // for the whole first flight — the derive works a detached shadow of the
6137
7484
  // seed so draft writes cannot tear through to readers (#2988). Every commit
6138
7485
  // point reconciles the shadow through the normal commit path.
6139
- const shadow = owner._loading
6140
- ? JSON.parse(JSON.stringify(wrappedStore[$TARGET][STORE_VALUE]))
6141
- : null;
7486
+ const shadow = owner._loading ? cloneState(target[STORE_VALUE], target.s) : null;
6142
7487
  const draft = wrapDraft(
6143
7488
  wrappedStore,
6144
7489
  () => !settled || owner._x?._inFlight === result,
6145
- aroundDraftWrite
7490
+ aroundDraftWrite,
7491
+ target.s
6146
7492
  );
6147
7493
  storeSetterNext(
6148
7494
  draft,
@@ -6153,7 +7499,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraf
6153
7499
  // Shadow run: commit a detached snapshot, never the shadow itself
6154
7500
  // (adoption takes the value by identity — handing it the live shadow
6155
7501
  // would fuse the draft to the observable store).
6156
- if (shadow && (v === undefined || v === shadow)) v = JSON.parse(JSON.stringify(shadow));
7502
+ if (shadow && (v === undefined || v === shadow)) v = cloneState(shadow, target.s);
6157
7503
  if (v === s || v === undefined) return;
6158
7504
  const write = () =>
6159
7505
  storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
@@ -6214,7 +7560,10 @@ function installNextBlockedHalf() {
6214
7560
  applyTentative,
6215
7561
  retainsOptimism: transitionHoldsOptimism
6216
7562
  });
6217
- setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw));
7563
+ // The affects() declaration walk is a WRITER channel (A28 (5)): tagging a
7564
+ // parent covers the record as the writer sees it, this tick's optimistic
7565
+ // writes included — the draft view, not the reader view.
7566
+ setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw, true));
6218
7567
  // Scheduler flush tails call _clearOptimisticStores whenever tracked
6219
7568
  // stores exist; next has no layer to clear — reverts are engine-native —
6220
7569
  // so the hook only empties the batch set.
@@ -6707,7 +8056,7 @@ function notifyOptimisticWrites(t, pb) {
6707
8056
  * Authoritative-view reads (until()'s predicate) skip composition entirely:
6708
8057
  * the predicate observes authoritative truth, never the caller's tentative
6709
8058
  * overlay. (Write-side emission callers never run under such a compute.) */
6710
- function optimisticView(t, src) {
8059
+ function optimisticView(t, src, draft = false) {
6711
8060
  if (t.fam?.opt !== true || authoritativeRead()) return src;
6712
8061
  let out = null;
6713
8062
  const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
@@ -6715,7 +8064,9 @@ function optimisticView(t, src) {
6715
8064
  if (nodes !== null) {
6716
8065
  for (const key of Reflect.ownKeys(nodes)) {
6717
8066
  const node = nodes[key];
6718
- if (!hasActiveOverride(node)) continue;
8067
+ // A28 (5): readers see an optimistic write once a flush carried it;
8068
+ // the draft (writer channel) composes on it now.
8069
+ if (!(draft ? hasActiveOverride(node) : visibleOverride(node))) continue;
6719
8070
  const ov = unwrapOverride(node._x?._overrideValue);
6720
8071
  if (key === "length" && Array.isArray(src)) {
6721
8072
  if (src.length !== ov) ensure().length = ov;
@@ -6726,7 +8077,9 @@ function optimisticView(t, src) {
6726
8077
  if (has !== null) {
6727
8078
  for (const key of Reflect.ownKeys(has)) {
6728
8079
  const node = has[key];
6729
- if (!hasActiveOverride(node)) continue;
8080
+ // A28 (5): readers see an optimistic write once a flush carried it;
8081
+ // the draft (writer channel) composes on it now.
8082
+ if (!(draft ? hasActiveOverride(node) : visibleOverride(node))) continue;
6730
8083
  const present = !!unwrapOverride(node._x?._overrideValue);
6731
8084
  if (!present && key in (out ?? src)) delete ensure()[key];
6732
8085
  }
@@ -7680,6 +9033,8 @@ class CollectionQueue extends Queue {
7680
9033
  _initialized = false;
7681
9034
  _onFn;
7682
9035
  _prevOn = ON_INIT;
9036
+ /** The boundary's owner — where a `caught` report locates itself, set before the children are built (a creation-time throw arrives before `_tree`). */
9037
+ _owner;
7683
9038
  constructor(type) {
7684
9039
  super();
7685
9040
  this._collectionType = type;
@@ -7688,20 +9043,47 @@ class CollectionQueue extends Queue {
7688
9043
  if (!type || (read(this._disabled) && (!_revealUsed || read(this._collapsed)))) return;
7689
9044
  return super.run(type);
7690
9045
  }
9046
+ /** The `on` key, or ON_INIT when it throws. Evaluated mid-propagation,
9047
+ * inside the pending node's own pass: read as a spectator so the key's
9048
+ * sources never become that node's dependencies (#3528). */
9049
+ _readOn() {
9050
+ return spectate(() => {
9051
+ try {
9052
+ return this._onFn();
9053
+ } catch {
9054
+ return ON_INIT;
9055
+ }
9056
+ });
9057
+ }
7691
9058
  notify(node, type, flags, error) {
7692
9059
  if (!(type & this._collectionType)) return super.notify(node, type, flags, error);
7693
9060
  if (this._initialized && this._onFn) {
7694
- const currentOn = untrack(() => {
7695
- try {
7696
- return this._onFn();
7697
- } catch {
7698
- return ON_INIT;
7699
- }
7700
- });
9061
+ const currentOn = this._readOn();
7701
9062
  if (currentOn !== this._prevOn) {
7702
9063
  this._prevOn = currentOn;
7703
9064
  this._initialized = false;
7704
9065
  this._sources.clear();
9066
+ // Readers forwarded while this boundary showed content are behind the
9067
+ // fallback now: they stop blocking (`reporterBlocksSource`), and the
9068
+ // transactions they were holding must be re-judged for it (A33, #3375).
9069
+ // What those readers still wait on is this boundary's to wait on now:
9070
+ // they never re-notify (status propagation dedupes on the reader's
9071
+ // `_pendingSources`), so the reset collects it from their registrations
9072
+ // — the one place a forwarded reader is recorded (INV-3) — or a sibling
9073
+ // reader's flight that lands first reveals them stale (A33, #3459).
9074
+ for (const t of transitions)
9075
+ for (const [source, reporters] of t._asyncReporters)
9076
+ for (const reporter of reporters)
9077
+ if (this._holds(reporter)) {
9078
+ this._sources.add(source);
9079
+ reporter._x?._pendingSources?.forEach(s => this._sources.add(s));
9080
+ }
9081
+ if (this._sources.size) {
9082
+ setSignal(this._disabled, true);
9083
+ if (attrHooks !== null && this._collectionType & STATUS_PENDING)
9084
+ attrHooks.boundaryFallback(this, this._tree, true);
9085
+ }
9086
+ wakeParked();
7705
9087
  }
7706
9088
  }
7707
9089
  // Routing is dimension-independent: each boundary consumes only its own
@@ -7722,19 +9104,47 @@ class CollectionQueue extends Queue {
7722
9104
  if (source) {
7723
9105
  const wasEmpty = this._sources.size === 0;
7724
9106
  this._sources.add(source);
9107
+ // A collecting boundary waits on everything the effect is pending on,
9108
+ // not only the source this notification carries. Status propagation
9109
+ // dedupes on the effect's `_pendingSources`: a source it already
9110
+ // carries (a flight that started before an `on` reset cleared the
9111
+ // set) is never re-reported, and that source's later re-flight
9112
+ // stays invisible — the boundary revealed when its one collected
9113
+ // source settled while the effect was still pending (#3375).
9114
+ if (this._collectionType & STATUS_PENDING)
9115
+ node._x?._pendingSources?.forEach(s => this._sources.add(s));
7725
9116
  if (wasEmpty) {
7726
9117
  setSignal(this._disabled, true);
7727
9118
  if (attrHooks !== null && this._collectionType & STATUS_PENDING)
7728
9119
  attrHooks.boundaryFallback(this, this._tree, true);
7729
9120
  }
7730
9121
  if (this._collectionType & STATUS_ERROR) {
7731
- setSignal(this._error, unwrapStatusError(source._x?._error));
9122
+ const caught = unwrapStatusError(source._x?._error);
9123
+ setSignal(this._error, caught);
9124
+ // The client error hook: this boundary renders its fallback for
9125
+ // it — the one road a rendered failure took that no global handler
9126
+ // ever saw. `source` is the computation that threw (the status
9127
+ // wrapper's, made at the first landing and kept downstream), so the
9128
+ // hook hears where it broke as well as where it was met. Once per
9129
+ // error object; a `reset()` re-collecting the same failure says
9130
+ // nothing new.
9131
+ reportClientError(caught, this._owner, source);
7732
9132
  }
7733
9133
  }
7734
9134
  }
7735
9135
  type &= ~this._collectionType;
7736
9136
  return type ? super.notify(node, type, flags, error) : true;
7737
9137
  }
9138
+ /** Is `reporter` live and routed to this boundary — under it, with no
9139
+ * collecting pending-type boundary in between (`reporterBlocksSource`'s test)? */
9140
+ _holds(reporter) {
9141
+ if (reporter._flags & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
9142
+ for (let q = reporter._queue; q; q = q._parent) {
9143
+ if (q === this) return true;
9144
+ if (q._collectionType & STATUS_PENDING && !q._initialized) return false;
9145
+ }
9146
+ return false;
9147
+ }
7738
9148
  _checkSources() {
7739
9149
  for (const source of this._sources) {
7740
9150
  // A source with a live affects() mark holds display state for the
@@ -7765,11 +9175,10 @@ class CollectionQueue extends Queue {
7765
9175
  if (attrHooks !== null && this._collectionType & STATUS_PENDING)
7766
9176
  attrHooks.boundaryFallback(this, this._tree, false);
7767
9177
  if (this._onFn) {
7768
- try {
7769
- this._prevOn = untrack(() => this._onFn());
7770
- } catch {
7771
- /* value not yet committed — _prevOn stays stale, next notify will reset */
7772
- }
9178
+ // A throw (value not yet committed) leaves _prevOn stale; the next
9179
+ // notify then resets.
9180
+ const on = this._readOn();
9181
+ if (on !== ON_INIT) this._prevOn = on;
7773
9182
  }
7774
9183
  }
7775
9184
  }
@@ -7793,6 +9202,7 @@ function createCollectionBoundary(type, fn, fallback, onFn) {
7793
9202
  const owner = createOwner();
7794
9203
  if (_revealUsed) setContext(RevealControllerContext, null, owner);
7795
9204
  const queue = new CollectionQueue(type);
9205
+ queue._owner = owner;
7796
9206
  if (type === STATUS_ERROR)
7797
9207
  queue._error = signal(undefined, { ownedWrite: true, _noSnapshot: true });
7798
9208
  if (onFn) queue._onFn = onFn;
@@ -8063,13 +9473,22 @@ export {
8063
9473
  $TRACK,
8064
9474
  ContextNotFoundError,
8065
9475
  DEV,
9476
+ MergeView,
8066
9477
  NoOwnerError,
8067
9478
  NotReadyError,
8068
9479
  OBSERVE,
9480
+ OmitView,
9481
+ ROOT_ERROR_HOOK,
9482
+ SOURCE_MEMO,
9483
+ SOURCE_MERGE,
9484
+ SOURCE_OMIT,
9485
+ SOURCE_PLAIN,
9486
+ SOURCE_PROXY,
8069
9487
  SUPPORTS_PROXY,
8070
9488
  TimeoutError,
8071
9489
  action,
8072
9490
  affects,
9491
+ configureClientErrors,
8073
9492
  createContext,
8074
9493
  createEffect,
8075
9494
  createErrorBoundary,
@@ -8093,27 +9512,37 @@ export {
8093
9512
  getContext,
8094
9513
  getObserver,
8095
9514
  getOwner,
9515
+ hasStaticKeys,
8096
9516
  isEqual,
8097
9517
  isPending,
9518
+ isStatic,
8098
9519
  isWrappable,
8099
9520
  latest,
8100
9521
  mapArray,
8101
9522
  merge,
8102
9523
  mergeSources,
9524
+ mergeView,
8103
9525
  omit,
9526
+ omitView,
8104
9527
  onCleanup,
8105
9528
  onSettled,
8106
9529
  reconcile,
8107
9530
  refresh,
8108
9531
  repeat,
8109
9532
  resolve,
9533
+ resolvedTable,
8110
9534
  runWithOwner,
8111
9535
  setContext,
8112
9536
  snapshot,
9537
+ sourceGet,
9538
+ sourceHas,
9539
+ sourceKeys,
9540
+ sourceOwners,
8113
9541
  storeHasFamily,
8114
9542
  storeHasOptimisticFamily,
8115
9543
  storeIsShallow,
8116
9544
  storePath,
8117
9545
  until,
8118
- untrack
9546
+ untrack,
9547
+ viewOf
8119
9548
  };