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

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.
@@ -1932,7 +1932,11 @@ class GlobalQueue extends Queue {
1932
1932
  * commit whatever its relation to the committed value — a landing equal to
1933
1933
  * committed still differs from the override — then _supersedeOverride
1934
1934
  * decides. Installed with the optimistic engine; only reachable on a node
1935
- * that has an override. */
1935
+ * that has an override — or, from `mapArray` once a lane pass has run over
1936
+ * the map, on a per-slot signal (never CONFIG_OPTIMISTIC): the slot arm
1937
+ * publishes a lane pass's write as the slot's derived override, lands a
1938
+ * plain pass's write over one, and is the plain `setSignal` otherwise (F1,
1939
+ * see optimistic.ts landOnOverride). */
1936
1940
  static _landOnOverride = null;
1937
1941
  static _trackOptimisticStore = null;
1938
1942
  flush() {
@@ -2073,8 +2077,16 @@ class GlobalQueue extends Queue {
2073
2077
  // sweep or recompute wrote a node it owns): effects computed under it
2074
2078
  // since are its to apply, not this flush's — runEffect leaves them queued
2075
2079
  // and the next pass parks them with it (#3319). Everything computed
2076
- // mainline applies now.
2077
- scheduled = dirtyQueue._max >= dirtyQueue._min || activeTransition !== null;
2080
+ // mainline applies now. A write the finalize staged in the ambient
2081
+ // batch with no subscriber to dirty (an optimistic store settle's
2082
+ // keyset bump under a reader that never tracks the key set) is work
2083
+ // too — the fast drain and the park exit already count it — so the
2084
+ // next round commits it and the woken re-entry below does not adopt
2085
+ // it into a parked transaction it never belonged to (matrix F6).
2086
+ scheduled =
2087
+ dirtyQueue._max >= dirtyQueue._min ||
2088
+ activeTransition !== null ||
2089
+ this._batch._pendingNodes.length !== 0;
2078
2090
  // Run lane effects first (for ready lanes), then regular effects
2079
2091
  activeLanes.size && GlobalQueue._runLaneEffects(EFFECT_RENDER);
2080
2092
  this.run(EFFECT_RENDER);
@@ -2105,9 +2117,14 @@ class GlobalQueue extends Queue {
2105
2117
  // idle pass: entering adopts the ambient batch, and staged or dirty
2106
2118
  // ambient work would be held behind flights it never read. `scheduled`
2107
2119
  // is that test here — after the park exit as well as the normal one:
2108
- // it was recomputed from the heap this pass, every write since re-armed
2109
- // it, and optimistic ambient nodes reverted with the finalize — so a
2110
- // wake in a pass with work simply falls to the next. Entering re-arms
2120
+ // it was recomputed from the heap and the ambient batch's staged nodes
2121
+ // this pass, every write since re-armed it, and optimistic ambient
2122
+ // nodes reverted with the finalize — so a wake in a pass with work
2123
+ // simply falls to the next. (A staged node with no subscriber — the
2124
+ // finalize's keyset bump under a length-only reader — used to be
2125
+ // missed here: the wake adopted it, stamped it, and a later ambient
2126
+ // write to the same node joined the parked transaction and never
2127
+ // reverted; matrix F6.) Entering re-arms
2111
2128
  // it itself; a dead (completed) wake is a bare return in
2112
2129
  // initTransition, and the loop moves on to the next.
2113
2130
  while (!scheduled && !activeTransition && wokenTransitions.length)
@@ -6967,13 +6984,13 @@ export {
6967
6984
  currentTransition as M,
6968
6985
  NoOwnerError as N,
6969
6986
  OVERRIDE_UNDEFINED as O,
6970
- queuePendingNode as P,
6971
- latestReadActive as Q,
6987
+ assignOrMergeLane as P,
6988
+ queuePendingNode as Q,
6972
6989
  REACTIVE_DIRTY as R,
6973
6990
  STATUS_UNINITIALIZED as S,
6974
- REACTIVE_MANUAL_WRITE as T,
6975
- REACTIVE_OPTIMISTIC_DIRTY as U,
6976
- assignOrMergeLane as V,
6991
+ latestReadActive as T,
6992
+ REACTIVE_MANUAL_WRITE as U,
6993
+ REACTIVE_OPTIMISTIC_DIRTY as V,
6977
6994
  LANE_RUN as W,
6978
6995
  attrHooks as X,
6979
6996
  EFFECT_USER as Y,
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);
@@ -8960,6 +9005,30 @@ function updateKeyedMap() {
8960
9005
  // entered rows, for the list-identity census.
8961
9006
  removedItems,
8962
9007
  createdItems;
9008
+ // The pass's write to a per-slot signal (a row accessor in index mode,
9009
+ // an index accessor in keyed mode). The list's frame lives in these
9010
+ // writes as much as in the computed's result, so under a LANE pass they
9011
+ // are the lane's frame too (F1): a plain `setSignal` staged them into
9012
+ // the action's transaction and the row readers were served the
9013
+ // committed value until the action landed — `<For>` without `keyed`
9014
+ // showed the pre-action list for the whole action while the keyed modes
9015
+ // showed the optimistic one. The engine publishes the write as a
9016
+ // derived override on the slot (lanes stage, A17) and lands or
9017
+ // supersedes it when a plain pass later writes a slot still carrying
9018
+ // one (A18) — `landOnOverride`'s slot arm. Decided once per pass, not
9019
+ // per slot: a lane pass marks the map (`_laneSlots`: its slots may
9020
+ // carry overrides), a marked map routes every slot write through the
9021
+ // engine while any lane is live — a slot's override resolves with its
9022
+ // lane's transaction, or with the batch for an orphan lane
9023
+ // (resolveOptimisticNodes, then cleanupCompletedLanes, both before the
9024
+ // completion wakes any recompute), so a pass that finds no lane live
9025
+ // knows the slots are clean and drops the mark — and an unmarked map
9026
+ // takes `setSignal` directly. The engine is installed whenever a lane
9027
+ // is active.
9028
+ const write = (this._laneSlots =
9029
+ currentOptimisticLane !== null || (this._laneSlots && activeLanes.size !== 0))
9030
+ ? GlobalQueue._landOnOverride
9031
+ : setSignal;
8963
9032
  // skip common prefix
8964
9033
  for (
8965
9034
  start = 0, end = Math.min(this._len, newLen);
@@ -8968,7 +9037,7 @@ function updateKeyedMap() {
8968
9037
  (this._rows && compare(this._key, this._items[start], newItems[start])));
8969
9038
  start++
8970
9039
  ) {
8971
- if (this._rows) setSignal(this._rows[start], newItems[start]);
9040
+ if (this._rows) write(this._rows[start], newItems[start]);
8972
9041
  }
8973
9042
  // skip common suffix — counted only; retained entries land in one pass
8974
9043
  // at commit instead of being staged and copied twice
@@ -9069,19 +9138,19 @@ function updateKeyedMap() {
9069
9138
  indexes && (indexes[i] = this._indexes[i]);
9070
9139
  }
9071
9140
  for (j = start; j <= newEnd; j++) {
9072
- if (rows) setSignal(rows[j], newItems[j]);
9073
- if (indexes) setSignal(indexes[j], j);
9141
+ if (rows) write(rows[j], newItems[j]);
9142
+ if (indexes) write(indexes[j], j);
9074
9143
  }
9075
9144
  for (j = newEnd + 1; j < newLen; j++) {
9076
9145
  temp[j] = this._mappings[j - dif];
9077
9146
  tempNodes[j] = this._nodes[j - dif];
9078
9147
  if (rows) {
9079
9148
  rows[j] = this._rows[j - dif];
9080
- setSignal(rows[j], newItems[j]);
9149
+ write(rows[j], newItems[j]);
9081
9150
  }
9082
9151
  if (indexes) {
9083
9152
  indexes[j] = this._indexes[j - dif];
9084
- if (dif !== 0) setSignal(indexes[j], j);
9153
+ if (dif !== 0) write(indexes[j], j);
9085
9154
  }
9086
9155
  }
9087
9156
  this._mappings = temp;
@@ -2,7 +2,7 @@ import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, REACTIVE_DIRTY, REAC
2
2
 
3
3
  import { attrHooks } from "./attribution-hooks.js";
4
4
 
5
- import { ext, hasActiveOverride, stale, currentOptimisticLane, enterStagedRead, latestReadActive } from "./core.js";
5
+ import { ext, hasActiveOverride, stale, currentOptimisticLane, enterStagedRead, setSignal, latestReadActive } from "./core.js";
6
6
 
7
7
  import { NotReadyError } from "./error.js";
8
8
 
@@ -27,17 +27,17 @@ import { GlobalQueue, activeTransition, globalQueue, origin, clock, insertSubs,
27
27
  */
28
28
  /** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */ function optimisticWrite(e, n) {
29
29
  const i = e.o?.we !== NOT_PENDING;
30
- const t = i ? unwrapOverride(e.o?.we) : e.ce;
31
- if (typeof n === "function") n = n(t);
32
- const r = !!(e.S & STATUS_UNINITIALIZED) ||
30
+ const r = i ? unwrapOverride(e.o?.we) : e.ce;
31
+ if (typeof n === "function") n = n(r);
32
+ const t = !!(e.S & STATUS_UNINITIALIZED) ||
33
33
  // A dirty node's _value is stale (its queued recompute hasn't run — e.g.
34
34
  // a latest() shadow marked by the previous landing's companion snap), so
35
35
  // equality against it must not swallow the write. Without this, a sync
36
36
  // push returning the shadow to that stale value was dropped, the snap
37
37
  // recompute then committed the parent's old value, and the banner showed
38
38
  // the previous transition's target (#3041 follow-up).
39
- !!((e.oe ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) || !e.Fe || !e.Fe(t, n);
40
- if (!r) {
39
+ !!((e.oe ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) || !e.Fe || !e.Fe(r, n);
40
+ if (!t) {
41
41
  // Same-value write with an active override still entangles the current
42
42
  // action's transition — the hold must outlast all overlapping actions —
43
43
  // and renews the override's PROVENANCE: the newer action re-asks the
@@ -164,7 +164,7 @@ function resolveOptimisticNodes(e) {
164
164
  // override-covered authoritative values hold in _pendingValue and
165
165
  // elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
166
166
  if (!(n.S & STATUS_PENDING)) n.S &= ~STATUS_UNINITIALIZED;
167
- const t = n.o?.we;
167
+ const r = n.o?.we;
168
168
  // A derived override (lanes stage, #3479) has no truth of its own: the
169
169
  // slot disarms — the memo is plain again — and the override PROMOTES to
170
170
  // `_value`. Not superseded, nothing it derives from told it otherwise
@@ -174,18 +174,18 @@ function resolveOptimisticNodes(e) {
174
174
  // the override IS what a recompute from the truth yields. Re-deriving
175
175
  // instead re-asked an async member's flight and held the transaction on
176
176
  // it — a waterfall after the reveal.
177
- const r = n.C & CONFIG_DERIVED_OVERRIDE;
178
- ext(n).we = r && !(n.C & CONFIG_OPTIMISTIC) ? undefined : NOT_PENDING;
177
+ const t = n.C & CONFIG_DERIVED_OVERRIDE;
178
+ ext(n).we = t && !(n.C & CONFIG_OPTIMISTIC) ? undefined : NOT_PENDING;
179
179
  // A superseded override's subscribers already re-derived from the truth
180
180
  // when it arrived (#3331) — the drop changes nothing they read. Everyone
181
181
  // else learns of the correction here: this drop IS their notification.
182
182
  const u = (n.C & CONFIG_OVERRIDE_SUPERSEDED) !== 0;
183
183
  n.C &= -8912897;
184
- if (!u && t !== NOT_PENDING && n.ce !== unwrapOverride(t)) {
185
- if (r) n.ce = unwrapOverride(t); else {
184
+ if (!u && r !== NOT_PENDING && n.ce !== unwrapOverride(r)) {
185
+ if (t) n.ce = unwrapOverride(r); else {
186
186
  // The guess lifts and what was beneath it differs: the screen
187
187
  // changes from the override to the committed value.
188
- if (attrHooks !== null) attrHooks.optimisticReverted(n, unwrapOverride(t), n.ce, "reverted");
188
+ if (attrHooks !== null) attrHooks.optimisticReverted(n, unwrapOverride(r), n.ce, "reverted");
189
189
  insertSubs(n, true);
190
190
  }
191
191
  }
@@ -198,8 +198,8 @@ function resolveOptimisticNodes(e) {
198
198
  for (let i = 0; i < n; i++) {
199
199
  const n = e[i];
200
200
  if (n.o?.xe || n.o?.Me) GlobalQueue.Qn(n);
201
- const t = n.o?.$n;
202
- if (t && (t.o?.xe === n || t.o?.Me === n)) GlobalQueue.Qn(t);
201
+ const r = n.o?.$n;
202
+ if (r && (r.o?.xe === n || r.o?.Me === n)) GlobalQueue.Qn(r);
203
203
  }
204
204
  e.splice(0, n);
205
205
  }
@@ -257,14 +257,14 @@ function resolveOptimisticNodes(e) {
257
257
  const i = e.o?.Ce;
258
258
  if (i) {
259
259
  const n = findLane(i);
260
- const t = [ e ];
261
- while (t.length) {
262
- const e = t.pop();
260
+ const r = [ e ];
261
+ while (r.length) {
262
+ const e = r.pop();
263
263
  const i = e.o?.Ce;
264
264
  if (!i || findLane(i) !== n) continue;
265
265
  e.o.Ce = undefined;
266
266
  n.ke.delete(e);
267
- for (let n = e.u; n !== null; n = n.Pe) t.push(n.Ae);
267
+ for (let n = e.u; n !== null; n = n.Pe) r.push(n.Ae);
268
268
  }
269
269
  }
270
270
  }
@@ -403,7 +403,49 @@ function resolveOptimisticNodes(e) {
403
403
  * action provenance, or an A17-silent confirmation (#3331). Before this the
404
404
  * landing took setSignal's plain path: a differing truth staged silently
405
405
  * under the override and the graph never moved until the commit.
406
+ *
407
+ * Slot arm (list-matrix F1): `mapArray`'s writes to its per-slot signals —
408
+ * the row accessors of index mode, the index accessors of keyed mode; never
409
+ * CONFIG_OPTIMISTIC, which is how the arm tells them from the store landings
410
+ * above — once a lane pass has run over the map. The list's frame lives in
411
+ * these writes as much as in the computed's result. Under a LANE pass the
412
+ * write is the lane's frame, as the computed's result is (lanes stage,
413
+ * `laneOverride`): the slot joins the lane carrying a DERIVED override —
414
+ * `_value` stays the committed row, the lane's readers and untracked reads
415
+ * see the override (A17), an off-lane render effect sees the committed frame
416
+ * (#3460), the revert promotes or drops it with the lane's transaction. The
417
+ * gate compares against the slot the pass publishes to (INV-11): the override
418
+ * when armed, the committed row otherwise. A plain `setSignal` here staged
419
+ * the write into the ACTION's transaction: `<For>` without `keyed` showed the
420
+ * pre-action list for the whole action while the keyed modes showed the
421
+ * optimistic one. A PLAIN pass over a slot still carrying the lane's frame —
422
+ * the landing, the reversion, a mainline re-pass — is the truth arriving
423
+ * under an override and takes the landing path below: a differing truth
424
+ * supersedes, an equal one confirms and the revert promotes. Every pass
425
+ * rewrites every slot whose value differs from the previous frame's (index
426
+ * mode rewrites every surviving slot), so a slot the landing does NOT write
427
+ * holds, by construction, what the truth yields. A slot with neither a lane
428
+ * nor an override is the plain write.
406
429
  */ function landOnOverride(e, n) {
430
+ if (!(e.C & CONFIG_OPTIMISTIC)) {
431
+ const i = currentOptimisticLane;
432
+ if (i === null) {
433
+ if (!hasActiveOverride(e)) return setSignal(e, n);
434
+ } else {
435
+ // INV-11: the gate compares against the slot this pass publishes to —
436
+ // the override when one is armed, the committed row otherwise.
437
+ if (!e.Fe || !e.Fe(hasActiveOverride(e) ? unwrapOverride(e.o.we) : e.ce, n)) {
438
+ // Membership before the publish: `laneOverride` files the slot under
439
+ // the lane's transaction, and the read path routes a tracked reader
440
+ // of a lane member through `overrideRead` (CONFIG_HAS_LANE).
441
+ assignOrMergeLane(e, i);
442
+ laneOverride(e, n, i);
443
+ insertSubs(e, true);
444
+ schedule();
445
+ }
446
+ return n;
447
+ }
448
+ }
407
449
  const i = e._e === NOT_PENDING ? e.ce : e._e;
408
450
  if (typeof n === "function") n = n(i);
409
451
  if (attrHooks !== null) attrHooks.write(e, i, n);
@@ -631,7 +631,11 @@ class GlobalQueue extends Queue {
631
631
  * commit whatever its relation to the committed value — a landing equal to
632
632
  * committed still differs from the override — then _supersedeOverride
633
633
  * decides. Installed with the optimistic engine; only reachable on a node
634
- * that has an override. */
634
+ * that has an override — or, from `mapArray` once a lane pass has run over
635
+ * the map, on a per-slot signal (never CONFIG_OPTIMISTIC): the slot arm
636
+ * publishes a lane pass's write as the slot's derived override, lands a
637
+ * plain pass's write over one, and is the plain `setSignal` otherwise (F1,
638
+ * see optimistic.ts landOnOverride). */
635
639
  static Vn=null;
636
640
  static Jn=null;
637
641
  flush() {
@@ -793,8 +797,13 @@ class GlobalQueue extends Queue {
793
797
  // sweep or recompute wrote a node it owns): effects computed under it
794
798
  // since are its to apply, not this flush's — runEffect leaves them queued
795
799
  // and the next pass parks them with it (#3319). Everything computed
796
- // mainline applies now.
797
- scheduled = dirtyQueue.EE >= dirtyQueue.st || activeTransition !== null;
800
+ // mainline applies now. A write the finalize staged in the ambient
801
+ // batch with no subscriber to dirty (an optimistic store settle's
802
+ // keyset bump under a reader that never tracks the key set) is work
803
+ // too — the fast drain and the park exit already count it — so the
804
+ // next round commits it and the woken re-entry below does not adopt
805
+ // it into a parked transaction it never belonged to (matrix F6).
806
+ scheduled = dirtyQueue.EE >= dirtyQueue.st || activeTransition !== null || this.m.In.length !== 0;
798
807
  // Run lane effects first (for ready lanes), then regular effects
799
808
  activeLanes.size && GlobalQueue.Kn(EFFECT_RENDER);
800
809
  this.run(EFFECT_RENDER);
@@ -808,9 +817,14 @@ class GlobalQueue extends Queue {
808
817
  // idle pass: entering adopts the ambient batch, and staged or dirty
809
818
  // ambient work would be held behind flights it never read. `scheduled`
810
819
  // is that test here — after the park exit as well as the normal one:
811
- // it was recomputed from the heap this pass, every write since re-armed
812
- // it, and optimistic ambient nodes reverted with the finalize — so a
813
- // wake in a pass with work simply falls to the next. Entering re-arms
820
+ // it was recomputed from the heap and the ambient batch's staged nodes
821
+ // this pass, every write since re-armed it, and optimistic ambient
822
+ // nodes reverted with the finalize — so a wake in a pass with work
823
+ // simply falls to the next. (A staged node with no subscriber — the
824
+ // finalize's keyset bump under a length-only reader — used to be
825
+ // missed here: the wake adopted it, stamped it, and a later ambient
826
+ // write to the same node joined the parked transaction and never
827
+ // reverted; matrix F6.) Entering re-arms
814
828
  // it itself; a dead (completed) wake is a bare return in
815
829
  // initTransition, and the loop moves on to the next.
816
830
  while (!scheduled && !activeTransition && wokenTransitions.length) this.initTransition(wokenTransitions.pop());