@solidjs/signals 2.0.0-rc.2 → 2.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/dev.js +1454 -118
  2. package/dist/node.cjs +2709 -1396
  3. package/dist/prod/affects.js +13 -12
  4. package/dist/prod/boundaries.js +39 -34
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +48 -46
  7. package/dist/prod/core/core.js +99 -67
  8. package/dist/prod/core/effect.js +25 -28
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +85 -49
  11. package/dist/prod/core/heap.js +10 -10
  12. package/dist/prod/core/lanes.js +19 -19
  13. package/dist/prod/core/optimistic.js +66 -41
  14. package/dist/prod/core/owner.js +13 -13
  15. package/dist/prod/core/scheduler.js +131 -85
  16. package/dist/prod/core/verdict.js +36 -15
  17. package/dist/prod/index.js +4 -0
  18. package/dist/prod/map.js +101 -101
  19. package/dist/prod/signals.js +1 -1
  20. package/dist/prod/store/index.js +2 -0
  21. package/dist/prod/store/next/optimistic.js +65 -11
  22. package/dist/prod/store/next/patch-hooks.js +13 -0
  23. package/dist/prod/store/next/patch.js +614 -0
  24. package/dist/prod/store/next/projection.js +107 -45
  25. package/dist/prod/store/next/reconcile.js +307 -120
  26. package/dist/prod/store/next/store.js +321 -92
  27. package/dist/prod/store/next/target.js +13 -4
  28. package/dist/prod/store/store.js +5 -5
  29. package/dist/types/core/core.d.ts +15 -1
  30. package/dist/types/core/dev.d.ts +8 -0
  31. package/dist/types/core/graph.d.ts +22 -0
  32. package/dist/types/core/invariants.d.ts +1 -1
  33. package/dist/types/core/scheduler.d.ts +12 -0
  34. package/dist/types/store/index.d.ts +2 -0
  35. package/dist/types/store/next/patch-hooks.d.ts +41 -0
  36. package/dist/types/store/next/patch.d.ts +91 -0
  37. package/dist/types/store/next/reconcile.d.ts +14 -0
  38. package/dist/types/store/next/store.d.ts +30 -2
  39. package/dist/types/store/next/target.d.ts +58 -8
  40. package/dist/types-cjs/core/core.d.cts +15 -1
  41. package/dist/types-cjs/core/dev.d.cts +8 -0
  42. package/dist/types-cjs/core/graph.d.cts +22 -0
  43. package/dist/types-cjs/core/invariants.d.cts +1 -1
  44. package/dist/types-cjs/core/scheduler.d.cts +12 -0
  45. package/dist/types-cjs/store/index.d.cts +2 -0
  46. package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
  47. package/dist/types-cjs/store/next/patch.d.cts +91 -0
  48. package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
  49. package/dist/types-cjs/store/next/store.d.cts +30 -2
  50. package/dist/types-cjs/store/next/target.d.cts +58 -8
  51. package/package.json +14 -14
  52. package/dist/types/store/optimistic.d.ts +0 -45
  53. package/dist/types/store/projection.d.ts +0 -70
  54. package/dist/types/store/reconcile.d.ts +0 -46
  55. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  56. package/dist/types-cjs/store/projection.d.cts +0 -70
  57. package/dist/types-cjs/store/reconcile.d.cts +0 -46
package/dist/dev.js CHANGED
@@ -236,7 +236,7 @@ function captureStack() {
236
236
  // remaining frames are the user code that performed the write.
237
237
  return raw
238
238
  .slice(1)
239
- .filter(line => !/solid-signals[/\\](src|dist)[/\\]/.test(line))
239
+ .filter(line => !/(?:^|[/\\])(?:packages[/\\])?signals[/\\](src|dist)[/\\]/.test(line))
240
240
  .slice(0, 3)
241
241
  .map(line => line.trim());
242
242
  }
@@ -708,11 +708,17 @@ const hooks = {};
708
708
  const diagnosticListeners = new Set();
709
709
  const diagnosticCaptures = new Set();
710
710
  let diagnosticSequence = 0;
711
+ let consoleFooter;
712
+ const footeredCodes = new Set();
711
713
  const diagnostics = {
712
714
  subscribe(listener) {
713
715
  diagnosticListeners.add(listener);
714
716
  return () => diagnosticListeners.delete(listener);
715
717
  },
718
+ setConsoleFooter(footer) {
719
+ consoleFooter = footer;
720
+ footeredCodes.clear();
721
+ },
716
722
  capture() {
717
723
  const events = [];
718
724
  diagnosticCaptures.add(events);
@@ -752,6 +758,13 @@ function emitDiagnostic(event) {
752
758
  };
753
759
  for (const listener of diagnosticListeners) listener(entry);
754
760
  for (const capture of diagnosticCaptures) capture.push(entry);
761
+ if (consoleFooter && !footeredCodes.has(entry.code)) {
762
+ footeredCodes.add(entry.code);
763
+ const footer = consoleFooter(entry);
764
+ // Call sites console.warn/error their message after emitDiagnostic
765
+ // returns; a microtask lands the footer right below that report.
766
+ if (footer) queueMicrotask(() => console.warn(footer));
767
+ }
755
768
  return entry;
756
769
  }
757
770
  /**
@@ -1097,7 +1110,7 @@ function cancelZombieRecompute(el) {
1097
1110
  }
1098
1111
  let clock = 0;
1099
1112
  let activeTransition = null;
1100
- let scheduled = false;
1113
+ let scheduled$1 = false;
1101
1114
  let halted = false;
1102
1115
  let haltNotified = false;
1103
1116
  let syncDepth = 0;
@@ -1207,6 +1220,27 @@ function mergeTransitionState(target, outgoing) {
1207
1220
  outgoing._affectsNodes.length = 0;
1208
1221
  }
1209
1222
  for (const store of outgoing._optimisticStores) target._optimisticStores.add(store);
1223
+ // Patch-channel stash (store/next/patch.ts): entries held for the outgoing
1224
+ // transition must ride the merge like every other per-transition
1225
+ // collection — releaseBatch only reads the COMMITTING transition's stash,
1226
+ // so a stranded sidecar would silently drop its patches. Move (don't
1227
+ // copy), same aliasing rule as the collections above. The field is an
1228
+ // expando so this module stays free of patch imports (pay-for-use).
1229
+ const heldPatches = outgoing._heldPatches;
1230
+ if (heldPatches !== undefined) {
1231
+ outgoing._heldPatches = undefined;
1232
+ let dest = target._heldPatches;
1233
+ if (dest !== undefined) dest.push(...heldPatches);
1234
+ else dest = target._heldPatches = heldPatches;
1235
+ // Retarget the entries' coalescing stamps to the surviving stash
1236
+ // (opaque backref contract with store/next/patch.ts): without this a
1237
+ // post-merge emission misses the stamp and pushes a SECOND entry —
1238
+ // the record's patch applies twice at commit (re-audit 5, P1-2).
1239
+ for (let i = 0; i < heldPatches.length; i++) {
1240
+ const pc = heldPatches[i].pc;
1241
+ if (pc !== undefined && pc.qe === heldPatches[i]) pc.qa = dest;
1242
+ }
1243
+ }
1210
1244
  for (const [source, reporters] of outgoing._asyncReporters) {
1211
1245
  let targetReporters = target._asyncReporters.get(source);
1212
1246
  if (!targetReporters) target._asyncReporters.set(source, (targetReporters = new Set()));
@@ -1219,8 +1253,8 @@ function schedule() {
1219
1253
  notifyHalted();
1220
1254
  return;
1221
1255
  }
1222
- if (scheduled) return;
1223
- scheduled = true;
1256
+ if (scheduled$1) return;
1257
+ scheduled$1 = true;
1224
1258
  if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);
1225
1259
  }
1226
1260
  /**
@@ -1403,6 +1437,10 @@ class GlobalQueue extends Queue {
1403
1437
  static _transitionBlocked = null;
1404
1438
  static _cleanupLanes = null;
1405
1439
  static _runLaneEffects = null;
1440
+ /** Patch-channel optimistic drain (next/patch.ts): optimistic emissions
1441
+ * apply at lane-effect timing — visible in flight, unlike the regular
1442
+ * effect queues an action stashes. Injected; null when unused. */
1443
+ static _drainPatchOptimistic = null;
1406
1444
  static _gatedRead = null;
1407
1445
  static _laneSuspends = null;
1408
1446
  static _laneReadsCommitted = null;
@@ -1415,6 +1453,10 @@ class GlobalQueue extends Queue {
1415
1453
  this._running = true;
1416
1454
  try {
1417
1455
  if (true) devCheckFlushStart();
1456
+ // Before runHeap for the same reason as the fast drain above; late
1457
+ // subscribers (an effect reading a swept memo this flush) revive it,
1458
+ // which is the pay-for-use contract.
1459
+ sweepDormant();
1418
1460
  runHeap(dirtyQueue, GlobalQueue._update);
1419
1461
  if (activeTransition) {
1420
1462
  const isComplete = transitionComplete(activeTransition);
@@ -1449,7 +1491,7 @@ class GlobalQueue extends Queue {
1449
1491
  // A kept ambient batch may hold pending nodes (#2916): stay
1450
1492
  // scheduled so the outer drain loop commits them via the plain
1451
1493
  // flush path instead of leaving them until the next natural flush.
1452
- scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
1494
+ scheduled$1 = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
1453
1495
  reassignPendingTransition(stashedTransition._pendingNodes);
1454
1496
  activeTransition = null;
1455
1497
  finalizePureQueue(null, true);
@@ -1489,7 +1531,7 @@ class GlobalQueue extends Queue {
1489
1531
  }
1490
1532
  clock++;
1491
1533
  // Check if finalization added items to the heap (from optimistic reversion)
1492
- scheduled = dirtyQueue._max >= dirtyQueue._min;
1534
+ scheduled$1 = dirtyQueue._max >= dirtyQueue._min;
1493
1535
  // Run lane effects first (for ready lanes), then regular effects
1494
1536
  activeLanes.size && GlobalQueue._runLaneEffects(EFFECT_RENDER);
1495
1537
  this.run(EFFECT_RENDER);
@@ -1506,7 +1548,7 @@ class GlobalQueue extends Queue {
1506
1548
  }
1507
1549
  if (
1508
1550
  true &&
1509
- !scheduled &&
1551
+ !scheduled$1 &&
1510
1552
  !activeTransition &&
1511
1553
  transitions.size === 0 &&
1512
1554
  activeLanes.size === 0
@@ -1692,6 +1734,16 @@ let storeCommitHook = null;
1692
1734
  function setStoreCommitHook(fn) {
1693
1735
  storeCommitHook = fn;
1694
1736
  }
1737
+ /** Patch-channel release hook (next/patch.ts): transition-stamped patch
1738
+ * emissions are released when THEIR batch commits. Transitions never
1739
+ * abort: failed actions still commit (only optimistic overrides revert),
1740
+ * and merged-away transitions hand their stash to the survivor
1741
+ * (mergeTransitionState) — every stash drains exactly once. Injected like
1742
+ * storeCommitHook to stay tree-shakeable. */
1743
+ let patchCommitHook = null;
1744
+ function setPatchCommitHook(fn) {
1745
+ patchCommitHook = fn;
1746
+ }
1695
1747
  function commitPendingNodes() {
1696
1748
  const pendingNodes = currentBatch._pendingNodes;
1697
1749
  for (let i = 0; i < pendingNodes.length; i++) {
@@ -1699,6 +1751,7 @@ function commitPendingNodes() {
1699
1751
  }
1700
1752
  pendingNodes.length = 0;
1701
1753
  storeCommitHook?.();
1754
+ patchCommitHook?.(currentBatch);
1702
1755
  }
1703
1756
  function finalizePureQueue(completingTransition = null, incomplete = false) {
1704
1757
  // For incomplete transitions, skip pending resolution and optimistic reversion
@@ -1825,7 +1878,7 @@ function flush(fn) {
1825
1878
  let count = 0;
1826
1879
  // `flush()` is an explicit drain point, so it must also process an active
1827
1880
  // transition even if no microtask was scheduled for it yet.
1828
- while (scheduled || activeTransition) {
1881
+ while (scheduled$1 || activeTransition) {
1829
1882
  if (++count === 1e5) throw new Error("Potential Infinite Loop Detected.");
1830
1883
  globalQueue.flush();
1831
1884
  }
@@ -2363,7 +2416,7 @@ function unlinkSubs(link) {
2363
2416
  // transition holding it) is an observer — tearing down would orphan
2364
2417
  // the work and re-execute it on the next read. The settle path runs
2365
2418
  // this same last-one-out check when that observer releases (the
2366
- // untracked-read dispose in core.ts guards on pending identically).
2419
+ // untracked-read dormancy sweep guards on pending identically).
2367
2420
  const c = dep;
2368
2421
  c._fn &&
2369
2422
  c._config & CONFIG_AUTO_DISPOSE &&
@@ -2403,6 +2456,46 @@ function unobserved(el) {
2403
2456
  clearDeps(el);
2404
2457
  disposeChildren(el, true);
2405
2458
  }
2459
+ /**
2460
+ * Deferred dormancy for never-observed auto-dispose computeds (#3078).
2461
+ *
2462
+ * An untracked top-level read of a subscriber-less observation-lifecycle memo
2463
+ * used to call unobserved() inline at the end of read(). That kept the leak
2464
+ * closed (the compute links the memo into its deps' sub lists — without a
2465
+ * teardown point a never-observed memo is retained by its sources forever;
2466
+ * upstream alien-signals has exactly this retention), but it made reads
2467
+ * destructive: each read disposed the node, the next read revived it with a
2468
+ * full recompute in whatever ambient transition/lane context happened to be
2469
+ * current, so consecutive reads could return different answers with no write
2470
+ * in between.
2471
+ *
2472
+ * Instead, reads queue the node here and the scheduler sweeps at the top of
2473
+ * the next flush (before runHeap, so a same-tick dirtying is reclaimed
2474
+ * instead of recomputed). Reads become idempotent within a tick (the node
2475
+ * stays alive and serves its cache, uniform with observed memos) while
2476
+ * reclamation still happens within one microtask — the enqueue site arms
2477
+ * schedule(), so a flush is guaranteed even when no other work is queued.
2478
+ */
2479
+ const dormantNodes = new Set();
2480
+ function sweepDormant() {
2481
+ if (dormantNodes.size === 0) return;
2482
+ for (const el of dormantNodes) {
2483
+ // Re-validate at sweep time: the node may have gained a subscriber (its
2484
+ // lifecycle is the unlinkSubs cascade now), gone pending (in-flight async
2485
+ // is an observer; the settle path re-runs last-one-out), lost its
2486
+ // AUTO_DISPOSE bit (owner teardown strips it, #3024), or already been
2487
+ // torn down.
2488
+ if (
2489
+ !el._subs &&
2490
+ el._config & CONFIG_AUTO_DISPOSE &&
2491
+ !(el._statusFlags & STATUS_PENDING) &&
2492
+ !(el._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
2493
+ ) {
2494
+ unobserved(el);
2495
+ }
2496
+ }
2497
+ dormantNodes.clear();
2498
+ }
2406
2499
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
2407
2500
  function link(dep, sub, pendingObserver = false) {
2408
2501
  // Repeat touches within one pass AND-combine `_pendingObserver`: a probe
@@ -3084,7 +3177,8 @@ function clearStatus(el, clearUninitialized = false) {
3084
3177
  GlobalQueue._updateChildCompanions !== null
3085
3178
  )
3086
3179
  GlobalQueue._updateChildCompanions(el);
3087
- if (el._x?._notifyStatus) el._x._notifyStatus.call(el);
3180
+ const notify = statusNotifierOf(el);
3181
+ if (notify) notify.call(el);
3088
3182
  }
3089
3183
  function notifyStatus(el, status, error, blockStatus, lane) {
3090
3184
  // Wrap regular errors to track source node
@@ -3126,14 +3220,15 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3126
3220
  }
3127
3221
  const downstreamBlockStatus = blockStatus || startsBlocking;
3128
3222
  const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
3129
- if (el._x?._notifyStatus) {
3223
+ const elNotify = statusNotifierOf(el);
3224
+ if (elNotify) {
3130
3225
  if (blockStatus && status === STATUS_PENDING) {
3131
3226
  return;
3132
3227
  }
3133
3228
  if (downstreamBlockStatus) {
3134
- el._x._notifyStatus.call(el, status, error);
3229
+ elNotify.call(el, status, error);
3135
3230
  } else {
3136
- el._x._notifyStatus.call(el);
3231
+ elNotify.call(el);
3137
3232
  }
3138
3233
  return;
3139
3234
  }
@@ -3668,7 +3763,7 @@ function ext(el) {
3668
3763
  * mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip
3669
3764
  * the auto-dispose CONFIG bit (effect() previously cleared it post-construction).
3670
3765
  */
3671
- function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
3766
+ function createEffectNode(fn, effectFn, errorFn, type, options) {
3672
3767
  const transparent = options?.transparent ?? false;
3673
3768
  const self = {
3674
3769
  id: inheritId(options, transparent, context),
@@ -3712,12 +3807,36 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
3712
3807
  _x: null
3713
3808
  };
3714
3809
  self._name = options?.name ?? "effect";
3715
- // Boundary effects carry a status channel; most effects never touch _x.
3716
- if (notifyStatus !== undefined) ext(self)._notifyStatus = notifyStatus;
3810
+ // Effects dispatch status through the SHARED notifier (statusNotifierOf,
3811
+ // keyed off _type) storing it per node forced a full NodeExtension
3812
+ // allocation on EVERY effect at creation (an alloc + 19 field stores,
3813
+ // +23% effect creation, caught by the creation benches). Only genuinely
3814
+ // per-node channels (boundaries) live on _x.
3717
3815
  if (options?.unobserved) ext(self)._unobserved = options.unobserved;
3718
3816
  setupComputedNode(self, lazyOptions);
3719
3817
  return self;
3720
3818
  }
3819
+ /**
3820
+ * The shared status notifier for effect nodes, installed once by effect.ts
3821
+ * at module evaluation (`this`-dispatched — one function serves every
3822
+ * effect, so nodes never store it). Boundary computeds keep their own
3823
+ * per-node channel on `_x._notifyStatus`, which takes precedence.
3824
+ */
3825
+ let effectStatusNotify = null;
3826
+ function setEffectStatusNotify(fn) {
3827
+ effectStatusNotify = fn;
3828
+ }
3829
+ /** Resolve a node's status notifier: an own `_x` channel (boundaries) wins;
3830
+ * effect nodes (`_type` — EFFECT_PURE is 0, and only effect literals carry
3831
+ * the field) fall back to the shared notifier. Presence doubles as the
3832
+ * "display consumer" membership test in the status walks, exactly as the
3833
+ * per-node field did when every effect carried one. */
3834
+ function statusNotifierOf(el) {
3835
+ const x = el._x;
3836
+ const own = x !== null && x !== undefined ? x._notifyStatus : undefined;
3837
+ if (own !== undefined) return own;
3838
+ return el._type ? (effectStatusNotify ?? undefined) : undefined;
3839
+ }
3721
3840
  const lazyOptions = { lazy: true };
3722
3841
  function setupComputedNode(self, options) {
3723
3842
  self._prevHeap = self;
@@ -4094,7 +4213,14 @@ function read(el) {
4094
4213
  !(owner._statusFlags & STATUS_PENDING) &&
4095
4214
  !el._subs
4096
4215
  ) {
4097
- unobserved(el);
4216
+ // Deferred, not inline (#3078): an inline unobserved() here made untracked
4217
+ // reads destructive — dispose on this read, full revival recompute on the
4218
+ // next — so consecutive reads could answer differently with no write in
4219
+ // between (the revival samples the ambient transition/lane context).
4220
+ // The sweep at flush finalization re-validates and reclaims; schedule()
4221
+ // guarantees that flush happens even if nothing else is queued.
4222
+ dormantNodes.add(el);
4223
+ schedule();
4098
4224
  }
4099
4225
  return value;
4100
4226
  }
@@ -4507,7 +4633,16 @@ function optimisticWrite(el, v) {
4507
4633
  const currentValue = hasOverride ? unwrapOverride(el._x?._overrideValue) : el._value;
4508
4634
  if (typeof v === "function") v = v(currentValue);
4509
4635
  const valueChanged =
4510
- !!(el._statusFlags & STATUS_UNINITIALIZED) || !el._equals || !el._equals(currentValue, v);
4636
+ !!(el._statusFlags & STATUS_UNINITIALIZED) ||
4637
+ // A dirty node's _value is stale (its queued recompute hasn't run — e.g.
4638
+ // a latest() shadow marked by the previous landing's companion snap), so
4639
+ // equality against it must not swallow the write. Without this, a sync
4640
+ // push returning the shadow to that stale value was dropped, the snap
4641
+ // recompute then committed the parent's old value, and the banner showed
4642
+ // the previous transition's target (#3041 follow-up).
4643
+ !!((el._flags ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) ||
4644
+ !el._equals ||
4645
+ !el._equals(currentValue, v);
4511
4646
  if (!valueChanged) {
4512
4647
  // Same-value write with an active override still entangles the current
4513
4648
  // action's transition — the hold must outlast all overlapping actions.
@@ -4608,6 +4743,9 @@ function runLaneEffects(type) {
4608
4743
  runQueue(effects, type);
4609
4744
  }
4610
4745
  }
4746
+ // Optimistic patch applications ride the same visibility slot as lane
4747
+ // effects (in-flight DOM updates); no-op unless patches registered.
4748
+ if (type === EFFECT_RENDER) GlobalQueue._drainPatchOptimistic?.();
4611
4749
  }
4612
4750
  function cleanupCompletedLanes(completingTransition) {
4613
4751
  for (const lane of activeLanes) {
@@ -4663,18 +4801,33 @@ function laneReadsCommitted(el, owner, c) {
4663
4801
  el._x?._overrideValue !== undefined ||
4664
4802
  !!el._x?._optimisticLane ||
4665
4803
  !!(owner._statusFlags & STATUS_PENDING)
4666
- )
4804
+ ) {
4805
+ // The committed view hides a staged in-flight value that will promote
4806
+ // silently (commitPendingNode never re-notifies). gatedRead records plain
4807
+ // signals for replay at commit; async memos are excluded from it by the
4808
+ // `_fn` check and reach here instead — a lane-assigned source whose async
4809
+ // already settled (laneAsyncSettled keeps _optimisticLane) served its
4810
+ // committed value to a reader that never re-ran after the landing, so a
4811
+ // pending-gated branch stayed one value behind permanently (#3041
4812
+ // follow-up). Record the reader under the same replay contract.
4813
+ if (el._pendingValue !== NOT_PENDING)
4814
+ (activeTransition ?? globalQueue._batch)._gatedSubs.add(c);
4667
4815
  return true;
4816
+ }
4668
4817
  if (owner === el && stale && c._x?._parentSource !== el) {
4669
- // The committed view can hide a same-tick ambient write (a lane member —
4670
- // even just an isPending companion flip — puts the reader "under a lane"
4671
- // against unrelated plain writes). With no transaction the write commits
4672
- // at THIS flush's end with no re-delivery, so record the reader for
4673
- // replay at commit the same contract gatedRead provides when a
4674
- // transaction is active (#2963). If a transaction forms mid-flush,
4675
- // initTransition carries the recording over to it.
4676
- if (el._pendingValue !== NOT_PENDING && activeTransition === null)
4677
- globalQueue._batch._gatedSubs.add(c);
4818
+ // The committed view can hide a staged write (a lane member — even just
4819
+ // an isPending companion flip — puts the reader "under a lane"). The
4820
+ // staged value commits with no re-delivery (commitPendingNode never
4821
+ // re-notifies), so record the reader for replay at commit — the same
4822
+ // contract gatedRead provides (#2963). gatedRead itself only covers
4823
+ // signal reads where the reading computed differs from the source; the
4824
+ // owner === el memo/self read lands here instead. With a transaction
4825
+ // active the staged value promotes silently at ITS landing, so record
4826
+ // into the transaction (#3041 follow-up: a pending-gated branch that
4827
+ // first read its async source during the landing flush stayed one value
4828
+ // behind permanently); with none, into the ambient batch.
4829
+ if (el._pendingValue !== NOT_PENDING)
4830
+ (activeTransition ?? globalQueue._batch)._gatedSubs.add(c);
4678
4831
  return true;
4679
4832
  }
4680
4833
  return false;
@@ -5030,6 +5183,13 @@ function snapCompanionsToState(owner) {
5030
5183
  }
5031
5184
  function getLatestValueComputed(el) {
5032
5185
  let lvc = el._x?._latestValueComputed;
5186
+ // A shadow disposed while unobserved (its gated reader unmounted at a
5187
+ // landing) is a corpse: sync writes into it equality-swallow against its
5188
+ // frozen _value, and a later read revives it via recompute — clearing
5189
+ // DISPOSED and re-deriving from the committed view, so the banner showed
5190
+ // the previous transition's target (#3041 follow-up). Treat it as absent;
5191
+ // recreation backfills from the in-flight write below.
5192
+ if (lvc && lvc._flags & REACTIVE_DISPOSED) lvc = undefined;
5033
5193
  if (!lvc) {
5034
5194
  const prevPending = latestReadActive;
5035
5195
  setLatestReadActive(false);
@@ -5130,8 +5290,22 @@ function pendingCheckRead(el, c, owner, firewall) {
5130
5290
  */
5131
5291
  function heldAwaitingAsync(el) {
5132
5292
  const et = el._transition;
5133
- const t = et ? currentTransition(et) : null;
5293
+ const t = et ? currentTransition(et) : activeTransition;
5134
5294
  if (!t || t._done) return false;
5295
+ // A plain staged write (a signal/store leaf — no _fn) held while an action
5296
+ // is still running is an INPUT to a computation still in flight (#3078):
5297
+ // the pairing rule must not suppress the verdict, or a memo recomputing
5298
+ // mid-action reads the staged value, gets told "not pending", and
5299
+ // disagrees with a direct isPending() probe for the whole action window.
5300
+ // A computed's staged value is the opposite case — a LANDED answer
5301
+ // awaiting reveal — where the pairing rule stands even inside an open
5302
+ // action (#2831: a reader that saw the new value must not also see
5303
+ // pending); still-computing answers are covered by the reporter scan.
5304
+ if (t._actions.length && !el._fn) return true;
5305
+ // A node not yet stamped with a transition only qualifies through the
5306
+ // action check above; the reporter scan below is for transition-held
5307
+ // writes whose source async is still computing.
5308
+ if (!et) return false;
5135
5309
  for (const [source, reporters] of t._asyncReporters) {
5136
5310
  if (
5137
5311
  reporters.size &&
@@ -5248,7 +5422,6 @@ function effect(compute, effect, error, options) {
5248
5422
  effect,
5249
5423
  error,
5250
5424
  isUser ? EFFECT_USER : EFFECT_RENDER,
5251
- notifyEffectStatus,
5252
5425
  options
5253
5426
  );
5254
5427
  recompute(node, true);
@@ -5421,17 +5594,9 @@ function trackedEffect(fn, options) {
5421
5594
  node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
5422
5595
  node._modified = true;
5423
5596
  node._type = EFFECT_TRACKED;
5424
- ext(node)._notifyStatus = (status, error) => {
5425
- const actualStatus = status !== undefined ? status : node._statusFlags;
5426
- if (actualStatus & STATUS_ERROR) {
5427
- node._queue.notify(node, STATUS_PENDING, 0);
5428
- const err = error !== undefined ? error : node._x?._error;
5429
- if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
5430
- haltReactivity(unwrapStatusError(err));
5431
- throw err;
5432
- }
5433
- }
5434
- };
5597
+ // Status dispatch rides the SHARED notifier (statusNotifierOf keys off
5598
+ // _type): its error arm is behavior-identical to the closure that used to
5599
+ // live here, without the per-node NodeExtension allocation.
5435
5600
  node._run = run;
5436
5601
  node._queue.enqueue(EFFECT_USER, run);
5437
5602
  if (!node._parent) {
@@ -5449,6 +5614,10 @@ function trackedEffect(fn, options) {
5449
5614
  console.warn(message);
5450
5615
  }
5451
5616
  }
5617
+ // Install the shared effect status notifier (statusNotifierOf serves it to
5618
+ // every effect node) — module-scope: any bundle that creates effects
5619
+ // evaluates this module.
5620
+ setEffectStatusNotify(notifyEffectStatus);
5452
5621
 
5453
5622
  const ACTION_CALLED_IN_OWNED_SCOPE_MESSAGE =
5454
5623
  "[ACTION_CALLED_IN_OWNED_SCOPE] Calling an action inside an owned scope (component, computation) is not allowed. " +
@@ -6069,6 +6238,15 @@ let optHooks = null;
6069
6238
  function setOptHooks(h) {
6070
6239
  optHooks = h;
6071
6240
  }
6241
+ /** Sticky descendants flag walk (§6d): reconcile's keyed pruning descends
6242
+ * only where subscriptions exist at/below. Nodes AND patches count. */
6243
+ function markDescendants(target) {
6244
+ let t = target;
6245
+ while (t && !t.d) {
6246
+ t.d = true;
6247
+ t = t.u;
6248
+ }
6249
+ }
6072
6250
 
6073
6251
  /**
6074
6252
  * Brand symbols used internally by the store proxy / projection plumbing.
@@ -6417,8 +6595,9 @@ function notifyMarkBoundaries(node) {
6417
6595
  visited.add(sub);
6418
6596
  // Display consumers (render effects, boundary computeds) act on the
6419
6597
  // notification; descent stops there, exactly like the status rails.
6420
- if (sub._x?._notifyStatus) {
6421
- sub._x._notifyStatus.call(sub, STATUS_PENDING, error);
6598
+ const notify = statusNotifierOf(sub);
6599
+ if (notify) {
6600
+ notify.call(sub, STATUS_PENDING, error);
6422
6601
  return;
6423
6602
  }
6424
6603
  forEachDependent(sub, visit);
@@ -6526,6 +6705,15 @@ function affects(target, key) {
6526
6705
  }
6527
6706
  }
6528
6707
 
6708
+ let patchHooks = null;
6709
+ let rowHooks = null;
6710
+ function installPatchHooks(hooks) {
6711
+ patchHooks = hooks;
6712
+ }
6713
+ function installRowHooks(hooks) {
6714
+ rowHooks = hooks;
6715
+ }
6716
+
6529
6717
  /**
6530
6718
  * Store rewrite — increment 2: plain deep stores with pending-backing writes.
6531
6719
  * Contract: INTERNALS-STORE-STATE.md.
@@ -6554,8 +6742,13 @@ function affects(target, key) {
6554
6742
  * headroom for future fields. The prototype is reset to `Object.prototype`
6555
6743
  * so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
6556
6744
  * plain object's. Array targets keep the bare-`[]` path — they must carry
6557
- * the array exotic class for `Array.isArray(proxy)`, and arrays store named
6558
- * fields off-object where this cliff does not apply. */
6745
+ * the array exotic class for `Array.isArray(proxy)`.
6746
+ *
6747
+ * ARRAY SHAPE RULE: arrays normalize their named properties to dictionary
6748
+ * mode as the count grows (V8 13.x: counts ≡ 0 mod 3 from 18 up), so the
6749
+ * target's named field count is capped at 20 — write-side patch-channel
6750
+ * state lives inside the single `pc` extension (see target.ts), never as
6751
+ * new named fields here. */
6559
6752
  function TargetShape() {
6560
6753
  this.v = undefined;
6561
6754
  this.ch = undefined;
@@ -6576,9 +6769,15 @@ function TargetShape() {
6576
6769
  this.s = undefined;
6577
6770
  this.ovl = undefined;
6578
6771
  this.del = undefined;
6579
- this.wk = undefined;
6772
+ this.pc = undefined;
6773
+ this.hv = undefined;
6774
+ this.ht = undefined;
6580
6775
  }
6581
6776
  TargetShape.prototype = Object.prototype;
6777
+ /** Lazily allocate the patch-channel extension (one literal shape). */
6778
+ function pcOf(t) {
6779
+ return t.pc ?? (t.pc = { sp: null, p: null, ro: null, wk: null, qa: null, qe: null });
6780
+ }
6582
6781
  function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
6583
6782
  // The proxy target carries the array exotic class when the value is an
6584
6783
  // array, so Array.isArray(proxy) is true; the fields live on it directly.
@@ -6595,6 +6794,7 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
6595
6794
  t.h = null;
6596
6795
  t.k = null;
6597
6796
  t.dk = null;
6797
+ t.pc = null;
6598
6798
  t.u = parent;
6599
6799
  t.pk = parentKey;
6600
6800
  t.px = null;
@@ -6607,7 +6807,8 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
6607
6807
  t.s = false;
6608
6808
  t.ovl = false;
6609
6809
  t.del = null;
6610
- t.wk = null;
6810
+ t.hv = null;
6811
+ t.ht = null;
6611
6812
  t.px = new Proxy(t, traps);
6612
6813
  // Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
6613
6814
  // proxy off looked-up targets as a field.
@@ -6788,13 +6989,6 @@ function getDeepNode(target) {
6788
6989
  function bumpDeep(t) {
6789
6990
  if (t.dk !== null) setSignal(t.dk, 1);
6790
6991
  }
6791
- function markDescendants(target) {
6792
- let t = target;
6793
- while (t && !t.d) {
6794
- t.d = true;
6795
- t = t.u;
6796
- }
6797
- }
6798
6992
  // ---------------------------------------------------------------------------
6799
6993
  // pending backing + fold (the single mutation point)
6800
6994
  /** target → committed backing at batch start (the fold diff's old side). */
@@ -6819,6 +7013,12 @@ function cloneRaw(source, t) {
6819
7013
  ? Object.defineProperties([], descs)
6820
7014
  : Object.create(Object.getPrototypeOf(source), descs);
6821
7015
  }
7016
+ /** Scanned plainness for patch admission (patchableRaw): runs the one-time
7017
+ * accessor scan if it hasn't happened yet — the sticky `a` flag alone is not
7018
+ * trustworthy before a scan (it starts false and is discovered lazily). */
7019
+ function targetIsPlain(target) {
7020
+ return target.sc ? !target.a : scanAccessorsOnce(target);
7021
+ }
6822
7022
  /** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
6823
7023
  * returns true when the container is plain data (overlay-safe). */
6824
7024
  function scanAccessorsOnce(target) {
@@ -6860,6 +7060,7 @@ function materializePB(target) {
6860
7060
  target.ovl = false;
6861
7061
  }
6862
7062
  function ensurePB(target) {
7063
+ if (activeTransition !== null) foldBatches.set(target, activeTransition);
6863
7064
  let pb = target.pb;
6864
7065
  if (pb === null) {
6865
7066
  // Prototype-chain overlay (#3044): plain-data non-array containers
@@ -6903,6 +7104,25 @@ function ensurePB(target) {
6903
7104
  }
6904
7105
  return pb;
6905
7106
  }
7107
+ /** Sentinel holder for `t.ht`: a latest()-pull staged this adoption outside
7108
+ * any transition — the hold lasts until the fold commit (drainFolds). */
7109
+ const PLAIN_HOLD = Symbol("plainHold");
7110
+ /** True while a latest() read is pulling the projection computed up to date
7111
+ * (see the get trap): adoptions landing during the pull are speculative
7112
+ * against the un-flushed batch and stage a held view. (Not injectable — the
7113
+ * derived createStore overload retains projection machinery in every store
7114
+ * bundle, see treeshake.test.ts.) */
7115
+ let latestPullActive = false;
7116
+ /** Resolve the held committed view (#3074): answers the masked old backing
7117
+ * while the hold is live, and lazily clears a hold whose transition has
7118
+ * committed (transitions merge — resolve through currentTransition, same as
7119
+ * foldHeld's node stamps). */
7120
+ function heldMaskView(t) {
7121
+ const ht = t.ht;
7122
+ if (ht === null) return null;
7123
+ if (ht !== PLAIN_HOLD && currentTransition(ht)?._done === true) return (t.ht = t.hv = null);
7124
+ return t.hv;
7125
+ }
6906
7126
  /**
6907
7127
  * Adoption (2026-08-16c): the incoming object becomes the committed backing
6908
7128
  * IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
@@ -6918,6 +7138,20 @@ function adoptPB(target, incoming, eager = false) {
6918
7138
  if (!eager) {
6919
7139
  queueFold(target); // records the pre-batch old before we swap
6920
7140
  target.adopted = true;
7141
+ // #3074/#3075: a projection recompute deriving from uncommitted inputs
7142
+ // swaps the backing SPECULATIVELY — committed-visibility readers must
7143
+ // keep the pre-hold view until the hold resolves (a source held by a
7144
+ // live transition, or a latest()-pull ahead of the flush). Post-await
7145
+ // landings (write-override) stay immediately visible — landed truth —
7146
+ // and clear any hold; optimistic families ride the lane machinery.
7147
+ if (target.fam?.opt !== true) {
7148
+ if (getWriteOverride()) {
7149
+ target.ht = target.hv = null;
7150
+ } else if (activeTransition !== null || latestPullActive) {
7151
+ if (heldMaskView(target) === null) target.hv = target.v;
7152
+ target.ht = activeTransition ?? PLAIN_HOLD;
7153
+ }
7154
+ }
6921
7155
  }
6922
7156
  target.pb = null;
6923
7157
  // Overlay and accessor-scan state describe the OUTGOING backing — a
@@ -6929,24 +7163,43 @@ function adoptPB(target, incoming, eager = false) {
6929
7163
  // draft rescans once (#3044 audit follow-up).
6930
7164
  target.ovl = false;
6931
7165
  target.del = null;
6932
- target.wk = null; // adoption supersedes any staged trap writes
6933
7166
  target.sc = false;
6934
7167
  target.a = false;
7168
+ if (target.pc !== null) target.pc.wk = null; // adoption supersedes staged trap writes
6935
7169
  target.v = incoming;
6936
7170
  target.ch = incoming[$TARGET] !== undefined;
6937
7171
  (target.fam?.map ?? storeNextLookup).set(incoming, target);
6938
7172
  }
7173
+ /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
7174
+ * array length write implicitly deleted indices) — consumers full-scan. */
7175
+ const WK_ALL = new Set();
7176
+ const plainProto = o => {
7177
+ const p = Object.getPrototypeOf(o);
7178
+ return p === Object.prototype || p === Array.prototype || p === null;
7179
+ };
6939
7180
  function queueFold(target) {
6940
7181
  if (foldOlds.has(target)) return;
6941
- if (foldOlds.size === 0) {
6942
- if (!hookInstalled) {
6943
- hookInstalled = true;
6944
- setStoreCommitHook(drainFolds);
6945
- }
6946
- schedule(); // once per batch drain clears the map
6947
- }
7182
+ if (!hookInstalled) {
7183
+ hookInstalled = true;
7184
+ setStoreCommitHook(drainFolds);
7185
+ }
7186
+ // Always arm — "map non-empty ⇒ drain scheduled" is NOT an invariant: a
7187
+ // held re-queue, or an incomplete-transition flush (which skips
7188
+ // commitPendingNodes entirely), leaves entries behind after `scheduled`
7189
+ // was consumed. A size-gated arm then strands every LATER fold — queued
7190
+ // silently, never drained, committed base frozen at stale state while its
7191
+ // nodes commit (#3089). schedule() early-returns when already armed.
7192
+ schedule();
6948
7193
  foldOlds.set(target, target.v);
6949
7194
  }
7195
+ /** Fold write-attribution (#3089): a draft written while a transition is
7196
+ * active belongs to that transition — its fold must not commit before the
7197
+ * transition settles. Observed keys already defer through the held check in
7198
+ * drainFolds (their nodes carry _pendingValue); this write-time stamp is the
7199
+ * equivalent hold for UNOBSERVED keys, which have no node to consult.
7200
+ * Refreshed on every write; resolved through currentTransition at drain
7201
+ * (transitions merge — same rule as heldMaskView). */
7202
+ const foldBatches = new WeakMap();
6950
7203
  /** Committed-time privatization for parent-chain slot updates (path copying). */
6951
7204
  function privatizeCommitted(target) {
6952
7205
  if (ownedRaw.has(target.v)) return;
@@ -6966,7 +7219,27 @@ function drainFolds() {
6966
7219
  const entries = [...foldOlds];
6967
7220
  foldOlds.clear();
6968
7221
  for (const [t, old] of entries) {
7222
+ // A latest()-pull staging holds only until the fold commit: this flush
7223
+ // is committing the batch the pull ran ahead of. Transition holds stay —
7224
+ // they clear when their transition is done (heldMaskView).
7225
+ if (t.ht === PLAIN_HOLD) t.ht = t.hv = null;
7226
+ // Eager (write-override) family folds swap pb -> v at notifyWrites'
7227
+ // tail: by the time this drain runs they carry no pb, and their
7228
+ // structural ops must emit at the fold-commit site below (the clone
7229
+ // branch never sees them). Re-audit blocker 4.
7230
+ const foldedEager = t.pb === null;
6969
7231
  if (t.pb !== null) {
7232
+ // #3089: a fold written under a still-running transition defers to
7233
+ // that transition's settle (the write-time stamp covers unobserved
7234
+ // keys; observed keys also hit the pending-node held check below).
7235
+ const fb = foldBatches.get(t);
7236
+ if (fb !== undefined) {
7237
+ if (currentTransition(fb)._done === false) {
7238
+ foldOlds.set(t, old);
7239
+ continue;
7240
+ }
7241
+ foldBatches.delete(t);
7242
+ }
6970
7243
  // Setter path: nodes were setSignal'd at setter exit (write-time
6971
7244
  // notification — transitions/holds ride core machinery). Commit the
6972
7245
  // backing only for keys whose nodes have committed; a still-pending
@@ -6978,9 +7251,14 @@ function drainFolds() {
6978
7251
  // Only written keys can hold (their nodes took the setSignal); the
6979
7252
  // wk bound keeps this O(written) — see notifyWrites. Same fallback
6980
7253
  // rules as the notify (WK_ALL / accessors / non-plain prototypes).
6981
- const wkh = t.wk;
7254
+ const wkh = t.pc !== null ? t.pc.wk : null;
6982
7255
  const keys =
6983
- wkh === null || wkh === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb)
7256
+ wkh === null ||
7257
+ wkh === WK_ALL ||
7258
+ t.a === true ||
7259
+ // Overlay pbs chain to the COMMITTED object (#3044) — plainness is
7260
+ // the committed container's prototype, not the overlay's.
7261
+ !plainProto(t.ovl ? t.v : pb)
6984
7262
  ? Reflect.ownKeys(nodes)
6985
7263
  : wkh;
6986
7264
  for (const key of keys) {
@@ -7018,15 +7296,70 @@ function drainFolds() {
7018
7296
  (t.fam?.map ?? storeNextLookup).delete(pb);
7019
7297
  t.pb = null;
7020
7298
  t.ovl = false;
7021
- t.wk = null; // written-keys window closes with the fold commit
7299
+ if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
7022
7300
  } else {
7301
+ // Setter-channel structural ops: a fold that changes an array's shape
7302
+ // (push/splice/permutation through the setter — the reconcile walk
7303
+ // never queues here) is a structural visibility transition for any
7304
+ // registered list driver. Identity-keyed; aligned folds emit nothing.
7305
+ // Family targets defer to their own adoption emission (fam reconcile).
7306
+ // Arrays always fold on this clone branch (overlay is non-array only).
7307
+ // Family setter drafts (writable projection push/splice through the
7308
+ // masked setter) fold on this branch too and the fold IS their
7309
+ // visibility moment — emit unless the structure already rode another
7310
+ // channel: adoption folds (reconcile walk emitted ops) and
7311
+ // optimistic families (lane-timed override channel). Re-audit
7312
+ // blocker 4.
7313
+ if (
7314
+ t.pc !== null &&
7315
+ t.pc.ro !== null &&
7316
+ !t.adopted &&
7317
+ t.fam?.opt !== true &&
7318
+ Array.isArray(pb) &&
7319
+ Array.isArray(t.v)
7320
+ )
7321
+ rowHooks.emitSetterRowOps(t, t.v, pb);
7023
7322
  t.v = pb;
7024
7323
  t.ch = false; // pb is always a plain clone
7025
7324
  t.pb = null;
7026
- t.wk = null; // written-keys window closes with the fold commit
7325
+ if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
7326
+ }
7327
+ }
7328
+ if (t.v === old) {
7329
+ // A no-op adoption (A -> B -> A before flush) still consumed its walk:
7330
+ // clear the flag or every later setter row-op gate (!t.adopted) stays
7331
+ // failed and a driven family list freezes (re-audit 5, P1-1).
7332
+ t.adopted = false;
7333
+ continue;
7334
+ }
7335
+ // Patch channel (fold-commit site): family targets emit HERE — the fold
7336
+ // IS their visibility moment (held folds re-queued above emit when they
7337
+ // actually commit) — and so do PLAIN fold-adopted targets (setter-
7338
+ // returned root replacements, chained-store swaps: adoptions WITHOUT a
7339
+ // reconcile walk, so no walk-site emission ever happened — re-audit 2,
7340
+ // P1-2). Plain eager targets emitted at their walk/setter sites already.
7341
+ if (t.pc !== null && (t.fam !== null || t.adopted)) {
7342
+ // Structural ops for folds whose structure rode no other channel:
7343
+ // eager-folded family SETTER drafts (write-override swaps pb -> v at
7344
+ // notifyWrites' tail — the clone branch never sees them; adoption
7345
+ // folds re-emitting would double the walk's ops) and PLAIN fold
7346
+ // adoptions (no walk at all). Optimistic families ride the override
7347
+ // channel (lane-timed ops + revert RESYNC) — never re-emit here.
7348
+ if (
7349
+ t.pc.ro !== null &&
7350
+ t.fam?.opt !== true &&
7351
+ (t.fam !== null ? foldedEager && !t.adopted : t.adopted) &&
7352
+ Array.isArray(t.v) &&
7353
+ Array.isArray(old)
7354
+ )
7355
+ rowHooks.emitSetterRowOps(t, old, t.v);
7356
+ if (t.pc.p !== null) {
7357
+ // Accessor demotion at the fold-commit seam is DEV-ONLY (see the
7358
+ // reconcile seam note: prod never pays per-adoption scans).
7359
+ if (!targetIsPlain(t)) patchHooks.demoteToEffects(t);
7360
+ else patchHooks.emitPatchLocal(t, t.v, old);
7027
7361
  }
7028
7362
  }
7029
- if (t.v === old) continue; // adopted then re-adopted back, or no-op
7030
7363
  // Path copying (CAS: see the eager-fold twin above).
7031
7364
  if (t.u && t.u.v[t.pk] === old) {
7032
7365
  privatizeCommitted(t.u);
@@ -7047,17 +7380,6 @@ function drainFolds() {
7047
7380
  * "pending home = the node when a node exists"). Unobserved keys stay in the
7048
7381
  * pending backing and fold directly at commit.
7049
7382
  */
7050
- /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
7051
- * array length write implicitly deleted indices) — consumers full-scan. */
7052
- const WK_ALL = new Set();
7053
- /** Plain-prototype check for the written-keys bound: prototype getters on
7054
- * class instances can derive from ANY field, so only plain-data containers
7055
- * may bound the notify to written keys. Overlay pbs chain to the COMMITTED
7056
- * object (#3044), so overlay plainness is judged on the committed proto. */
7057
- const plainProto = o => {
7058
- const p = Object.getPrototypeOf(o);
7059
- return p === Object.prototype || p === Array.prototype || p === null;
7060
- };
7061
7383
  function notifyWrites(t) {
7062
7384
  let pb = t.pb;
7063
7385
  if (pb === null) return;
@@ -7112,8 +7434,15 @@ function notifyWrites(t) {
7112
7434
  // not a full scan). Falls back to the full node scan when the bound can't
7113
7435
  // hold: no trap granularity (wk null), an array length write (WK_ALL —
7114
7436
  // implicit index deletes), accessors on the record (t.a — a getter node's
7115
- // value can change when ANY key is written), or a non-plain prototype.
7116
- const wk0 = t.wk;
7437
+ // value can change when ANY key is written), or a non-plain prototype
7438
+ // (class instances: prototype getters derive from arbitrary fields).
7439
+ const wk0 = t.pc !== null ? t.pc.wk : null;
7440
+ // Overlay pbs chain to the COMMITTED object (#3044): a prototype-overlay
7441
+ // draft is plain data on its own layer, but its getPrototypeOf is the
7442
+ // committed container — judge plainness by the COMMITTED prototype or the
7443
+ // bound never engages for overlay writes (every plain-object setter batch
7444
+ // would full-scan: the exact selection-map workload wk exists for; jf
7445
+ // `select` regressed 2x on this).
7117
7446
  const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
7118
7447
  if (nodes !== null) {
7119
7448
  const keys = writtenKeys ?? Reflect.ownKeys(nodes);
@@ -7146,15 +7475,18 @@ function notifyWrites(t) {
7146
7475
  }
7147
7476
  const has = t.h;
7148
7477
  if (has !== null) {
7149
- for (const key of Reflect.ownKeys(has))
7150
- setSignal(has[key], key in pb && !(t.del !== null && t.del.has(key)));
7478
+ const keys = writtenKeys ?? Reflect.ownKeys(has);
7479
+ for (const key of keys) {
7480
+ const node = has[key];
7481
+ if (node !== undefined) setSignal(node, key in pb && !(t.del !== null && t.del.has(key)));
7482
+ }
7151
7483
  }
7152
7484
  // Deep-witness (dk): setter writes must notify a deep() subscriber even on
7153
- // keys with no node. O(pb keys) equality only when a witness exists.
7485
+ // keys with no node. O(written/pb keys) equality only when a witness exists.
7154
7486
  if (t.dk !== null) {
7155
7487
  if (t.del !== null && t.del.size !== 0) bumpDeep(t);
7156
7488
  else
7157
- for (const key of Reflect.ownKeys(pb)) {
7489
+ for (const key of writtenKeys ?? Reflect.ownKeys(pb)) {
7158
7490
  const nv = pb[key];
7159
7491
  const ov = old[key];
7160
7492
  if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
@@ -7184,6 +7516,13 @@ function notifyWrites(t) {
7184
7516
  }
7185
7517
  if (changed) setSignal(t.k, v => v + 1);
7186
7518
  }
7519
+ // Patch channel (setter site): a committed write transitions this record —
7520
+ // queue its patches and bubble to ancestors (targeted nested writes must
7521
+ // reach the row patch, §4b). One number compare when no patches exist.
7522
+ // Family targets skip this site: their visibility moment is the FOLD
7523
+ // commit (drainFolds emits), not the recompute/draft write.
7524
+ if (t.fam === null && patchHooks !== null && patchHooks.hasPatches())
7525
+ patchHooks.emitPatch(t, pb, old);
7187
7526
  // Projection backing folds split by channel (two pinned contracts):
7188
7527
  // - sync-derive drafts (recompute body): NEVER eager — a downstream async
7189
7528
  // hold can form LATER in the same flush and the leaf must stay at stale
@@ -7195,9 +7534,11 @@ function notifyWrites(t) {
7195
7534
  // downstream consumer's own async still holds the effect-level reveal
7196
7535
  // (spec-async "verdicts never inherit consumers' in-flight state").
7197
7536
  if (t.fam !== null && t.pb !== null && getWriteOverride()) {
7537
+ // Landed truth (post-await write-override): immediately visible to every
7538
+ // reader — any staged held view is superseded.
7539
+ if (t.ht !== null) t.ht = t.hv = null;
7198
7540
  const oldBacking = t.v;
7199
7541
  t.pb = null;
7200
- t.wk = null; // written-keys window closes with the eager fold
7201
7542
  t.v = pb;
7202
7543
  t.ch = false;
7203
7544
  if (t.u && t.u.v[t.pk] === oldBacking) {
@@ -7402,6 +7743,17 @@ function inOwnerContext() {
7402
7743
  const eff = c._root ? c._parentComputed : c;
7403
7744
  return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
7404
7745
  }
7746
+ /** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
7747
+ * callbacks). Distinct from context-free: these scopes get committed
7748
+ * visibility even against a projection's authoritative-elect pending
7749
+ * backing (#3082) — parity with signals, where core read() serves
7750
+ * committed to them regardless of staged writes. */
7751
+ function inForbiddenScope() {
7752
+ const c = getOwner();
7753
+ if (c === null) return false;
7754
+ const eff = c._root ? c._parentComputed : c;
7755
+ return eff != null && !!(eff._config & CONFIG_CHILDREN_FORBIDDEN);
7756
+ }
7405
7757
  /** A pending fold is transition-held when any written node's parked value is
7406
7758
  * stamped by a live transition (a plain batch parking — the lazy-recompute
7407
7759
  * read case — has no transition stamp and serves fresh). */
@@ -7420,6 +7772,20 @@ function foldHeld(target) {
7420
7772
  return false;
7421
7773
  }
7422
7774
  function readSource(target) {
7775
+ // Held view first (#3074): an adoption staged under a live hold serves the
7776
+ // pre-hold committed backing to committed-visibility readers. Speculative
7777
+ // readers — drafts, write-override, owner-context computeds recomputing
7778
+ // inside the transaction, and latest() reads — see the adopted backing.
7779
+ if (
7780
+ target.ht !== null &&
7781
+ !latestReadActive &&
7782
+ !inDraft(target) &&
7783
+ !getWriteOverride() &&
7784
+ !inOwnerContext()
7785
+ ) {
7786
+ const hv = heldMaskView(target);
7787
+ if (hv !== null) return hv;
7788
+ }
7423
7789
  // Signal-parity visibility (core read(): owner-context reads serve
7424
7790
  // _pendingValue, context-free reads serve committed — effects recompute
7425
7791
  // BEFORE commitPendingNodes in the flush, so the pending view must be
@@ -7433,8 +7799,10 @@ function readSource(target) {
7433
7799
  inOwnerContext() ||
7434
7800
  // A projection's pending backing is authoritative-elect: serve it to
7435
7801
  // context-free readers too UNLESS a transition is holding the node
7436
- // commits (downstream async hold — stale committed is the contract).
7437
- (target.fam !== null && !foldHeld(target)))
7802
+ // commits (downstream async hold — stale committed is the contract)
7803
+ // or the reader is a CHILDREN_FORBIDDEN scope, which never observes
7804
+ // its own unsettled write (#3082, signal parity per #3006).
7805
+ (target.fam !== null && !foldHeld(target) && !inForbiddenScope()))
7438
7806
  )
7439
7807
  return target.pb;
7440
7808
  return target.v;
@@ -7478,9 +7846,11 @@ function hasActiveOverride(node) {
7478
7846
  * FORCE sentinels never surface (they only bump subscribers of accessor
7479
7847
  * keys, which are served by the trap, not the node). */
7480
7848
  function nodeValue(node, backing) {
7849
+ // latest() sees the in-flight parked value like an owner-context reader
7850
+ // does (#3075) — signal/memo parity for store-node-backed keys.
7481
7851
  const v = hasActiveOverride(node)
7482
7852
  ? unwrapOverride(node._x?._overrideValue)
7483
- : node._pendingValue !== NOT_PENDING && inOwnerContext()
7853
+ : node._pendingValue !== NOT_PENDING && (latestReadActive || inOwnerContext())
7484
7854
  ? node._pendingValue
7485
7855
  : backing;
7486
7856
  return v === FORCE ? backing : v;
@@ -7571,6 +7941,27 @@ function firewallGate(target) {
7571
7941
  const fw = target.fam?.node;
7572
7942
  if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
7573
7943
  }
7944
+ /** latest() pull (#3075): bring the projection computed up to date so the
7945
+ * read serves the IN-FLIGHT derivation — signal/memo parity, where core
7946
+ * read() routes latest() through a companion that recomputes speculatively.
7947
+ * The latest flag is suspended for the recompute (the derive's own reads
7948
+ * are normal reads), and latestPullActive marks any adoption it commits as
7949
+ * staged (see adoptPB) — the speculative swap must not leak to
7950
+ * committed-visibility readers before the flush. */
7951
+ function pullProjectionForLatest(target) {
7952
+ const fw = target.fam.node;
7953
+ if (fw == null) return;
7954
+ const prevLatest = latestReadActive;
7955
+ setLatestReadActive(false);
7956
+ const prevPull = latestPullActive;
7957
+ latestPullActive = true;
7958
+ try {
7959
+ prepareComputed(fw, true);
7960
+ } finally {
7961
+ latestPullActive = prevPull;
7962
+ setLatestReadActive(prevLatest);
7963
+ }
7964
+ }
7574
7965
  const traps = {
7575
7966
  get(target, key, receiver) {
7576
7967
  // One typeof gates every brand-symbol compare off the hot string path
@@ -7598,6 +7989,11 @@ const traps = {
7598
7989
  }
7599
7990
  if (pendingCheckActive) witnessAffectsMark(target, key);
7600
7991
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
7992
+ // latest() pull (#3075): store traps never reach core read() without an
7993
+ // observer, so bring the projection computed up to date here — signal/
7994
+ // memo parity for latest() reads through a projection.
7995
+ if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
7996
+ pullProjectionForLatest(target);
7601
7997
  const src = readSource(target);
7602
7998
  // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
7603
7999
  // deleted keys are tracked aside and read as absent in the pending view.
@@ -7821,14 +8217,15 @@ const traps = {
7821
8217
  // Array length writes implicitly delete indices — the written-keys bound
7822
8218
  // can't see them, so poison to the full scan for this batch. Index
7823
8219
  // writes implicitly GROW length, so arrays always record it alongside.
8220
+ const pcs = pcOf(target);
7824
8221
  if (Array.isArray(pb)) {
7825
- if (key === "length") target.wk = WK_ALL;
7826
- else if (target.wk !== WK_ALL) {
7827
- const wk = (target.wk ??= new Set());
8222
+ if (key === "length") pcs.wk = WK_ALL;
8223
+ else if (pcs.wk !== WK_ALL) {
8224
+ const wk = (pcs.wk ??= new Set());
7828
8225
  wk.add(key);
7829
8226
  wk.add("length");
7830
8227
  }
7831
- } else if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
8228
+ } else if (pcs.wk !== WK_ALL) (pcs.wk ??= new Set()).add(key);
7832
8229
  // Own data keys literally named "prototype"/"constructor" land as data —
7833
8230
  // defineProperty sidesteps a proto-chain setter named the same.
7834
8231
  if (UNSAFE_KEYS.has(key)) {
@@ -7866,12 +8263,20 @@ const traps = {
7866
8263
  const override = !draft && getWriteOverride();
7867
8264
  if (!draft && !override) return true;
7868
8265
  if (key === "__proto__") return true;
7869
- if (desc.get || desc.set) target.a = true;
8266
+ if (desc.get || desc.set) {
8267
+ target.a = true;
8268
+ // Accessor demotion (re-audit blocker 3): a record that acquires an
8269
+ // accessor after patch registration stops being patchable — pull its
8270
+ // patches and re-drive them as tracked effect fallbacks. Hooks are
8271
+ // installed whenever pc.p exists (registration installs them).
8272
+ if (target.pc !== null && target.pc.p !== null) patchHooks.demoteToEffects(target);
8273
+ }
7870
8274
  // Unwrap before ensurePB (see the set trap: self-reference materializes).
7871
8275
  if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
7872
8276
  const pb = ensurePB(target);
7873
8277
  pendingNotify.add(target);
7874
- if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
8278
+ const pcd = pcOf(target);
8279
+ if (pcd.wk !== WK_ALL) (pcd.wk ??= new Set()).add(key);
7875
8280
  Object.defineProperty(pb, key, desc);
7876
8281
  if (target.del !== null) target.del.delete(key);
7877
8282
  if (override) notifyWrites(target);
@@ -7883,7 +8288,8 @@ const traps = {
7883
8288
  if (!draft && !override) return true;
7884
8289
  const pb = ensurePB(target);
7885
8290
  pendingNotify.add(target);
7886
- if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
8291
+ const pcx = pcOf(target);
8292
+ if (pcx.wk !== WK_ALL) (pcx.wk ??= new Set()).add(key);
7887
8293
  delete pb[key];
7888
8294
  // A prototype overlay cannot shadow a delete of a committed key —
7889
8295
  // record it aside (#3044); reads/has/ownKeys/commit consult the set.
@@ -7956,8 +8362,39 @@ function createStoreNext(init, shallow = false) {
7956
8362
  const setter = fn => storeSetterNext(proxy, fn);
7957
8363
  return [proxy, setter];
7958
8364
  }
8365
+ /** True when `proxy` is a SHALLOW store (children served verbatim, slots
8366
+ * replaced by reference — #2932). The list driver uses this to choose the
8367
+ * slot-patch channel (collected row bodies) over per-record registration. */
8368
+ function storeIsShallow(proxy) {
8369
+ const t = proxy?.[$TARGET];
8370
+ return t !== undefined && t.s === true;
8371
+ }
8372
+ /** True when `proxy` belongs to a projection/optimistic FAMILY. The list
8373
+ * driver must DECLINE family arrays (external audit finding): family
8374
+ * structural changes never emit row/slot ops (the setter channel is
8375
+ * fam-gated; optimistic writes ride node overrides), and the proxy identity
8376
+ * is stable so the each-watch cannot catch the change either — an engaged
8377
+ * list would freeze on optimistic/projection structural updates. Record-
8378
+ * level family patches are unaffected (they have their own emission). */
8379
+ function storeHasFamily(proxy) {
8380
+ const t = proxy?.[$TARGET];
8381
+ return t !== undefined && t.fam !== null;
8382
+ }
8383
+ /** True when `proxy` belongs to an OPTIMISTIC family specifically. The list
8384
+ * driver declines these (audit finding, narrowed): optimistic user writes
8385
+ * ride node-level overrides — they never enter the reconcile walk, so no
8386
+ * row/slot ops are emitted and an engaged list would freeze on optimistic
8387
+ * structural changes. PROJECTION (non-optimistic) families are drivable:
8388
+ * their recomputes go through the reconcile walk, whose emissions are
8389
+ * transition-stamped in the apply queue like any other (equivalence-matrix
8390
+ * gated). Re-admitting optimistic families requires a lane-timed structural
8391
+ * emission mirroring emitPatchOptimistic, plus revert resync. */
8392
+ function storeHasOptimisticFamily(proxy) {
8393
+ const t = proxy?.[$TARGET];
8394
+ return t !== undefined && t.fam?.opt === true;
8395
+ }
7959
8396
  /** Tracking deep snapshot (`deep()` for next targets): subscribes to the
7960
- * key-set and every property node at every reachable level, then returns the
8397
+ * key-set and deep-witness node at every reachable level, then returns the
7961
8398
  * plain view. Shared references and cycles handled via the visited set. */
7962
8399
  function deepNext(value) {
7963
8400
  const t0 = value?.[$TARGET];
@@ -8157,7 +8594,7 @@ function reconcileNextState(value, state, key, replace = false) {
8157
8594
  // positional so old-entity subtrees never merge into the new entity's).
8158
8595
  const prev = t.pb ?? t.v;
8159
8596
  const eq = keyFn(prev);
8160
- if (eq !== undefined && keyFn(incoming) !== eq) {
8597
+ if (eq !== undefined && !sameKey(keyFn(incoming), eq)) {
8161
8598
  if (!replace) throw new Error("Cannot reconcile states with different identity");
8162
8599
  // Entity change: wholesale swap. The root proxy is stable for life
8163
8600
  // (proj R5) but NOTHING below survives — children are never matched
@@ -8198,6 +8635,33 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8198
8635
  const shallow = t.s === true;
8199
8636
  const old = t.v;
8200
8637
  adoptPB(t, incoming, eager);
8638
+ // Patch channel (adoption site): this record transitioned — queue its
8639
+ // patches with the pre-adopt prev. No bubbling walk: the adoption walk
8640
+ // visits parents before children, so ancestors emitted already. EAGER
8641
+ // only — family targets' visibility moment is their fold commit
8642
+ // (drainFolds emits there; emitting here too would double-fire).
8643
+ if (patchHooks !== null && eager && t.pc !== null && t.pc.p !== null) {
8644
+ // Accessor demotion at the ADOPTION seam is DEV-ONLY (prod principle:
8645
+ // explicitly-odd input must not cost correct-input prod — the
8646
+ // per-adoption scan was ~12% of dbmon's tick since adoptPB resets the
8647
+ // verdict every adoption). Dev demotes AND warns; prod emits directly,
8648
+ // so a getter adoptee's OUTSIDE deps (signals) won't re-apply in prod —
8649
+ // caught loudly during development instead. Registration-time admission
8650
+ // (patchableRaw) keeps its full one-time scan in both modes.
8651
+ if (!targetIsPlain(t)) {
8652
+ console.warn(
8653
+ "A reconcile adopted an object with own getters into a record that " +
8654
+ "carries compiled patches. Patches read raw values and will not " +
8655
+ "track the getters' reactive dependencies — this record's patches " +
8656
+ "are demoted to effects in development, but production will NOT " +
8657
+ "demote. Avoid getters on patched records, or key them out of " +
8658
+ "patch-eligible templates."
8659
+ );
8660
+ patchHooks.demoteToEffects(t);
8661
+ } else {
8662
+ patchHooks.emitPatchLocal(t, incoming, old);
8663
+ }
8664
+ }
8201
8665
  // Shallow adoption: records are slot values — sticky raw-mark the incoming
8202
8666
  // set (R41) and never descend; slot notification is the positional diff.
8203
8667
  if (shallow) markRawIngest(incoming);
@@ -8237,7 +8701,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8237
8701
  typeof pvRaw === "object" &&
8238
8702
  nv !== null &&
8239
8703
  typeof nv === "object" &&
8240
- keyFn(pvRaw) === keyFn(nv)
8704
+ sameKey(keyFn(pvRaw), keyFn(nv))
8241
8705
  )
8242
8706
  )
8243
8707
  break; // misaligned: fall to the keyed remainder below
@@ -8265,6 +8729,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8265
8729
  }
8266
8730
  }
8267
8731
  if (t.dk !== null && !dkBumpedA && i < nextRows.length) bumpDeep(t);
8732
+ const structStart = i; // misalignment point (== nlen on aligned ticks)
8268
8733
  let prevByKey = null;
8269
8734
  for (; i < nextRows.length; i++) {
8270
8735
  const nv = nextRows[i];
@@ -8274,16 +8739,39 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8274
8739
  let pv;
8275
8740
  if (nk !== undefined) {
8276
8741
  if (prevByKey === null) {
8742
+ // Occurrence-aware (re-audit 2, P1-5): duplicate keys queue
8743
+ // their prev INDICES (rows can themselves be arrays, so index
8744
+ // queues are the unambiguous encoding — same as buildRowOps)
8745
+ // and each is consumed ONCE. First-wins would adopt two next
8746
+ // rows into the SAME prev target while row ops retain two
8747
+ // separate DOM rows (the second one stale).
8277
8748
  prevByKey = new Map();
8278
- for (let j = 0; j < prevRows.length; j++) {
8749
+ // From structStart, not 0 (re-audit 3, P1-2): prefix-aligned
8750
+ // rows already adopted their incoming counterparts — re-offering
8751
+ // them here let a duplicate key adopt a prefix row AGAIN while
8752
+ // row ops (which correctly window from structStart) retained
8753
+ // the later occurrence's DOM row against a never-adopted target.
8754
+ for (let j = structStart; j < prevRows.length; j++) {
8279
8755
  const p = unwrapValue(prevRows[j]);
8280
8756
  if (p !== null && typeof p === "object") {
8281
8757
  const pk = keyFn(p);
8282
- if (pk !== undefined && !prevByKey.has(pk)) prevByKey.set(pk, p);
8758
+ if (pk === undefined) continue;
8759
+ const existing = prevByKey.get(pk);
8760
+ if (existing === undefined) prevByKey.set(pk, j);
8761
+ else if (Array.isArray(existing)) existing.push(j);
8762
+ else prevByKey.set(pk, [existing, j]);
8283
8763
  }
8284
8764
  }
8285
8765
  }
8286
- pv = prevByKey.get(nk);
8766
+ const m = prevByKey.get(nk);
8767
+ if (m === undefined) pv = undefined;
8768
+ else if (Array.isArray(m)) {
8769
+ pv = unwrapValue(prevRows[m.shift()]);
8770
+ if (m.length === 1) prevByKey.set(nk, m[0]);
8771
+ } else {
8772
+ pv = unwrapValue(prevRows[m]);
8773
+ prevByKey.delete(nk);
8774
+ }
8287
8775
  } else {
8288
8776
  pv = unwrapValue(prevRows[i]); // keyless item: positional fallback
8289
8777
  }
@@ -8297,12 +8785,62 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8297
8785
  }
8298
8786
  }
8299
8787
  }
8788
+ // Row ops (PR-B): emit structural ops ONLY when structure changed —
8789
+ // aligned value ticks pay nothing. Built after the walk so retained
8790
+ // rows' value patches queue first (adds bind at op-apply).
8791
+ if (
8792
+ rowHooks !== null &&
8793
+ t.pc !== null &&
8794
+ t.pc.ro !== null &&
8795
+ (structStart < nlen || plen !== nlen)
8796
+ )
8797
+ buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn);
8300
8798
  } else {
8301
8799
  const dlen = Math.min(prevRows.length, nextRows.length);
8302
8800
  const nlen = nextRows.length;
8303
8801
  let dkBumpedP = false;
8802
+ const sp = rowHooks !== null && t.pc !== null ? t.pc.sp : null;
8803
+ // Row ops for shallow/positional lists: track the key-aligned prefix
8804
+ // (keyed) so aligned value ticks emit nothing; keyless lists emit only
8805
+ // on length change (append/truncate). Slot-patch consumers need the
8806
+ // alignment tracking too (aligned = value tick, misaligned = ops).
8807
+ const ro = rowHooks !== null && t.pc !== null ? t.pc.ro : null;
8808
+ let keyAligned = keyFn !== null && (ro !== null || sp !== null);
8809
+ let keyPrefix = 0;
8304
8810
  for (let i = 0; i < nlen; i++) {
8305
8811
  const nvP = nextRows[i];
8812
+ if (keyAligned && i < dlen) {
8813
+ const pvK = prevRows[i];
8814
+ if (
8815
+ pvK !== null &&
8816
+ typeof pvK === "object" &&
8817
+ nvP !== null &&
8818
+ typeof nvP === "object" &&
8819
+ // SameValueZero (self-sweep): strict === here broke slot
8820
+ // alignment on NaN keys while buildRowOps retained the row —
8821
+ // retained DOM with suppressed value ticks (the round-1 NaN
8822
+ // staleness, in the shallow branch).
8823
+ sameKey(keyFn(pvK), keyFn(nvP))
8824
+ )
8825
+ keyPrefix++;
8826
+ else keyAligned = false;
8827
+ }
8828
+ // Slot-patch dispatch (shallow): a KEY-ALIGNED slot whose value was
8829
+ // replaced by reference is a value tick — emit through the queue.
8830
+ // Misaligned/appended slots are STRUCTURE (row ops rebuild or move
8831
+ // them; new rows initial-apply at bind), so they emit nothing here.
8832
+ // Keyless positional lists treat same-index replacement as the value
8833
+ // tick for indices below the common length.
8834
+ // `i < dlen` is load-bearing for BOTH modes: an appended position
8835
+ // past a fully-aligned prefix (vacuously aligned when prev is empty)
8836
+ // has no previous slot — emitting a slot tick for it races the row
8837
+ // ops that CREATE the row (the slot queue applies first, indexing a
8838
+ // row that does not exist yet). Equivalence-matrix finding:
8839
+ // clear-then-refill and pure appends crashed the driver.
8840
+ if (sp !== null && i < dlen && (keyFn === null || keyAligned)) {
8841
+ const pvS = prevRows[i];
8842
+ if (pvS !== nvP) rowHooks.emitSlotPatch(t, i, nvP, pvS);
8843
+ }
8306
8844
  if (!shallow && i < dlen && nvP !== null && typeof nvP === "object")
8307
8845
  descend(unwrapValue(prevRows[i]), nvP, keyFn, fam, proj);
8308
8846
  if (
@@ -8323,6 +8861,15 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8323
8861
  }
8324
8862
  }
8325
8863
  }
8864
+ if (ro !== null) {
8865
+ const plen = prevRows.length;
8866
+ if (keyFn !== null) {
8867
+ if (keyPrefix < nlen || plen !== nlen)
8868
+ buildAndEmitRowOps(t, prevRows, nextRows, keyPrefix, keyFn);
8869
+ } else if (plen !== nlen) {
8870
+ buildAndEmitRowOps(t, prevRows, nextRows, dlen, null);
8871
+ }
8872
+ }
8326
8873
  }
8327
8874
  if (eager) {
8328
8875
  if (nodes !== null && nodesHit < t.nc) {
@@ -8343,6 +8890,22 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8343
8890
  // slots must not notify, R9). This replaces the notifyFold re-walk that
8344
8891
  // doubled dbmon's diff cost. for-in covers own enumerable string keys
8345
8892
  // with no key-array allocation; symbols get a pass only when present.
8893
+ // PROTOTYPE compiled-patch fast path: a pure-patch record (no nodes,
8894
+ // no presence/key-set/deep subscribers, no family) adopts and hands the
8895
+ // (next, prev) pair to its compiled patch — no per-key walk at all.
8896
+ if (
8897
+ t.pc !== null &&
8898
+ t.pc.p !== null &&
8899
+ eager &&
8900
+ t.n === null &&
8901
+ t.h === null &&
8902
+ t.k === null &&
8903
+ t.dk === null &&
8904
+ fam === null
8905
+ ) {
8906
+ // Adoption already ran at applyAdopt entry; emission was queued there.
8907
+ return;
8908
+ }
8346
8909
  const nodes = eager ? t.n : null;
8347
8910
  let nodesHit = 0;
8348
8911
  let dkBumped = false;
@@ -8408,6 +8971,93 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8408
8971
  }
8409
8972
  }
8410
8973
  const hasOwnP = Object.prototype.hasOwnProperty;
8974
+ /** Setter-channel row ops (the fold site calls this for array targets with
8975
+ * ops consumers): structural mutation through the setter — push/splice/index
8976
+ * assignment/permutation — is a visibility transition for the list container
8977
+ * just like a reconcile walk, and drivers consuming registerRowOps must see
8978
+ * it. Setter mutations move the SAME row objects around, so RAW IDENTITY is
8979
+ * the key. Aligned arrays (value-only folds) emit nothing. */
8980
+ const identityKey = r => unwrapValue(r);
8981
+ /** Key equality for EVERY key comparison in this module (re-audit 2, P1-5):
8982
+ * SameValueZero, matching the Map-based matchers (buildRowOps, the adoption
8983
+ * window) — NaN keys are equal to themselves, so aligned NaN rows stay
8984
+ * aligned in the prefix walk instead of forever misaligning. Adoption and
8985
+ * row ops MUST agree on key equality or retained DOM rows go stale. */
8986
+ function sameKey(a, b) {
8987
+ return a === b || (a !== a && b !== b);
8988
+ }
8989
+ function emitSetterRowOps(t, prevRows, nextRows) {
8990
+ const ops = buildIdentityRowOps(prevRows, nextRows);
8991
+ if (ops !== null) rowHooks.emitRowOps(t, nextRows, ops);
8992
+ }
8993
+ /** Identity-keyed structural diff, returned rather than emitted: shared by
8994
+ * the setter channel (regular queue) and the OPTIMISTIC write channel (lane
8995
+ * queue) — same retention semantics, different dispatch timing. Returns
8996
+ * null when the lists are identity-aligned (no structure changed). */
8997
+ function buildIdentityRowOps(prevRows, nextRows) {
8998
+ let p = 0;
8999
+ const min = prevRows.length < nextRows.length ? prevRows.length : nextRows.length;
9000
+ while (p < min && unwrapValue(prevRows[p]) === unwrapValue(nextRows[p])) p++;
9001
+ if (p === prevRows.length && p === nextRows.length) return null;
9002
+ return buildRowOps(prevRows, nextRows, p, identityKey);
9003
+ }
9004
+ /** Shared row-ops builder (keyed deep branch + shallow/positional branch):
9005
+ * key-matches the misaligned window into { prefix, sources, removed }.
9006
+ * `keyFn === null` degrades to positional ops (append/truncate only). */
9007
+ function buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn) {
9008
+ rowHooks.emitRowOps(t, nextRows, buildRowOps(prevRows, nextRows, structStart, keyFn));
9009
+ }
9010
+ function buildRowOps(prevRows, nextRows, structStart, keyFn) {
9011
+ const plen = prevRows.length;
9012
+ const nlen = nextRows.length;
9013
+ const sources = new Array(nlen - structStart);
9014
+ // Occurrence-aware matching (re-audit): duplicate keys queue their old
9015
+ // indices and each is consumed ONCE — first-wins reuse would hand the same
9016
+ // source (and its one DOM row) to multiple next positions. The no-dup fast
9017
+ // shape stays a bare number; collisions upgrade to a queue.
9018
+ let oldIndexByKey = null;
9019
+ if (keyFn !== null && structStart < plen) {
9020
+ oldIndexByKey = new Map();
9021
+ for (let j = structStart; j < plen; j++) {
9022
+ const p = unwrapValue(prevRows[j]);
9023
+ if (p !== null && typeof p === "object") {
9024
+ const pk = keyFn(p);
9025
+ if (pk === undefined) continue;
9026
+ const existing = oldIndexByKey.get(pk);
9027
+ if (existing === undefined) oldIndexByKey.set(pk, j);
9028
+ else if (Array.isArray(existing)) existing.push(j);
9029
+ else oldIndexByKey.set(pk, [existing, j]);
9030
+ }
9031
+ }
9032
+ }
9033
+ const consumed = oldIndexByKey !== null ? new Set() : null;
9034
+ for (let k = structStart; k < nlen; k++) {
9035
+ const nv = nextRows[k];
9036
+ let oldIdx = -1;
9037
+ if (nv !== null && typeof nv === "object" && oldIndexByKey !== null) {
9038
+ const nk = keyFn(nv);
9039
+ if (nk !== undefined) {
9040
+ const m = oldIndexByKey.get(nk);
9041
+ if (m !== undefined) {
9042
+ if (Array.isArray(m)) {
9043
+ oldIdx = m.shift();
9044
+ if (m.length === 1) oldIndexByKey.set(nk, m[0]);
9045
+ } else {
9046
+ oldIdx = m;
9047
+ oldIndexByKey.delete(nk);
9048
+ }
9049
+ consumed.add(oldIdx);
9050
+ }
9051
+ }
9052
+ }
9053
+ sources[k - structStart] = oldIdx;
9054
+ }
9055
+ const removed = [];
9056
+ for (let j = structStart; j < plen; j++) {
9057
+ if (consumed === null || !consumed.has(j)) removed.push(unwrapValue(prevRows[j]));
9058
+ }
9059
+ return { prefix: structStart, sources, removed };
9060
+ }
8411
9061
  function descend(pv, nv, keyFn, fam, proj = false) {
8412
9062
  if (pv === null || typeof pv !== "object" || nv === null || typeof nv !== "object") return;
8413
9063
  // Lookup FIRST: a hit implies pv was wrappable and never raw-marked (only
@@ -8431,7 +9081,10 @@ function descend(pv, nv, keyFn, fam, proj = false) {
8431
9081
  const nk = keyFn(nv);
8432
9082
  // Key mismatch detaches: the slot takes the new entity; the old proxy
8433
9083
  // keeps its (old) backing and a fresh proxy wraps the new value on read.
8434
- if (pk !== undefined && nk !== undefined && pk !== nk) return;
9084
+ // SameValueZero (re-audit 2, P1-5): NaN keys are self-equal strict
9085
+ // inequality detached every NaN-keyed slot on every tick while the
9086
+ // Map-based row-ops matcher retained its DOM row (stale forever).
9087
+ if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return;
8435
9088
  }
8436
9089
  // Reachability pruning (§6d) is MODE-dependent, both pinned:
8437
9090
  // - keyed matching descends only where subscriptions exist at/below (`d`) —
@@ -8463,10 +9116,29 @@ function descend(pv, nv, keyFn, fam, proj = false) {
8463
9116
  * primitives; the generic draft write-traps are reused from the legacy
8464
9117
  * module unchanged.
8465
9118
  */
8466
- function createWriteTraps(isActive, onDraftWrite) {
8467
- // Save/restore, never hard-reset: the draft can be driven from inside an
8468
- // enclosing authoritative-write scope (next-store optimistic derives), and
8469
- // a hard `false` would clobber it mid-derive.
9119
+ /**
9120
+ * Wrap a store proxy as a projection DRAFT: every operation carries the write
9121
+ * override (the derive is the author — its ops must not hit the §6c firewall
9122
+ * gate, even in a continuation after an `await`/`yield` where the sync write
9123
+ * scope has closed).
9124
+ *
9125
+ * FAKE TARGET, not the store proxy itself (#3060): after a proxy trap
9126
+ * returns, the engine runs spec invariant validation against the proxy's
9127
+ * TARGET — [[OwnPropertyKeys]] after ownKeys, [[GetOwnProperty]] after
9128
+ * set/getOwnPropertyDescriptor/defineProperty. With the store proxy as
9129
+ * target those checks re-enter the store's traps OUTSIDE the override
9130
+ * bracket (the trap's finally has already run), so `Object.keys(state)` in
9131
+ * a derive continuation fired the firewall gate and re-threw the
9132
+ * projection's own pending NotReadyError into the derive. A dummy of
9133
+ * matching kind (array/object, same trick as the store's own TargetShape)
9134
+ * keeps invariant validation away from the store entirely; the traps
9135
+ * forward to the closed-over inner proxy inside the bracket.
9136
+ *
9137
+ * Save/restore projectionWriteActive, never hard-reset: the draft can be
9138
+ * driven from inside an enclosing authoritative-write scope (next-store
9139
+ * optimistic derives), and a hard `false` would clobber it mid-derive.
9140
+ */
9141
+ function wrapDraft(inner, isActive, onDraftWrite) {
8470
9142
  const traps = {
8471
9143
  get(_, prop) {
8472
9144
  let value;
@@ -8474,13 +9146,15 @@ function createWriteTraps(isActive, onDraftWrite) {
8474
9146
  setWriteOverride(true);
8475
9147
  setProjectionWriteActive(true);
8476
9148
  try {
8477
- value = _[prop];
9149
+ value = inner[prop];
8478
9150
  } finally {
8479
9151
  setWriteOverride(false);
8480
9152
  setProjectionWriteActive(was);
8481
9153
  }
8482
9154
  if (prop === $TARGET) return value;
8483
- return typeof value === "object" && value !== null ? new Proxy(value, traps) : value;
9155
+ return typeof value === "object" && value !== null
9156
+ ? wrapDraft(value, isActive, onDraftWrite)
9157
+ : value;
8484
9158
  },
8485
9159
  has(_, prop) {
8486
9160
  let value;
@@ -8488,7 +9162,7 @@ function createWriteTraps(isActive, onDraftWrite) {
8488
9162
  setWriteOverride(true);
8489
9163
  setProjectionWriteActive(true);
8490
9164
  try {
8491
- value = prop in _;
9165
+ value = prop in inner;
8492
9166
  } finally {
8493
9167
  setWriteOverride(false);
8494
9168
  setProjectionWriteActive(was);
@@ -8501,7 +9175,7 @@ function createWriteTraps(isActive, onDraftWrite) {
8501
9175
  setWriteOverride(true);
8502
9176
  setProjectionWriteActive(true);
8503
9177
  try {
8504
- _[prop] = value;
9178
+ inner[prop] = value;
8505
9179
  onDraftWrite?.();
8506
9180
  } finally {
8507
9181
  setWriteOverride(false);
@@ -8515,7 +9189,49 @@ function createWriteTraps(isActive, onDraftWrite) {
8515
9189
  setWriteOverride(true);
8516
9190
  setProjectionWriteActive(true);
8517
9191
  try {
8518
- delete _[prop];
9192
+ delete inner[prop];
9193
+ onDraftWrite?.();
9194
+ } finally {
9195
+ setWriteOverride(false);
9196
+ setProjectionWriteActive(was);
9197
+ }
9198
+ return true;
9199
+ },
9200
+ ownKeys() {
9201
+ const was = projectionWriteActive;
9202
+ setWriteOverride(true);
9203
+ setProjectionWriteActive(true);
9204
+ try {
9205
+ return Reflect.ownKeys(inner);
9206
+ } finally {
9207
+ setWriteOverride(false);
9208
+ setProjectionWriteActive(was);
9209
+ }
9210
+ },
9211
+ getOwnPropertyDescriptor(_, prop) {
9212
+ let d;
9213
+ const was = projectionWriteActive;
9214
+ setWriteOverride(true);
9215
+ setProjectionWriteActive(true);
9216
+ try {
9217
+ d = Reflect.getOwnPropertyDescriptor(inner, prop);
9218
+ } finally {
9219
+ setWriteOverride(false);
9220
+ setProjectionWriteActive(was);
9221
+ }
9222
+ // The dummy target doesn't hold the key, so a non-configurable report
9223
+ // would violate the proxy invariant. Store descriptors are already
9224
+ // normalized configurable; enforce it for raw leaves too.
9225
+ if (d) d.configurable = true;
9226
+ return d;
9227
+ },
9228
+ defineProperty(_, prop, desc) {
9229
+ if (isActive && !isActive()) return true;
9230
+ const was = projectionWriteActive;
9231
+ setWriteOverride(true);
9232
+ setProjectionWriteActive(true);
9233
+ try {
9234
+ Reflect.defineProperty(inner, prop, desc);
8519
9235
  onDraftWrite?.();
8520
9236
  } finally {
8521
9237
  setWriteOverride(false);
@@ -8524,7 +9240,8 @@ function createWriteTraps(isActive, onDraftWrite) {
8524
9240
  return true;
8525
9241
  }
8526
9242
  };
8527
- return traps;
9243
+ // Matching-kind dummy so Array.isArray(draft) answers like the store.
9244
+ return new Proxy(Array.isArray(inner) ? [] : {}, traps);
8528
9245
  }
8529
9246
  function createProjectionNextInternal(fn, seed, options) {
8530
9247
  const fam = {
@@ -8578,9 +9295,10 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
8578
9295
  const shadow = owner._loading
8579
9296
  ? JSON.parse(JSON.stringify(wrappedStore[$TARGET][STORE_VALUE]))
8580
9297
  : null;
8581
- const draft = new Proxy(
9298
+ const draft = wrapDraft(
8582
9299
  wrappedStore,
8583
- createWriteTraps(() => !settled || owner._x?._inFlight === result, onDraftWrite)
9300
+ () => !settled || owner._x?._inFlight === result,
9301
+ onDraftWrite
8584
9302
  );
8585
9303
  storeSetterNext(
8586
9304
  draft,
@@ -8605,6 +9323,558 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
8605
9323
  return owner;
8606
9324
  }
8607
9325
 
9326
+ /**
9327
+ * PR-A: the patch channel (DESIGN-PATCH-CHANNEL.md).
9328
+ *
9329
+ * Compiled patch functions — per-record compare-and-write consumers —
9330
+ * dispatched by the store's visibility transitions instead of render
9331
+ * effects. This module owns registration, the per-flush apply queue
9332
+ * (effect-phase timing, §2b), the owned-prev rule (§2c), and dispatch
9333
+ * bubbling (§4b). Emission calls live at the four visibility-transition
9334
+ * sites (adoption walk, setter notify, fold commit, override lifecycle)
9335
+ * and are gated on registration, so unpatched stores pay a null check.
9336
+ *
9337
+ * Bubbling contract: a targeted nested write reaches ancestor patches as a
9338
+ * FORCED re-apply — the third `force` argument makes every compiled compare
9339
+ * pass, so the ancestor rewrites its bound fields from its current backing
9340
+ * (idempotent, and prev-free: an ancestor's pre-state is not reconstructible
9341
+ * after in-place folds). Compiled bodies therefore have the signature
9342
+ * `(next, prev, force?)`.
9343
+ *
9344
+ * Tree-shaking: core never imports this module; stores without patches
9345
+ * never schedule the queue.
9346
+ */
9347
+ let queue = null;
9348
+ let scheduled = false;
9349
+ function drainApplyQueue() {
9350
+ // Settle-time fallback for optimistic emissions (a reverting flush may
9351
+ // have no active lanes left to run the lane-slot drain).
9352
+ drainOptimistic();
9353
+ const q = queue;
9354
+ queue = null;
9355
+ scheduled = false;
9356
+ if (q === null) return;
9357
+ // Per-entry isolation: one throwing patch must not abort its siblings
9358
+ // (effect parity — each effect isolates its failure). A throwing patch
9359
+ // routes through its REGISTERING OWNER's queue chain exactly like a
9360
+ // render-effect error (§2b): an Errored boundary above the row collects
9361
+ // it (source = the owner, error read via owner._x?._error). Unhandled errors
9362
+ // rethrow after the drain so they still surface.
9363
+ let firstError = UNSET;
9364
+ for (let i = 0; i < q.length; i++) {
9365
+ clearStamp(q[i]);
9366
+ const { list, prev, force, t } = q[i];
9367
+ const next = t !== null ? (t.pb ?? t.v) : q[i].next;
9368
+ firstError = applyEntries(list, next, prev, force, firstError);
9369
+ }
9370
+ if (firstError !== UNSET) {
9371
+ // Unhandled patch errors HALT like unhandled effect errors (re-audit 2,
9372
+ // P1-4): app state is undefined past an unboundaried throw.
9373
+ haltReactivity(firstError);
9374
+ throw firstError;
9375
+ }
9376
+ }
9377
+ const UNSET = Symbol();
9378
+ /** ONE callback/error primitive for every drain (normal, transition-held,
9379
+ * optimistic): per-entry isolation — a throwing patch must not abort its
9380
+ * siblings (effect parity) — and failures route through the REGISTERING
9381
+ * OWNER's queue chain exactly like a render-effect error (§2b): an Errored
9382
+ * boundary above the row collects it. Unhandled errors are aggregated by the
9383
+ * caller (first one rethrows after its drain completes). */
9384
+ function applyEntries(list, next, prev, force, firstError) {
9385
+ // SNAPSHOT multi-consumer lists (re-audit 5, P1-3): a callback can dispose
9386
+ // a sibling's owner, whose unbind SPLICES this same array mid-iteration —
9387
+ // index-walking the live array skips the shifted consumer. The dominant
9388
+ // single-consumer case pays nothing; unbound entries are marked so a
9389
+ // snapshot never applies a consumer severed by an earlier callback.
9390
+ const snap = list.length > 1 ? list.slice() : list;
9391
+ for (let j = 0; j < snap.length; j++) {
9392
+ const entry = snap[j];
9393
+ if (entry.u === true) continue;
9394
+ // Disposed owners drop their patches (the row unmounted mid-flush).
9395
+ if (entry.owner !== null && isDisposed(entry.owner)) continue;
9396
+ try {
9397
+ entry.fn(next, prev, force);
9398
+ } catch (err) {
9399
+ let handled = false;
9400
+ const owner = entry.owner;
9401
+ if (owner !== null) {
9402
+ // Route through the nearest COMPUTED ancestor (re-audit 2, P1-4):
9403
+ // <Errored>.reset() recomputes its sources, and a plain owner (the
9404
+ // list driver's listOwner) is not recomputable — the component/memo
9405
+ // scope above it is, and recomputing it rebuilds the rows, exactly
9406
+ // what reset means for a throwing render effect.
9407
+ let source = owner;
9408
+ while (source !== null && source._fn === undefined) source = source._parent;
9409
+ source ??= owner;
9410
+ const statusErr = new StatusError(source, err);
9411
+ ext(source)._error = statusErr;
9412
+ source._statusFlags = (source._statusFlags ?? 0) | STATUS_ERROR;
9413
+ handled = owner._queue.notify(source, STATUS_ERROR, STATUS_ERROR, statusErr);
9414
+ }
9415
+ if (!handled && firstError === UNSET) firstError = err;
9416
+ }
9417
+ }
9418
+ return firstError;
9419
+ }
9420
+ // Transition-stamped emissions (§2b, "the walk is not the visibility moment
9421
+ // inside a transition"): entries stash DIRECTLY on their transition
9422
+ // (`_heldPatches`) and release into the live queue when THAT batch commits
9423
+ // (patchCommitHook). Reverted transitions never commit — their stash drops
9424
+ // with the transition object, no revert bookkeeping. The field (rather than
9425
+ // a WeakMap) keeps the every-flush commit-hook check to one property read;
9426
+ // the ambient batch never stashes.
9427
+ let commitHookInstalled = false;
9428
+ function releaseBatch(batch) {
9429
+ const held = batch._heldPatches;
9430
+ if (held === undefined) return;
9431
+ batch._heldPatches = undefined;
9432
+ for (let i = 0; i < held.length; i++) pushLive(held[i]);
9433
+ }
9434
+ function pushLive(item) {
9435
+ if (queue === null) queue = [];
9436
+ queue.push(item);
9437
+ if (!scheduled) {
9438
+ scheduled = true;
9439
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
9440
+ }
9441
+ }
9442
+ function push(item) {
9443
+ const tx = activeTransition;
9444
+ if (tx !== null) {
9445
+ let held = tx._heldPatches;
9446
+ if (held === undefined) tx._heldPatches = held = [];
9447
+ held.push(item);
9448
+ return;
9449
+ }
9450
+ pushLive(item);
9451
+ }
9452
+ /** Self-entry push with SAME-BATCH COALESCING (re-audit 2/3): a record's
9453
+ * later non-forced emission into the same container UPDATES the queued
9454
+ * entry in place — `next` takes the newest capture (adoption swaps the
9455
+ * backing object per emission; dropping the later one applied STALE state),
9456
+ * `prev` keeps the batch's earliest (effect semantics: one application per
9457
+ * batch spanning the whole window). The entry's consumer list is the live
9458
+ * pc.p array, so mid-batch registrants ride the single application. Forced
9459
+ * entries and row/slot ops never coalesce; the drain clears the stamps so a
9460
+ * quiet record retains nothing from its last batch. */
9461
+ function pushSelf(pc, item) {
9462
+ const tx = activeTransition;
9463
+ let arr;
9464
+ if (tx !== null) {
9465
+ let held = tx._heldPatches;
9466
+ if (held === undefined) tx._heldPatches = held = [];
9467
+ arr = held;
9468
+ } else {
9469
+ if (queue === null) queue = [];
9470
+ arr = queue;
9471
+ }
9472
+ if (pc.qa === arr && pc.qe !== null) {
9473
+ const qe = pc.qe;
9474
+ qe.next = item.next;
9475
+ qe.list = item.list; // pc.p can be re-created if emptied mid-batch
9476
+ return;
9477
+ }
9478
+ pc.qa = arr;
9479
+ pc.qe = item;
9480
+ item.pc = pc;
9481
+ arr.push(item);
9482
+ if (arr === queue && !scheduled) {
9483
+ scheduled = true;
9484
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
9485
+ }
9486
+ }
9487
+ /** Drain-side stamp clear (re-audit 3, P2-6): without it a quiet long-lived
9488
+ * record's channel retains its last batch's container array, entry, and both
9489
+ * captured backings for the record's lifetime. */
9490
+ function clearStamp(item) {
9491
+ const pc = item.pc;
9492
+ if (pc !== undefined && pc.qe === item) {
9493
+ pc.qa = null;
9494
+ pc.qe = null;
9495
+ }
9496
+ }
9497
+ /** Shallow clone for the owned-prev rule (§2c): owned backings fold values
9498
+ * INTO the same raw at commit, so a queued prev must be snapshotted. */
9499
+ function clonePrev(prev) {
9500
+ return Array.isArray(prev) ? prev.slice() : { ...prev };
9501
+ }
9502
+ /**
9503
+ * Emit a record's visibility transition. Callers gate on `hasPatches()` and
9504
+ * `t.d` cheaply; this function re-checks and walks ancestors (§4b).
9505
+ */
9506
+ function emitPatch(t, next, prev) {
9507
+ const p = t.pc !== null ? t.pc.p : null;
9508
+ if (p !== null)
9509
+ pushSelf(t.pc, {
9510
+ list: p,
9511
+ next,
9512
+ prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
9513
+ force: false,
9514
+ t: null
9515
+ });
9516
+ // Bubbling: ancestors force-re-apply from their LIVE backing, resolved at
9517
+ // drain (privatization may clone it between now and then).
9518
+ let u = t.u;
9519
+ while (u !== null) {
9520
+ const up = u.pc !== null ? u.pc.p : null;
9521
+ if (up !== null) push({ list: up, next: null, prev: null, force: true, t: u });
9522
+ u = u.u;
9523
+ }
9524
+ }
9525
+ /** Emission for sites that already stand at the record with both sides in
9526
+ * hand and have already handled ancestors (the adoption walk descends —
9527
+ * parents were visited first), so no bubbling walk. */
9528
+ function emitPatchLocal(t, next, prev) {
9529
+ const p = t.pc !== null ? t.pc.p : null;
9530
+ if (p !== null)
9531
+ pushSelf(t.pc, {
9532
+ list: p,
9533
+ next,
9534
+ prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
9535
+ force: false,
9536
+ t: null
9537
+ });
9538
+ }
9539
+ /** Optimistic-channel emission: overrides are visible THIS flush while the
9540
+ * transaction is in flight — that is what optimism means. These ride a
9541
+ * dedicated queue drained at LANE-EFFECT timing (the regular effect queues
9542
+ * are stashed by an in-flight action), with the regular drain as the
9543
+ * settle-time fallback. `next === null` = forced re-apply from the live
9544
+ * target (the revert shape: committed truth back onto the DOM). */
9545
+ let optQueue = null;
9546
+ function drainOptimistic() {
9547
+ const q = optQueue;
9548
+ optQueue = null;
9549
+ if (q === null) return;
9550
+ // Same isolation/routing primitive as the normal drain (re-audit blocker
9551
+ // 5): one throwing optimistic patch must not abort its siblings, and it
9552
+ // must reach the registering owner's Errored boundary.
9553
+ let firstError = UNSET;
9554
+ for (let i = 0; i < q.length; i++) {
9555
+ clearStamp(q[i]);
9556
+ const { list, prev, force, t } = q[i];
9557
+ const next = t !== null ? (t.pb ?? t.v) : q[i].next;
9558
+ firstError = applyEntries(list, next, prev, force, firstError);
9559
+ }
9560
+ if (firstError !== UNSET) {
9561
+ haltReactivity(firstError);
9562
+ throw firstError;
9563
+ }
9564
+ }
9565
+ function emitPatchOptimistic(t, next, prev) {
9566
+ const p = t.pc !== null ? t.pc.p : null;
9567
+ if (p === null) return;
9568
+ if (optQueue === null) optQueue = [];
9569
+ if (next === null) optQueue.push({ list: p, next: null, prev: null, force: true, t });
9570
+ else {
9571
+ // Same-batch coalescing, optimistic container (re-audit 3): later
9572
+ // non-forced emission updates the queued entry's next in place.
9573
+ const pc = t.pc;
9574
+ if (pc.qa === optQueue && pc.qe !== null) {
9575
+ const qe = pc.qe;
9576
+ qe.next = next;
9577
+ qe.list = p;
9578
+ } else {
9579
+ const item = { list: p, next, prev, force: false, t: null };
9580
+ pc.qa = optQueue;
9581
+ pc.qe = item;
9582
+ item.pc = pc;
9583
+ optQueue.push(item);
9584
+ }
9585
+ }
9586
+ // Backup scheduling: the lane-slot drain covers in-flight application; a
9587
+ // stashed regular drain guarantees settle-time application when no lane
9588
+ // survives to the final flush (pure reverts).
9589
+ if (!scheduled) {
9590
+ scheduled = true;
9591
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
9592
+ }
9593
+ }
9594
+ /** Row-ops emission at OPTIMISTIC (lane) timing: user drafts on an
9595
+ * optimistic family must show structure IN FLIGHT — bypassing the
9596
+ * transition stash exactly like emitPatchOptimistic. Two forms:
9597
+ * - `ops` given (write site): `nextRows` is the draft's intended visible
9598
+ * list, ops the identity diff against the pre-write optimistic view.
9599
+ * - `ops === null` (revert site): RESYNC — the consumer rebuilds retention
9600
+ * by row identity against the live post-revert view, resolved from the
9601
+ * target at drain time (overrides are gone by then, so `pb ?? v` IS the
9602
+ * committed truth). */
9603
+ function emitRowOpsOptimistic(t, nextRows, ops) {
9604
+ const list = t.pc !== null ? t.pc.ro : null;
9605
+ if (list === null) return;
9606
+ if (optQueue === null) optQueue = [];
9607
+ optQueue.push({
9608
+ list: list.map(e => ({
9609
+ owner: e.owner,
9610
+ fn: (n, _p) => e.fn(n, ops)
9611
+ })),
9612
+ next: nextRows,
9613
+ prev: null,
9614
+ force: false,
9615
+ t: nextRows === null ? t : null
9616
+ });
9617
+ if (!scheduled) {
9618
+ scheduled = true;
9619
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
9620
+ }
9621
+ }
9622
+ /**
9623
+ * Register a compiled patch on a store record. Multi-consumer (two lists
9624
+ * can render one record); owner-scoped for disposal. Returns unbind.
9625
+ */
9626
+ // Global registration count: the cheap gate emission sites check before any
9627
+ // per-record work (unpatched apps pay one number compare per transition).
9628
+ let patchCount = 0;
9629
+ function hasPatches() {
9630
+ return patchCount > 0;
9631
+ }
9632
+ function registerPatch(record, fn) {
9633
+ let t = record?.[$TARGET];
9634
+ if (t === undefined) throw new Error("registerPatch: not a store record");
9635
+ // Chained backings (§7b): register on the ULTIMATE owner — that is where
9636
+ // value transitions fold and dispatch; the wrapper's identity is stable
9637
+ // and would never fire (see ultimateTarget).
9638
+ t = ultimateTarget(t) ?? t;
9639
+ if (!commitHookInstalled) {
9640
+ commitHookInstalled = true;
9641
+ armPatchHooks();
9642
+ setPatchCommitHook(releaseBatch);
9643
+ GlobalQueue._drainPatchOptimistic = drainOptimistic;
9644
+ }
9645
+ const entry = { fn, owner: getOwner() };
9646
+ const pc = pcOf(t);
9647
+ const list = (pc.p ??= []);
9648
+ list.push(entry);
9649
+ patchCount++;
9650
+ // Bindings are subscriptions for reachability (§6d pruning must descend
9651
+ // into bound records).
9652
+ markDescendants(t);
9653
+ let unbound = false;
9654
+ return () => {
9655
+ if (unbound) return;
9656
+ unbound = true;
9657
+ entry.u = true; // dispatch snapshots skip severed consumers
9658
+ // Decrement ONLY on actual removal: a demotion (demoteToEffects) may
9659
+ // have already pulled this entry and repaired the count — the splice
9660
+ // miss is how this closure learns that.
9661
+ const idx = list.indexOf(entry);
9662
+ if (idx >= 0) {
9663
+ list.splice(idx, 1);
9664
+ patchCount--;
9665
+ }
9666
+ if (list.length === 0 && pc.p === list) pc.p = null;
9667
+ };
9668
+ }
9669
+ /** Resolve a target through CHAINED backings (§7b) to the ultimate owner.
9670
+ * A projection family wrapper's backing IS another store's proxy: value
9671
+ * transitions fold on the ULTIMATE target (the wrapper's identity never
9672
+ * changes), so patch registration and raw resolution must land there or
9673
+ * registered patches never fire (equivalence-matrix finding: projection
9674
+ * value ticks froze driver rows while classic effects tracked through). */
9675
+ function ultimateTarget(t) {
9676
+ while (t.ch) {
9677
+ const u = (t.pb ?? t.v)?.[$TARGET];
9678
+ if (u === undefined) return undefined;
9679
+ t = u;
9680
+ }
9681
+ return t;
9682
+ }
9683
+ /** Dual-driver bind probe (compiler runtime contract): when `record` is a
9684
+ * patchable store record, returns its CURRENT raw backing (the driver's
9685
+ * initial force-apply reads it directly — no proxy traffic, no tracking);
9686
+ * returns undefined otherwise (driver falls back to the effect path).
9687
+ * Not patchable: non-records, non-proxies, accessor-bearing records
9688
+ * (patches read raw — getters need tracked evaluation), broken chains. */
9689
+ function patchableRaw(record) {
9690
+ let t = record?.[$TARGET];
9691
+ if (t === undefined || t.px !== record || t.a === true) return undefined;
9692
+ t = ultimateTarget(t);
9693
+ // SCAN before trusting (re-audit blocker 3): `a` starts false and is only
9694
+ // discovered lazily (first draft, deep walks) — admission must run the
9695
+ // one-time own-accessor scan itself, or a getter-bearing record takes the
9696
+ // patch path and its getter's OUTSIDE dependencies (signals, other
9697
+ // records) never re-apply. Sticky `sc` makes this one probe pass per
9698
+ // record lifetime.
9699
+ if (t === undefined || !targetIsPlain(t)) return undefined;
9700
+ return t.pb ?? t.v;
9701
+ }
9702
+ /** Accessor demotion (design §5): a record that acquires an accessor after
9703
+ * registration stops being patchable — reads must go through tracked
9704
+ * evaluation. Clears patches and repairs the global count; callers re-drive
9705
+ * the pulled bodies (demoteToEffects). */
9706
+ function demotePatches(t) {
9707
+ if (t.pc === null) return null;
9708
+ const p = t.pc.p;
9709
+ t.pc.p = null;
9710
+ if (p === null) return null;
9711
+ patchCount -= p.length;
9712
+ // Drain IN PLACE: unbind closures captured this array — a late unbind must
9713
+ // miss its indexOf and not double-decrement the repaired count.
9714
+ return p.splice(0, p.length);
9715
+ }
9716
+ /** The demotion re-drive (re-audit blocker 3): each pulled body becomes the
9717
+ * SAME dual-driver effect fallback the web runtime would have chosen had the
9718
+ * record carried the accessor at bind — a tracked compute pass (next === prev
9719
+ * short-circuits every compare into a pure read THROUGH THE PROXY, so getter
9720
+ * dependencies track) plus an untracked force-apply at effect timing.
9721
+ *
9722
+ * Creation is DEFERRED to the effect phase: the trap that discovers the
9723
+ * accessor runs mid-draft, and an effect's initial pass must not read
9724
+ * through the proxy inside the write window. The record's own transition
9725
+ * for that draft is covered by the new effect's initial force-apply.
9726
+ *
9727
+ * Known edge (documented): a demoted LIST-ROW body re-drives under its
9728
+ * registering owner (the list owner), so per-row severing on removal is
9729
+ * lost for demoted rows — the effect lives until the LIST disposes. Rows
9730
+ * only demote when user code defines an accessor on a row record at
9731
+ * runtime. */
9732
+ function demoteToEffects(t) {
9733
+ const entries = demotePatches(t);
9734
+ if (entries === null || entries.length === 0) return;
9735
+ const proxy = t.px;
9736
+ globalQueue.enqueue(EFFECT_RENDER, () => {
9737
+ for (let i = 0; i < entries.length; i++) {
9738
+ const entry = entries[i];
9739
+ if (entry.owner !== null && isDisposed(entry.owner)) continue;
9740
+ const fn = entry.fn;
9741
+ runWithOwner(entry.owner, () =>
9742
+ createRenderEffect(
9743
+ () => {
9744
+ fn(proxy, proxy, false);
9745
+ },
9746
+ () => {
9747
+ // Block body: a compiled patch body's return value must not be
9748
+ // mistaken for an effect cleanup.
9749
+ untrack(() => fn(proxy, undefined, true));
9750
+ }
9751
+ )
9752
+ );
9753
+ }
9754
+ });
9755
+ }
9756
+ /** Register a structural-ops consumer on a keyed store array (the list
9757
+ * container's channel — what `For` consumes through the seam). */
9758
+ function registerRowOps(array, fn) {
9759
+ let t = array?.[$TARGET];
9760
+ if (t === undefined) throw new Error("registerRowOps: not a store array");
9761
+ // Chained backings resolve to the ULTIMATE owner, same as registerPatch
9762
+ // (§7b) — the walk/fold emits there (re-audit blocker 4).
9763
+ t = ultimateTarget(t) ?? t;
9764
+ armRowHooks();
9765
+ if (!commitHookInstalled) {
9766
+ commitHookInstalled = true;
9767
+ armPatchHooks();
9768
+ setPatchCommitHook(releaseBatch);
9769
+ GlobalQueue._drainPatchOptimistic = drainOptimistic;
9770
+ }
9771
+ const entry = { fn, owner: getOwner() };
9772
+ const pc = pcOf(t);
9773
+ const list = (pc.ro ??= []);
9774
+ list.push(entry);
9775
+ patchCount++;
9776
+ markDescendants(t);
9777
+ let unbound = false;
9778
+ return () => {
9779
+ if (unbound) return;
9780
+ unbound = true;
9781
+ patchCount--;
9782
+ const idx = list.indexOf(entry);
9783
+ if (idx >= 0) list.splice(idx, 1);
9784
+ if (list.length === 0 && pc.ro === list) pc.ro = null;
9785
+ };
9786
+ }
9787
+ /** Slot patches (shallow arrays) ride the same apply queue: the walk emits
9788
+ * per aligned value-replaced slot; application happens at effect phase under
9789
+ * the registration owner's lifetime. */
9790
+ function emitSlotPatch(t, index, next, prev) {
9791
+ const sp = t.pc !== null ? t.pc.sp : null;
9792
+ if (sp === null) return;
9793
+ push({
9794
+ list: sp.map(e => ({ owner: e.owner, fn: () => e.fn(index, next, prev) })),
9795
+ next,
9796
+ prev,
9797
+ force: false,
9798
+ t: null
9799
+ });
9800
+ }
9801
+ /** Slot patch for shallow arrays: the reconcile walk emits (index, next,
9802
+ * prev) for KEY-ALIGNED value-replaced slots (structure rides row ops), and
9803
+ * the emission queues through the patch apply queue — effect-phase timing,
9804
+ * transition stamping, disposed-owner drop — like every other channel. */
9805
+ function registerSlotPatchNext(arr, fn) {
9806
+ let t = arr?.[$TARGET];
9807
+ if (t === undefined) throw new Error("registerSlotPatchNext: not a store array");
9808
+ // Chained backings resolve to the ULTIMATE owner, same as registerPatch
9809
+ // (§7b) — the walk emits slot ticks there (re-audit blocker 4).
9810
+ t = ultimateTarget(t) ?? t;
9811
+ armRowHooks();
9812
+ if (!commitHookInstalled) {
9813
+ commitHookInstalled = true;
9814
+ armPatchHooks();
9815
+ setPatchCommitHook(releaseBatch);
9816
+ GlobalQueue._drainPatchOptimistic = drainOptimistic;
9817
+ }
9818
+ // Multi-consumer (external audit): one shallow array can drive several
9819
+ // lists — registrations are a list, unbinds splice their own entry.
9820
+ const pc = pcOf(t);
9821
+ const entry = { fn, owner: getOwner() };
9822
+ (pc.sp ??= []).push(entry);
9823
+ markDescendants(t);
9824
+ let unbound = false;
9825
+ return () => {
9826
+ if (unbound || pc.sp === null) return;
9827
+ unbound = true;
9828
+ const idx = pc.sp.indexOf(entry);
9829
+ if (idx >= 0) pc.sp.splice(idx, 1);
9830
+ if (pc.sp.length === 0) pc.sp = null;
9831
+ };
9832
+ }
9833
+ /** Row-ops ride the SAME apply queue/timing as record patches: transition-
9834
+ * stamped, applied at effect phase, in emission order (structure before the
9835
+ * new rows' own patches can exist; retained rows' value patches commute). */
9836
+ function emitRowOps(t, next, ops) {
9837
+ const list = t.pc !== null ? t.pc.ro : null;
9838
+ if (list === null) return;
9839
+ push({
9840
+ list: list.map(e => ({
9841
+ owner: e.owner,
9842
+ fn: (n, _p) => e.fn(n, ops)
9843
+ })),
9844
+ next,
9845
+ prev: null,
9846
+ force: false,
9847
+ t: null
9848
+ });
9849
+ }
9850
+ // Pay-for-use seams: the write paths (store/reconcile/optimistic) emit
9851
+ // through installed hooks instead of importing this module. Installation is
9852
+ // LAZY (first registration) rather than a module-scope call — the dist is a
9853
+ // flat bundle, and a top-level side effect would retain the whole channel in
9854
+ // every consumer. TWO TIERS so a value-only registration (registerPatch —
9855
+ // present in ~every bundle under patch-mode default) does not retain the
9856
+ // list machinery (row-ops emitters + reconcile's diff builders): row hooks
9857
+ // arm only from the list driver's registrations. Sound because every
9858
+ // emission site is guarded by the matching pc channel, which only the
9859
+ // corresponding registration creates. See patch-hooks.ts.
9860
+ function armPatchHooks() {
9861
+ installPatchHooks({
9862
+ emitPatch,
9863
+ emitPatchLocal,
9864
+ emitPatchOptimistic,
9865
+ hasPatches,
9866
+ demoteToEffects
9867
+ });
9868
+ }
9869
+ function armRowHooks() {
9870
+ installRowHooks({
9871
+ emitRowOps,
9872
+ emitSlotPatch,
9873
+ emitSetterRowOps,
9874
+ emitRowOpsOptimistic
9875
+ });
9876
+ }
9877
+
8608
9878
  /**
8609
9879
  * Store rewrite — optimistic stores (§3/§7, RUL-3): no store-side layer, no
8610
9880
  * backup snapshots. Nodes in an optimistic family are ARMED core signals
@@ -8634,6 +9904,23 @@ function installNextBlockedHalf() {
8634
9904
  // so the hook only empties the batch set.
8635
9905
  if (!GlobalQueue._clearOptimisticStores) {
8636
9906
  GlobalQueue._clearOptimisticStores = stores => {
9907
+ // Patch channel (revert site): engine-native reverts flip node values
9908
+ // back to committed; patched records need a forced DOM re-apply from
9909
+ // the post-revert view. Emission only — next keeps no layer to clear.
9910
+ for (const px of stores) {
9911
+ const t = px?.[$TARGET];
9912
+ const overlaid = t?.fam?.overlaid;
9913
+ if (overlaid !== undefined) {
9914
+ for (const ot of overlaid) {
9915
+ if (ot.pc !== null && ot.pc.p !== null) patchHooks.emitPatchOptimistic(ot, null, null);
9916
+ // Row-ops resync (family increment 2): reverts flip node values
9917
+ // back engine-natively; a driven list must rebuild retention by
9918
+ // row identity against the post-revert view (resolved from the
9919
+ // target at drain — overrides are gone by then).
9920
+ if (ot.pc !== null && ot.pc.ro !== null) rowHooks.emitRowOpsOptimistic(ot, null, null);
9921
+ }
9922
+ }
9923
+ }
8637
9924
  stores.clear();
8638
9925
  };
8639
9926
  }
@@ -8740,6 +10027,22 @@ function notifyOptimisticWrites(t, pb) {
8740
10027
  const fw = t.fam?.node;
8741
10028
  if (fw?._transition) globalQueue.initTransition(fw._transition);
8742
10029
  const old = t.v;
10030
+ // Patch channel (override-application site): the draft IS the intended
10031
+ // visible state; prev is the view before these overrides apply. Bypasses
10032
+ // the transition stash — optimism is visible in flight.
10033
+ if (t.pc !== null && t.pc.p !== null)
10034
+ patchHooks.emitPatchOptimistic(t, pb, optimisticView(t, old));
10035
+ // Row-ops channel (family increment 2): optimistic STRUCTURE on an array
10036
+ // rides node overrides — it never enters the reconcile walk — so a driven
10037
+ // list must get its structural ops here, lane-timed. Identity diff of the
10038
+ // pre-write optimistic view against the draft; aligned writes emit nothing.
10039
+ if (t.pc !== null && t.pc.ro !== null && Array.isArray(pb)) {
10040
+ const prevView = optimisticView(t, old);
10041
+ if (Array.isArray(prevView)) {
10042
+ const ops = buildIdentityRowOps(prevView, pb);
10043
+ if (ops !== null) rowHooks.emitRowOpsOptimistic(t, pb, ops);
10044
+ }
10045
+ }
8743
10046
  const visible = (key, fallback) => {
8744
10047
  const node = t.n?.[key];
8745
10048
  return node !== undefined && hasActiveOverride(node)
@@ -8867,6 +10170,10 @@ function consumeOverridesNext(fam) {
8867
10170
  insertSubs(t.k, true);
8868
10171
  schedule();
8869
10172
  }
10173
+ // Patch channel (override-consumption site): visible truth flipped to
10174
+ // committed for the consumed keys — force a re-apply from the live
10175
+ // view so the DOM leaves the override state.
10176
+ if (t.pc !== null && t.pc.p !== null) patchHooks.emitPatchOptimistic(t, null, null);
8870
10177
  }
8871
10178
  overlaid.clear();
8872
10179
  });
@@ -8915,7 +10222,9 @@ function applyTentative(t, incoming, keyFn) {
8915
10222
  if (keyFn) {
8916
10223
  const pk = keyFn(pv);
8917
10224
  const nk = keyFn(nv);
8918
- if (pk !== undefined && nk !== undefined && pk !== nk) return null;
10225
+ // SameValueZero (re-audit 3, P1-3): parity with the plain reconcile
10226
+ // channel — NaN keys are self-equal.
10227
+ if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return null;
8919
10228
  }
8920
10229
  return map.get(unwrapValue(pv)) ?? null;
8921
10230
  };
@@ -8930,16 +10239,31 @@ function applyTentative(t, incoming, keyFn) {
8930
10239
  const nk = keyFn(nv);
8931
10240
  if (nk !== undefined) {
8932
10241
  if (viewByKey === null) {
10242
+ // Occurrence-aware index queues (re-audit 3, P1-3): parity with
10243
+ // the plain adoption window — duplicate keys match per
10244
+ // occurrence, each view row consumed once.
8933
10245
  viewByKey = new Map();
8934
10246
  for (let j = 0; j < viewRows.length; j++) {
8935
10247
  const p = unwrapValue(viewRows[j]);
8936
10248
  if (isWrappable(p)) {
8937
10249
  const pk = keyFn(p);
8938
- if (pk !== undefined && !viewByKey.has(pk)) viewByKey.set(pk, p);
10250
+ if (pk === undefined) continue;
10251
+ const existing = viewByKey.get(pk);
10252
+ if (existing === undefined) viewByKey.set(pk, j);
10253
+ else if (Array.isArray(existing)) existing.push(j);
10254
+ else viewByKey.set(pk, [existing, j]);
8939
10255
  }
8940
10256
  }
8941
10257
  }
8942
- pv = viewByKey.get(nk);
10258
+ const m = viewByKey.get(nk);
10259
+ if (m === undefined) pv = undefined;
10260
+ else if (Array.isArray(m)) {
10261
+ pv = unwrapValue(viewRows[m.shift()]);
10262
+ if (m.length === 1) viewByKey.set(nk, m[0]);
10263
+ } else {
10264
+ pv = unwrapValue(viewRows[m]);
10265
+ viewByKey.delete(nk);
10266
+ }
8943
10267
  } else pv = unwrapValue(viewRows[i]);
8944
10268
  } else pv = unwrapValue(viewRows[i]);
8945
10269
  const ct = match(pv, nv);
@@ -10043,7 +11367,12 @@ function createLoadingBoundary(fn, fallback, options) {
10043
11367
  function createErrorBoundary(fn, fallback) {
10044
11368
  return createCollectionBoundary(STATUS_ERROR, fn, queue => {
10045
11369
  return fallback(accessor(queue._error), () => {
10046
- for (const source of queue._sources) recompute(source);
11370
+ for (const source of queue._sources) {
11371
+ // Non-computed sources (patch-channel registrations under plain
11372
+ // owners) are not recomputable — their reset is the record's next
11373
+ // transition re-applying the patch (re-audit 2, P1-4).
11374
+ if (source._fn !== undefined) recompute(source);
11375
+ }
10047
11376
  schedule();
10048
11377
  });
10049
11378
  });
@@ -10244,9 +11573,13 @@ export {
10244
11573
  omit,
10245
11574
  onCleanup,
10246
11575
  onSettled,
11576
+ patchableRaw,
10247
11577
  peekNextChildId,
10248
11578
  reconcile,
10249
11579
  refresh,
11580
+ registerPatch,
11581
+ registerRowOps,
11582
+ registerSlotPatchNext as registerSlotPatch,
10250
11583
  releaseSnapshotScope,
10251
11584
  repeat,
10252
11585
  resetErrorHalt,
@@ -10255,6 +11588,9 @@ export {
10255
11588
  setContext,
10256
11589
  setSnapshotCapture,
10257
11590
  snapshot,
11591
+ storeHasFamily,
11592
+ storeHasOptimisticFamily,
11593
+ storeIsShallow,
10258
11594
  storePath,
10259
11595
  untrack
10260
11596
  };