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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/dev.js +1454 -118
  2. package/dist/node.cjs +2709 -1396
  3. package/dist/prod/affects.js +13 -12
  4. package/dist/prod/boundaries.js +39 -34
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +48 -46
  7. package/dist/prod/core/core.js +99 -67
  8. package/dist/prod/core/effect.js +25 -28
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +85 -49
  11. package/dist/prod/core/heap.js +10 -10
  12. package/dist/prod/core/lanes.js +19 -19
  13. package/dist/prod/core/optimistic.js +66 -41
  14. package/dist/prod/core/owner.js +13 -13
  15. package/dist/prod/core/scheduler.js +131 -85
  16. package/dist/prod/core/verdict.js +36 -15
  17. package/dist/prod/index.js +4 -0
  18. package/dist/prod/map.js +101 -101
  19. package/dist/prod/signals.js +1 -1
  20. package/dist/prod/store/index.js +2 -0
  21. package/dist/prod/store/next/optimistic.js +65 -11
  22. package/dist/prod/store/next/patch-hooks.js +13 -0
  23. package/dist/prod/store/next/patch.js +614 -0
  24. package/dist/prod/store/next/projection.js +107 -45
  25. package/dist/prod/store/next/reconcile.js +307 -120
  26. package/dist/prod/store/next/store.js +321 -92
  27. package/dist/prod/store/next/target.js +13 -4
  28. package/dist/prod/store/store.js +5 -5
  29. package/dist/types/core/core.d.ts +15 -1
  30. package/dist/types/core/dev.d.ts +8 -0
  31. package/dist/types/core/graph.d.ts +22 -0
  32. package/dist/types/core/invariants.d.ts +1 -1
  33. package/dist/types/core/scheduler.d.ts +12 -0
  34. package/dist/types/store/index.d.ts +2 -0
  35. package/dist/types/store/next/patch-hooks.d.ts +41 -0
  36. package/dist/types/store/next/patch.d.ts +91 -0
  37. package/dist/types/store/next/reconcile.d.ts +14 -0
  38. package/dist/types/store/next/store.d.ts +30 -2
  39. package/dist/types/store/next/target.d.ts +58 -8
  40. package/dist/types-cjs/core/core.d.cts +15 -1
  41. package/dist/types-cjs/core/dev.d.cts +8 -0
  42. package/dist/types-cjs/core/graph.d.cts +22 -0
  43. package/dist/types-cjs/core/invariants.d.cts +1 -1
  44. package/dist/types-cjs/core/scheduler.d.cts +12 -0
  45. package/dist/types-cjs/store/index.d.cts +2 -0
  46. package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
  47. package/dist/types-cjs/store/next/patch.d.cts +91 -0
  48. package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
  49. package/dist/types-cjs/store/next/store.d.cts +30 -2
  50. package/dist/types-cjs/store/next/target.d.cts +58 -8
  51. package/package.json +14 -14
  52. package/dist/types/store/optimistic.d.ts +0 -45
  53. package/dist/types/store/projection.d.ts +0 -70
  54. package/dist/types/store/reconcile.d.ts +0 -46
  55. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  56. package/dist/types-cjs/store/projection.d.cts +0 -70
  57. package/dist/types-cjs/store/reconcile.d.cts +0 -46
@@ -1,4 +1,4 @@
1
- import { CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE, STATUS_PENDING, REACTIVE_RECOMPUTING_DEPS } from "./constants.js";
1
+ import { CONFIG_AUTO_DISPOSE, STATUS_PENDING, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, REACTIVE_RECOMPUTING_DEPS } from "./constants.js";
2
2
 
3
3
  import { deleteFromHeap, queueFor } from "./heap.js";
4
4
 
@@ -8,36 +8,36 @@ import { bumpNotifyEpoch } from "./scheduler.js";
8
8
 
9
9
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
10
10
  function unlinkSubs(e) {
11
- const l = e.ut;
12
- const n = e.it;
13
- const u = e.fe;
14
- const s = e.el;
15
- if (u !== null) u.el = s; else l.rt = s;
16
- if (s !== null) s.fe = u; else {
17
- l.u = u;
18
- if (u === null) {
19
- l.o?.ft?.();
11
+ const n = e.ut;
12
+ const l = e.it;
13
+ const o = e.ae;
14
+ const u = e.en;
15
+ if (o !== null) o.en = u; else n.rt = u;
16
+ if (u !== null) u.ae = o; else {
17
+ n.u = o;
18
+ if (o === null) {
19
+ n.o?.ft?.();
20
20
  // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
21
21
  // A pending node is exempt: its in-flight async work (or the
22
22
  // transition holding it) is an observer — tearing down would orphan
23
23
  // the work and re-execute it on the next read. The settle path runs
24
24
  // this same last-one-out check when that observer releases (the
25
- // untracked-read dispose in core.ts guards on pending identically).
26
- const e = l;
27
- e.Se && e.T & CONFIG_AUTO_DISPOSE && !(e.ie & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
25
+ // untracked-read dormancy sweep guards on pending identically).
26
+ const e = n;
27
+ e.oe && e.T & CONFIG_AUTO_DISPOSE && !(e.ie & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
28
28
  }
29
29
  }
30
- return n;
30
+ return l;
31
31
  }
32
32
 
33
33
  function trimStaleDeps(e) {
34
- const l = e.Ye;
35
- let n = l !== null ? l.it : e.nt;
36
- if (n !== null) {
34
+ const n = e.Ye;
35
+ let l = n !== null ? n.it : e.nt;
36
+ if (l !== null) {
37
37
  do {
38
- n = unlinkSubs(n);
39
- } while (n !== null);
40
- if (l !== null) l.it = null; else e.nt = null;
38
+ l = unlinkSubs(l);
39
+ } while (l !== null);
40
+ if (n !== null) n.it = null; else e.nt = null;
41
41
  }
42
42
  }
43
43
 
@@ -46,11 +46,11 @@ function trimStaleDeps(e) {
46
46
  // and skipping early also avoids adding one (hidden-class churn) via the
47
47
  // null-out below.
48
48
  function clearDeps(e) {
49
- let l = e.nt;
50
- if (!l) return;
49
+ let n = e.nt;
50
+ if (!n) return;
51
51
  do {
52
- l = unlinkSubs(l);
53
- } while (l !== null);
52
+ n = unlinkSubs(n);
53
+ } while (n !== null);
54
54
  e.nt = null;
55
55
  e.Ye = null;
56
56
  }
@@ -61,27 +61,63 @@ function unobserved(e) {
61
61
  disposeChildren(e, true);
62
62
  }
63
63
 
64
+ /**
65
+ * Deferred dormancy for never-observed auto-dispose computeds (#3078).
66
+ *
67
+ * An untracked top-level read of a subscriber-less observation-lifecycle memo
68
+ * used to call unobserved() inline at the end of read(). That kept the leak
69
+ * closed (the compute links the memo into its deps' sub lists — without a
70
+ * teardown point a never-observed memo is retained by its sources forever;
71
+ * upstream alien-signals has exactly this retention), but it made reads
72
+ * destructive: each read disposed the node, the next read revived it with a
73
+ * full recompute in whatever ambient transition/lane context happened to be
74
+ * current, so consecutive reads could return different answers with no write
75
+ * in between.
76
+ *
77
+ * Instead, reads queue the node here and the scheduler sweeps at the top of
78
+ * the next flush (before runHeap, so a same-tick dirtying is reclaimed
79
+ * instead of recomputed). Reads become idempotent within a tick (the node
80
+ * stays alive and serves its cache, uniform with observed memos) while
81
+ * reclamation still happens within one microtask — the enqueue site arms
82
+ * schedule(), so a flush is guaranteed even when no other work is queued.
83
+ */ const dormantNodes = new Set;
84
+
85
+ function sweepDormant() {
86
+ if (dormantNodes.size === 0) return;
87
+ for (const e of dormantNodes) {
88
+ // Re-validate at sweep time: the node may have gained a subscriber (its
89
+ // lifecycle is the unlinkSubs cascade now), gone pending (in-flight async
90
+ // is an observer; the settle path re-runs last-one-out), lost its
91
+ // AUTO_DISPOSE bit (owner teardown strips it, #3024), or already been
92
+ // torn down.
93
+ if (!e.u && e.T & CONFIG_AUTO_DISPOSE && !(e.S & STATUS_PENDING) && !(e.ie & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
94
+ unobserved(e);
95
+ }
96
+ }
97
+ dormantNodes.clear();
98
+ }
99
+
64
100
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
65
- function link(e, l, n = false) {
101
+ function link(e, n, l = false) {
66
102
  // Repeat touches within one pass AND-combine `_pendingObserver`: a probe
67
103
  // read (`isPending(() => x())`) beside a value read of the same dep must
68
104
  // not relabel the value dependency as probe-only — the value read is what
69
105
  // real-error propagation and affects() coverage key off, regardless of
70
106
  // read order within the computation.
71
- const u = l.Ye;
72
- if (u !== null && u.ut === e) {
73
- u.me &&= n;
107
+ const o = n.Ye;
108
+ if (o !== null && o.ut === e) {
109
+ o.me &&= l;
74
110
  return;
75
111
  }
76
- let s = null;
77
- const o = l.ie & REACTIVE_RECOMPUTING_DEPS;
78
- if (o) {
79
- s = u !== null ? u.it : l.nt;
80
- if (s !== null && s.ut === e) {
81
- s.ll = l.Ze;
82
- l.Ye = s;
112
+ let u = null;
113
+ const t = n.ie & REACTIVE_RECOMPUTING_DEPS;
114
+ if (t) {
115
+ u = o !== null ? o.it : n.nt;
116
+ if (u !== null && u.ut === e) {
117
+ u.nn = n.Ze;
118
+ n.Ye = u;
83
119
  // First touch of this pass: the previous pass's label is stale.
84
- s.me = n;
120
+ u.me = l;
85
121
  return;
86
122
  }
87
123
  }
@@ -90,26 +126,26 @@ function link(e, l, n = false) {
90
126
  // [deps.._depsTail] prefix — the O(1) equivalent of scanning the dep list
91
127
  // (the old alien-signals `isValidLink` walk, O(n²) when a computation
92
128
  // re-reads earlier deps non-consecutively, e.g. store leaf reads).
93
- const t = e.rt;
94
- if (t !== null && t.ae === l && (!o || t.ll === l.Ze)) {
129
+ const s = e.rt;
130
+ if (s !== null && s.ce === n && (!t || s.nn === n.Ze)) {
95
131
  // Gen-matched during a recompute = repeat touch this pass (AND); outside
96
132
  // a recompute there is no pass boundary, so the latest read labels it.
97
- if (o) t.me &&= n; else t.me = n;
133
+ if (t) s.me &&= l; else s.me = l;
98
134
  return;
99
135
  }
100
- const i = l.Ye = e.rt = {
136
+ const r = n.Ye = e.rt = {
101
137
  ut: e,
102
- ae: l,
103
- it: s,
104
- el: t,
105
- fe: null,
106
- ll: l.Ze,
107
- me: n
138
+ ce: n,
139
+ it: u,
140
+ en: s,
141
+ ae: null,
142
+ nn: n.Ze,
143
+ me: l
108
144
  };
109
- if (u !== null) u.it = i; else l.nt = i;
110
- if (t !== null) t.fe = i; else e.u = i;
145
+ if (o !== null) o.it = r; else n.nt = r;
146
+ if (s !== null) s.ae = r; else e.u = r;
111
147
  // New subscriber edge: staged-rewrite skips (§12d) must not miss it.
112
148
  bumpNotifyEpoch();
113
149
  }
114
150
 
115
- export { clearDeps, link, trimStaleDeps, unlinkSubs, unobserved };
151
+ export { clearDeps, dormantNodes, link, sweepDormant, trimStaleDeps, unlinkSubs, unobserved };
@@ -1,6 +1,6 @@
1
1
  import "./core.js";
2
2
 
3
- import { REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, EFFECT_TRACKED, EFFECT_USER, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_ZOMBIE, REACTIVE_CHECK, REACTIVE_DIRTY, CONFIG_FW_CHILDREN } from "./constants.js";
3
+ import { REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, EFFECT_TRACKED, EFFECT_USER, REACTIVE_ZOMBIE, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_CHECK, REACTIVE_DIRTY, CONFIG_FW_CHILDREN } from "./constants.js";
4
4
 
5
5
  import { zombieQueue, dirtyQueue } from "./scheduler.js";
6
6
 
@@ -27,7 +27,7 @@ import { zombieQueue, dirtyQueue } from "./scheduler.js";
27
27
  }
28
28
 
29
29
  function actualInsertIntoHeap(e, E) {
30
- const t = (e.ve?.Ct ? e.ve.Ot?.Le : e.ve?.Le) ?? -1;
30
+ const t = (e.ke?.Ct ? e.ke.Ot?.Le : e.ke?.Le) ?? -1;
31
31
  if (t >= e.Le) e.Le = t + 1;
32
32
  const n = e.Le;
33
33
  const I = E.eE[n];
@@ -97,16 +97,16 @@ function markNode(e, E = REACTIVE_DIRTY) {
97
97
  const t = e.ie;
98
98
  if ((t & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= E) return;
99
99
  e.ie = t & -4 | E;
100
- for (let E = e.u; E !== null; E = E.fe) {
101
- markNode(E.ae, REACTIVE_CHECK);
100
+ for (let E = e.u; E !== null; E = E.ae) {
101
+ markNode(E.ce, REACTIVE_CHECK);
102
102
  }
103
103
  // Firewall children (projection machinery only): gate the cold-extension
104
104
  // deref on the config bit — markNode runs per sub edge per write, and an
105
105
  // unconditional _x chase here taxed every propagation (diamond -22%).
106
106
  if (e.T & CONFIG_FW_CHILDREN) {
107
- for (let E = e.o.l; E !== null; E = E.ce) {
108
- for (let e = E.u; e !== null; e = e.fe) {
109
- markNode(e.ae, REACTIVE_CHECK);
107
+ for (let E = e.o.i; E !== null; E = E.Se) {
108
+ for (let e = E.u; e !== null; e = e.ae) {
109
+ markNode(e.ce, REACTIVE_CHECK);
110
110
  }
111
111
  }
112
112
  }
@@ -130,17 +130,17 @@ function adjustHeight(e, E) {
130
130
  for (let E = e.nt; E; E = E.it) {
131
131
  const e = E.ut;
132
132
  const n = e.lt || e;
133
- if (n.Se && n.Le >= t) t = n.Le + 1;
133
+ if (n.oe && n.Le >= t) t = n.Le + 1;
134
134
  }
135
135
  if (e.Le !== t) {
136
136
  e.Le = t;
137
- for (let E = e.u; E !== null; E = E.fe) {
137
+ for (let E = e.u; E !== null; E = E.ae) {
138
138
  // Route each subscriber by its own zombie flag, mirroring the
139
139
  // post-recompute height-adjust path. Inserting into the running `heap`
140
140
  // unconditionally can park a zombie in `dirtyQueue` (or a live node in
141
141
  // `zombieQueue`), breaking the flag/queue invariant `deleteFromHeap`
142
142
  // relies on — the same corruption class as #2759.
143
- insertIntoHeapHeight(E.ae, queueFor(E.ae));
143
+ insertIntoHeapHeight(E.ce, queueFor(E.ce));
144
144
  }
145
145
  }
146
146
  }
@@ -20,16 +20,16 @@ const activeLanes = new Set;
20
20
  }
21
21
  // Detect parent lane: _parentSource chains from pendingSignal → pendingValueComputed → original.
22
22
  // The child lane should not merge with the parent lane.
23
- const i = n.o?.It;
23
+ const i = n.o?.Et;
24
24
  const t = i?.o?.Be;
25
25
  const r = t ? findLane(t) : null;
26
26
  e = {
27
- nn: n,
27
+ tn: n,
28
28
  Ae: new Set,
29
- en: [ [], [] ],
30
- tn: null,
29
+ rn: [ [], [] ],
30
+ an: null,
31
31
  _e: activeTransition,
32
- rn: r
32
+ sn: r
33
33
  };
34
34
  signalLanes.set(n, e);
35
35
  activeLanes.add(e);
@@ -51,13 +51,13 @@ function adoptCompanionLane(n, e) {
51
51
  const t = findLane(i);
52
52
  // Only the companion's own unmerged root is safely re-parentable: a root
53
53
  // that absorbed other lanes carries work that is not a child of this owner.
54
- if (t !== e && t.nn === n && !t.rn) t.rn = e;
54
+ if (t !== e && t.tn === n && !t.sn) t.sn = e;
55
55
  }
56
56
 
57
57
  /**
58
58
  * Union-find: find the root lane.
59
59
  */ function findLane(n) {
60
- while (n.tn) n = n.tn;
60
+ while (n.an) n = n.an;
61
61
  return n;
62
62
  }
63
63
 
@@ -67,16 +67,16 @@ function adoptCompanionLane(n, e) {
67
67
  n = findLane(n);
68
68
  e = findLane(e);
69
69
  if (n === e) return n;
70
- e.tn = n;
70
+ e.an = n;
71
71
  // Move (not copy) the merged lane's work: after the merge all routing goes
72
72
  // through findLane() to the root, so anything left behind here is dead —
73
73
  // and anything *added* here later is a routing bug (INV-5).
74
74
  for (const i of e.Ae) n.Ae.add(i);
75
75
  e.Ae.clear();
76
- n.en[0].push(...e.en[0]);
77
- n.en[1].push(...e.en[1]);
78
- e.en[0].length = 0;
79
- e.en[1].length = 0;
76
+ n.rn[0].push(...e.rn[0]);
77
+ n.rn[1].push(...e.rn[1]);
78
+ e.rn[0].length = 0;
79
+ e.rn[1].length = 0;
80
80
  return n;
81
81
  }
82
82
 
@@ -97,10 +97,10 @@ function resolveTransition(n) {
97
97
  // transactions (#2912) — the merged root's _transition would hand this
98
98
  // node's override to whichever action wrote last through the shared
99
99
  // reader. Chase merge chains; a dead owner settled through another path.
100
- if (hasActiveOverride(n) && n.o?.Et) {
101
- const e = ext(n).Et = currentTransition(n.o?.Et);
102
- if (e.an !== true) return e;
103
- if (n.o !== null) n.o.Et = null;
100
+ if (hasActiveOverride(n) && n.o?.Nt) {
101
+ const e = ext(n).Nt = currentTransition(n.o?.Nt);
102
+ if (e.fn !== true) return e;
103
+ if (n.o !== null) n.o.Nt = null;
104
104
  }
105
105
  return resolveLane(n)?._e ?? n._e;
106
106
  }
@@ -122,7 +122,7 @@ function resolveTransition(n) {
122
122
  // If the subscriber's lane was merged into another lane, it's stale —
123
123
  // replace it with the new source lane instead of following the merge chain
124
124
  // (which would incorrectly merge the new lane into the old group)
125
- if (t.tn) {
125
+ if (t.an) {
126
126
  ext(n).Be = e;
127
127
  n.T |= CONFIG_HAS_LANE;
128
128
  return;
@@ -132,10 +132,10 @@ function resolveTransition(n) {
132
132
  if (r !== i && !hasActiveOverride(n)) {
133
133
  // Parent-child lanes stay independent so isPending resolves without
134
134
  // waiting for the parent's async. The child keeps ownership.
135
- if (i.rn && findLane(i.rn) === r) {
135
+ if (i.sn && findLane(i.sn) === r) {
136
136
  ext(n).Be = e;
137
137
  n.T |= CONFIG_HAS_LANE;
138
- } else if (r.rn && findLane(r.rn) === i) ; else mergeLanes(i, r);
138
+ } else if (r.sn && findLane(r.sn) === i) ; else mergeLanes(i, r);
139
139
  }
140
140
  return;
141
141
  }
@@ -1,4 +1,4 @@
1
- import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, CONFIG_HAS_LANE, OVERRIDE_UNDEFINED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_RENDER, EFFECT_USER } from "./constants.js";
1
+ import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, REACTIVE_DIRTY, REACTIVE_CHECK, CONFIG_HAS_LANE, OVERRIDE_UNDEFINED, STATUS_PENDING, EFFECT_RENDER, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_USER } from "./constants.js";
2
2
 
3
3
  import { ext, latestReadActive, currentOptimisticLane, stale } from "./core.js";
4
4
 
@@ -27,7 +27,14 @@ import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule
27
27
  const t = e.o?.De !== NOT_PENDING;
28
28
  const i = t ? unwrapOverride(e.o?.De) : e.be;
29
29
  if (typeof n === "function") n = n(i);
30
- const u = !!(e.S & STATUS_UNINITIALIZED) || !e.Ue || !e.Ue(i, n);
30
+ const u = !!(e.S & STATUS_UNINITIALIZED) ||
31
+ // A dirty node's _value is stale (its queued recompute hasn't run — e.g.
32
+ // a latest() shadow marked by the previous landing's companion snap), so
33
+ // equality against it must not swallow the write. Without this, a sync
34
+ // push returning the shadow to that stale value was dropped, the snap
35
+ // recompute then committed the parent's old value, and the banner showed
36
+ // the previous transition's target (#3041 follow-up).
37
+ !!((e.ie ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) || !e.Ue || !e.Ue(i, n);
31
38
  if (!u) {
32
39
  // Same-value write with an active override still entangles the current
33
40
  // action's transition — the hold must outlast all overlapping actions.
@@ -41,11 +48,11 @@ import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule
41
48
  // No revert target is stashed: while the override is active every reader
42
49
  // sees it (A17), so authoritative arrivals commit silently into _value and
43
50
  // reverting is just dropping the override — _value is already correct.
44
- else globalQueue.k.Ke.push(e);
51
+ else globalQueue.m.Ke.push(e);
45
52
  // Stamp ownership on the node (post-merge, so entangled writers share the
46
53
  // joint root). resolveTransition prefers this over the lane's _transition,
47
54
  // which a shared subscriber can merge across transactions (#2912).
48
- ext(e).Et = activeTransition;
55
+ ext(e).Nt = activeTransition;
49
56
  const l = getOrCreateLane(e);
50
57
  ext(e).Be = l;
51
58
  e.T |= CONFIG_HAS_LANE;
@@ -56,7 +63,7 @@ import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule
56
63
  // syncCompanions only pokes _pendingSignal/_latestValueComputed — with
57
64
  // neither companion present the call is a guaranteed no-op.
58
65
  (e.o?.Ge !== undefined || e.o?.ge !== undefined) && GlobalQueue.Oe !== null && GlobalQueue.Oe(e, n);
59
- if (e.Se !== undefined) e.Te = clock;
66
+ if (e.oe !== undefined) e.Te = clock;
60
67
  // §12e: computed-only slot
61
68
  insertSubs(e, true);
62
69
  schedule();
@@ -93,7 +100,7 @@ function resolveOptimisticNodes(e) {
93
100
  ext(n).De = NOT_PENDING;
94
101
  if (i !== NOT_PENDING && n.be !== unwrapOverride(i)) insertSubs(n, true);
95
102
  n._e = null;
96
- if (n.o !== null) n.o.Et = null;
103
+ if (n.o !== null) n.o.Nt = null;
97
104
  }
98
105
  // Settlement checkpoint (#2838): companions caught in this batch (or owned
99
106
  // by a node in it) re-derive from committed state, so verdicts survive the
@@ -101,7 +108,7 @@ function resolveOptimisticNodes(e) {
101
108
  for (let t = 0; t < n; t++) {
102
109
  const n = e[t];
103
110
  if (n.o?.Ge || n.o?.ge) GlobalQueue.un(n);
104
- const i = n.o?.It;
111
+ const i = n.o?.Et;
105
112
  if (i && (i.o?.Ge === n || i.o?.ge === n)) GlobalQueue.un(i);
106
113
  }
107
114
  e.splice(0, n);
@@ -115,29 +122,32 @@ function runQueue(e, n) {
115
122
  * Run effects from all lanes that are ready (no pending async).
116
123
  */ function runLaneEffects(e) {
117
124
  for (const n of activeLanes) {
118
- if (n.tn || n.Ae.size > 0) continue;
119
- const t = n.en[e - 1];
125
+ if (n.an || n.Ae.size > 0) continue;
126
+ const t = n.rn[e - 1];
120
127
  if (t.length) {
121
- n.en[e - 1] = [];
128
+ n.rn[e - 1] = [];
122
129
  runQueue(t, e);
123
130
  }
124
131
  }
132
+ // Optimistic patch applications ride the same visibility slot as lane
133
+ // effects (in-flight DOM updates); no-op unless patches registered.
134
+ if (e === EFFECT_RENDER) GlobalQueue.ln?.();
125
135
  }
126
136
 
127
137
  function cleanupCompletedLanes(e) {
128
138
  for (const n of activeLanes) {
129
139
  const t = e ? n._e === e : !n._e;
130
140
  if (!t) continue;
131
- if (!n.tn) {
132
- if (n.en[0].length) runQueue(n.en[0], EFFECT_RENDER);
133
- if (n.en[1].length) runQueue(n.en[1], EFFECT_USER);
141
+ if (!n.an) {
142
+ if (n.rn[0].length) runQueue(n.rn[0], EFFECT_RENDER);
143
+ if (n.rn[1].length) runQueue(n.rn[1], EFFECT_USER);
134
144
  }
135
- if (n.nn.o?.Be === n) if (n.nn.o !== null) n.nn.o.Be = undefined;
145
+ if (n.tn.o?.Be === n) if (n.tn.o !== null) n.tn.o.Be = undefined;
136
146
  n.Ae.clear();
137
- n.en[0].length = 0;
138
- n.en[1].length = 0;
147
+ n.rn[0].length = 0;
148
+ n.rn[1].length = 0;
139
149
  activeLanes.delete(n);
140
- signalLanes.delete(n.nn);
150
+ signalLanes.delete(n.tn);
141
151
  }
142
152
  }
143
153
 
@@ -155,10 +165,10 @@ function cleanupCompletedLanes(e) {
155
165
  * that reads a pending mid-transition write sees the committed value; the sub
156
166
  * is recorded for replay at commit.
157
167
  */ function gatedRead(e, n, t) {
158
- if (latestReadActive || e.Pe === NOT_PENDING || e.Se || n !== e && !(n.ie & REACTIVE_MANUAL_WRITE)) {
168
+ if (latestReadActive || e.Pe === NOT_PENDING || e.oe || n !== e && !(n.ie & REACTIVE_MANUAL_WRITE)) {
159
169
  return false;
160
170
  }
161
- activeTransition.ln.add(t);
171
+ activeTransition.cn.add(t);
162
172
  return true;
163
173
  }
164
174
 
@@ -166,16 +176,31 @@ function cleanupCompletedLanes(e) {
166
176
  * read()'s value selection under a lane: return the committed `_value` for
167
177
  * optimistic/lane-assigned signals, stale-mode reads, and pending owners.
168
178
  */ function laneReadsCommitted(e, n, t) {
169
- if (e.o?.De !== undefined || !!e.o?.Be || !!(n.S & STATUS_PENDING)) return true;
170
- if (n === e && stale && t.o?.It !== e) {
171
- // The committed view can hide a same-tick ambient write (a lane member —
172
- // even just an isPending companion flip puts the reader "under a lane"
173
- // against unrelated plain writes). With no transaction the write commits
174
- // at THIS flush's end with no re-delivery, so record the reader for
175
- // replay at commit the same contract gatedRead provides when a
176
- // transaction is active (#2963). If a transaction forms mid-flush,
177
- // initTransition carries the recording over to it.
178
- if (e.Pe !== NOT_PENDING && activeTransition === null) globalQueue.k.ln.add(t);
179
+ if (e.o?.De !== undefined || !!e.o?.Be || !!(n.S & STATUS_PENDING)) {
180
+ // The committed view hides a staged in-flight value that will promote
181
+ // silently (commitPendingNode never re-notifies). gatedRead records plain
182
+ // signals for replay at commit; async memos are excluded from it by the
183
+ // `_fn` check and reach here instead a lane-assigned source whose async
184
+ // already settled (laneAsyncSettled keeps _optimisticLane) served its
185
+ // committed value to a reader that never re-ran after the landing, so a
186
+ // pending-gated branch stayed one value behind permanently (#3041
187
+ // follow-up). Record the reader under the same replay contract.
188
+ if (e.Pe !== NOT_PENDING) (activeTransition ?? globalQueue.m).cn.add(t);
189
+ return true;
190
+ }
191
+ if (n === e && stale && t.o?.Et !== e) {
192
+ // The committed view can hide a staged write (a lane member — even just
193
+ // an isPending companion flip — puts the reader "under a lane"). The
194
+ // staged value commits with no re-delivery (commitPendingNode never
195
+ // re-notifies), so record the reader for replay at commit — the same
196
+ // contract gatedRead provides (#2963). gatedRead itself only covers
197
+ // signal reads where the reading computed differs from the source; the
198
+ // owner === el memo/self read lands here instead. With a transaction
199
+ // active the staged value promotes silently at ITS landing, so record
200
+ // into the transaction (#3041 follow-up: a pending-gated branch that
201
+ // first read its async source during the landing flush stayed one value
202
+ // behind permanently); with none, into the ambient batch.
203
+ if (e.Pe !== NOT_PENDING) (activeTransition ?? globalQueue.m).cn.add(t);
179
204
  return true;
180
205
  }
181
206
  return false;
@@ -198,7 +223,7 @@ function cleanupCompletedLanes(e) {
198
223
  // recompute() runs plain: the value stages and commits with the flush.
199
224
  // (el's own override slot excludes companions themselves; a lane merged
200
225
  // into a real optimistic lane resolves to a non-companion source.)
201
- if (!globalQueue.sn && !activeTransition && !n._e && n.nn.o?.It !== undefined && e.o?.De === undefined) {
226
+ if (!globalQueue.En && !activeTransition && !n._e && n.tn.o?.Et !== undefined && e.o?.De === undefined) {
202
227
  if (e.o !== null) e.o.Be = undefined;
203
228
  return false;
204
229
  }
@@ -220,11 +245,11 @@ function cleanupCompletedLanes(e) {
220
245
 
221
246
  /** recompute()'s catch path: track pending async in the current lane. */ function laneAsyncPending(e) {
222
247
  const n = findLane(currentOptimisticLane);
223
- if (n.nn !== e) {
248
+ if (n.tn !== e) {
224
249
  n.Ae.add(e);
225
250
  ext(e).Be = n;
226
251
  e.T |= CONFIG_HAS_LANE;
227
- GlobalQueue.de !== null && GlobalQueue.de(n.nn);
252
+ GlobalQueue.de !== null && GlobalQueue.de(n.tn);
228
253
  }
229
254
  }
230
255
 
@@ -232,13 +257,13 @@ function cleanupCompletedLanes(e) {
232
257
  const n = resolveLane(e);
233
258
  if (n) {
234
259
  n.Ae.delete(e);
235
- GlobalQueue.de !== null && GlobalQueue.de(n.nn);
260
+ GlobalQueue.de !== null && GlobalQueue.de(n.tn);
236
261
  }
237
262
  }
238
263
 
239
264
  function trackOptimisticStore(e) {
240
265
  // After initTransition, globalQueue._batch IS activeTransition (same reference)
241
- globalQueue.k.cn.add(e);
266
+ globalQueue.m.Tn.add(e);
242
267
  schedule();
243
268
  }
244
269
 
@@ -247,19 +272,19 @@ function trackOptimisticStore(e) {
247
272
  * create optimistic state (verdict.ts at module top level, createOptimistic
248
273
  * and createOptimisticStore at first call) BEFORE any optimistic node exists.
249
274
  */ function installOptimisticEngine() {
250
- if (GlobalQueue.vt !== null) return;
251
- GlobalQueue.vt = optimisticWrite;
252
- GlobalQueue.fn = resolveOptimisticNodes;
253
- GlobalQueue.dn = transitionBlocked;
254
- GlobalQueue.En = cleanupCompletedLanes;
255
- GlobalQueue.Tn = runLaneEffects;
275
+ if (GlobalQueue.kt !== null) return;
276
+ GlobalQueue.kt = optimisticWrite;
277
+ GlobalQueue.dn = resolveOptimisticNodes;
278
+ GlobalQueue.In = transitionBlocked;
279
+ GlobalQueue.Nn = cleanupCompletedLanes;
280
+ GlobalQueue._n = runLaneEffects;
256
281
  GlobalQueue.Ft = gatedRead;
257
282
  GlobalQueue.ht = laneSuspends;
258
283
  GlobalQueue.gt = laneReadsCommitted;
259
284
  GlobalQueue.je = recomputeLane;
260
285
  GlobalQueue.$e = laneAsyncPending;
261
286
  GlobalQueue.ze = laneAsyncSettled;
262
- GlobalQueue.Nn = trackOptimisticStore;
287
+ GlobalQueue.An = trackOptimisticStore;
263
288
  }
264
289
 
265
290
  export { installOptimisticEngine };
@@ -1,4 +1,4 @@
1
- import { REACTIVE_DISPOSED, REACTIVE_ZOMBIE, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, CONFIG_TRANSPARENT, defaultContext, CONFIG_AUTO_DISPOSE } from "./constants.js";
1
+ import { REACTIVE_DISPOSED, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, CONFIG_TRANSPARENT, defaultContext } from "./constants.js";
2
2
 
3
3
  import { context, runWithOwner, pendingCheckActive, latestReadActive, tracking } from "./core.js";
4
4
 
@@ -12,7 +12,7 @@ const PENDING_OWNER = {};
12
12
 
13
13
  // Dummy owner to trigger store's read() path
14
14
  function markDisposal(e) {
15
- let t = e.ke;
15
+ let t = e.ve;
16
16
  while (t) {
17
17
  const e = t.ie;
18
18
  t.ie = e | REACTIVE_ZOMBIE;
@@ -54,8 +54,8 @@ function disposeChildren(e, t = false, n) {
54
54
  const t = e;
55
55
  if (t.o?.Ge || t.o?.ge) GlobalQueue.un(t);
56
56
  }
57
- if (t && e.Se && e.o !== null) e.o.Ee = null;
58
- let o = n ? e.o?.qe ?? null : e.ke;
57
+ if (t && e.oe && e.o !== null) e.o.Ee = null;
58
+ let o = n ? e.o?.qe ?? null : e.ve;
59
59
  while (o) {
60
60
  const e = o.Ve;
61
61
  const t = o;
@@ -82,17 +82,17 @@ function disposeChildren(e, t = false, n) {
82
82
  if (n) {
83
83
  if (e.o !== null) e.o.qe = null;
84
84
  } else {
85
- e.ke = null;
85
+ e.ve = null;
86
86
  e.Me = 0;
87
87
  }
88
88
  // O(1) splice out of parent's chain on individual dispose. Skipped during
89
89
  // batch dispose (parent already disposed) and zombie disposal (node sits on
90
90
  // parent's _pendingFirstChild). We leave node._nextSibling intact so outer
91
91
  // walks that already advanced past us still reach later siblings.
92
- if (t && !n && !(i & REACTIVE_ZOMBIE) && e.ve !== null && !(e.ve.ie & REACTIVE_DISPOSED)) {
92
+ if (t && !n && !(i & REACTIVE_ZOMBIE) && e.ke !== null && !(e.ke.ie & REACTIVE_DISPOSED)) {
93
93
  const t = e.ct;
94
94
  const n = e.Ve;
95
- if (t !== null) t.Ve = n; else e.ve.ke = n;
95
+ if (t !== null) t.Ve = n; else e.ke.ve = n;
96
96
  if (n !== null) n.ct = t;
97
97
  e.ct = null;
98
98
  }
@@ -124,7 +124,7 @@ function runDisposal(e, t) {
124
124
 
125
125
  function childId(e, t) {
126
126
  let n = e;
127
- while (n.T & CONFIG_TRANSPARENT && n.ve) n = n.ve;
127
+ while (n.T & CONFIG_TRANSPARENT && n.ke) n = n.ke;
128
128
  if (n.id != null) return formatId(n.id, t ? n.Me++ : n.Me);
129
129
  throw new Error("");
130
130
  }
@@ -248,7 +248,7 @@ function disposeRootSelf(e = true) {
248
248
  T: n ? CONFIG_TRANSPARENT : 0,
249
249
  Ct: true,
250
250
  Ot: t?.Ct ? t.Ot : t,
251
- ke: null,
251
+ ve: null,
252
252
  Ve: null,
253
253
  ct: null,
254
254
  he: null,
@@ -256,17 +256,17 @@ function disposeRootSelf(e = true) {
256
256
  we: t?.we || defaultContext,
257
257
  Me: 0,
258
258
  o: null,
259
- ve: t,
259
+ ke: t,
260
260
  dispose: disposeRootSelf
261
261
  };
262
262
  if (t) {
263
- const e = t.ke;
263
+ const e = t.ve;
264
264
  if (e === null) {
265
- t.ke = i;
265
+ t.ve = i;
266
266
  } else {
267
267
  i.Ve = e;
268
268
  e.ct = i;
269
- t.ke = i;
269
+ t.ve = i;
270
270
  }
271
271
  }
272
272
  return i;