@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.
package/dist/dev.js CHANGED
@@ -42,11 +42,11 @@ import {
42
42
  K as currentOptimisticLane,
43
43
  L as enterStagedRead,
44
44
  M as currentTransition,
45
- P as queuePendingNode,
46
- Q as latestReadActive,
47
- T as REACTIVE_MANUAL_WRITE,
48
- U as REACTIVE_OPTIMISTIC_DIRTY,
49
- V as assignOrMergeLane,
45
+ P as assignOrMergeLane,
46
+ Q as queuePendingNode,
47
+ T as latestReadActive,
48
+ U as REACTIVE_MANUAL_WRITE,
49
+ V as REACTIVE_OPTIMISTIC_DIRTY,
50
50
  W as LANE_RUN,
51
51
  X as attrHooks,
52
52
  Y as EFFECT_USER,
@@ -750,8 +750,53 @@ function overrideRead(el, c) {
750
750
  * action provenance, or an A17-silent confirmation (#3331). Before this the
751
751
  * landing took setSignal's plain path: a differing truth staged silently
752
752
  * under the override and the graph never moved until the commit.
753
+ *
754
+ * Slot arm (list-matrix F1): `mapArray`'s writes to its per-slot signals —
755
+ * the row accessors of index mode, the index accessors of keyed mode; never
756
+ * CONFIG_OPTIMISTIC, which is how the arm tells them from the store landings
757
+ * above — once a lane pass has run over the map. The list's frame lives in
758
+ * these writes as much as in the computed's result. Under a LANE pass the
759
+ * write is the lane's frame, as the computed's result is (lanes stage,
760
+ * `laneOverride`): the slot joins the lane carrying a DERIVED override —
761
+ * `_value` stays the committed row, the lane's readers and untracked reads
762
+ * see the override (A17), an off-lane render effect sees the committed frame
763
+ * (#3460), the revert promotes or drops it with the lane's transaction. The
764
+ * gate compares against the slot the pass publishes to (INV-11): the override
765
+ * when armed, the committed row otherwise. A plain `setSignal` here staged
766
+ * the write into the ACTION's transaction: `<For>` without `keyed` showed the
767
+ * pre-action list for the whole action while the keyed modes showed the
768
+ * optimistic one. A PLAIN pass over a slot still carrying the lane's frame —
769
+ * the landing, the reversion, a mainline re-pass — is the truth arriving
770
+ * under an override and takes the landing path below: a differing truth
771
+ * supersedes, an equal one confirms and the revert promotes. Every pass
772
+ * rewrites every slot whose value differs from the previous frame's (index
773
+ * mode rewrites every surviving slot), so a slot the landing does NOT write
774
+ * holds, by construction, what the truth yields. A slot with neither a lane
775
+ * nor an override is the plain write.
753
776
  */
754
777
  function landOnOverride(el, v) {
778
+ if (!(el._config & CONFIG_OPTIMISTIC)) {
779
+ const lane = currentOptimisticLane;
780
+ if (lane === null) {
781
+ if (!hasActiveOverride(el)) return setSignal(el, v);
782
+ } else {
783
+ // INV-11: the gate compares against the slot this pass publishes to —
784
+ // the override when one is armed, the committed row otherwise.
785
+ if (
786
+ !el._equals ||
787
+ !el._equals(hasActiveOverride(el) ? unwrapOverride(el._x._overrideValue) : el._value, v)
788
+ ) {
789
+ // Membership before the publish: `laneOverride` files the slot under
790
+ // the lane's transaction, and the read path routes a tracked reader
791
+ // of a lane member through `overrideRead` (CONFIG_HAS_LANE).
792
+ assignOrMergeLane(el, lane);
793
+ laneOverride(el, v, lane);
794
+ insertSubs(el, true);
795
+ schedule();
796
+ }
797
+ return v;
798
+ }
799
+ }
755
800
  const currentValue = el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
756
801
  if (typeof v === "function") v = v(currentValue);
757
802
  if (attrHooks !== null) attrHooks.write(el, currentValue, v);
@@ -4749,6 +4794,8 @@ function getNode(
4749
4794
  // committed is the held view `hv[key]`, staged is `v[key]`.
4750
4795
  const fold = heldFoldTransition(target);
4751
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;
4752
4799
  if (held !== null) current = target.hv[key];
4753
4800
  else if ((held = fold) !== null) current = target.v[key];
4754
4801
  // Create-floor diet: slotSignal bakes the whole node into one literal —
@@ -4871,6 +4918,45 @@ function holdVisible(txn, c) {
4871
4918
  enterStagedRead(null, txn);
4872
4919
  return true;
4873
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
+ }
4874
4960
  function stageHeldKey(node, nv, txn) {
4875
4961
  if (slotNodeEquals.call(node, node._value, nv)) return;
4876
4962
  node._pendingValue = nv;
@@ -5071,6 +5157,23 @@ function cloneRaw(source, t) {
5071
5157
  clone[$OWNER] = t;
5072
5158
  return clone;
5073
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
+ }
5074
5177
  /** Copy own `key` from `from` onto `to`. A plain data slot (enumerable,
5075
5178
  * writable, configurable, no accessor) is a bare assignment — the common case
5076
5179
  * and the cheap one; anything else goes through defineProperty so accessors
@@ -5109,22 +5212,68 @@ function scanAccessorsOnce(target) {
5109
5212
  * backing into a prototype, and every later flatten writes into that
5110
5213
  * prototype); above it the clone's O(keys) copy dominates (#3044). */
5111
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
+ }
5112
5250
  /** Downgrade a prototype-overlay pending backing to the clone path: builds
5113
5251
  * the real container (committed + overlay writes − deletes) that fold will
5114
5252
  * SWAP in as the committed backing, exactly as if the draft had started on
5115
5253
  * the clone path. Consumers that need a complete container (reconcile's
5116
- * 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). */
5117
5257
  function materializePB(target) {
5118
- if (!target.ovl) return;
5119
- const proto = target.pb;
5120
- const clone = cloneRaw(target.v, target);
5121
- 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.
5122
5266
  if (target.del !== null) {
5123
5267
  for (const key of target.del) delete clone[key];
5268
+ target.kc -= target.del.size;
5124
5269
  target.del = null;
5125
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);
5126
5274
  target.pb = clone;
5127
5275
  target.ovl = false;
5276
+ return clone;
5128
5277
  }
5129
5278
  function ensurePB(target) {
5130
5279
  let pb = target.pb;
@@ -5276,6 +5425,7 @@ function adoptPB(target, incoming, eager = false) {
5276
5425
  if (heldMaskView(target) === null) target.hv = target.v;
5277
5426
  target.ht = activeTransition ?? PLAIN_HOLD;
5278
5427
  if (!eager && activeTransition !== null) heldAdoptions.add(target);
5428
+ heldKeys.set(target, [incoming]);
5279
5429
  }
5280
5430
  target.pb = null;
5281
5431
  // Overlay and accessor-scan state describe the OUTGOING backing — a
@@ -5405,6 +5555,7 @@ function flattenOverlay(t, pb) {
5405
5555
  for (const key of Reflect.ownKeys(pb)) t.sc === 2 ? (v[key] = pb[key]) : copyOwn(v, pb, key);
5406
5556
  if (t.del !== null) {
5407
5557
  for (const key of t.del) delete v[key];
5558
+ t.kc -= t.del.size;
5408
5559
  t.del = null;
5409
5560
  }
5410
5561
  t.pb = null;
@@ -5465,11 +5616,13 @@ function drainFolds() {
5465
5616
  foldOlds.set(t, old); // re-queue: commit happens when the hold settles
5466
5617
  continue;
5467
5618
  }
5468
- if (t.ovl) {
5619
+ if (t.ovl && (t.v !== old || !overlayRebuilds(t, pb))) {
5469
5620
  // Overlay flatten (#3044): the backing keeps its identity, so the
5470
5621
  // `t.v === old` gate below skips path copying (the parent slot
5471
5622
  // already points here) and the adopted-notify (setter notifications
5472
- // 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.
5473
5626
  flattenOverlay(t, pb);
5474
5627
  } else if (t.v !== old) {
5475
5628
  // Privatized mid-batch (#3271): an earlier fold in this drain
@@ -5512,7 +5665,10 @@ function drainFolds() {
5512
5665
  t.pb = null;
5513
5666
  t.wk = null; // written-keys window closes with the fold commit
5514
5667
  } else {
5515
- 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;
5516
5672
  t.ch = false; // pb is always a plain clone
5517
5673
  t.pb = null;
5518
5674
  t.wk = null; // written-keys window closes with the fold commit
@@ -5778,8 +5934,12 @@ function notifyWrites(t) {
5778
5934
  if (t.ht !== null) t.ht = t.hv = null;
5779
5935
  // Overlay landing (#3352): flatten in place — identity-stable, and
5780
5936
  // privatizeCommitted re-slots the parent itself when it has to clone an
5781
- // unowned seed, so no path copy is needed.
5782
- 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
+ }
5783
5943
  const oldBacking = t.v;
5784
5944
  t.pb = null;
5785
5945
  t.v = pb;
@@ -6037,7 +6197,10 @@ function foldHeld(target) {
6037
6197
  }
6038
6198
  return false;
6039
6199
  }
6040
- 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) {
6041
6204
  // Adoption hold first (#3074): an adoption staged under a live transaction
6042
6205
  // (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
6043
6206
  // committed-visibility readers — context-free and children-forbidden ones,
@@ -6046,7 +6209,9 @@ function readSource(target) {
6046
6209
  const ht = target.ht;
6047
6210
  if (ht !== null && !latestReadActive && !inDraft(target) && !getWriteOverride()) {
6048
6211
  const hv = heldMaskView(target);
6049
- 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))) {
6050
6215
  const c = readerContext();
6051
6216
  if (
6052
6217
  c === null ||
@@ -6056,7 +6221,7 @@ function readSource(target) {
6056
6221
  return hv;
6057
6222
  }
6058
6223
  }
6059
- return pendingBackingVisible(target, false) ? target.pb : target.v;
6224
+ return pendingBackingVisible(target, false, key) ? target.pb : target.v;
6060
6225
  }
6061
6226
  /** The single pb-vs-committed visibility decision (#3147), shared by per-key
6062
6227
  * backing reads (readSource) and deep()/snapshot composition (snapshotWalk)
@@ -6073,7 +6238,7 @@ function readSource(target) {
6073
6238
  * ordinary pending staging regardless of owner context (the documented
6074
6239
  * divergence from context-free per-key reads) — but never through a hold:
6075
6240
  * held truth stays masked exactly as it is for per-key readers. */
6076
- function pendingBackingVisible(target, speculative) {
6241
+ function pendingBackingVisible(target, speculative, key) {
6077
6242
  if (target.pb === null) return false;
6078
6243
  // The writer's own channels compose on the pending backing regardless.
6079
6244
  if (inDraft(target) || getWriteOverride()) return true;
@@ -6097,7 +6262,26 @@ function pendingBackingVisible(target, speculative) {
6097
6262
  if (speculative) return txn === null || ownsHold(txn);
6098
6263
  return target.fam !== null && c === null && !foldHeld(target) && txn === null;
6099
6264
  }
6100
- 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);
6101
6285
  }
6102
6286
  /** #3164 fold: HELD truth on an optimistic family — a pending backing
6103
6287
  * stamped by a live transition that retains optimism — is masked from LANE
@@ -6411,7 +6595,7 @@ const traps = {
6411
6595
  // memo parity for latest() reads through a projection.
6412
6596
  if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
6413
6597
  pullProjectionForLatest(target);
6414
- const src = readSource(target);
6598
+ const src = readSource(target, key);
6415
6599
  // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
6416
6600
  // deleted keys are tracked aside and read as absent in the pending view.
6417
6601
  if (target.del !== null && src === target.pb && target.del.has(key)) {
@@ -6587,7 +6771,7 @@ const traps = {
6587
6771
  if (key === $OWNER || key === $RECORD) return false;
6588
6772
  if (pendingCheckActive) witnessAffectsMark(target, key);
6589
6773
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
6590
- const src = readSource(target);
6774
+ const src = readSource(target, key);
6591
6775
  let present = key in src;
6592
6776
  // Overlay deletes read as absent in the pending view (#3044).
6593
6777
  if (present && target.del !== null && src === target.pb && target.del.has(key)) present = false;
@@ -6644,7 +6828,7 @@ const traps = {
6644
6828
  if (pendingCheckActive) witnessAffectsMark(target, key);
6645
6829
  const obs = getObserver();
6646
6830
  if (target.fam !== null && obs === null && !inDraft(target)) firewallGate(target);
6647
- const src = readSource(target);
6831
+ const src = readSource(target, key);
6648
6832
  const desc = visibleDescriptor(target, src, key);
6649
6833
  if (!inDraft(target) && obs !== null && !observerHoldsKeySet(target, obs)) {
6650
6834
  // The node is born from the source's presence (as `has` births it),
@@ -6690,10 +6874,12 @@ const traps = {
6690
6874
  }
6691
6875
  } else {
6692
6876
  if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
6693
- // Live own-key estimate for the overlay/clone choice (#3360): `in`
6694
- // sees through an overlay to the committed keys, so this counts keys
6695
- // NEW to the container. Deletes are not un-counted (a stale high
6696
- // 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).
6697
6883
  if (!(key in pb)) target.kc++;
6698
6884
  }
6699
6885
  // Own data keys literally named "prototype"/"constructor" land as data —
@@ -8960,6 +9146,30 @@ function updateKeyedMap() {
8960
9146
  // entered rows, for the list-identity census.
8961
9147
  removedItems,
8962
9148
  createdItems;
9149
+ // The pass's write to a per-slot signal (a row accessor in index mode,
9150
+ // an index accessor in keyed mode). The list's frame lives in these
9151
+ // writes as much as in the computed's result, so under a LANE pass they
9152
+ // are the lane's frame too (F1): a plain `setSignal` staged them into
9153
+ // the action's transaction and the row readers were served the
9154
+ // committed value until the action landed — `<For>` without `keyed`
9155
+ // showed the pre-action list for the whole action while the keyed modes
9156
+ // showed the optimistic one. The engine publishes the write as a
9157
+ // derived override on the slot (lanes stage, A17) and lands or
9158
+ // supersedes it when a plain pass later writes a slot still carrying
9159
+ // one (A18) — `landOnOverride`'s slot arm. Decided once per pass, not
9160
+ // per slot: a lane pass marks the map (`_laneSlots`: its slots may
9161
+ // carry overrides), a marked map routes every slot write through the
9162
+ // engine while any lane is live — a slot's override resolves with its
9163
+ // lane's transaction, or with the batch for an orphan lane
9164
+ // (resolveOptimisticNodes, then cleanupCompletedLanes, both before the
9165
+ // completion wakes any recompute), so a pass that finds no lane live
9166
+ // knows the slots are clean and drops the mark — and an unmarked map
9167
+ // takes `setSignal` directly. The engine is installed whenever a lane
9168
+ // is active.
9169
+ const write = (this._laneSlots =
9170
+ currentOptimisticLane !== null || (this._laneSlots && activeLanes.size !== 0))
9171
+ ? GlobalQueue._landOnOverride
9172
+ : setSignal;
8963
9173
  // skip common prefix
8964
9174
  for (
8965
9175
  start = 0, end = Math.min(this._len, newLen);
@@ -8968,7 +9178,7 @@ function updateKeyedMap() {
8968
9178
  (this._rows && compare(this._key, this._items[start], newItems[start])));
8969
9179
  start++
8970
9180
  ) {
8971
- if (this._rows) setSignal(this._rows[start], newItems[start]);
9181
+ if (this._rows) write(this._rows[start], newItems[start]);
8972
9182
  }
8973
9183
  // skip common suffix — counted only; retained entries land in one pass
8974
9184
  // at commit instead of being staged and copied twice
@@ -9069,19 +9279,19 @@ function updateKeyedMap() {
9069
9279
  indexes && (indexes[i] = this._indexes[i]);
9070
9280
  }
9071
9281
  for (j = start; j <= newEnd; j++) {
9072
- if (rows) setSignal(rows[j], newItems[j]);
9073
- if (indexes) setSignal(indexes[j], j);
9282
+ if (rows) write(rows[j], newItems[j]);
9283
+ if (indexes) write(indexes[j], j);
9074
9284
  }
9075
9285
  for (j = newEnd + 1; j < newLen; j++) {
9076
9286
  temp[j] = this._mappings[j - dif];
9077
9287
  tempNodes[j] = this._nodes[j - dif];
9078
9288
  if (rows) {
9079
9289
  rows[j] = this._rows[j - dif];
9080
- setSignal(rows[j], newItems[j]);
9290
+ write(rows[j], newItems[j]);
9081
9291
  }
9082
9292
  if (indexes) {
9083
9293
  indexes[j] = this._indexes[j - dif];
9084
- if (dif !== 0) setSignal(indexes[j], j);
9294
+ if (dif !== 0) write(indexes[j], j);
9085
9295
  }
9086
9296
  }
9087
9297
  this._mappings = temp;
@@ -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