@solidjs/signals 2.0.0-rc.11 → 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.
package/dist/dev.js CHANGED
@@ -4794,6 +4794,8 @@ function getNode(
4794
4794
  // committed is the held view `hv[key]`, staged is `v[key]`.
4795
4795
  const fold = heldFoldTransition(target);
4796
4796
  let held = heldAdoptionTransition(target);
4797
+ // A key the adoption left unchanged is not born holding (#3706).
4798
+ if (held !== null && !heldKey(target, key)) held = null;
4797
4799
  if (held !== null) current = target.hv[key];
4798
4800
  else if ((held = fold) !== null) current = target.v[key];
4799
4801
  // Create-floor diet: slotSignal bakes the whole node into one literal —
@@ -4916,6 +4918,45 @@ function holdVisible(txn, c) {
4916
4918
  enterStagedRead(null, txn);
4917
4919
  return true;
4918
4920
  }
4921
+ // #3706: the keys an adoption under a live hold changed against the held
4922
+ // view — the adoption twin of `wk` (#3688), keyed on the target. adoptPB
4923
+ // records `[adopted object]`; the keys are diffed against IT, once, on the
4924
+ // first held read (its children are re-pointed by then), and replace the
4925
+ // entry. Never against the live backing — a mainline write during the hold
4926
+ // replaces that, and its key is not the adoption's. WK_ALL holds the whole
4927
+ // container.
4928
+ const heldKeys = new WeakMap();
4929
+ function heldKey(target, key) {
4930
+ let keys = heldKeys.get(target);
4931
+ if (Array.isArray(keys)) heldKeys.set(target, (keys = adoptionChangedKeys(target, keys[0])));
4932
+ return keys === WK_ALL || keys.has(key);
4933
+ }
4934
+ function adoptionChangedKeys(target, v) {
4935
+ const hv = target.hv;
4936
+ // A chained backing, an optimistic family, a chained held view, a swapped
4937
+ // or non-plain prototype (inherited accessors read through `this`): whole.
4938
+ if (
4939
+ v[$TARGET] !== undefined ||
4940
+ target.fam?.opt === true ||
4941
+ hv[$TARGET] !== undefined ||
4942
+ Object.getPrototypeOf(hv) !== Object.getPrototypeOf(v) ||
4943
+ !plainProto(v)
4944
+ )
4945
+ return WK_ALL;
4946
+ const keys = new Set();
4947
+ // Accessors are never invoked: they and a flipped enumerability are changes.
4948
+ for (const key of Reflect.ownKeys(hv))
4949
+ if (
4950
+ !hasOwn.call(v, key) ||
4951
+ isOwnAccessor(hv, key) ||
4952
+ isOwnAccessor(v, key) ||
4953
+ propertyIsEnumerable.call(hv, key) !== propertyIsEnumerable.call(v, key) ||
4954
+ !(isEqual(hv[key], v[key]) || sameLogicalSlot(target, hv[key], v[key]))
4955
+ )
4956
+ keys.add(key);
4957
+ for (const key of Reflect.ownKeys(v)) if (!hasOwn.call(hv, key)) keys.add(key);
4958
+ return keys;
4959
+ }
4919
4960
  function stageHeldKey(node, nv, txn) {
4920
4961
  if (slotNodeEquals.call(node, node._value, nv)) return;
4921
4962
  node._pendingValue = nv;
@@ -5116,6 +5157,23 @@ function cloneRaw(source, t) {
5116
5157
  clone[$OWNER] = t;
5117
5158
  return clone;
5118
5159
  }
5160
+ /** Shallow copy of a WIDE plain-data (sc 2) backing for the overlay rebuild
5161
+ * (#3689). Own keys land on a null-prototype object — V8 creates those in
5162
+ * dictionary mode, so the copy is O(keys) hash inserts — and the prototype
5163
+ * is attached after. A spread builds a fast-mode object one map transition
5164
+ * per key; from a source in dictionary mode (a backing that took deletes)
5165
+ * whose key order differs from the last copy's, every transition is a fresh
5166
+ * map with an O(keys) descriptor copy: measured 270 µs vs 28 µs at 400 keys,
5167
+ * 1.3 ms vs 75 µs at 1000. Narrow, stable-shaped backings keep the spread
5168
+ * (cloneRaw): there the clone IC copies the property store at once and is
5169
+ * 3–8× cheaper than any per-key loop. */
5170
+ function wideClone(source, t) {
5171
+ const clone = Object.create(null);
5172
+ for (const key of Reflect.ownKeys(source)) clone[key] = source[key];
5173
+ Object.setPrototypeOf(clone, Object.prototype);
5174
+ clone[$OWNER] = t;
5175
+ return clone;
5176
+ }
5119
5177
  /** Copy own `key` from `from` onto `to`. A plain data slot (enumerable,
5120
5178
  * writable, configurable, no accessor) is a bare assignment — the common case
5121
5179
  * and the cheap one; anything else goes through defineProperty so accessors
@@ -5154,22 +5212,68 @@ function scanAccessorsOnce(target) {
5154
5212
  * backing into a prototype, and every later flatten writes into that
5155
5213
  * prototype); above it the clone's O(keys) copy dominates (#3044). */
5156
5214
  const OVERLAY_MIN_KEYS = 32;
5215
+ /** Overlay commit choice (#3689): flatten in place, or rebuild the backing
5216
+ * (materializePB, then the clone path's swap)? Measured on V8 (Node 26): a
5217
+ * committed backing that has served as an overlay prototype is kept as a
5218
+ * FAST-MODE prototype object up to V8's descriptor limit (1020 own
5219
+ * properties), and there every in-place DELETE is O(keys) — normalize to
5220
+ * dictionary plus re-optimize as a prototype, ~0.08 µs × keys — and every
5221
+ * in-place ADD is a map transition copying the descriptors, ~0.006 µs ×
5222
+ * keys. Detaching the overlay first does not help: prototype-ness is a
5223
+ * property of the object's map. One rebuild (wideClone, deletes, writes) is
5224
+ * ~0.07–0.09 µs × keys — the price of ONE in-place delete. So a fold that
5225
+ * deleted anything, or added more than ~16 keys, commits cheaper rebuilt (a
5226
+ * 400-key record churning 100 keys per commit, 10,000 membership readers:
5227
+ * 4.7 ms → 0.22 ms per step); a pure rewrite of existing keys — the
5228
+ * overlay's home case — stays in place at O(written).
5229
+ * Past the descriptor limit V8 keeps the backing in dictionary mode where
5230
+ * every in-place op is O(1), and a rebuild would be a pure O(keys) loss on
5231
+ * exactly the growing-record pattern the overlay exists for (#3044) — the
5232
+ * count gate keeps those in place. The gate reads `kc`, which the set trap
5233
+ * counts up for keys new to the container and the delete commit counts down. */
5234
+ const OVERLAY_REBUILD_MAX_KEYS = 1024;
5235
+ const OVERLAY_REBUILD_MIN_ADDS = 16;
5236
+ function overlayRebuilds(t, pb) {
5237
+ if (t.kc > OVERLAY_REBUILD_MAX_KEYS) return false;
5238
+ if (t.del !== null && t.del.size !== 0) return true;
5239
+ // Every trap write records its key, so the written-key count bounds the
5240
+ // adds — the common few-key fold (#3044's growing record) answers here
5241
+ // without enumerating the overlay.
5242
+ const wk = t.wk;
5243
+ if (wk !== null && wk !== WK_ALL && wk.size <= OVERLAY_REBUILD_MIN_ADDS) return false;
5244
+ const v = t.v;
5245
+ let adds = 0;
5246
+ for (const key of Reflect.ownKeys(pb))
5247
+ if (!hasOwn.call(v, key) && ++adds > OVERLAY_REBUILD_MIN_ADDS) return true;
5248
+ return false;
5249
+ }
5157
5250
  /** Downgrade a prototype-overlay pending backing to the clone path: builds
5158
5251
  * the real container (committed + overlay writes − deletes) that fold will
5159
5252
  * SWAP in as the committed backing, exactly as if the draft had started on
5160
5253
  * the clone path. Consumers that need a complete container (reconcile's
5161
- * diff walks, drafts escaping into other storage) call this. */
5254
+ * diff walks, drafts escaping into other storage) call this, and so does the
5255
+ * commit itself when the fold changed the key set (overlayRebuilds). Returns
5256
+ * the pending backing (the clone, or the pb as-is when not an overlay). */
5162
5257
  function materializePB(target) {
5163
- if (!target.ovl) return;
5164
- const proto = target.pb;
5165
- const clone = cloneRaw(target.v, target);
5166
- for (const key of Reflect.ownKeys(proto)) copyOwn(clone, proto, key);
5258
+ const pb = target.pb;
5259
+ if (!target.ovl) return pb;
5260
+ const clone = target.sc === 2 ? wideClone(target.v, target) : cloneRaw(target.v, target);
5261
+ // Deletes before writes (#3689): on a fast-mode clone (the descriptor
5262
+ // path) the first delete normalizes it once, and the writes then land as
5263
+ // O(1) dictionary adds instead of O(keys) map transitions each. Order is
5264
+ // free semantically — a rewritten key left `del` at write time, so no
5265
+ // write targets a deleted key.
5167
5266
  if (target.del !== null) {
5168
5267
  for (const key of target.del) delete clone[key];
5268
+ target.kc -= target.del.size;
5169
5269
  target.del = null;
5170
5270
  }
5271
+ // Plain-data grade (sc 2): bare assignment, as flattenOverlay does.
5272
+ for (const key of Reflect.ownKeys(pb))
5273
+ target.sc === 2 ? (clone[key] = pb[key]) : copyOwn(clone, pb, key);
5171
5274
  target.pb = clone;
5172
5275
  target.ovl = false;
5276
+ return clone;
5173
5277
  }
5174
5278
  function ensurePB(target) {
5175
5279
  let pb = target.pb;
@@ -5321,6 +5425,7 @@ function adoptPB(target, incoming, eager = false) {
5321
5425
  if (heldMaskView(target) === null) target.hv = target.v;
5322
5426
  target.ht = activeTransition ?? PLAIN_HOLD;
5323
5427
  if (!eager && activeTransition !== null) heldAdoptions.add(target);
5428
+ heldKeys.set(target, [incoming]);
5324
5429
  }
5325
5430
  target.pb = null;
5326
5431
  // Overlay and accessor-scan state describe the OUTGOING backing — a
@@ -5450,6 +5555,7 @@ function flattenOverlay(t, pb) {
5450
5555
  for (const key of Reflect.ownKeys(pb)) t.sc === 2 ? (v[key] = pb[key]) : copyOwn(v, pb, key);
5451
5556
  if (t.del !== null) {
5452
5557
  for (const key of t.del) delete v[key];
5558
+ t.kc -= t.del.size;
5453
5559
  t.del = null;
5454
5560
  }
5455
5561
  t.pb = null;
@@ -5510,11 +5616,13 @@ function drainFolds() {
5510
5616
  foldOlds.set(t, old); // re-queue: commit happens when the hold settles
5511
5617
  continue;
5512
5618
  }
5513
- if (t.ovl) {
5619
+ if (t.ovl && (t.v !== old || !overlayRebuilds(t, pb))) {
5514
5620
  // Overlay flatten (#3044): the backing keeps its identity, so the
5515
5621
  // `t.v === old` gate below skips path copying (the parent slot
5516
5622
  // already points here) and the adopted-notify (setter notifications
5517
- // happened at write time).
5623
+ // happened at write time). A backing privatized mid-batch is a fresh
5624
+ // clone, never a prototype — in-place writes into it are cheap, and
5625
+ // the merge branch below is what a materialized pb would need anyway.
5518
5626
  flattenOverlay(t, pb);
5519
5627
  } else if (t.v !== old) {
5520
5628
  // Privatized mid-batch (#3271): an earlier fold in this drain
@@ -5557,7 +5665,10 @@ function drainFolds() {
5557
5665
  t.pb = null;
5558
5666
  t.wk = null; // written-keys window closes with the fold commit
5559
5667
  } else {
5560
- t.v = pb;
5668
+ // An overlay whose fold changed the key set rebuilds (#3689) and
5669
+ // takes the swap like a clone-path draft: the parent slot re-points
5670
+ // in the path copy below.
5671
+ t.v = t.ovl ? materializePB(t) : pb;
5561
5672
  t.ch = false; // pb is always a plain clone
5562
5673
  t.pb = null;
5563
5674
  t.wk = null; // written-keys window closes with the fold commit
@@ -5823,8 +5934,12 @@ function notifyWrites(t) {
5823
5934
  if (t.ht !== null) t.ht = t.hv = null;
5824
5935
  // Overlay landing (#3352): flatten in place — identity-stable, and
5825
5936
  // privatizeCommitted re-slots the parent itself when it has to clone an
5826
- // unowned seed, so no path copy is needed.
5827
- if (t.ovl) return flattenOverlay(t, pb);
5937
+ // unowned seed, so no path copy is needed. A landing that changed the
5938
+ // key set rebuilds instead (#3689) and swaps like a clone-path landing.
5939
+ if (t.ovl) {
5940
+ if (!overlayRebuilds(t, pb)) return flattenOverlay(t, pb);
5941
+ pb = materializePB(t);
5942
+ }
5828
5943
  const oldBacking = t.v;
5829
5944
  t.pb = null;
5830
5945
  t.v = pb;
@@ -6082,7 +6197,10 @@ function foldHeld(target) {
6082
6197
  }
6083
6198
  return false;
6084
6199
  }
6085
- function readSource(target) {
6200
+ /** The backing a reader is served. `key` (the get/has/descriptor traps)
6201
+ * scopes a fold hold to the keys the fold touched — see pendingBackingVisible —
6202
+ * and an adoption hold to the keys the adoption changed (heldKey). */
6203
+ function readSource(target, key) {
6086
6204
  // Adoption hold first (#3074): an adoption staged under a live transaction
6087
6205
  // (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
6088
6206
  // committed-visibility readers — context-free and children-forbidden ones,
@@ -6091,7 +6209,9 @@ function readSource(target) {
6091
6209
  const ht = target.ht;
6092
6210
  if (ht !== null && !latestReadActive && !inDraft(target) && !getWriteOverride()) {
6093
6211
  const hv = heldMaskView(target);
6094
- if (hv !== null) {
6212
+ // A key the adoption left unchanged derives nothing from the hold, for
6213
+ // any reader (#3706): the backing serves it.
6214
+ if (hv !== null && (key === undefined || heldKey(target, key))) {
6095
6215
  const c = readerContext();
6096
6216
  if (
6097
6217
  c === null ||
@@ -6101,7 +6221,7 @@ function readSource(target) {
6101
6221
  return hv;
6102
6222
  }
6103
6223
  }
6104
- return pendingBackingVisible(target, false) ? target.pb : target.v;
6224
+ return pendingBackingVisible(target, false, key) ? target.pb : target.v;
6105
6225
  }
6106
6226
  /** The single pb-vs-committed visibility decision (#3147), shared by per-key
6107
6227
  * backing reads (readSource) and deep()/snapshot composition (snapshotWalk)
@@ -6118,7 +6238,7 @@ function readSource(target) {
6118
6238
  * ordinary pending staging regardless of owner context (the documented
6119
6239
  * divergence from context-free per-key reads) — but never through a hold:
6120
6240
  * held truth stays masked exactly as it is for per-key readers. */
6121
- function pendingBackingVisible(target, speculative) {
6241
+ function pendingBackingVisible(target, speculative, key) {
6122
6242
  if (target.pb === null) return false;
6123
6243
  // The writer's own channels compose on the pending backing regardless.
6124
6244
  if (inDraft(target) || getWriteOverride()) return true;
@@ -6142,7 +6262,26 @@ function pendingBackingVisible(target, speculative) {
6142
6262
  if (speculative) return txn === null || ownsHold(txn);
6143
6263
  return target.fam !== null && c === null && !foldHeld(target) && txn === null;
6144
6264
  }
6145
- return holdVisible(liveFoldTransition(target), c);
6265
+ const txn = liveFoldTransition(target);
6266
+ // A hold changes what a reader of ONE key sees only through the fold's
6267
+ // writes to that key. A key the fold left alone reads the same from either
6268
+ // backing, so a pass reading it derives nothing from the hold: serve
6269
+ // committed, with no transaction entry and no stale replay (#3688 — a memo
6270
+ // reading an unchanged key beside an independent signal was held with
6271
+ // someone else's action). Per-node reads already have this precision (a
6272
+ // node with nothing staged enters nothing; stageHeldKey skips an equal
6273
+ // value); this is the same rule at the container gate. `wk` is the trap's
6274
+ // record of every write and delete this batch (deletes included, on the
6275
+ // overlay and clone paths alike); null (a fold with no trap writes) or
6276
+ // WK_ALL (an array length write) leaves the whole container held. Same
6277
+ // exclusions as heldFoldTransition: an optimistic family's draft is seeded
6278
+ // from node overrides the trap never saw, and a chained backing's
6279
+ // committed layer is a live proxy, not a frozen twin of the clone.
6280
+ if (txn !== null && key !== undefined && !target.ch && target.fam?.opt !== true) {
6281
+ const wk = target.wk;
6282
+ if (wk != null && wk !== WK_ALL && !wk.has(key)) return false;
6283
+ }
6284
+ return holdVisible(txn, c);
6146
6285
  }
6147
6286
  /** #3164 fold: HELD truth on an optimistic family — a pending backing
6148
6287
  * stamped by a live transition that retains optimism — is masked from LANE
@@ -6456,7 +6595,7 @@ const traps = {
6456
6595
  // memo parity for latest() reads through a projection.
6457
6596
  if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
6458
6597
  pullProjectionForLatest(target);
6459
- const src = readSource(target);
6598
+ const src = readSource(target, key);
6460
6599
  // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
6461
6600
  // deleted keys are tracked aside and read as absent in the pending view.
6462
6601
  if (target.del !== null && src === target.pb && target.del.has(key)) {
@@ -6632,7 +6771,7 @@ const traps = {
6632
6771
  if (key === $OWNER || key === $RECORD) return false;
6633
6772
  if (pendingCheckActive) witnessAffectsMark(target, key);
6634
6773
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
6635
- const src = readSource(target);
6774
+ const src = readSource(target, key);
6636
6775
  let present = key in src;
6637
6776
  // Overlay deletes read as absent in the pending view (#3044).
6638
6777
  if (present && target.del !== null && src === target.pb && target.del.has(key)) present = false;
@@ -6689,7 +6828,7 @@ const traps = {
6689
6828
  if (pendingCheckActive) witnessAffectsMark(target, key);
6690
6829
  const obs = getObserver();
6691
6830
  if (target.fam !== null && obs === null && !inDraft(target)) firewallGate(target);
6692
- const src = readSource(target);
6831
+ const src = readSource(target, key);
6693
6832
  const desc = visibleDescriptor(target, src, key);
6694
6833
  if (!inDraft(target) && obs !== null && !observerHoldsKeySet(target, obs)) {
6695
6834
  // The node is born from the source's presence (as `has` births it),
@@ -6735,10 +6874,12 @@ const traps = {
6735
6874
  }
6736
6875
  } else {
6737
6876
  if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
6738
- // Live own-key estimate for the overlay/clone choice (#3360): `in`
6739
- // sees through an overlay to the committed keys, so this counts keys
6740
- // NEW to the container. Deletes are not un-counted (a stale high
6741
- // count only picks the overlay a little early).
6877
+ // Live own-key estimate for the overlay/clone choice (#3360) and the
6878
+ // overlay's commit choice (#3689): `in` sees through an overlay to the
6879
+ // committed keys, so this counts keys NEW to the container. Overlay
6880
+ // deletes are counted down when they commit (`del` is exact); clone-
6881
+ // path deletes are not (a stale high count on a narrow container only
6882
+ // picks the overlay a little early).
6742
6883
  if (!(key in pb)) target.kc++;
6743
6884
  }
6744
6885
  // Own data keys literally named "prototype"/"constructor" land as data —
@@ -7,12 +7,13 @@ import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, i
7
7
  /** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */ const FALLBACK_FLASH_MS = 150;
8
8
 
9
9
  /**
10
- * Under an owner the observer marked as its own (`OBSERVE.exclude`), or
11
- * framework plumbing itself (`CONFIG_PLUMBING` — the HMR memo between a
12
- * component's root and its body, which is nobody's node): the engine records
13
- * nothing about the node. Plumbing is a bit read and excludes the node alone,
14
- * not what it owns; the observer exclusion is cached per node once any
15
- * exists, before that a flag read.
10
+ * Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
11
+ * re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
12
+ * (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
13
+ * body, which is nobody's node): the engine records nothing about the node.
14
+ * Plumbing is a bit read and excludes the node alone, not what it owns; the
15
+ * observer exclusion is cached per node once any exists, before that a flag
16
+ * read.
16
17
  */ function excludedNode(e) {
17
18
  if ((e.C & CONFIG_PLUMBING) !== 0) return true;
18
19
  if (!anyExcluded()) return false;
@@ -373,15 +374,22 @@ function popFrame(e) {
373
374
  return;
374
375
  }
375
376
  }
376
- const n = {
377
+ // The initial navigation is the document's: its request is the time origin
378
+ // unless the router says otherwise, and it left nowhere.
379
+ const n = e.initial === true;
380
+ const t = {
377
381
  kind: e.kind,
378
- at: e.at ?? now()
382
+ at: e.at ?? (n ? 0 : now())
379
383
  };
380
- if (e.from !== undefined) n.from = e.from;
381
- const t = enclosingInteraction();
382
- if (t !== undefined) n.interaction = t;
383
- originFrames.push(n);
384
- openNavigation(n, e);
384
+ if (!n && e.from !== undefined) t.from = e.from;
385
+ // Declared beats ambient: a router that awaited before writing hands back
386
+ // the origin it captured in the request (`undefined` when the request ran
387
+ // under none — still a declaration); what is on the stack now is whatever
388
+ // happened to be running.
389
+ const o = "interaction" in e ? interactionOf(e.interaction) : enclosingInteraction();
390
+ if (o !== undefined) t.interaction = o;
391
+ originFrames.push(t);
392
+ openNavigation(t, e);
385
393
  }
386
394
 
387
395
  function originEnd() {
@@ -415,7 +423,8 @@ function originEnd() {
415
423
  if (e.to !== undefined && e.to !== n) t.push(e.to);
416
424
  const o = navStates.get(e)?.event.redirects;
417
425
  if (o !== undefined) t.push(`redirected from ${o.map(e => e.to ?? e.name ?? "?").join(" → ")}`);
418
- return `navigation to ${n}${t.length > 0 ? ` (${t.join(", ")})` : ""}`;
426
+ const i = navStates.get(e)?.event.initial === true;
427
+ return `${i ? "initial " : ""}navigation to ${n}${t.length > 0 ? ` (${t.join(", ")})` : ""}`;
419
428
  }
420
429
 
421
430
  default:
@@ -1778,8 +1787,10 @@ function checkLongHold(e, n) {
1778
1787
  // derived override promotes rather than reverts. Same predicate the hold
1779
1788
  // census uses to skip them.
1780
1789
  if (!options.optimisticReverts || isCompanion(e)) return;
1781
- const i = e.Fe;
1782
- if (i && i(n, t)) return;
1790
+ // Method call, as every commit path does: store slot nodes share one
1791
+ // comparator that reads `this._host` (#3687).
1792
+ const i = e.Fe;
1793
+ if (i && i.call(e, n, t)) return;
1783
1794
  const s = nodeName(e);
1784
1795
  // The two values are user data: quoted only under `values: "full"`; the
1785
1796
  // other levels say what happened without saying what was shown.
@@ -1841,17 +1852,17 @@ function checkLongHold(e, n) {
1841
1852
  for (const t of e) {
1842
1853
  n.owners++;
1843
1854
  // Iterative: a deep tree must not grow the call stack.
1844
- let e = t._n;
1855
+ let e = t.On;
1845
1856
  for (;;) {
1846
1857
  while (e !== null) {
1847
1858
  n.owners++;
1848
1859
  if (typeof e.Se === "function") meet(e);
1849
- if (e._n !== null) i.push(e);
1850
- e = e.Fn;
1860
+ if (e.On !== null) i.push(e);
1861
+ e = e._n;
1851
1862
  }
1852
1863
  const t = i.pop();
1853
1864
  if (t === undefined) break;
1854
- e = t._n;
1865
+ e = t.On;
1855
1866
  }
1856
1867
  }
1857
1868
  for (let e = 0; e < o.length; e++) {
@@ -2070,6 +2081,7 @@ function openNavigation(e, n) {
2070
2081
  writes: 0,
2071
2082
  origin: e
2072
2083
  };
2084
+ if (n.initial === true) t.initial = true;
2073
2085
  if (e.from !== undefined) t.from = e.from;
2074
2086
  if (e.interaction !== undefined) t.interaction = e.interaction;
2075
2087
  const o = {
@@ -2295,7 +2307,10 @@ function settleNavigation(e, n, t) {
2295
2307
  o.settledMs = now() - o.at;
2296
2308
  o.outcome = n;
2297
2309
  if (t !== undefined) o.hold = t;
2298
- for (const e of folds) e.navigation?.(o);
2310
+ // The initial declaration is not a navigation the person waited on: its
2311
+ // `settledMs` is document start → router built, which would read as the
2312
+ // route's responsiveness in the feedback tables. Consumers get the record.
2313
+ if (o.initial !== true) for (const e of folds) e.navigation?.(o);
2299
2314
  records.emit("navigation", o, undefined);
2300
2315
  trackGraph(o);
2301
2316
  // The interaction that performed it may have been waiting only on this.
@@ -180,20 +180,29 @@ const CONFIG_HAS_LANE = 1 << 10;
180
180
  * transaction. A parked node (status propagation stamps `_transition`
181
181
  * without recomputing) recomputed when its source lands otherwise disposed
182
182
  * its committed children mid-hold, running their cleanups before the
183
- * transaction's atomic reveal. Cleared by `commitPendingNode`. */ const CONFIG_HELD_CHILDREN = 1 << 20;
183
+ * transaction's atomic reveal. Cleared by `commitPendingNode`. Transaction
184
+ * work only (A15 lane work and transaction work, #3698): zombies are parked
185
+ * by a pass under a held transaction; a pass over a lane parks a LANE frame
186
+ * instead (`CONFIG_LANE_FRAME`), whatever the node's kind. */ const CONFIG_HELD_CHILDREN = 1 << 20;
184
187
 
185
188
  /** The frame parked in `_pendingFirstChild` / `_pendingDisposal` is a LANE
186
- * frame (#3662, A15 lanes corollary): an effect's lane pass direct-commits,
187
- * so the frame it replaces leaves the screen when the lane's queue applies
188
- * the run (A30) — not at the action's commit like #3404's transaction
189
- * zombies, and not at the pass (a held lane defers the run). Drained by the
190
- * lane's render entry the parking site pushed ahead of the new frame's
191
- * effects (cleanups before side effects), by `commitPendingNode` if a hold
192
- * commits the node first, or with the owner's death. While set the parked
193
- * frame is not a hold (the node is not queued or stamped for it), a
194
- * superseding pass disposes the never-shown live children on the spot, and a
195
- * lane-channel dirty on a member is cancelled (`laneZombie`). Effects only: a
196
- * memo's lane pass publishes an override (A17, #3479). */ const CONFIG_LANE_FRAME = 1 << 26;
189
+ * frame (#3662, #3698; A15 lane work and transaction work): a lane pass —
190
+ * on an effect or a memo alike — publishes into the lane's frame (an
191
+ * effect's run; a memo's derived override, A17), so the frame it replaces
192
+ * leaves the screen when the lane applies (A30) — not at the action's
193
+ * commit like #3404's transaction zombies, and not at the pass (a held lane
194
+ * defers the apply with the frame still displayed). Drained by the lane's
195
+ * render entry the parking site pushed ahead of the new frame's effects
196
+ * (cleanups before side effects), by `commitPendingNode` if a hold commits
197
+ * the node first, or with the owner's death. While set the parked frame is
198
+ * not a hold (the node is not queued or stamped for it — lane work never
199
+ * makes its node transaction work), a superseding pass disposes the
200
+ * never-shown live children on the spot, and a lane-channel dirty on a
201
+ * member is cancelled (`laneZombie`). Ruled 2026-09-28 (#3698): a pass is
202
+ * lane work or transaction work by its owner, never by node kind. #3662
203
+ * flagged effects only, and a memo's lane pass parked a transaction zombie
204
+ * that queued and stamped the memo as the action's pending node, so its
205
+ * next mainline recompute re-entered the hold. */ const CONFIG_LANE_FRAME = 1 << 26;
197
206
 
198
207
  /** In-flight async node whose inputs were PUBLISHED while it was pending: a
199
208
  * batch or transaction committed with the node still `STATUS_PENDING` (an