@solidjs/signals 2.0.0-rc.10 → 2.0.0-rc.13

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.
@@ -203,8 +203,10 @@ r = -1) {
203
203
  // - a held ADOPTION (ht): the adopted value already swapped into `v`;
204
204
  // committed is the held view `hv[key]`, staged is `v[key]`.
205
205
  const o = heldFoldTransition(e);
206
- let f = heldAdoptionTransition(e);
207
- if (f !== null) n = e.hv[t]; else if ((f = o) !== null) n = e.v[t];
206
+ let s = heldAdoptionTransition(e);
207
+ // A key the adoption left unchanged is not born holding (#3706).
208
+ if (s !== null && !heldKey(e, t)) s = null;
209
+ if (s !== null) n = e.hv[t]; else if ((s = o) !== null) n = e.v[t];
208
210
  // Create-floor diet: slotSignal bakes the whole node into one literal —
209
211
  // no options object, no equals/unobserved closures, no NodeExtension,
210
212
  // no post-construction expandos (acc + the wrap cache px/pxv are
@@ -215,7 +217,7 @@ r = -1) {
215
217
  // notification machinery. Projection nodes carry the projection
216
218
  // computed as their firewall: reads through them link the derive's
217
219
  // status/lifecycle (§7b).
218
- const s = l = slotSignal(n, slotNodeEquals, e, t,
220
+ const f = l = slotSignal(n, slotNodeEquals, e, t,
219
221
  // Accessor-ness resolved ONCE per node (no per-object descriptor
220
222
  // scan on reads): accessor keys serve through Reflect.get with the
221
223
  // proxy receiver.
@@ -223,13 +225,13 @@ r = -1) {
223
225
  // Optimistic families: arm the override slot — setSignal routes armed
224
226
  // nodes through the core engine (lanes, ownership, reverts all native).
225
227
  if (e.fam?.opt) {
226
- ext(s).Fe = NOT_PENDING;
227
- s.C |= CONFIG_OPTIMISTIC;
228
+ ext(f).Fe = NOT_PENDING;
229
+ f.C |= CONFIG_OPTIMISTIC;
228
230
  }
229
231
  // A node born inside a live mark's identity scope inherits the mark
230
232
  // (the declaration walk could only cover nodes existing then).
231
- if (t !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(s, e.v, t);
232
- if (f !== null) stageHeldKey(s, o !== null ? e.del !== null && e.del.has(t) ? undefined : e.pb[t] : e.v[t], f);
233
+ if (t !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(f, e.v, t);
234
+ if (s !== null) stageHeldKey(f, o !== null ? e.del !== null && e.del.has(t) ? undefined : e.pb[t] : e.v[t], s);
233
235
  i[t] = l;
234
236
  e.nc++;
235
237
  markDescendants(e);
@@ -301,6 +303,33 @@ r = -1) {
301
303
  return true;
302
304
  }
303
305
 
306
+ // #3706: the keys an adoption under a live hold changed against the held
307
+ // view — the adoption twin of `wk` (#3688), keyed on the target. adoptPB
308
+ // records `[adopted object]`; the keys are diffed against IT, once, on the
309
+ // first held read (its children are re-pointed by then), and replace the
310
+ // entry. Never against the live backing — a mainline write during the hold
311
+ // replaces that, and its key is not the adoption's. WK_ALL holds the whole
312
+ // container.
313
+ const heldKeys = new WeakMap;
314
+
315
+ function heldKey(e, t) {
316
+ let n = heldKeys.get(e);
317
+ if (Array.isArray(n)) heldKeys.set(e, n = adoptionChangedKeys(e, n[0]));
318
+ return n === WK_ALL || n.has(t);
319
+ }
320
+
321
+ function adoptionChangedKeys(e, t) {
322
+ const n = e.hv;
323
+ // A chained backing, an optimistic family, a chained held view, a swapped
324
+ // or non-plain prototype (inherited accessors read through `this`): whole.
325
+ if (t[$TARGET] !== undefined || e.fam?.opt === true || n[$TARGET] !== undefined || Object.getPrototypeOf(n) !== Object.getPrototypeOf(t) || !plainProto(t)) return WK_ALL;
326
+ const r = new Set;
327
+ // Accessors are never invoked: they and a flipped enumerability are changes.
328
+ for (const i of Reflect.ownKeys(n)) if (!hasOwn.call(t, i) || isOwnAccessor(n, i) || isOwnAccessor(t, i) || propertyIsEnumerable.call(n, i) !== propertyIsEnumerable.call(t, i) || !(isEqual(n[i], t[i]) || sameLogicalSlot(e, n[i], t[i]))) r.add(i);
329
+ for (const e of Reflect.ownKeys(t)) if (!hasOwn.call(n, e)) r.add(e);
330
+ return r;
331
+ }
332
+
304
333
  function stageHeldKey(e, t, n) {
305
334
  if (slotNodeEquals.call(e, e.ce, t)) return;
306
335
  e._e = t;
@@ -449,6 +478,23 @@ let hookInstalled = false;
449
478
  return r;
450
479
  }
451
480
 
481
+ /** Shallow copy of a WIDE plain-data (sc 2) backing for the overlay rebuild
482
+ * (#3689). Own keys land on a null-prototype object — V8 creates those in
483
+ * dictionary mode, so the copy is O(keys) hash inserts — and the prototype
484
+ * is attached after. A spread builds a fast-mode object one map transition
485
+ * per key; from a source in dictionary mode (a backing that took deletes)
486
+ * whose key order differs from the last copy's, every transition is a fresh
487
+ * map with an O(keys) descriptor copy: measured 270 µs vs 28 µs at 400 keys,
488
+ * 1.3 ms vs 75 µs at 1000. Narrow, stable-shaped backings keep the spread
489
+ * (cloneRaw): there the clone IC copies the property store at once and is
490
+ * 3–8× cheaper than any per-key loop. */ function wideClone(e, t) {
491
+ const n = Object.create(null);
492
+ for (const t of Reflect.ownKeys(e)) n[t] = e[t];
493
+ Object.setPrototypeOf(n, Object.prototype);
494
+ n[$OWNER] = t;
495
+ return n;
496
+ }
497
+
452
498
  /** Copy own `key` from `from` onto `to`. A plain data slot (enumerable,
453
499
  * writable, configurable, no accessor) is a bare assignment — the common case
454
500
  * and the cheap one; anything else goes through defineProperty so accessors
@@ -485,21 +531,67 @@ let hookInstalled = false;
485
531
  * backing into a prototype, and every later flatten writes into that
486
532
  * prototype); above it the clone's O(keys) copy dominates (#3044). */ const OVERLAY_MIN_KEYS = 32;
487
533
 
534
+ /** Overlay commit choice (#3689): flatten in place, or rebuild the backing
535
+ * (materializePB, then the clone path's swap)? Measured on V8 (Node 26): a
536
+ * committed backing that has served as an overlay prototype is kept as a
537
+ * FAST-MODE prototype object up to V8's descriptor limit (1020 own
538
+ * properties), and there every in-place DELETE is O(keys) — normalize to
539
+ * dictionary plus re-optimize as a prototype, ~0.08 µs × keys — and every
540
+ * in-place ADD is a map transition copying the descriptors, ~0.006 µs ×
541
+ * keys. Detaching the overlay first does not help: prototype-ness is a
542
+ * property of the object's map. One rebuild (wideClone, deletes, writes) is
543
+ * ~0.07–0.09 µs × keys — the price of ONE in-place delete. So a fold that
544
+ * deleted anything, or added more than ~16 keys, commits cheaper rebuilt (a
545
+ * 400-key record churning 100 keys per commit, 10,000 membership readers:
546
+ * 4.7 ms → 0.22 ms per step); a pure rewrite of existing keys — the
547
+ * overlay's home case — stays in place at O(written).
548
+ * Past the descriptor limit V8 keeps the backing in dictionary mode where
549
+ * every in-place op is O(1), and a rebuild would be a pure O(keys) loss on
550
+ * exactly the growing-record pattern the overlay exists for (#3044) — the
551
+ * count gate keeps those in place. The gate reads `kc`, which the set trap
552
+ * counts up for keys new to the container and the delete commit counts down. */ const OVERLAY_REBUILD_MAX_KEYS = 1024;
553
+
554
+ const OVERLAY_REBUILD_MIN_ADDS = 16;
555
+
556
+ function overlayRebuilds(e, t) {
557
+ if (e.kc > OVERLAY_REBUILD_MAX_KEYS) return false;
558
+ if (e.del !== null && e.del.size !== 0) return true;
559
+ // Every trap write records its key, so the written-key count bounds the
560
+ // adds — the common few-key fold (#3044's growing record) answers here
561
+ // without enumerating the overlay.
562
+ const n = e.wk;
563
+ if (n !== null && n !== WK_ALL && n.size <= OVERLAY_REBUILD_MIN_ADDS) return false;
564
+ const r = e.v;
565
+ let i = 0;
566
+ for (const e of Reflect.ownKeys(t)) if (!hasOwn.call(r, e) && ++i > OVERLAY_REBUILD_MIN_ADDS) return true;
567
+ return false;
568
+ }
569
+
488
570
  /** Downgrade a prototype-overlay pending backing to the clone path: builds
489
571
  * the real container (committed + overlay writes − deletes) that fold will
490
572
  * SWAP in as the committed backing, exactly as if the draft had started on
491
573
  * the clone path. Consumers that need a complete container (reconcile's
492
- * diff walks, drafts escaping into other storage) call this. */ function materializePB(e) {
493
- if (!e.ovl) return;
574
+ * diff walks, drafts escaping into other storage) call this, and so does the
575
+ * commit itself when the fold changed the key set (overlayRebuilds). Returns
576
+ * the pending backing (the clone, or the pb as-is when not an overlay). */ function materializePB(e) {
494
577
  const t = e.pb;
495
- const n = cloneRaw(e.v, e);
496
- for (const e of Reflect.ownKeys(t)) copyOwn(n, t, e);
497
- if (e.del !== null) {
578
+ if (!e.ovl) return t;
579
+ const n = e.sc === 2 ? wideClone(e.v, e) : cloneRaw(e.v, e);
580
+ // Deletes before writes (#3689): on a fast-mode clone (the descriptor
581
+ // path) the first delete normalizes it once, and the writes then land as
582
+ // O(1) dictionary adds instead of O(keys) map transitions each. Order is
583
+ // free semantically — a rewritten key left `del` at write time, so no
584
+ // write targets a deleted key.
585
+ if (e.del !== null) {
498
586
  for (const t of e.del) delete n[t];
587
+ e.kc -= e.del.size;
499
588
  e.del = null;
500
589
  }
590
+ // Plain-data grade (sc 2): bare assignment, as flattenOverlay does.
591
+ for (const r of Reflect.ownKeys(t)) e.sc === 2 ? n[r] = t[r] : copyOwn(n, t, r);
501
592
  e.pb = n;
502
593
  e.ovl = false;
594
+ return n;
503
595
  }
504
596
 
505
597
  function ensurePB(e) {
@@ -639,6 +731,7 @@ function ensurePB(e) {
639
731
  if (heldMaskView(e) === null) e.hv = e.v;
640
732
  e.ht = activeTransition ?? PLAIN_HOLD;
641
733
  if (!n && activeTransition !== null) heldAdoptions.add(e);
734
+ heldKeys.set(e, [ t ]);
642
735
  }
643
736
  e.pb = null;
644
737
  // Overlay and accessor-scan state describe the OUTGOING backing — a
@@ -770,6 +863,7 @@ function queueFold(e) {
770
863
  for (const r of Reflect.ownKeys(t)) e.sc === 2 ? n[r] = t[r] : copyOwn(n, t, r);
771
864
  if (e.del !== null) {
772
865
  for (const t of e.del) delete n[t];
866
+ e.kc -= e.del.size;
773
867
  e.del = null;
774
868
  }
775
869
  e.pb = null;
@@ -828,11 +922,13 @@ function drainFolds() {
828
922
  // re-queue: commit happens when the hold settles
829
923
  continue;
830
924
  }
831
- if (t.ovl) {
925
+ if (t.ovl && (t.v !== n || !overlayRebuilds(t, i))) {
832
926
  // Overlay flatten (#3044): the backing keeps its identity, so the
833
927
  // `t.v === old` gate below skips path copying (the parent slot
834
928
  // already points here) and the adopted-notify (setter notifications
835
- // happened at write time).
929
+ // happened at write time). A backing privatized mid-batch is a fresh
930
+ // clone, never a prototype — in-place writes into it are cheap, and
931
+ // the merge branch below is what a materialized pb would need anyway.
836
932
  flattenOverlay(t, i);
837
933
  } else if (t.v !== n) {
838
934
  // Privatized mid-batch (#3271): an earlier fold in this drain
@@ -873,7 +969,10 @@ function drainFolds() {
873
969
  t.wk = null;
874
970
  // written-keys window closes with the fold commit
875
971
  } else {
876
- t.v = i;
972
+ // An overlay whose fold changed the key set rebuilds (#3689) and
973
+ // takes the swap like a clone-path draft: the parent slot re-points
974
+ // in the path copy below.
975
+ t.v = t.ovl ? materializePB(t) : i;
877
976
  t.ch = false;
878
977
  // pb is always a plain clone
879
978
  t.pb = null;
@@ -1052,8 +1151,12 @@ function drainFolds() {
1052
1151
  if (e.ht !== null) e.ht = e.hv = null;
1053
1152
  // Overlay landing (#3352): flatten in place — identity-stable, and
1054
1153
  // privatizeCommitted re-slots the parent itself when it has to clone an
1055
- // unowned seed, so no path copy is needed.
1056
- if (e.ovl) return flattenOverlay(e, t);
1154
+ // unowned seed, so no path copy is needed. A landing that changed the
1155
+ // key set rebuilds instead (#3689) and swaps like a clone-path landing.
1156
+ if (e.ovl) {
1157
+ if (!overlayRebuilds(e, t)) return flattenOverlay(e, t);
1158
+ t = materializePB(e);
1159
+ }
1057
1160
  const n = e.v;
1058
1161
  e.pb = null;
1059
1162
  e.v = t;
@@ -1124,8 +1227,8 @@ i = true) {
1124
1227
  return;
1125
1228
  }
1126
1229
  const o = i?.value;
1127
- const f = l?.value;
1128
- if (!isEqual(o, f) && !targetsEqual(o, f)) setSignal(e, typeof f === "function" ? () => f : f);
1230
+ const s = l?.value;
1231
+ if (!isEqual(o, s) && !targetsEqual(o, s)) setSignal(e, typeof s === "function" ? () => s : s);
1129
1232
  } else {
1130
1233
  const i = n[t];
1131
1234
  const l = r[t];
@@ -1295,21 +1398,25 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
1295
1398
  return false;
1296
1399
  }
1297
1400
 
1298
- function readSource(e) {
1401
+ /** The backing a reader is served. `key` (the get/has/descriptor traps)
1402
+ * scopes a fold hold to the keys the fold touched — see pendingBackingVisible —
1403
+ * and an adoption hold to the keys the adoption changed (heldKey). */ function readSource(e, t) {
1299
1404
  // Adoption hold first (#3074): an adoption staged under a live transaction
1300
1405
  // (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
1301
1406
  // committed-visibility readers — context-free and children-forbidden ones,
1302
1407
  // and stale passes off a foreign hold. Drafts, write-override and latest()
1303
1408
  // see the adopted backing.
1304
- const t = e.ht;
1305
- if (t !== null && !latestReadActive && !inDraft(e) && !getWriteOverride()) {
1306
- const n = heldMaskView(e);
1307
- if (n !== null) {
1409
+ const n = e.ht;
1410
+ if (n !== null && !latestReadActive && !inDraft(e) && !getWriteOverride()) {
1411
+ const r = heldMaskView(e);
1412
+ // A key the adoption left unchanged derives nothing from the hold, for
1413
+ // any reader (#3706): the backing serves it.
1414
+ if (r !== null && (t === undefined || heldKey(e, t))) {
1308
1415
  const e = readerContext();
1309
- if (e === null || e.C & CONFIG_CHILDREN_FORBIDDEN || !holdVisible(t === PLAIN_HOLD ? null : currentTransition(t), e)) return n;
1416
+ if (e === null || e.C & CONFIG_CHILDREN_FORBIDDEN || !holdVisible(n === PLAIN_HOLD ? null : currentTransition(n), e)) return r;
1310
1417
  }
1311
1418
  }
1312
- return pendingBackingVisible(e, false) ? e.pb : e.v;
1419
+ return pendingBackingVisible(e, false, t) ? e.pb : e.v;
1313
1420
  }
1314
1421
 
1315
1422
  /** The single pb-vs-committed visibility decision (#3147), shared by per-key
@@ -1326,7 +1433,7 @@ function readSource(e) {
1326
1433
  * `speculative` is deep()/snapshot's posture: an untrack/deep PEEK that sees
1327
1434
  * ordinary pending staging regardless of owner context (the documented
1328
1435
  * divergence from context-free per-key reads) — but never through a hold:
1329
- * held truth stays masked exactly as it is for per-key readers. */ function pendingBackingVisible(e, t) {
1436
+ * held truth stays masked exactly as it is for per-key readers. */ function pendingBackingVisible(e, t, n) {
1330
1437
  if (e.pb === null) return false;
1331
1438
  // The writer's own channels compose on the pending backing regardless.
1332
1439
  if (inDraft(e) || getWriteOverride()) return true;
@@ -1337,8 +1444,8 @@ function readSource(e) {
1337
1444
  // hold arms below: committed for no pass, the staged backing for the
1338
1445
  // holding pass, stale-of-foreign or A29 for a foreign one.
1339
1446
  if (heldTruthMasked(e)) return false;
1340
- const n = readerContext();
1341
- if (n === null || n.C & CONFIG_CHILDREN_FORBIDDEN) {
1447
+ const r = readerContext();
1448
+ if (r === null || r.C & CONFIG_CHILDREN_FORBIDDEN) {
1342
1449
  // No pass, or a children-forbidden one: the committed frame (A32) —
1343
1450
  // except the speculative peek (deep()/snapshot()), which sees ordinary
1344
1451
  // pending staging but never through a live foreign hold, and a
@@ -1346,11 +1453,30 @@ function readSource(e) {
1346
1453
  // readers UNLESS a transition holds the node commits (downstream async
1347
1454
  // hold — stale committed is the contract; the write-time stamp covers
1348
1455
  // keys with no node, #3336) or the scope is children-forbidden (#3082).
1349
- const r = liveFoldTransition(e);
1350
- if (t) return r === null || ownsHold(r);
1351
- return e.fam !== null && n === null && !foldHeld(e) && r === null;
1456
+ const n = liveFoldTransition(e);
1457
+ if (t) return n === null || ownsHold(n);
1458
+ return e.fam !== null && r === null && !foldHeld(e) && n === null;
1459
+ }
1460
+ const i = liveFoldTransition(e);
1461
+ // A hold changes what a reader of ONE key sees only through the fold's
1462
+ // writes to that key. A key the fold left alone reads the same from either
1463
+ // backing, so a pass reading it derives nothing from the hold: serve
1464
+ // committed, with no transaction entry and no stale replay (#3688 — a memo
1465
+ // reading an unchanged key beside an independent signal was held with
1466
+ // someone else's action). Per-node reads already have this precision (a
1467
+ // node with nothing staged enters nothing; stageHeldKey skips an equal
1468
+ // value); this is the same rule at the container gate. `wk` is the trap's
1469
+ // record of every write and delete this batch (deletes included, on the
1470
+ // overlay and clone paths alike); null (a fold with no trap writes) or
1471
+ // WK_ALL (an array length write) leaves the whole container held. Same
1472
+ // exclusions as heldFoldTransition: an optimistic family's draft is seeded
1473
+ // from node overrides the trap never saw, and a chained backing's
1474
+ // committed layer is a live proxy, not a frozen twin of the clone.
1475
+ if (i !== null && n !== undefined && !e.ch && e.fam?.opt !== true) {
1476
+ const t = e.wk;
1477
+ if (t != null && t !== WK_ALL && !t.has(n)) return false;
1352
1478
  }
1353
- return holdVisible(liveFoldTransition(e), n);
1479
+ return holdVisible(i, r);
1354
1480
  }
1355
1481
 
1356
1482
  /** #3164 fold: HELD truth on an optimistic family — a pending backing
@@ -1481,7 +1607,7 @@ function isOwnAccessor(e, t) {
1481
1607
  * node is linked only for adoption-swap notification, its value never
1482
1608
  * shadows the live chain. */ function serveDataKey(e, t, n, r, i, l = -1) {
1483
1609
  const o = e.ch && r === e.v;
1484
- let f = n;
1610
+ let s = n;
1485
1611
  // §6: on optimistic arrays LENGTH IS A VIEW, not a node value — one home
1486
1612
  // (backing ± presence overrides) for both length and indices makes torn
1487
1613
  // iteration impossible (a length node's value rides different visibility
@@ -1531,7 +1657,7 @@ function isOwnAccessor(e, t) {
1531
1657
  // seeding rule, applied to the read side (#3108).
1532
1658
  if (e.fam?.opt && draftSeesOverrides(e) && !authoritativeServe()) {
1533
1659
  const n = e.n?.[t];
1534
- if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.o?.Fe);
1660
+ if (n !== undefined && hasActiveOverride(n)) s = unwrapOverride(n.o?.Fe);
1535
1661
  }
1536
1662
  } else {
1537
1663
  // §7b: a lane value on the outer node SHADOWS read-through — an active
@@ -1548,26 +1674,26 @@ function isOwnAccessor(e, t) {
1548
1674
  // parity): READ_SLOW = a global read window or non-plain node.
1549
1675
  let r = readNodeFast(i);
1550
1676
  if (r === READ_SLOW) r = read(i);
1551
- if (!o || hasActiveOverride(i)) f = r === FORCE ? n : r;
1677
+ if (!o || hasActiveOverride(i)) s = r === FORCE ? n : r;
1552
1678
  } else if (i !== undefined && (!o || hasActiveOverride(i))) {
1553
- f = nodeValue(i, n);
1679
+ s = nodeValue(i, n);
1554
1680
  }
1555
1681
  }
1556
1682
  // Shallow stores serve data raw; store-proxy slots get boundary wrappers.
1557
- if (e.s) return serveShallow(e, t, f);
1558
- if (e.ch && !o && f !== null && typeof f === "object" && f[$TARGET] === undefined) f = resolveChainedRaw(e, t, f);
1683
+ if (e.s) return serveShallow(e, t, s);
1684
+ if (e.ch && !o && s !== null && typeof s === "object" && s[$TARGET] === undefined) s = resolveChainedRaw(e, t, s);
1559
1685
  if (i !== undefined) {
1560
1686
  // Wrap cache (see getNode): only wrappables are ever cached, so a hit
1561
1687
  // skips isWrappable too — pointer-compare replaces both checks.
1562
- if (i.pxv === f && f !== undefined) return draftServe(e, i.px);
1563
- if (!isWrappable(f)) return f;
1564
- const n = wrapNext(f, e, t);
1688
+ if (i.pxv === s && s !== undefined) return draftServe(e, i.px);
1689
+ if (!isWrappable(s)) return s;
1690
+ const n = wrapNext(s, e, t);
1565
1691
  i.px = n;
1566
- i.pxv = f;
1692
+ i.pxv = s;
1567
1693
  return draftServe(e, n);
1568
1694
  }
1569
- if (!isWrappable(f)) return f;
1570
- return draftServe(e, wrapNext(f, e, t));
1695
+ if (!isWrappable(s)) return s;
1696
+ return draftServe(e, wrapNext(s, e, t));
1571
1697
  }
1572
1698
 
1573
1699
  /** §6c store-wide status gate for reads that DON'T flow through a node:
@@ -1646,7 +1772,7 @@ const traps = {
1646
1772
  // observer, so bring the projection computed up to date here — signal/
1647
1773
  // memo parity for latest() reads through a projection.
1648
1774
  if (e.fam !== null && latestReadActive && !inDraft(e) && !getWriteOverride()) pullProjectionForLatest(e);
1649
- const r = readSource(e);
1775
+ const r = readSource(e, t);
1650
1776
  // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
1651
1777
  // deleted keys are tracked aside and read as absent in the pending view.
1652
1778
  if (e.del !== null && r === e.pb && e.del.has(t)) {
@@ -1716,15 +1842,15 @@ const traps = {
1716
1842
  // that is own on NEITHER.
1717
1843
  const o = e.ovl && r === e.pb;
1718
1844
  if ((t === "constructor" || t === "__proto__" || t === "prototype") && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return undefined;
1719
- let f = r[t];
1720
- if (f === undefined ? !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t)) : false) {
1845
+ let s = r[t];
1846
+ if (s === undefined ? !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t)) : false) {
1721
1847
  // Inherited: prototype getters/methods run with the proxy receiver.
1722
- f = Reflect.get(r, t, n);
1723
- if (typeof f === "function") return f;
1848
+ s = Reflect.get(r, t, n);
1849
+ if (typeof s === "function") return s;
1724
1850
  // proto methods untracked
1725
1851
  // Reading a currently-absent own key subscribes to it (R12) — for any
1726
1852
  // target OUTSIDE its own draft scope, even mid-setter (#3037, above).
1727
- if (f === undefined && !inDraft(e)) {
1853
+ if (s === undefined && !inDraft(e)) {
1728
1854
  if (getObserver() !== null) read(getNode(e, t, undefined, l));
1729
1855
  const n = e.n?.[t];
1730
1856
  if (n) {
@@ -1732,7 +1858,7 @@ const traps = {
1732
1858
  if (e.s) return serveShallow(e, t, r);
1733
1859
  return isWrappable(r) ? draftServe(e, wrapNext(r, e, t)) : r;
1734
1860
  }
1735
- } else if (f === undefined && inDraft(e) && e.fam?.opt && draftSeesOverrides(e) &&
1861
+ } else if (s === undefined && inDraft(e) && e.fam?.opt && draftSeesOverrides(e) &&
1736
1862
  // AUTHORITATIVE drafts (landing folds) never seed from overrides —
1737
1863
  // the caller's optimism is not truth (has-trap twin below).
1738
1864
  !authoritativeServe()) {
@@ -1742,21 +1868,21 @@ const traps = {
1742
1868
  // arm, visibleKeys and optimisticView do. Reader-gating this arm
1743
1869
  // (visibleOverride) made a second setter in the same action read
1744
1870
  // `undefined` where the first had pushed a row (#3665, rc.9).
1745
- if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.o?.Fe);
1871
+ if (n !== undefined && hasActiveOverride(n)) s = unwrapOverride(n.o?.Fe);
1746
1872
  }
1747
- if (e.s) return serveShallow(e, t, f);
1748
- return isWrappable(f) ? draftServe(e, wrapNext(f, e, t)) : f;
1873
+ if (e.s) return serveShallow(e, t, s);
1874
+ return isWrappable(s) ? draftServe(e, wrapNext(s, e, t)) : s;
1749
1875
  }
1750
- if (typeof f === "function" && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return f;
1876
+ if (typeof s === "function" && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return s;
1751
1877
  // proto method
1752
- return serveDataKey(e, t, f, r, i, l);
1878
+ return serveDataKey(e, t, s, r, i, l);
1753
1879
  },
1754
1880
  has(e, t) {
1755
1881
  if (t === $TARGET || t === $PROXY || t === $TRACK) return true;
1756
1882
  if (t === $OWNER || t === $RECORD) return false;
1757
1883
  if (pendingCheckActive) witnessAffectsMark(e, t);
1758
1884
  if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
1759
- const n = readSource(e);
1885
+ const n = readSource(e, t);
1760
1886
  let r = t in n;
1761
1887
  // Overlay deletes read as absent in the pending view (#3044).
1762
1888
  if (r && e.del !== null && n === e.pb && e.del.has(t)) r = false;
@@ -1812,7 +1938,7 @@ const traps = {
1812
1938
  if (pendingCheckActive) witnessAffectsMark(e, t);
1813
1939
  const n = getObserver();
1814
1940
  if (e.fam !== null && n === null && !inDraft(e)) firewallGate(e);
1815
- const r = readSource(e);
1941
+ const r = readSource(e, t);
1816
1942
  const i = visibleDescriptor(e, r, t);
1817
1943
  if (!inDraft(e) && n !== null && !observerHoldsKeySet(e, n)) {
1818
1944
  // The node is born from the source's presence (as `has` births it),
@@ -1857,10 +1983,12 @@ const traps = {
1857
1983
  }
1858
1984
  } else {
1859
1985
  if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
1860
- // Live own-key estimate for the overlay/clone choice (#3360): `in`
1861
- // sees through an overlay to the committed keys, so this counts keys
1862
- // NEW to the container. Deletes are not un-counted (a stale high
1863
- // count only picks the overlay a little early).
1986
+ // Live own-key estimate for the overlay/clone choice (#3360) and the
1987
+ // overlay's commit choice (#3689): `in` sees through an overlay to the
1988
+ // committed keys, so this counts keys NEW to the container. Overlay
1989
+ // deletes are counted down when they commit (`del` is exact); clone-
1990
+ // path deletes are not (a stale high count on a narrow container only
1991
+ // picks the overlay a little early).
1864
1992
  if (!(t in o)) e.kc++;
1865
1993
  }
1866
1994
  // Own data keys literally named "prototype"/"constructor" land as data —
@@ -2269,9 +2397,9 @@ function snapshotWalk(e, t, n) {
2269
2397
  // Snapshot runs mid-flush (tracked memos execute before commit), so a
2270
2398
  // pending prototype overlay must present as a REAL merged container.
2271
2399
  if (o && l.ovl) materializePB(l);
2272
- const f = o ? l.pb : l.v;
2273
- if (f === r) break;
2274
- r = f;
2400
+ const s = o ? l.pb : l.v;
2401
+ if (s === r) break;
2402
+ r = s;
2275
2403
  }
2276
2404
  if (!isWrappable(r)) return r;
2277
2405
  // Optimistic families: compose the visible view; a composed view is a fresh
@@ -2322,11 +2450,11 @@ function snapshotWalk(e, t, n) {
2322
2450
  Object.defineProperty(i, l, o);
2323
2451
  continue;
2324
2452
  }
2325
- const f = o.value;
2326
- const s = f !== null && typeof f === "object" ? snapshotWalk(f, t, n) : f;
2327
- if (o.enumerable && o.writable && o.configurable) i[l] = s; else Object.defineProperty(i, l, {
2453
+ const s = o.value;
2454
+ const f = s !== null && typeof s === "object" ? snapshotWalk(s, t, n) : s;
2455
+ if (o.enumerable && o.writable && o.configurable) i[l] = f; else Object.defineProperty(i, l, {
2328
2456
  ...o,
2329
- value: s
2457
+ value: f
2330
2458
  });
2331
2459
  }
2332
2460
  if (e && i.length !== r.length) i.length = r.length;
@@ -2341,13 +2469,13 @@ function snapshotWalk(e, t, n) {
2341
2469
  if (!i || i.get || i.set) continue;
2342
2470
  const l = i.value;
2343
2471
  if (l === null || typeof l !== "object") continue;
2344
- const f = snapshotWalk(l, t, n);
2345
- if (f !== l) {
2472
+ const s = snapshotWalk(l, t, n);
2473
+ if (s !== l) {
2346
2474
  if (o === null) {
2347
2475
  o = Array.isArray(r) ? [ ...r ] : Object.create(Object.getPrototypeOf(r), Object.getOwnPropertyDescriptors(r));
2348
2476
  t.set(r, o);
2349
2477
  }
2350
- o[e] = f;
2478
+ o[e] = s;
2351
2479
  }
2352
2480
  }
2353
2481
  return o ?? r;
@@ -258,6 +258,34 @@ export interface NavigationRef {
258
258
  * a pending navigation to fold onto it opens a navigation of its own.
259
259
  */
260
260
  redirect?: number;
261
+ /**
262
+ * The route the document arrived on — declared by the router around the
263
+ * work that establishes its initial match (building its context), not
264
+ * around a write: there is no location write on a fresh document, and
265
+ * without this a consumer has the route pattern for every navigation
266
+ * but the first. The record opens at `at`, which defaults to `0` (the
267
+ * `performance.now()` origin — the document's own navigation start, so
268
+ * the record joins Navigation Timing); a router mounted long after the
269
+ * document loaded passes its own start. It settles when the frame closes,
270
+ * `committed` with no writes: it declares the route, it does not time the
271
+ * mount — the holds the mount waits in are their own records. `from` is
272
+ * meaningless for it and ignored.
273
+ */
274
+ initial?: boolean;
275
+ /**
276
+ * The interaction the navigation is for, when the router already knows it
277
+ * will not be on the stack at write time. A router that awaits between
278
+ * the request and the write (guards or loaders resolved in its core before
279
+ * it publishes the location) captures `OBSERVE.attribution.currentOrigin()`
280
+ * in the request and hands it back here, and the record — and every hold
281
+ * and re-run its write causes — joins the click as if the write had been
282
+ * synchronous. Any origin will do: the engine takes the interaction it
283
+ * carries. Declared beats ambient: the key's presence is the declaration
284
+ * — `interaction: currentOrigin()` with nothing in effect at the request
285
+ * (`undefined`) declares that the navigation was for no interaction, and an
286
+ * interaction on the stack at write time is used only when the key is absent.
287
+ */
288
+ interaction?: ChangeOrigin;
261
289
  }
262
290
  export declare let attrHooks: AttributionHooks | null;
263
291
  export declare function setAttributionHooks(hooks: AttributionHooks | null): void;
@@ -815,6 +815,15 @@ export interface NavigationEvent {
815
815
  params?: Readonly<Record<string, string | undefined>>;
816
816
  /** When the navigation was requested (`performance.now()` clock). */
817
817
  at: number;
818
+ /**
819
+ * The route the document arrived on, declared by the router at its initial
820
+ * match (`NavigationRef.initial`): `at` is the document's navigation start
821
+ * (`0` unless the router passed its own), there is no `from`, `writes` is
822
+ * `0`, and it settles `committed` when the router's frame closes — the
823
+ * record names the route the page loaded as; its timing is Navigation
824
+ * Timing's, not the runtime's.
825
+ */
826
+ initial?: true;
818
827
  /** The user interaction it ran under, when known — a link click. */
819
828
  interaction?: ChangeOrigin;
820
829
  /** Root writes the frame performed, redirect hops included. */
@@ -148,20 +148,29 @@ export declare const CONFIG_OVERRIDE_SUPERSEDED: number;
148
148
  * transaction. A parked node (status propagation stamps `_transition`
149
149
  * without recomputing) recomputed when its source lands otherwise disposed
150
150
  * its committed children mid-hold, running their cleanups before the
151
- * transaction's atomic reveal. Cleared by `commitPendingNode`. */
151
+ * transaction's atomic reveal. Cleared by `commitPendingNode`. Transaction
152
+ * work only (A15 lane work and transaction work, #3698): zombies are parked
153
+ * by a pass under a held transaction; a pass over a lane parks a LANE frame
154
+ * instead (`CONFIG_LANE_FRAME`), whatever the node's kind. */
152
155
  export declare const CONFIG_HELD_CHILDREN: number;
153
156
  /** The frame parked in `_pendingFirstChild` / `_pendingDisposal` is a LANE
154
- * frame (#3662, A15 lanes corollary): an effect's lane pass direct-commits,
155
- * so the frame it replaces leaves the screen when the lane's queue applies
156
- * the run (A30) — not at the action's commit like #3404's transaction
157
- * zombies, and not at the pass (a held lane defers the run). Drained by the
158
- * lane's render entry the parking site pushed ahead of the new frame's
159
- * effects (cleanups before side effects), by `commitPendingNode` if a hold
160
- * commits the node first, or with the owner's death. While set the parked
161
- * frame is not a hold (the node is not queued or stamped for it), a
162
- * superseding pass disposes the never-shown live children on the spot, and a
163
- * lane-channel dirty on a member is cancelled (`laneZombie`). Effects only: a
164
- * memo's lane pass publishes an override (A17, #3479). */
157
+ * frame (#3662, #3698; A15 lane work and transaction work): a lane pass —
158
+ * on an effect or a memo alike — publishes into the lane's frame (an
159
+ * effect's run; a memo's derived override, A17), so the frame it replaces
160
+ * leaves the screen when the lane applies (A30) — not at the action's
161
+ * commit like #3404's transaction zombies, and not at the pass (a held lane
162
+ * defers the apply with the frame still displayed). Drained by the lane's
163
+ * render entry the parking site pushed ahead of the new frame's effects
164
+ * (cleanups before side effects), by `commitPendingNode` if a hold commits
165
+ * the node first, or with the owner's death. While set the parked frame is
166
+ * not a hold (the node is not queued or stamped for it — lane work never
167
+ * makes its node transaction work), a superseding pass disposes the
168
+ * never-shown live children on the spot, and a lane-channel dirty on a
169
+ * member is cancelled (`laneZombie`). Ruled 2026-09-28 (#3698): a pass is
170
+ * lane work or transaction work by its owner, never by node kind. #3662
171
+ * flagged effects only, and a memo's lane pass parked a transaction zombie
172
+ * that queued and stamped the memo as the action's pending node, so its
173
+ * next mainline recompute re-entered the hold. */
165
174
  export declare const CONFIG_LANE_FRAME: number;
166
175
  /** In-flight async node whose inputs were PUBLISHED while it was pending: a
167
176
  * batch or transaction committed with the node still `STATUS_PENDING` (an