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

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 +1618 -232
  2. package/dist/node.cjs +2262 -1199
  3. package/dist/prod/boundaries.js +4 -1
  4. package/dist/prod/core/async.js +152 -101
  5. package/dist/prod/core/constants.js +55 -1
  6. package/dist/prod/core/core.js +305 -234
  7. package/dist/prod/core/effect.js +28 -28
  8. package/dist/prod/core/error.js +13 -1
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +27 -27
  11. package/dist/prod/core/heap.js +30 -30
  12. package/dist/prod/core/lanes.js +32 -32
  13. package/dist/prod/core/optimistic.js +57 -57
  14. package/dist/prod/core/owner.js +34 -34
  15. package/dist/prod/core/scheduler.js +346 -152
  16. package/dist/prod/core/verdict.js +122 -65
  17. package/dist/prod/index.js +3 -3
  18. package/dist/prod/map.js +106 -106
  19. package/dist/prod/signals.js +321 -26
  20. package/dist/prod/store/next/optimistic.js +363 -151
  21. package/dist/prod/store/next/patch.js +6 -6
  22. package/dist/prod/store/next/projection.js +25 -21
  23. package/dist/prod/store/next/store.js +223 -113
  24. package/dist/prod/store/store.js +2 -2
  25. package/dist/types/core/async.d.ts +2 -0
  26. package/dist/types/core/attribution-hooks.d.ts +11 -0
  27. package/dist/types/core/attribution.d.ts +57 -4
  28. package/dist/types/core/constants.d.ts +54 -0
  29. package/dist/types/core/core.d.ts +19 -20
  30. package/dist/types/core/dev.d.ts +8 -2
  31. package/dist/types/core/error.d.ts +9 -0
  32. package/dist/types/core/index.d.ts +2 -2
  33. package/dist/types/core/scheduler.d.ts +39 -0
  34. package/dist/types/core/types.d.ts +12 -0
  35. package/dist/types/index.d.ts +3 -3
  36. package/dist/types/signals.d.ts +108 -11
  37. package/dist/types/store/next/optimistic.d.ts +13 -10
  38. package/dist/types/store/next/projection.d.ts +1 -1
  39. package/dist/types/store/next/store.d.ts +22 -0
  40. package/dist/types/store/next/target.d.ts +25 -0
  41. package/dist/types-cjs/core/async.d.cts +2 -0
  42. package/dist/types-cjs/core/attribution-hooks.d.cts +11 -0
  43. package/dist/types-cjs/core/attribution.d.cts +57 -4
  44. package/dist/types-cjs/core/constants.d.cts +54 -0
  45. package/dist/types-cjs/core/core.d.cts +19 -20
  46. package/dist/types-cjs/core/dev.d.cts +8 -2
  47. package/dist/types-cjs/core/error.d.cts +9 -0
  48. package/dist/types-cjs/core/index.d.cts +2 -2
  49. package/dist/types-cjs/core/scheduler.d.cts +39 -0
  50. package/dist/types-cjs/core/types.d.cts +12 -0
  51. package/dist/types-cjs/index.d.cts +3 -3
  52. package/dist/types-cjs/signals.d.cts +108 -11
  53. package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
  54. package/dist/types-cjs/store/next/projection.d.cts +1 -1
  55. package/dist/types-cjs/store/next/store.d.cts +22 -0
  56. package/dist/types-cjs/store/next/target.d.cts +25 -0
  57. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
- import { computed, ext, setSignal, isEqual } from "../../core/core.js";
1
+ import { CONFIG_AUTO_DISPOSE, STATUS_PENDING, NOT_PENDING, CONFIG_OPTIMISTIC, CONFIG_HELD_TRUTH, unwrapOverride } from "../../core/constants.js";
2
2
 
3
- import { CONFIG_AUTO_DISPOSE, STATUS_PENDING, NOT_PENDING, unwrapOverride, CONFIG_OPTIMISTIC } from "../../core/constants.js";
3
+ import { computed, setSignal, isEqual } from "../../core/core.js";
4
4
 
5
- import { GlobalQueue, insertSubs, schedule, globalQueue } from "../../core/scheduler.js";
5
+ import { getOwner } from "../../core/owner.js";
6
+
7
+ import { activeTransition, GlobalQueue, currentTransition, globalQueue, createTransition, runAsTransitionBatch } from "../../core/scheduler.js";
6
8
 
7
9
  import "../../core/invariants.js";
8
10
 
@@ -16,11 +18,11 @@ import { $TARGET, markRawIngest, setNextOptimisticViewResolver, isWrappable, raw
16
18
 
17
19
  import { runProjectionComputedNext } from "./projection.js";
18
20
 
19
- import { wrapNext, runAuthoritative, storeSetterNext, hasActiveOverride, unwrapValue, getNode, getHasNode, targetsEqual, getKeySetNode, bumpDeep } from "./store.js";
21
+ import { wrapNext, runAuthoritative, storeSetterNext, hasActiveOverride, unwrapValue, targetsEqual, getNode, getHasNode, getKeySetNode, bumpDeep, stagedTruthPB, authoritativeRead } from "./store.js";
20
22
 
21
23
  import { patchHooks, rowHooks } from "./patch-hooks.js";
22
24
 
23
- import { buildIdentityRowOps, sameKey } from "./reconcile.js";
25
+ import { sameKey, buildIdentityRowOps } from "./reconcile.js";
24
26
 
25
27
  import { setOptHooks } from "./target.js";
26
28
 
@@ -33,12 +35,32 @@ import { setOptHooks } from "./target.js";
33
35
  * armed presence nodes (the §6 overlay), so structural optimism reverts with
34
36
  * the same per-transaction granularity (FINDING-2's fix by construction).
35
37
  *
36
- * Derived form = an optimistic projection: the derive's recompute and its
37
- * async commits run under projectionWriteActive (authoritative landings
38
- * commit silently beneath any active overrides). The transitionBlocked
39
- * store-half (#2951) is installed here for next-shaped targets, chaining the
38
+ * Derived form = an optimistic projection. Landings follow the fold rule
39
+ * (#3164, RUL-2 as re-ruled): while a transaction retains optimistic edits
40
+ * on the family, truth that lands STAGES into that transaction — a keyed
41
+ * identity-preserving walk written through the ordinary staged setter
42
+ * channel — and reveals atomically at settle, exactly like a signal landing
43
+ * under an active override (asyncWrite's held branch). Optimistic edits are
44
+ * never consumed by landings; they live exactly as long as their transaction
45
+ * and die by engine-native revert. With no retainer, landings commit
46
+ * immediately under projectionWriteActive (authoritative, silently beneath
47
+ * any bare-write overrides — those ride the flight's own transition, #2951).
48
+ * Authoritative readers (until()'s predicate) tunnel into staged truth via
49
+ * the node read path's pending-value arm. The transitionBlocked store-half
50
+ * (#2951) is installed here for next-shaped targets, chaining the
40
51
  * legacy/engine checks.
41
- */ let blockedInstalled = false;
52
+ */
53
+ /** #3164 fold: a stamped truth is HELD (masked from ordinary readers until
54
+ * the reveal) only while its transition is live AND retaining optimism —
55
+ * overrides are what make partial-coverage composition a tear. A plain
56
+ * async transition carries no overrides, so downstream computes must see
57
+ * staged values to converge (normal speculation). Resolves merges first:
58
+ * merge unions optimistic nodes/stores into the target. */ function transitionHoldsOptimism(e) {
59
+ const t = currentTransition(e);
60
+ return t.sn !== true && (t.ze.length !== 0 || t.En.size !== 0);
61
+ }
62
+
63
+ let blockedInstalled = false;
42
64
 
43
65
  function installNextBlockedHalf() {
44
66
  if (blockedInstalled) return;
@@ -49,14 +71,15 @@ function installNextBlockedHalf() {
49
71
  setOptHooks({
50
72
  notifyOptimisticWrites: notifyOptimisticWrites,
51
73
  optimisticView: optimisticView,
52
- applyTentative: applyTentative
74
+ applyTentative: applyTentative,
75
+ retainsOptimism: transitionHoldsOptimism
53
76
  });
54
77
  setNextOptimisticViewResolver((e, t) => optimisticView(e, t));
55
78
  // Scheduler flush tails call _clearOptimisticStores whenever tracked
56
79
  // stores exist; next has no layer to clear — reverts are engine-native —
57
80
  // so the hook only empties the batch set.
58
- if (!GlobalQueue.Bt) {
59
- GlobalQueue.Bt = e => {
81
+ if (!GlobalQueue.qt) {
82
+ GlobalQueue.qt = e => {
60
83
  // Patch channel (revert site): engine-native reverts flip node values
61
84
  // back to committed; patched records need a forced DOM re-apply from
62
85
  // the post-revert view. Emission only — next keeps no layer to clear.
@@ -71,23 +94,38 @@ function installNextBlockedHalf() {
71
94
  // row identity against the post-revert view (resolved from the
72
95
  // target at drain — overrides are gone by then).
73
96
  if (e.pc !== null && e.pc.ro !== null) rowHooks.emitRowOpsOptimistic(e, null, null);
97
+ // Keyset resync (classic channel twin): the keyset node's own
98
+ // revert can compare EQUAL (a landing's bump matched the
99
+ // tentative bump) while the arrangement underneath changed —
100
+ // mapArray/ownKeys subscribers must re-read the post-revert
101
+ // view. Authoritative bump: never re-arm the node we are
102
+ // clearing.
103
+ if (e.k !== null) runAuthoritative(() => setSignal(e.k, e => e + 1));
74
104
  }
75
105
  }
76
106
  }
77
107
  e.clear();
78
108
  };
79
109
  }
80
- const e = GlobalQueue.In;
81
- GlobalQueue.In = t => {
82
- for (const e of t.Tn) {
83
- const t = e?.[$TARGET];
84
- const n = t?.fam?.node;
110
+ const e = GlobalQueue.dn;
111
+ GlobalQueue.dn = t => {
112
+ for (const e of t.En) {
113
+ const n = e?.[$TARGET];
114
+ const i = n?.fam;
115
+ const o = i?.node;
85
116
  // The hold exists to keep optimistic state alive until the store's own
86
117
  // truth lands (#2951). Once the family carries NO live overrides (a
87
118
  // landing consumed them, or they never existed), a pending firewall is
88
119
  // no reason to park the transaction — blocking then leaks it forever
89
120
  // when the in-flight question is never answered (undisposed fixtures).
90
- if (n != null && n.S & STATUS_PENDING && familyHasLiveOverrides(t.fam)) return true;
121
+ if (o == null || !(o.S & STATUS_PENDING)) continue;
122
+ // Ownership is declared (#3146): only the flight's OWN transaction
123
+ // parks on the flight (the #2951 anchor routed the bare write there).
124
+ // A transaction that merely brushed the store never waits for truth
125
+ // it does not carry.
126
+ const r = i.ft != null ? liveTransition(i.ft) : null;
127
+ if (r !== null && r !== currentTransition(t)) continue;
128
+ if (familyHasLiveOverrides(i)) return true;
91
129
  }
92
130
  return e(t);
93
131
  };
@@ -101,10 +139,10 @@ function familyHasLiveOverrides(e) {
101
139
  if (t === null) continue;
102
140
  for (const e of Reflect.ownKeys(t)) {
103
141
  const n = t[e];
104
- if (n.o?.De !== undefined && n.o?.De !== NOT_PENDING) return true;
142
+ if (n.o?.Pe !== undefined && n.o?.Pe !== NOT_PENDING) return true;
105
143
  }
106
144
  }
107
- if (e.k !== null && e.k.o?.De !== undefined && e.k.o?.De !== NOT_PENDING) return true;
145
+ if (e.k !== null && e.k.o?.Pe !== undefined && e.k.o?.Pe !== NOT_PENDING) return true;
108
146
  }
109
147
  t.clear();
110
148
  // nothing live — drop the bookkeeping
@@ -125,34 +163,276 @@ function createOptimisticStoreNext(e, t, n) {
125
163
  shallow: !!n?.shallow,
126
164
  opt: true
127
165
  };
128
- const s = wrapNext(o, null, null, r);
129
- r.px = s;
166
+ const l = wrapNext(o, null, null, r);
167
+ r.px = l;
168
+ // Same key resolution the projection channels use ("id" default) — replay's
169
+ // satisfaction rule reads it off the family.
170
+ const s = n?.key === undefined ? "id" : n.key;
171
+ r.key = typeof s === "function" ? s : s === null ? null : e => isWrappable(e) ? e[s] : undefined;
130
172
  if (r.shallow) {
131
- s[$TARGET].s = true;
173
+ l[$TARGET].s = true;
132
174
  markRawIngest(o);
133
175
  }
134
176
  if (i) {
135
177
  const t = e;
136
- // Async commits land outside the computed's sync body — re-apply the
137
- // authoritative-write posture there too. Landings consume the family's
138
- // tentative overrides (RUL-2: visible landed truth replaces optimism)
139
- // both the reconcile-channel commit and per-op post-await draft writes.
140
- const consume = () => consumeOverridesNext(r);
141
- const wrapCommit = e => {
178
+ // #3146: an async settle event belongs to the flight's OWN transaction.
179
+ // A live declared one re-enters (a merge if the generic settle path
180
+ // already entered a graph-stamped stranger the landing supersedes any
181
+ // recompute deriving from that stranger's world); a dead one renews (per
182
+ // A18(1) each arrival reveals on its own schedule, so per-yield
183
+ // transactions die with their commit and the next settle event opens the
184
+ // flight's next one — still declared, never anonymous). An UNDECLARED
185
+ // flight (loading window) keeps the ambient reveal (#2933: the loading
186
+ // rail is transaction-invisible).
187
+ const enterFlightTransition = () => {
188
+ const e = r.ft;
189
+ if (e == null) return;
190
+ let t = liveTransition(e);
191
+ if (t === null) r.ft = t = createTransition();
192
+ r.node.Ae = t;
193
+ globalQueue.initTransition(t);
194
+ };
195
+ // Landing router (#3164 fold ruling): while a transaction retains
196
+ // optimistic edits on this family, truth landings stage INTO it and
197
+ // reveal atomically at settle; with no retainer they commit immediately
198
+ // under the authoritative posture (async commits land outside the
199
+ // computed's sync body, so the posture is re-applied here) — inside the
200
+ // flight-owned transaction (#3146). Sync commits (the derive's body,
201
+ // owner is the firewall itself) reveal with their own recompute's flush.
202
+ const wrapCommit = (e, t) => {
203
+ const n = retainingTransition(r);
204
+ if (n !== null) return void stageLanding(r, n, t);
205
+ if (getOwner() !== r.node) enterFlightTransition();
142
206
  runAuthoritative(e);
143
- consume();
207
+ };
208
+ // Draft writes (the derive mutating its draft, sync body and post-await
209
+ // continuations alike) are the same truth channel per-operation: bind
210
+ // each op to the retaining transaction so its node writes stage and its
211
+ // backing fold defers (ensurePB stamps foldBatches with the swapped-in
212
+ // batch; the write-override eager-commit branch in notifyWrites yields
213
+ // to any active transaction).
214
+ const aroundDraftWrite = e => {
215
+ const t = retainingTransition(r);
216
+ if (t === null) e(); else runFolded(t, e);
217
+ };
218
+ // Flight declaration (#3146): a recompute that registered a truth-flight
219
+ // OWNS its transaction. The ask's transaction is recorded on the family
220
+ // (created by the flight's own pending throw when none was ambient, the
221
+ // causing write's/refresh's when one was — graph-driven causality) and
222
+ // the firewall is stamped so every settle path resolves the flight's
223
+ // transaction by construction, not by whatever last brushed the node.
224
+ // When the ask took none (a dead stale stamp — the previous flight's,
225
+ // cleared nowhere — bare-returns the pre-throw entry), the flight opens
226
+ // its own here, same activation point as the pre-throw's creation: the
227
+ // ambient batch (the causing write, same-tick bare optimism) adopts into
228
+ // it exactly as it would have there, and the pending notification that
229
+ // follows this unwind registers observers against it. The flight
230
+ // also registers as its own async reporter: the transaction lives
231
+ // exactly as long as the question is unanswered, observed or not — the
232
+ // #2951 refetch-hold no longer depends on a tracked observer having
233
+ // happened to register one. Loading-window flights declare nothing
234
+ // (#2933: the loading rail is transaction-invisible); a sync run clears
235
+ // the declaration.
236
+ const declareFlight = e => {
237
+ if (e.o?.Ie == null) {
238
+ if (!e.Ne) r.ft = null;
239
+ return;
240
+ }
241
+ if (e.Ne) return;
242
+ let t = activeTransition;
243
+ if (t === null) globalQueue.initTransition(t = createTransition());
244
+ r.ft = t;
245
+ e.Ae = t;
246
+ let n = t._e.get(e);
247
+ if (n === undefined) t._e.set(e, n = new Set);
248
+ n.add(e);
144
249
  };
145
250
  let i;
146
251
  if (n?.seedLoadingValue) i = {
147
252
  loadingValue: undefined
148
253
  };
149
254
  const o = computed(() => {
150
- runAuthoritative(() => runProjectionComputedNext(s, t, n?.key === undefined ? "id" : n.key, wrapCommit, consume));
255
+ const e = getOwner();
256
+ try {
257
+ runAuthoritative(() => runProjectionComputedNext(l, t, n?.key === undefined ? "id" : n.key, wrapCommit, aroundDraftWrite));
258
+ } finally {
259
+ declareFlight(e);
260
+ }
151
261
  }, i);
152
262
  o.T &= ~CONFIG_AUTO_DISPOSE;
153
263
  r.node = o;
154
264
  }
155
- return [ s, e => storeSetterNext(s, e) ];
265
+ return [ l, e => {
266
+ // Retention ledger (#3164): record the owning transaction so landings
267
+ // know to fold. Captured at entry — the action machinery has the
268
+ // transaction ambient while user code runs; a bare write with no
269
+ // transaction retains nothing (it rides the flight's own transition
270
+ // per #2951 and dies with it).
271
+ const t = activeTransition;
272
+ storeSetterNext(l, e);
273
+ if (t !== null) (r.rt ??= new Set).add(t);
274
+ } ];
275
+ }
276
+
277
+ /** Resolve a retained transition through its merge chain (`_done` holds the
278
+ * merge target while merged, `true` once settled). Null = dead. */ function liveTransition(e) {
279
+ while (typeof e.sn === "object") e = e.sn;
280
+ return e.sn === true ? null : e;
281
+ }
282
+
283
+ /** The transaction truth landings fold into: the first live member of the
284
+ * family's retention ledger (dead members prune here). Multiple live
285
+ * retainers entangle through their shared family writes and settle
286
+ * together, so folding into the first reaches all of them. */ function retainingTransition(e) {
287
+ const t = e.rt;
288
+ if (t === undefined || t.size === 0) return null;
289
+ let n = null;
290
+ for (const e of t) {
291
+ const i = liveTransition(e);
292
+ if (i === null) t.delete(e); else n ??= i;
293
+ }
294
+ return n;
295
+ }
296
+
297
+ /** Fold a landing into the retaining transaction (#3164): the landed value
298
+ * is written through the ORDINARY staged setter channel — node writes park
299
+ * as `_pendingValue` registered with the transaction's batch (speculation
300
+ * and until()'s authoritative tunnel see them; live view stays coherent),
301
+ * and the backing fold defers via the foldBatches stamp — under the
302
+ * authoritative posture, so armed nodes take the engine bypass and no
303
+ * override is created. The engine's own commit machinery reveals everything
304
+ * atomically when the transaction settles (transitions never abort: failed
305
+ * actions still commit — only optimistic overrides revert). */ function stageLanding(e, t, n) {
306
+ runFolded(t, () => runAuthoritative(() => storeSetterNext(e.px, t => {
307
+ stagedApply(t, unwrapValue(n), e.key ?? null);
308
+ }, false)));
309
+ }
310
+
311
+ /** Run a fold write inside the retaining transaction's batch, then
312
+ * transition-stamp its staged nodes NOW (parity with the parked-transition
313
+ * flush path's reassignPendingTransition): the stamp is what routes stale
314
+ * (render) readers to the committed value — core read's cross-transaction
315
+ * guard — and what makes foldHeld defer the backing for context-free
316
+ * readers. A microtask staging never crosses that flush path, so without
317
+ * the stamp a render effect's speculative recompute would compose staged
318
+ * truth with live overrides — the #3164 tear, one window later. Armed
319
+ * nodes additionally raise CONFIG_HELD_TRUTH: their staged value is
320
+ * confirming truth masked from ordinary readers until the reveal (plain
321
+ * staged nodes stay visible — normal speculation; override-covered nodes
322
+ * stay unarmed — the override is their display and its revert their
323
+ * notification, A17). */ function runFolded(e, t) {
324
+ runAsTransitionBatch(e, t);
325
+ const n = e.Lt;
326
+ for (let t = 0; t < n.length; t++) {
327
+ const i = n[t];
328
+ i.Ae = e;
329
+ if (i.T & CONFIG_OPTIMISTIC && !hasActiveOverride(i)) i.T |= CONFIG_HELD_TRUTH;
330
+ }
331
+ }
332
+
333
+ /** Keyed identity-preserving deep merge through live draft proxies — the
334
+ * staged twin of the adoption walk. Reads see the pending backing (staged
335
+ * view), so consecutive landings during one hold compose; key-matched rows
336
+ * keep their raw (and so their proxy) in the slot with only changed leaves
337
+ * written; unmatched rows land wholesale. Runs inside stageLanding's
338
+ * authoritative bracket: drafts seed from committed truth, never overlays. */ function stagedApply(e, t, n) {
339
+ const i = Array.isArray(e);
340
+ if (i && Array.isArray(t)) {
341
+ const i = t.length;
342
+ if (n !== null) {
343
+ // Occurrence-aware key queues (parity with the adoption window):
344
+ // duplicate keys match per occurrence, each current row consumed once.
345
+ let o = null;
346
+ const r = e.length;
347
+ for (let t = 0; t < r; t++) {
348
+ const i = unwrapValue(e[t]);
349
+ if (!isWrappable(i)) continue;
350
+ const r = n(i);
351
+ if (r === undefined) continue;
352
+ const l = (o ??= new Map).get(r);
353
+ if (l === undefined) o.set(r, [ i ]); else l.push(i);
354
+ }
355
+ // Echo adoption: an optimistic structural add whose key the landing
356
+ // confirms must keep its raw (and so its proxy — list drivers keep the
357
+ // DOM row). Tentative rows never reach committed truth (they live in
358
+ // node overrides), so key-match the draft target's active override
359
+ // rows as a secondary pool. Committed rows queued first own their
360
+ // keys; overlay rows only extend coverage. Adopted raws enter staged
361
+ // truth; at settle the override reverts and the reveal re-seats the
362
+ // same raw.
363
+ const l = e[$TARGET]?.n;
364
+ if (l != null) {
365
+ for (const e of Reflect.ownKeys(l)) {
366
+ const t = l[e];
367
+ if (!hasActiveOverride(t)) continue;
368
+ const i = unwrapValue(unwrapOverride(t.o.Pe));
369
+ if (!isWrappable(i)) continue;
370
+ const r = n(i);
371
+ if (r === undefined) continue;
372
+ const s = (o ??= new Map).get(r);
373
+ if (s === undefined) o.set(r, [ i ]); else s.push(i);
374
+ }
375
+ }
376
+ for (let r = 0; r < i; r++) {
377
+ const i = t[r];
378
+ let l;
379
+ if (isWrappable(i) && o !== null) {
380
+ const e = n(i);
381
+ if (e !== undefined) {
382
+ for (const [t, n] of o) {
383
+ if (!sameKey(t, e)) continue;
384
+ l = n.shift();
385
+ if (n.length === 0) o.delete(t);
386
+ break;
387
+ }
388
+ }
389
+ }
390
+ if (l !== undefined) {
391
+ if (unwrapValue(e[r]) !== l) e[r] = l;
392
+ stagedApply(e[r], i, n);
393
+ } else {
394
+ const t = unwrapValue(e[r]);
395
+ if (!isEqual(t, i) && !targetsEqual(t, i)) e[r] = i;
396
+ }
397
+ }
398
+ } else {
399
+ for (let o = 0; o < i; o++) {
400
+ const i = t[o];
401
+ const r = unwrapValue(e[o]);
402
+ if (r === i) continue;
403
+ if (isWrappable(i) && isWrappable(r) && Array.isArray(i) === Array.isArray(r)) stagedApply(e[o], i, n); else if (!isEqual(r, i) && !targetsEqual(r, i)) e[o] = i;
404
+ }
405
+ }
406
+ if (e.length !== i) e.length = i;
407
+ return;
408
+ }
409
+ // Object merge; also the degenerate root-kind-change shape (arrays accept
410
+ // keyed writes/deletes, so a wholesale restatement still lands staged).
411
+ for (const o of Reflect.ownKeys(t)) {
412
+ if (i && o === "length") continue;
413
+ const r = t[o];
414
+ const l = unwrapValue(e[o]);
415
+ if (l === r) continue;
416
+ if (isWrappable(r) && isWrappable(l) && Array.isArray(r) === Array.isArray(l)) {
417
+ // Different-keyed entities never merge (tentative-channel parity):
418
+ // the incoming object replaces the slot wholesale.
419
+ if (n !== null) {
420
+ const t = n(l);
421
+ const i = n(r);
422
+ if (t !== undefined && i !== undefined && !sameKey(t, i)) {
423
+ e[o] = r;
424
+ continue;
425
+ }
426
+ }
427
+ stagedApply(e[o], r, n);
428
+ } else if (!isEqual(l, r) && !targetsEqual(l, r)) {
429
+ e[o] = r;
430
+ }
431
+ }
432
+ for (const n of Reflect.ownKeys(e)) {
433
+ if (i && n === "length" || n in t) continue;
434
+ delete e[n];
435
+ }
156
436
  }
157
437
 
158
438
  // ---- optimistic-only store machinery (moved from next/store.ts /
@@ -161,13 +441,18 @@ function createOptimisticStoreNext(e, t, n) {
161
441
  * overrides — the same view the draft was seeded from) and emit engine writes
162
442
  * for exactly the changed keys. Visible-view diffing keeps no-op writes from
163
443
  * entangling lanes (RUL-10 / opt R38). */ function notifyOptimisticWrites(e, t) {
164
- // A bare write while the store's own truth is in flight rides THAT
165
- // transaction (#2951, legacy parity): entangle the firewall's transition so
166
- // the override survives until the refetch settles instead of flash-reverting
444
+ // A bare write while the store's own truth is in flight rides the FLIGHT'S
445
+ // OWN transaction (#2951 via the #3146 declaration): entangle it so the
446
+ // override survives until the refetch settles instead of flash-reverting
167
447
  // at plain flush end. The blocked-check store-half keeps that transaction
168
- // from settling while the firewall is pending.
169
- const n = e.fam?.node;
170
- if (n?._e) globalQueue.initTransition(n._e);
448
+ // from settling while the firewall is pending. Declared ownership replaces
449
+ // the old circumstantial route through the firewall's `_transition` stamp,
450
+ // which was whatever last brushed the node.
451
+ const n = e.fam?.ft;
452
+ if (n != null) {
453
+ const e = liveTransition(n);
454
+ if (e !== null) globalQueue.initTransition(e);
455
+ }
171
456
  const i = e.v;
172
457
  // Patch channel (override-application site): the draft IS the intended
173
458
  // visible state; prev is the view before these overrides apply. Bypasses
@@ -186,26 +471,26 @@ function createOptimisticStoreNext(e, t, n) {
186
471
  }
187
472
  const visible = (t, n) => {
188
473
  const i = e.n?.[t];
189
- return i !== undefined && hasActiveOverride(i) ? unwrapOverride(i.o?.De) : n;
474
+ return i !== undefined && hasActiveOverride(i) ? unwrapOverride(i.o?.Pe) : n;
190
475
  };
191
476
  const visiblePresent = t => {
192
477
  const n = e.h?.[t];
193
- return n !== undefined && hasActiveOverride(n) ? !!unwrapOverride(n.o?.De) : t in i;
478
+ return n !== undefined && hasActiveOverride(n) ? !!unwrapOverride(n.o?.Pe) : t in i;
194
479
  };
195
480
  let o = false;
196
481
  const r = Array.isArray(t);
197
482
  for (const n of Reflect.ownKeys(t)) {
198
483
  if (r && n === "length") continue;
199
- const s = unwrapValue(t[n]);
484
+ const l = unwrapValue(t[n]);
200
485
  if (!visiblePresent(n)) {
201
486
  // Optimistic add: value node + presence node + membership bump.
202
- setSignal(getNode(e, n, i[n]), () => s);
487
+ setSignal(getNode(e, n, i[n]), () => l);
203
488
  setSignal(getHasNode(e, n, n in i), true);
204
489
  o = true;
205
490
  } else {
206
491
  const t = visible(n, i[n]);
207
- if (!isEqual(t, s) && !targetsEqual(t, s)) {
208
- setSignal(getNode(e, n, t), () => s);
492
+ if (!isEqual(t, l) && !targetsEqual(t, l)) {
493
+ setSignal(getNode(e, n, t), () => l);
209
494
  if (r) o = true;
210
495
  }
211
496
  }
@@ -230,95 +515,22 @@ function createOptimisticStoreNext(e, t, n) {
230
515
  // (structural ones already ride the key-set bump above).
231
516
  bumpDeep(e);
232
517
  // Discard the draft — committed raw is untouched (revert target by
233
- // construction). Register the root store for the scheduler's settle hooks
234
- // and the target for landing consumption (RUL-2).
235
- e.pb = null;
518
+ // construction) restoring any truth-staged backing this draft displaced
519
+ // (#3164 fold: ensurePB parked it so tentative writes could not pollute
520
+ // staged truth). Register the root store for the scheduler's settle hooks.
521
+ e.pb = stagedTruthPB.get(e) ?? null;
522
+ if (e.pb !== null) stagedTruthPB.delete(e);
236
523
  (e.fam.overlaid ??= new Set).add(e);
237
- GlobalQueue.An?.(e.fam.px ?? e.px);
238
- }
239
-
240
- /**
241
- * Landing consumption (RUL-2): fresh authoritative data supersedes every
242
- * tentative override in the family. Mirrors legacy clearProjectionOverride —
243
- * drop the override, clear lane/ownership, notify subscribers whose visible
244
- * value changes (reversion effects go to regular queues via the projection
245
- * write posture the caller holds).
246
- */ function consumeOverridesNext(e) {
247
- const t = e.overlaid;
248
- if (t === undefined || t.size === 0) return;
249
- runAuthoritative(() => {
250
- for (const e of t) {
251
- const drop = (e, t) => {
252
- if (!hasActiveOverride(e)) return;
253
- const n = unwrapOverride(e.o?.De);
254
- // Full legacy reset (clearOptimisticOverride parity): the landing is
255
- // authoritative NOW — fold committed into the node directly instead
256
- // of riding a transaction's commit (whose queues may be stashed with
257
- // the transaction parked; the wake would strand until it settles).
258
- ext(e).De = NOT_PENDING;
259
- e.T |= CONFIG_OPTIMISTIC;
260
- const i = e.o;
261
- if (i) {
262
- i.Nt = null;
263
- i.Be = undefined;
264
- }
265
- e.Pe = NOT_PENDING;
266
- e.be = t;
267
- if (!e.Ue || !e.Ue(n, t)) {
268
- insertSubs(e, true);
269
- schedule();
270
- }
271
- };
272
- // Landing consumes STRUCTURAL optimism only (legacy layer parity):
273
- // membership edits, array length, and the value overrides written WITH
274
- // them (a key carrying an active presence override is an add/delete —
275
- // classified BEFORE the adoption may have made the key exist in landed
276
- // data). A pure value override on a key the landing carries stays with
277
- // its owning transaction (rapid-toggle contract: a live action's edit
278
- // of an existing entity rides on top of landed truth).
279
- const t = Array.isArray(e.v);
280
- const n = e.h;
281
- let i = null;
282
- if (n !== null) {
283
- for (const e of Reflect.ownKeys(n)) {
284
- if (hasActiveOverride(n[e])) (i ??= new Set).add(e);
285
- }
286
- }
287
- const o = e.n;
288
- if (o !== null) {
289
- for (const n of Reflect.ownKeys(o)) {
290
- const r = i?.has(n) || !(n in e.v) || t && n === "length";
291
- if (!r) continue;
292
- drop(o[n], t && n === "length" ? e.v.length : e.v[n]);
293
- }
294
- }
295
- if (n !== null) {
296
- for (const t of Reflect.ownKeys(n)) drop(n[t], t in e.v);
297
- }
298
- if (e.k !== null && hasActiveOverride(e.k)) {
299
- ext(e.k).De = NOT_PENDING;
300
- e.k.T |= CONFIG_OPTIMISTIC;
301
- const t = e.k.o;
302
- if (t) {
303
- t.Nt = null;
304
- t.Be = undefined;
305
- }
306
- insertSubs(e.k, true);
307
- schedule();
308
- }
309
- // Patch channel (override-consumption site): visible truth flipped to
310
- // committed for the consumed keys — force a re-apply from the live
311
- // view so the DOM leaves the override state.
312
- if (e.pc !== null && e.pc.p !== null) patchHooks.emitPatchOptimistic(e, null, null);
313
- }
314
- t.clear();
315
- });
524
+ GlobalQueue._n?.(e.fam.px ?? e.px);
316
525
  }
317
526
 
318
527
  /** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
319
528
  * view, lane values included; a fresh copy per call during pending windows —
320
- * RUL-12). Returns `src` untouched when no override is active on `t`. */ function optimisticView(e, t) {
321
- if (e.fam?.opt !== true) return t;
529
+ * RUL-12). Returns `src` untouched when no override is active on `t`.
530
+ * Authoritative-view reads (until()'s predicate) skip composition entirely:
531
+ * the predicate observes authoritative truth, never the caller's tentative
532
+ * overlay. (Write-side emission callers never run under such a compute.) */ function optimisticView(e, t) {
533
+ if (e.fam?.opt !== true || authoritativeRead()) return t;
322
534
  let n = null;
323
535
  const ensure = () => n ??= Array.isArray(t) ? [ ...t ] : {
324
536
  ...t
@@ -328,7 +540,7 @@ function createOptimisticStoreNext(e, t, n) {
328
540
  for (const e of Reflect.ownKeys(i)) {
329
541
  const n = i[e];
330
542
  if (!hasActiveOverride(n)) continue;
331
- const o = unwrapOverride(n.o?.De);
543
+ const o = unwrapOverride(n.o?.Pe);
332
544
  if (e === "length" && Array.isArray(t)) {
333
545
  if (t.length !== o) ensure().length = o;
334
546
  } else if (!isEqual(t[e], o)) ensure()[e] = o;
@@ -339,7 +551,7 @@ function createOptimisticStoreNext(e, t, n) {
339
551
  for (const e of Reflect.ownKeys(o)) {
340
552
  const i = o[e];
341
553
  if (!hasActiveOverride(i)) continue;
342
- const r = !!unwrapOverride(i.o?.De);
554
+ const r = !!unwrapOverride(i.o?.Pe);
343
555
  if (!r && e in (n ?? t)) delete ensure()[e];
344
556
  }
345
557
  }
@@ -350,11 +562,11 @@ function applyTentative(e, t, n) {
350
562
  const i = e.pb ?? e.v;
351
563
  const o = optimisticView(e, i);
352
564
  const r = e.fam.map;
353
- const s = Array.isArray(t);
354
- if (Array.isArray(o) !== s) return;
565
+ const l = Array.isArray(t);
566
+ if (Array.isArray(o) !== l) return;
355
567
  // kind change at root: flat overrides below
356
- const l = [];
357
- const u = s ? [ ...t ] : shallowWithSymbols(t);
568
+ const s = [];
569
+ const u = l ? [ ...t ] : shallowWithSymbols(t);
358
570
  const match = (e, t) => {
359
571
  if (!isWrappable(e) || !isWrappable(t)) return null;
360
572
  if (rawValuesUsed && (isRawValue(e) || isRawValue(t))) return null;
@@ -368,13 +580,13 @@ function applyTentative(e, t, n) {
368
580
  }
369
581
  return r.get(unwrapValue(e)) ?? null;
370
582
  };
371
- if (s) {
583
+ if (l) {
372
584
  const e = o;
373
585
  let i = null;
374
586
  for (let o = 0; o < t.length; o++) {
375
587
  const r = t[o];
376
588
  if (!isWrappable(r)) continue;
377
- let s;
589
+ let l;
378
590
  if (n) {
379
591
  const t = n(r);
380
592
  if (t !== undefined) {
@@ -394,20 +606,20 @@ function applyTentative(e, t, n) {
394
606
  }
395
607
  }
396
608
  const o = i.get(t);
397
- if (o === undefined) s = undefined; else if (Array.isArray(o)) {
398
- s = unwrapValue(e[o.shift()]);
609
+ if (o === undefined) l = undefined; else if (Array.isArray(o)) {
610
+ l = unwrapValue(e[o.shift()]);
399
611
  if (o.length === 1) i.set(t, o[0]);
400
612
  } else {
401
- s = unwrapValue(e[o]);
613
+ l = unwrapValue(e[o]);
402
614
  i.delete(t);
403
615
  }
404
- } else s = unwrapValue(e[o]);
405
- } else s = unwrapValue(e[o]);
406
- const c = match(s, r);
407
- if (c !== null) {
616
+ } else l = unwrapValue(e[o]);
617
+ } else l = unwrapValue(e[o]);
618
+ const a = match(l, r);
619
+ if (a !== null) {
408
620
  // Keep the existing row in the slot (identity preserved); recurse.
409
- u[o] = unwrapValue(s);
410
- l.push([ c, r ]);
621
+ u[o] = unwrapValue(l);
622
+ s.push([ a, r ]);
411
623
  }
412
624
  }
413
625
  } else {
@@ -417,17 +629,17 @@ function applyTentative(e, t, n) {
417
629
  const r = match(n, i);
418
630
  if (r !== null) {
419
631
  u[e] = n;
420
- l.push([ r, i ]);
632
+ s.push([ r, i ]);
421
633
  }
422
634
  }
423
635
  }
424
636
  // Flat overrides for this level (adds, removals, moved slots, length, leaf
425
637
  // values) — preserve any live user draft backing across the call.
426
- const c = e.pb;
638
+ const a = e.pb;
427
639
  e.pb = null;
428
640
  notifyOptimisticWrites(e, u);
429
- e.pb = c;
430
- for (let e = 0; e < l.length; e++) applyTentative(l[e][0], unwrapValue(l[e][1]), n);
641
+ e.pb = a;
642
+ for (let e = 0; e < s.length; e++) applyTentative(s[e][0], unwrapValue(s[e][1]), n);
431
643
  }
432
644
 
433
645
  function shallowWithSymbols(e) {
@@ -436,4 +648,4 @@ function shallowWithSymbols(e) {
436
648
  return t;
437
649
  }
438
650
 
439
- export { consumeOverridesNext, createOptimisticStoreNext, notifyOptimisticWrites, optimisticView };
651
+ export { createOptimisticStoreNext, notifyOptimisticWrites, optimisticView, transitionHoldsOptimism };