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

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 (50) hide show
  1. package/dist/dev.js +1661 -361
  2. package/dist/node.cjs +1939 -1331
  3. package/dist/prod/affects.js +16 -16
  4. package/dist/prod/boundaries.js +90 -90
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +74 -72
  7. package/dist/prod/core/constants.js +39 -1
  8. package/dist/prod/core/core.js +332 -224
  9. package/dist/prod/core/effect.js +56 -56
  10. package/dist/prod/core/external.js +4 -4
  11. package/dist/prod/core/graph.js +65 -54
  12. package/dist/prod/core/heap.js +52 -44
  13. package/dist/prod/core/invariants.js +3 -2
  14. package/dist/prod/core/lanes.js +41 -34
  15. package/dist/prod/core/optimistic.js +63 -60
  16. package/dist/prod/core/owner.js +97 -96
  17. package/dist/prod/core/scheduler.js +243 -194
  18. package/dist/prod/core/verdict.js +184 -77
  19. package/dist/prod/map.js +104 -104
  20. package/dist/prod/signals.js +1 -1
  21. package/dist/prod/store/next/optimistic.js +31 -23
  22. package/dist/prod/store/next/projection.js +3 -3
  23. package/dist/prod/store/next/reconcile.js +78 -74
  24. package/dist/prod/store/next/store.js +357 -90
  25. package/dist/prod/store/store.js +7 -7
  26. package/dist/types/core/attribution-hooks.d.ts +52 -0
  27. package/dist/types/core/attribution.d.ts +186 -0
  28. package/dist/types/core/constants.d.ts +26 -0
  29. package/dist/types/core/core.d.ts +8 -1
  30. package/dist/types/core/dev.d.ts +20 -3
  31. package/dist/types/core/graph.d.ts +1 -0
  32. package/dist/types/core/lanes.d.ts +4 -16
  33. package/dist/types/core/scheduler.d.ts +8 -0
  34. package/dist/types/core/types.d.ts +85 -41
  35. package/dist/types/store/next/projection.d.ts +0 -16
  36. package/dist/types/store/next/store.d.ts +6 -0
  37. package/dist/types/store/next/target.d.ts +18 -0
  38. package/dist/types-cjs/core/attribution-hooks.d.cts +52 -0
  39. package/dist/types-cjs/core/attribution.d.cts +186 -0
  40. package/dist/types-cjs/core/constants.d.cts +26 -0
  41. package/dist/types-cjs/core/core.d.cts +8 -1
  42. package/dist/types-cjs/core/dev.d.cts +20 -3
  43. package/dist/types-cjs/core/graph.d.cts +1 -0
  44. package/dist/types-cjs/core/lanes.d.cts +4 -16
  45. package/dist/types-cjs/core/scheduler.d.cts +8 -0
  46. package/dist/types-cjs/core/types.d.cts +85 -41
  47. package/dist/types-cjs/store/next/projection.d.cts +0 -16
  48. package/dist/types-cjs/store/next/store.d.cts +6 -0
  49. package/dist/types-cjs/store/next/target.d.cts +18 -0
  50. package/package.json +1 -1
package/dist/dev.js CHANGED
@@ -75,6 +75,15 @@ const REACTIVE_MANUAL_WRITE = 1 << 10;
75
75
  * pending window does not read as pending (question-scoped pending model).
76
76
  */
77
77
  const REACTIVE_REASK = 1 << 11;
78
+ /**
79
+ * A dependency write landed while this subscriber was mid-recompute — a
80
+ * nested pull committed beneath one of its reads (#3037). The heap refuses
81
+ * RECOMPUTING nodes, so recompute's tail consumes this latch and reschedules:
82
+ * values the pass read before the nested commit are stale. Only set for
83
+ * links validated this pass (gen-current): a write to an untouched link is
84
+ * either re-read later in the pass (fresh) or trimmed with it (not a dep).
85
+ */
86
+ const REACTIVE_MISSED_WAKE = 1 << 12;
78
87
  // Static configuration bits packed into Owner/Computed/Signal _config.
79
88
  const CONFIG_OWNED_WRITE = 1 << 0;
80
89
  const CONFIG_NO_SNAPSHOT = 1 << 1;
@@ -83,6 +92,31 @@ const CONFIG_IN_SNAPSHOT_SCOPE = 1 << 3;
83
92
  const CONFIG_CHILDREN_FORBIDDEN = 1 << 4;
84
93
  const CONFIG_AUTO_DISPOSE = 1 << 5;
85
94
  const CONFIG_SYNC = 1 << 6;
95
+ // Presence bits (stage-3 hot-path monomorphism, DESIGN-PATCH-CHANNEL §11b):
96
+ // optional per-node slots (_overrideValue, _pendingSignal/_latestValueComputed,
97
+ // _snapshotValue, _optimisticLane) are NOT part of every node's hidden class —
98
+ // reading a missing property defeats V8's inline caches on the hottest write/
99
+ // notify loops. These bits live on the always-present `_config` so hot paths
100
+ // pay one monomorphic masked read and only touch the optional field when its
101
+ // installer flagged it. Bits are STICKY ("may be set") — the guarded field
102
+ // read remains authoritative.
103
+ const CONFIG_OPTIMISTIC = 1 << 7;
104
+ const CONFIG_HAS_COMPANIONS = 1 << 8;
105
+ const CONFIG_HAS_SNAPSHOT = 1 << 9;
106
+ const CONFIG_HAS_LANE = 1 << 10;
107
+ /** Set on a FIREWALL computed when any of its child signals creates an
108
+ * isPending()/latest() companion. Gates the post-recompute child-companion
109
+ * walk (#3038): a store computed's `_child` chain holds one node per
110
+ * materialized leaf, so walking it unconditionally makes every update cost
111
+ * O(all leaves ever read). Sticky — set at companion creation, never
112
+ * cleared; sync-only apps never set it and never pay the walk. */
113
+ const CONFIG_CHILD_COMPANIONS = 1 << 11;
114
+ /** Set on a computed when its first firewall child signal is installed
115
+ * (projection machinery). Gates markNode's firewall-children walk with one
116
+ * masked read of the always-present _config — the walk's old `_child` read
117
+ * moved into the cold extension (§12), and an unconditional `_x` deref per
118
+ * marked node measurably taxed the propagation hot path (diamond -22%). */
119
+ const CONFIG_FW_CHILDREN = 1 << 12;
86
120
  const STATUS_PENDING = 1 << 0;
87
121
  const STATUS_ERROR = 1 << 1;
88
122
  const STATUS_UNINITIALIZED = 1 << 2;
@@ -117,6 +151,559 @@ const defaultContext = {};
117
151
  */
118
152
  const $REFRESH = Symbol("refresh");
119
153
 
154
+ let attrHooks = null;
155
+ function setAttributionHooks(hooks) {
156
+ attrHooks = hooks;
157
+ }
158
+
159
+ let changeSeq = 0;
160
+ let runSeq = 0;
161
+ const defaultOptions = {
162
+ log: true,
163
+ stacks: false,
164
+ historyLimit: 200,
165
+ hotRuns: { count: 120, windowMs: 1000 },
166
+ wideDeps: 30,
167
+ hotTime: { budgetMs: 8, windowMs: 1000 },
168
+ unstableMemos: 4,
169
+ wideWrites: 250
170
+ };
171
+ let options = { ...defaultOptions };
172
+ const listeners = new Set();
173
+ let history = [];
174
+ const now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
175
+ const frames = [];
176
+ const scopeCosts = new Map();
177
+ const writeCosts = new Map();
178
+ function rootsOf(causes, out) {
179
+ for (const c of causes) {
180
+ if (c.kind === "derived" && c.causes && c.causes.length > 0) rootsOf(c.causes, out);
181
+ else out.add(c.name);
182
+ }
183
+ }
184
+ function recordCosts(event) {
185
+ let scope = scopeCosts.get(event.node);
186
+ if (scope === undefined) {
187
+ scope = {
188
+ name: event.nodeName,
189
+ kind: event.nodeKind,
190
+ runs: 0,
191
+ selfMs: 0,
192
+ wastedMs: 0,
193
+ overlayMs: 0
194
+ };
195
+ scopeCosts.set(event.node, scope);
196
+ }
197
+ scope.runs++;
198
+ scope.selfMs += event.selfMs;
199
+ if (event.phase !== "plain") scope.overlayMs += event.selfMs;
200
+ else if (!event.changed && !event.held) scope.wastedMs += event.selfMs;
201
+ const roots = new Set();
202
+ rootsOf(event.causes, roots);
203
+ for (const name of roots) {
204
+ let write = writeCosts.get(name);
205
+ if (write === undefined) writeCosts.set(name, (write = { name, runs: 0, downstreamMs: 0 }));
206
+ write.runs++;
207
+ write.downstreamMs += event.selfMs;
208
+ }
209
+ }
210
+ function nodeName(node) {
211
+ return node._name ?? "anonymous";
212
+ }
213
+ function preview(v) {
214
+ if (v === null) return "null";
215
+ switch (typeof v) {
216
+ case "undefined":
217
+ return "undefined";
218
+ case "string":
219
+ return JSON.stringify(v.length > 40 ? v.slice(0, 40) + "…" : v);
220
+ case "number":
221
+ case "boolean":
222
+ case "bigint":
223
+ return String(v);
224
+ case "function":
225
+ return "[function]";
226
+ case "symbol":
227
+ return v.toString();
228
+ default:
229
+ return Array.isArray(v) ? `Array(${v.length})` : `[${v.constructor?.name ?? "object"}]`;
230
+ }
231
+ }
232
+ function captureStack() {
233
+ if (!options.stacks) return undefined;
234
+ const raw = new Error().stack?.split("\n") ?? [];
235
+ // Drop the message line and every frame inside the reactive core; the first
236
+ // remaining frames are the user code that performed the write.
237
+ return raw
238
+ .slice(1)
239
+ .filter(line => !/solid-signals[/\\](src|dist)[/\\]/.test(line))
240
+ .slice(0, 3)
241
+ .map(line => line.trim());
242
+ }
243
+ /** Sentinel for "no value transition to record" (refresh() stamps). */
244
+ const NO_VALUES = Symbol("no-values");
245
+ /** Record a root change (setSignal / refresh / async landing) on the node. */
246
+ /**
247
+ * Written-fan-out warning — the write-time complement of the always-on
248
+ * HUGE_FAN_OUT link-time warning (see dev.ts). Static fan-out that never
249
+ * writes is harmless; a committed root invalidation reaching hundreds of
250
+ * subscribers re-runs all of them this flush. Uses the dev-maintained
251
+ * `_subCount` from the graph-size diagnostics — no core sites touched.
252
+ * Once per node; re-warns only when the subscriber count has doubled since
253
+ * the last warning, so it cannot spam alongside HUGE_FAN_OUT's own
254
+ * 2000-and-up milestones.
255
+ */
256
+ function checkWideWrite(node, kind) {
257
+ const limit = options.wideWrites;
258
+ if (typeof limit !== "number") return;
259
+ const subs = node._subCount ?? 0;
260
+ const attributed = node;
261
+ if (subs < limit || subs < (attributed._devWideWriteWarnedAt ?? 0) * 2) return;
262
+ attributed._devWideWriteWarnedAt = subs;
263
+ const verb =
264
+ kind === "refresh" ? "refresh of" : kind === "async" ? "async landing on" : "write to";
265
+ const message =
266
+ `[WIDE_WRITE] ${verb} "${nodeName(node)}" reached ${subs} subscribers — every one ` +
267
+ `re-runs this flush. If consumers ask keyed questions of this value (for example every ` +
268
+ `row comparing against one selected id), invert with createSelector or createProjection ` +
269
+ `so only the keys whose answer flipped update.`;
270
+ emitDiagnostic({
271
+ code: "WIDE_WRITE",
272
+ kind: "perf",
273
+ severity: "warn",
274
+ message,
275
+ nodeName: nodeName(node),
276
+ data: { subscribers: subs, write: kind }
277
+ });
278
+ console.warn(message);
279
+ }
280
+ function stampWrite(node, kind, prev = NO_VALUES, value = NO_VALUES) {
281
+ const record = { seq: ++changeSeq, kind, name: nodeName(node) };
282
+ if (value !== NO_VALUES) {
283
+ record.prev = prev === NO_VALUES ? undefined : preview(prev);
284
+ record.value = preview(value);
285
+ }
286
+ record.stack = captureStack();
287
+ node._devChange = record;
288
+ // stampWrite is the single funnel for committed root invalidations (sync
289
+ // writes, refresh(), async landings), which makes it the one place the
290
+ // written-fan-out check needs to live.
291
+ checkWideWrite(node, kind);
292
+ }
293
+ /** Record a derived change (memo produced a new value) with its causes. */
294
+ function stampDerived(node, causes) {
295
+ node._devChange = {
296
+ seq: ++changeSeq,
297
+ kind: "derived",
298
+ name: nodeName(node),
299
+ causes
300
+ };
301
+ }
302
+ /**
303
+ * Collect the deps whose committed change is newer than this node's previous
304
+ * run. Called at recompute entry, while `_deps` still holds the previous
305
+ * run's links. A refresh() stamp on the node itself also counts — that is a
306
+ * self-invalidation, not a dep change.
307
+ */
308
+ function collectCauses(el) {
309
+ const seen = el._devSeenSeq ?? 0;
310
+ const causes = [];
311
+ const self = el._devChange;
312
+ if (self !== undefined && self.seq > seen && self.kind === "refresh") causes.push(self);
313
+ for (let l = el._deps; l !== null; l = l._nextDep) {
314
+ const change = l._dep._devChange;
315
+ if (change !== undefined && change.seq > seen) causes.push(change);
316
+ }
317
+ return causes;
318
+ }
319
+ /** Advance the node's seen-cursor to the present. Call after every run. */
320
+ function markSeen(el) {
321
+ el._devSeenSeq = changeSeq;
322
+ }
323
+ /** Snapshot the node's current dep identities (call before a run replaces them). */
324
+ function captureDeps(el) {
325
+ const deps = [];
326
+ for (let l = el._deps; l !== null; l = l._nextDep) deps.push(l._dep);
327
+ return deps;
328
+ }
329
+ /**
330
+ * Wide-scope warning — the coarse-read / helper-leak signature: one scope
331
+ * subscribed to dozens of sources re-runs when ANY of them change. Fired from
332
+ * recordRerun for re-runs and directly from recompute for creation runs (a
333
+ * memo can be born too wide). Re-warns only on 50% further growth.
334
+ */
335
+ function checkDepWidth(el) {
336
+ const limit = options.wideDeps;
337
+ if (limit === false) return;
338
+ let count = 0;
339
+ const names = [];
340
+ for (let l = el._deps; l !== null; l = l._nextDep) {
341
+ count++;
342
+ if (names.length < 12) names.push(nodeName(l._dep));
343
+ }
344
+ const node = el;
345
+ if (count < limit || count < (node._devWideWarnedAt ?? 0) * 1.5) return;
346
+ node._devWideWarnedAt = count;
347
+ const kind = el._type ? "effect" : "memo";
348
+ const message =
349
+ `[WIDE_SCOPE_DEPS] ${kind} "${nodeName(el)}" is subscribed to ${count} sources — ` +
350
+ `it re-runs when any of them change. Narrow its reads or split it into smaller memos. ` +
351
+ `Sources: ${names.join(", ")}${count > names.length ? ", …" : ""}`;
352
+ emitDiagnostic({
353
+ code: "WIDE_SCOPE_DEPS",
354
+ kind: "perf",
355
+ severity: "warn",
356
+ message,
357
+ nodeName: nodeName(el),
358
+ data: { depCount: count, deps: names }
359
+ });
360
+ console.warn(message);
361
+ }
362
+ /**
363
+ * Hot-scope warning — flags a scope that re-ran more than `count` times
364
+ * inside one `windowMs` window. Warned once per window, with the most recent
365
+ * cause chain named so the leaking signal is identified in the message.
366
+ */
367
+ function checkHotRuns(el, event) {
368
+ const cfg = options.hotRuns;
369
+ if (cfg === false) return;
370
+ const node = el;
371
+ const now = Date.now();
372
+ if (node._devWinStart === undefined || now - node._devWinStart > cfg.windowMs) {
373
+ node._devWinStart = now;
374
+ node._devWinCount = 0;
375
+ node._devHotWarned = false;
376
+ }
377
+ node._devWinCount = (node._devWinCount ?? 0) + 1;
378
+ if (node._devHotWarned || node._devWinCount < cfg.count) return;
379
+ node._devHotWarned = true;
380
+ const rootCause = event.causes.map(c => `"${c.name}" (${c.kind})`).join(", ");
381
+ const message =
382
+ `[HOT_SCOPE_RERUNS] ${event.nodeKind} "${event.nodeName}" re-ran ${node._devWinCount} times ` +
383
+ `in ${Math.max(1, now - node._devWinStart)}ms — a hot signal is likely leaking into this ` +
384
+ `scope. Latest cause: ${rootCause || "(untracked pull)"}`;
385
+ emitDiagnostic({
386
+ code: "HOT_SCOPE_RERUNS",
387
+ kind: "perf",
388
+ severity: "warn",
389
+ message,
390
+ nodeName: event.nodeName,
391
+ data: {
392
+ runs: node._devWinCount,
393
+ windowMs: cfg.windowMs,
394
+ causes: event.causes.map(c => c.name)
395
+ }
396
+ });
397
+ console.warn(message);
398
+ }
399
+ /**
400
+ * Time-budget warning — the counterpart of checkHotRuns for the
401
+ * few-but-expensive scope: warns when one scope's summed self-time within a
402
+ * window exceeds the budget. Warned once per window.
403
+ */
404
+ function checkHotTime(el, event) {
405
+ const cfg = options.hotTime;
406
+ if (cfg === false) return;
407
+ const node = el;
408
+ const at = now();
409
+ if (node._devTimeWinStart === undefined || at - node._devTimeWinStart > cfg.windowMs) {
410
+ node._devTimeWinStart = at;
411
+ node._devTimeWinMs = 0;
412
+ node._devTimeWarned = false;
413
+ }
414
+ node._devTimeWinMs = (node._devTimeWinMs ?? 0) + event.selfMs;
415
+ if (node._devTimeWarned || node._devTimeWinMs < cfg.budgetMs) return;
416
+ node._devTimeWarned = true;
417
+ const rootCause = event.causes.map(c => `"${c.name}" (${c.kind})`).join(", ");
418
+ const message =
419
+ `[HOT_SCOPE_TIME] ${event.nodeKind} "${event.nodeName}" spent ` +
420
+ `${node._devTimeWinMs.toFixed(1)}ms of compute inside one ${cfg.windowMs}ms window ` +
421
+ `(budget ${cfg.budgetMs}ms). Latest cause: ${rootCause || "(untracked pull)"}`;
422
+ emitDiagnostic({
423
+ code: "HOT_SCOPE_TIME",
424
+ kind: "perf",
425
+ severity: "warn",
426
+ message,
427
+ nodeName: event.nodeName,
428
+ data: {
429
+ spentMs: node._devTimeWinMs,
430
+ budgetMs: cfg.budgetMs,
431
+ windowMs: cfg.windowMs,
432
+ causes: event.causes.map(c => c.name)
433
+ }
434
+ });
435
+ console.warn(message);
436
+ }
437
+ function recordRerun(el, causes, prevDeps, timing, changed, phase, held) {
438
+ const node = el;
439
+ // Subscription diff: `prevDeps` was captured at run entry; `_deps` now
440
+ // holds the fresh set. A changed set is the "helper edit changed distant
441
+ // call sites" signal — surfaced per-event and in the console format.
442
+ const newDeps = captureDeps(el);
443
+ const prevSet = new Set(prevDeps);
444
+ const newSet = new Set(newDeps);
445
+ const depsAdded = [];
446
+ const depsRemoved = [];
447
+ for (const d of newDeps) if (!prevSet.has(d)) depsAdded.push(nodeName(d));
448
+ for (const d of prevDeps) if (!newSet.has(d)) depsRemoved.push(nodeName(d));
449
+ const event = {
450
+ run: ++runSeq,
451
+ nodeRuns: (node._devRunCount = (node._devRunCount ?? 0) + 1),
452
+ nodeKind: el._type ? "effect" : "memo",
453
+ nodeName: nodeName(el),
454
+ node: el,
455
+ causes,
456
+ depCount: newDeps.length,
457
+ depsAdded,
458
+ depsRemoved,
459
+ selfMs: timing.selfMs,
460
+ totalMs: timing.totalMs,
461
+ changed,
462
+ phase,
463
+ held
464
+ };
465
+ history.push(event);
466
+ if (history.length > options.historyLimit) history.shift();
467
+ recordCosts(event);
468
+ checkHotRuns(el, event);
469
+ checkHotTime(el, event);
470
+ checkDepWidth(el);
471
+ for (const listener of listeners) listener(event);
472
+ if (options.log) console.log(formatRerun(event));
473
+ }
474
+ function formatCause(cause, depth, out) {
475
+ const pad = " ".repeat(depth + 1);
476
+ let line = `${pad}← ${cause.kind === "derived" ? "memo" : "signal"} "${cause.name}" ${cause.kind === "derived" ? "changed" : cause.kind} (#${cause.seq})`;
477
+ if (cause.prev !== undefined) line += ` ${cause.prev} → ${cause.value}`;
478
+ out.push(line);
479
+ if (cause.stack) for (const frame of cause.stack) out.push(`${pad} ${frame}`);
480
+ if (cause.causes && depth < 10) {
481
+ for (const upstream of cause.causes) formatCause(upstream, depth + 1, out);
482
+ }
483
+ }
484
+ function formatRerun(event) {
485
+ const out = [
486
+ `[why-run] ${event.nodeKind} "${event.nodeName}" ran (run ${event.nodeRuns}, ` +
487
+ `${event.selfMs.toFixed(2)}ms${event.changed ? "" : ", unchanged"}` +
488
+ `${event.phase === "plain" ? "" : `, ${event.phase}`}${event.held ? ", held" : ""})` +
489
+ (event.causes.length === 0 ? " — no tracked cause (pull or retry)" : "")
490
+ ];
491
+ for (const cause of event.causes) formatCause(cause, 0, out);
492
+ if (event.depsAdded.length > 0 || event.depsRemoved.length > 0) {
493
+ const delta = [
494
+ ...event.depsAdded.map(n => `+"${n}"`),
495
+ ...event.depsRemoved.map(n => `-"${n}"`)
496
+ ].join(" ");
497
+ out.push(` deps changed: ${delta} (${event.depCount} total)`);
498
+ }
499
+ return out.join("\n");
500
+ }
501
+ /**
502
+ * Values eligible for the unstable-output check: plain objects and arrays
503
+ * only. Promises, iterators, Dates, Maps, class instances etc. all have no
504
+ * (or unrepresentative) own enumerable keys, so a shallow compare would
505
+ * false-positive on them — a fresh Promise is a genuinely new value.
506
+ */
507
+ function isPlainShape(v) {
508
+ if (v === null || typeof v !== "object") return false;
509
+ if (Array.isArray(v)) return true;
510
+ const proto = Object.getPrototypeOf(v);
511
+ return proto === Object.prototype || proto === null;
512
+ }
513
+ /** Shallow structural equivalence, capped so hot paths stay cheap. */
514
+ const UNSTABLE_KEY_CAP = 64;
515
+ function shallowEquivalent(a, b) {
516
+ const aArr = Array.isArray(a);
517
+ if (aArr !== Array.isArray(b)) return false;
518
+ if (aArr) {
519
+ const arrA = a;
520
+ const arrB = b;
521
+ if (arrA.length !== arrB.length || arrA.length > UNSTABLE_KEY_CAP) return false;
522
+ for (let i = 0; i < arrA.length; i++) if (arrA[i] !== arrB[i]) return false;
523
+ return true;
524
+ }
525
+ const keys = Object.keys(a);
526
+ if (keys.length > UNSTABLE_KEY_CAP || keys.length !== Object.keys(b).length) return false;
527
+ for (const key of keys) {
528
+ if (!(key in b) || a[key] !== b[key]) return false;
529
+ }
530
+ return true;
531
+ }
532
+ /**
533
+ * Unstable-output warning — the fan-out amplifier signature: a memo whose
534
+ * committed value is referentially new but structurally identical run after
535
+ * run has an equality gate that never closes, so ALL its subscribers re-run
536
+ * on EVERY upstream change. Checked only on plain (non-overlay) changed runs;
537
+ * a genuinely different value (or a non-plain shape) resets the streak.
538
+ */
539
+ function checkUnstableOutput(el, prevValue, newValue) {
540
+ const limit = options.unstableMemos;
541
+ // typeof guard: an explicit `unstableMemos: undefined` in enable() options
542
+ // clobbers the default through the spread — treat any non-number as off.
543
+ if (typeof limit !== "number") return;
544
+ const node = el;
545
+ if (
546
+ prevValue === newValue || // paranoia: changed runs should never hit this
547
+ !isPlainShape(prevValue) ||
548
+ !isPlainShape(newValue) ||
549
+ !shallowEquivalent(prevValue, newValue)
550
+ ) {
551
+ node._devUnstableRuns = 0;
552
+ node._devUnstableWarned = false;
553
+ return;
554
+ }
555
+ node._devUnstableRuns = (node._devUnstableRuns ?? 0) + 1;
556
+ if (node._devUnstableWarned || node._devUnstableRuns < limit) return;
557
+ node._devUnstableWarned = true;
558
+ const shape = Array.isArray(newValue) ? "array" : "object";
559
+ const message =
560
+ `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(el)}" produced a new-but-equivalent ${shape} on ` +
561
+ `${node._devUnstableRuns} consecutive runs — its equality gate never closes, so every ` +
562
+ `subscriber re-runs on every upstream change. Return stable references or pass an ` +
563
+ `\`equals\` option.`;
564
+ emitDiagnostic({
565
+ code: "UNSTABLE_MEMO_OUTPUT",
566
+ kind: "perf",
567
+ severity: "warn",
568
+ message,
569
+ nodeName: nodeName(el),
570
+ data: { runs: node._devUnstableRuns, shape }
571
+ });
572
+ console.warn(message);
573
+ }
574
+ // The engine's implementation of the core's dev hook points. Installed by
575
+ // enable(), uninstalled by disable() — while uninstalled the core pays one
576
+ // null check per site and nothing else.
577
+ let asyncStartSeq = 0;
578
+ let asyncStartTime = 0;
579
+ let asyncStartValue;
580
+ const engineHooks = {
581
+ recomputeStart(el, create) {
582
+ frames.push({
583
+ start: now(),
584
+ childMs: 0,
585
+ causes: create ? null : collectCauses(el),
586
+ prevDeps: create ? null : captureDeps(el),
587
+ // Mirror recompute's own prev-value resolution: an earlier run in the
588
+ // same flush may still be holding in _pendingValue.
589
+ prevValue: el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
590
+ });
591
+ },
592
+ derivedChanged(el) {
593
+ const frame = frames[frames.length - 1];
594
+ stampDerived(el, frame !== undefined && frame.causes !== null ? frame.causes : []);
595
+ },
596
+ recomputeEnd(el, _create, changed, optimistic, transition, held) {
597
+ const frame = frames.pop();
598
+ // enable() can land mid-recompute: no opening frame, nothing to report.
599
+ if (frame === undefined) return;
600
+ const totalMs = now() - frame.start;
601
+ if (frames.length > 0) frames[frames.length - 1].childMs += totalMs;
602
+ const selfMs = Math.max(0, totalMs - frame.childMs);
603
+ // Unstable-output check: memos only, non-create, plain runs with a
604
+ // committed change. The fresh value sits in `_pendingValue` for held
605
+ // plain-flush memo commits and in `_value` for direct ones. Overlay runs
606
+ // are excluded — an optimistic re-derive legitimately produces fresh
607
+ // equivalents while the lane settles.
608
+ if (frame.causes !== null && changed && !optimistic && !transition && !el._type)
609
+ checkUnstableOutput(
610
+ el,
611
+ frame.prevValue,
612
+ el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
613
+ );
614
+ if (frame.causes !== null)
615
+ recordRerun(
616
+ el,
617
+ frame.causes,
618
+ frame.prevDeps,
619
+ { selfMs, totalMs },
620
+ changed,
621
+ optimistic ? "optimistic" : transition ? "transition" : "plain",
622
+ held
623
+ );
624
+ // Creation runs still get the wide-scope check: a memo can be born with
625
+ // its coarse-read problem already in place.
626
+ else checkDepWidth(el);
627
+ markSeen(el);
628
+ },
629
+ write(el, prev, value) {
630
+ stampWrite(el, "write", prev, value);
631
+ },
632
+ refreshed(el) {
633
+ stampWrite(el, "refresh");
634
+ },
635
+ asyncStart(el) {
636
+ asyncStartSeq = el._devChange?.seq ?? 0;
637
+ asyncStartTime = el._time;
638
+ asyncStartValue = el._value;
639
+ },
640
+ asyncEnd(el, prev, value, direct) {
641
+ if (direct) {
642
+ // Core calls this unconditionally (hook calls cannot live inside its
643
+ // try blocks — see attribution-hooks.ts), so committed-ness is detected
644
+ // here against the asyncStart snapshot: a direct commit moves `_value`
645
+ // (or `_time`, for a same-reference commit under `equals: false`), and
646
+ // a transition hold parks the value in `_pendingValue`. A landing the
647
+ // equality gate swallowed moves none of them and must leave no stamp.
648
+ const committed =
649
+ el._value !== asyncStartValue || el._time !== asyncStartTime || el._pendingValue === value;
650
+ if (committed) stampWrite(el, "async", prev === undefined ? NO_VALUES : prev, value);
651
+ return;
652
+ }
653
+ // Landed through setSignal: reclassify its "write" stamp as an async
654
+ // landing — but only if it actually stamped (the value changed) since
655
+ // asyncStart; a no-change landing must leave no fresh stamp behind.
656
+ const change = el._devChange;
657
+ if (change !== undefined && change.seq > asyncStartSeq && change.kind === "write")
658
+ stampWrite(el, "async", NO_VALUES, value);
659
+ }
660
+ };
661
+ const attribution = {
662
+ enable(opts) {
663
+ options = { ...defaultOptions, ...opts };
664
+ frames.length = 0;
665
+ scopeCosts.clear();
666
+ writeCosts.clear();
667
+ setAttributionHooks(engineHooks);
668
+ },
669
+ disable() {
670
+ listeners.clear();
671
+ history = [];
672
+ frames.length = 0;
673
+ scopeCosts.clear();
674
+ writeCosts.clear();
675
+ setAttributionHooks(null);
676
+ },
677
+ subscribe(listener) {
678
+ listeners.add(listener);
679
+ return () => listeners.delete(listener);
680
+ },
681
+ history() {
682
+ return history;
683
+ },
684
+ why(target) {
685
+ const node = target?.[$REFRESH] ?? target;
686
+ return history.filter(event => event.node === node);
687
+ },
688
+ subscriptions(target) {
689
+ const node = target?.[$REFRESH] ?? target;
690
+ const names = [];
691
+ for (let l = node?._deps ?? null; l !== null; l = l._nextDep) names.push(nodeName(l._dep));
692
+ return names;
693
+ },
694
+ costs() {
695
+ return {
696
+ scopes: [...scopeCosts.values()].sort((a, b) => b.selfMs - a.selfMs),
697
+ writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
698
+ };
699
+ },
700
+ format: formatRerun
701
+ };
702
+
703
+ /** First warning when a node's live edge count reaches this size. */
704
+ const GRAPH_SIZE_WARN_AT = 2000;
705
+ /** Repeat the warning at this interval after the first. */
706
+ const GRAPH_SIZE_WARN_EVERY = 500;
120
707
  const hooks = {};
121
708
  const diagnosticListeners = new Set();
122
709
  const diagnosticCaptures = new Set();
@@ -146,6 +733,12 @@ const diagnostics = {
146
733
  const DEV$1 = {
147
734
  hooks,
148
735
  diagnostics,
736
+ // Getter: attribution.ts imports emitDiagnostic back from this module,
737
+ // so when attribution.ts evaluates first the `attribution` binding is
738
+ // still uninitialized here — defer the read to access time.
739
+ get attribution() {
740
+ return attribution;
741
+ },
149
742
  getChildren,
150
743
  getSignals,
151
744
  getParent,
@@ -239,6 +832,64 @@ function getObservers(node) {
239
832
  }
240
833
  return observers;
241
834
  }
835
+ function shouldWarnGraphSize(count) {
836
+ return count >= GRAPH_SIZE_WARN_AT && (count - GRAPH_SIZE_WARN_AT) % GRAPH_SIZE_WARN_EVERY === 0;
837
+ }
838
+ /**
839
+ * DEV-only: bump live edge counts after a new graph link and warn when a
840
+ * node grows an unusually large fan-out (many subscribers on one source) or
841
+ * fan-in (many sources on one computation). Repeat-reads that `link()`
842
+ * dedupes never reach here. Always-on in dev — unlike the opt-in attribution
843
+ * engine, a graph-size pathology should surface without asking.
844
+ */
845
+ function noteGraphLink(dep, sub) {
846
+ const fanOut = (dep._subCount = (dep._subCount || 0) + 1);
847
+ const fanIn = (sub._depCount = (sub._depCount || 0) + 1);
848
+ if (shouldWarnGraphSize(fanOut)) {
849
+ const name = dep._name;
850
+ const message =
851
+ `[HUGE_FAN_OUT] ${name ? `Signal "${name}"` : "A signal"} has ${fanOut} subscribers. ` +
852
+ `Each will re-run when it changes. If many independent computations read the same value ` +
853
+ `(for example every row of a list comparing against one selected id), prefer a per-key ` +
854
+ `store or projection so only the items whose result flipped update.`;
855
+ emitDiagnostic({
856
+ code: "HUGE_FAN_OUT",
857
+ kind: "graph",
858
+ severity: "warn",
859
+ message,
860
+ nodeName: name,
861
+ ownerId: dep.id,
862
+ ownerName: name,
863
+ data: { count: fanOut }
864
+ });
865
+ console.warn(message);
866
+ }
867
+ if (shouldWarnGraphSize(fanIn)) {
868
+ const name = sub._name;
869
+ const message =
870
+ `[HUGE_FAN_IN] ${name ? `Computation "${name}"` : "A computation"} has ${fanIn} sources. ` +
871
+ `It will re-run when any of them change. Narrow the read or split the derivation so each ` +
872
+ `computation tracks only what it needs.`;
873
+ emitDiagnostic({
874
+ code: "HUGE_FAN_IN",
875
+ kind: "graph",
876
+ severity: "warn",
877
+ message,
878
+ nodeName: name,
879
+ ownerId: sub.id,
880
+ ownerName: name,
881
+ data: { count: fanIn }
882
+ });
883
+ console.warn(message);
884
+ }
885
+ }
886
+ /** DEV-only: drop live edge counts when a link is removed. */
887
+ function unnoteGraphLink(link) {
888
+ const dep = link._dep;
889
+ const sub = link._sub;
890
+ if (dep._subCount) dep._subCount--;
891
+ if (sub._depCount) sub._depCount--;
892
+ }
242
893
 
243
894
  function createAsyncReporters() {
244
895
  return new Map();
@@ -298,8 +949,9 @@ function getOrCreateLane(signal) {
298
949
  }
299
950
  // Detect parent lane: _parentSource chains from pendingSignal → pendingValueComputed → original.
300
951
  // The child lane should not merge with the parent lane.
301
- const parentSource = signal._parentSource;
302
- const parentLane = parentSource?._optimisticLane ? findLane(parentSource._optimisticLane) : null;
952
+ const parentSource = signal._x?._parentSource;
953
+ const parentOptLane = parentSource?._x?._optimisticLane;
954
+ const parentLane = parentOptLane ? findLane(parentOptLane) : null;
303
955
  lane = {
304
956
  _source: signal,
305
957
  _pendingAsync: new Set(),
@@ -316,8 +968,8 @@ function getOrCreateLane(signal) {
316
968
  // parent-child is a property of the nodes, not of write order — otherwise
317
969
  // the owner's write merges the companion's subscribers into this lane and
318
970
  // their effects wait on its async instead of flushing immediately.
319
- adoptCompanionLane(signal._pendingSignal, lane);
320
- adoptCompanionLane(signal._latestValueComputed, lane);
971
+ adoptCompanionLane(signal._x?._pendingSignal, lane);
972
+ adoptCompanionLane(signal._x?._latestValueComputed, lane);
321
973
  return lane;
322
974
  }
323
975
  function adoptCompanionLane(companion, parent) {
@@ -359,11 +1011,11 @@ function mergeLanes(lane1, lane2) {
359
1011
  * Resolve a node's lane: follow union-find chain, verify active, clear if stale.
360
1012
  */
361
1013
  function resolveLane(el) {
362
- const lane = el._optimisticLane;
1014
+ const lane = el._x?._optimisticLane;
363
1015
  if (!lane) return undefined;
364
1016
  const root = findLane(lane);
365
1017
  if (activeLanes.has(root)) return root;
366
- el._optimisticLane = undefined;
1018
+ if (el._x !== null) el._x._optimisticLane = undefined;
367
1019
  return undefined;
368
1020
  }
369
1021
  function resolveTransition(el) {
@@ -372,10 +1024,10 @@ function resolveTransition(el) {
372
1024
  // transactions (#2912) — the merged root's _transition would hand this
373
1025
  // node's override to whichever action wrote last through the shared
374
1026
  // reader. Chase merge chains; a dead owner settled through another path.
375
- if (hasActiveOverride$1(el) && el._overrideOwner) {
376
- const owner = (el._overrideOwner = currentTransition(el._overrideOwner));
1027
+ if (hasActiveOverride$1(el) && el._x?._overrideOwner) {
1028
+ const owner = (ext(el)._overrideOwner = currentTransition(el._x?._overrideOwner));
377
1029
  if (owner._done !== true) return owner;
378
- el._overrideOwner = null;
1030
+ if (el._x !== null) el._x._overrideOwner = null;
379
1031
  }
380
1032
  return resolveLane(el)?._transition ?? el._transition;
381
1033
  }
@@ -383,7 +1035,8 @@ function resolveTransition(el) {
383
1035
  * Check if a node has an active optimistic override.
384
1036
  */
385
1037
  function hasActiveOverride$1(el) {
386
- return !!(el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING);
1038
+ const x = el._x;
1039
+ return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
387
1040
  }
388
1041
  /**
389
1042
  * Assign or merge a lane onto a node. At convergence points (node already has
@@ -391,13 +1044,14 @@ function hasActiveOverride$1(el) {
391
1044
  */
392
1045
  function assignOrMergeLane(el, sourceLane) {
393
1046
  const sourceRoot = findLane(sourceLane);
394
- const existing = el._optimisticLane;
1047
+ const existing = el._x?._optimisticLane;
395
1048
  if (existing) {
396
1049
  // If the subscriber's lane was merged into another lane, it's stale —
397
1050
  // replace it with the new source lane instead of following the merge chain
398
1051
  // (which would incorrectly merge the new lane into the old group)
399
1052
  if (existing._mergedInto) {
400
- el._optimisticLane = sourceLane;
1053
+ ext(el)._optimisticLane = sourceLane;
1054
+ el._config |= CONFIG_HAS_LANE;
401
1055
  return;
402
1056
  }
403
1057
  const existingRoot = findLane(existing);
@@ -406,14 +1060,16 @@ function assignOrMergeLane(el, sourceLane) {
406
1060
  // Parent-child lanes stay independent so isPending resolves without
407
1061
  // waiting for the parent's async. The child keeps ownership.
408
1062
  if (sourceRoot._parentLane && findLane(sourceRoot._parentLane) === existingRoot) {
409
- el._optimisticLane = sourceLane;
1063
+ ext(el)._optimisticLane = sourceLane;
1064
+ el._config |= CONFIG_HAS_LANE;
410
1065
  } else if (existingRoot._parentLane && findLane(existingRoot._parentLane) === sourceRoot);
411
1066
  else mergeLanes(sourceRoot, existingRoot);
412
1067
  }
413
1068
  return;
414
1069
  }
415
1070
  }
416
- el._optimisticLane = sourceLane;
1071
+ ext(el)._optimisticLane = sourceLane;
1072
+ el._config |= CONFIG_HAS_LANE;
417
1073
  }
418
1074
 
419
1075
  const transitions = new Set();
@@ -474,13 +1130,13 @@ function sweepTransientStoreNodes() {
474
1130
  continue;
475
1131
  }
476
1132
  if (node._pendingValue !== NOT_PENDING) continue;
477
- if (node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING) continue;
1133
+ if (node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING) continue;
478
1134
  // A live affects() mark keeps the node addressable: sweeping it would
479
1135
  // detach the refcount from the slot (a fresh probe would upsert a new,
480
1136
  // unmarked node for the same property).
481
- if (node._affectsCount) continue;
1137
+ if (node._x?._affectsCount) continue;
482
1138
  transientStoreNodes.delete(node);
483
- node._unobserved?.();
1139
+ node._x?._unobserved?.();
484
1140
  }
485
1141
  }
486
1142
  function resetUnhandledAsync() {
@@ -732,6 +1388,9 @@ class GlobalQueue extends Queue {
732
1388
  static _applyReask = null;
733
1389
  static _repollVerdicts = null;
734
1390
  static _witnessAffects = null;
1391
+ // Re-asks probes whose verdict was provisionally suppressed by a fresh read
1392
+ // of a held value, once the transaction gains an async blocker (#3028).
1393
+ static _wakeSuppressedProbes = null;
735
1394
  // Optimistic-engine hooks (wired by core/optimistic.ts via
736
1395
  // installOptimisticEngine(), called from verdict.ts / createOptimistic /
737
1396
  // createOptimisticStore — every module that can create optimistic state).
@@ -864,7 +1523,7 @@ class GlobalQueue extends Queue {
864
1523
  // Only track async if the boundary is propagating STATUS_PENDING (not caught by boundary)
865
1524
  if (mask & STATUS_PENDING) {
866
1525
  if (flags & STATUS_PENDING) {
867
- const actualError = error !== undefined ? error : node._error;
1526
+ const actualError = error !== undefined ? error : node._x?._error;
868
1527
  // A visibility-only mark notification (the affects() boundary
869
1528
  // channel) updates display state on its way up but must be invisible
870
1529
  // to completion accounting BY CONSTRUCTION: it never registers a
@@ -876,7 +1535,10 @@ class GlobalQueue extends Queue {
876
1535
  if (!reporters) activeTransition._asyncReporters.set(source, (reporters = new Set()));
877
1536
  const prevSize = reporters.size;
878
1537
  reporters.add(node);
879
- if (reporters.size !== prevSize) schedule();
1538
+ if (reporters.size !== prevSize) {
1539
+ schedule();
1540
+ GlobalQueue._wakeSuppressedProbes?.(activeTransition);
1541
+ }
880
1542
  }
881
1543
  if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
882
1544
  }
@@ -939,32 +1601,60 @@ function queuePendingNode(node) {
939
1601
  // REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
940
1602
  // clear entirely in apps that never refresh.
941
1603
  let reaskArmed = false;
1604
+ /** §12d: bumped by every recompute and every new subscriber edge. A node's
1605
+ * staged-rewrite skip is sound only while NOTHING recomputed or linked since
1606
+ * its last notify — a mid-batch pull can clean a marked subscriber, and a
1607
+ * skipped re-write would leave it stale. */
1608
+ let notifyEpoch = 0;
1609
+ function bumpNotifyEpoch() {
1610
+ notifyEpoch++;
1611
+ }
942
1612
  function armReaskClear() {
943
1613
  reaskArmed = true;
944
1614
  }
945
1615
  function insertSubs(node, optimistic = false) {
1616
+ // §12d: stamp before walking — setSignal's staged-rewrite fast path skips
1617
+ // the next walk for this node while the epoch holds (marking is idempotent).
1618
+ node._notifiedAt = notifyEpoch;
946
1619
  // Get source lane: prefer node's own lane over current context
947
1620
  // This is important for isPending signals which need their own lane to flush immediately
948
- const sourceLane = node._optimisticLane || currentOptimisticLane;
949
- const hasSnapshot = node._snapshotValue !== undefined;
1621
+ // Presence bits gate the optional-slot probes (see constants.ts): one
1622
+ // masked read of the always-present _config instead of missing-property
1623
+ // lookups in the hottest notify loop. Bits are sticky — the field read
1624
+ // stays authoritative when a bit is set.
1625
+ const cfg = node._config;
1626
+ const sourceLane =
1627
+ (cfg & CONFIG_HAS_LANE ? node._x?._optimisticLane : undefined) || currentOptimisticLane;
1628
+ const hasSnapshot = (cfg & CONFIG_HAS_SNAPSHOT) !== 0 && node._x?._snapshotValue !== undefined;
950
1629
  const clearReask = reaskArmed;
951
1630
  for (let s = node._subs; s !== null; s = s._nextSub) {
1631
+ const sub = s._sub;
952
1632
  // A value-change notification is a new question for the subscriber: any
953
1633
  // pending re-ask mark (refresh) it carried is superseded.
954
- if (clearReask) s._sub._flags &= ~REACTIVE_REASK;
955
- if (hasSnapshot && s._sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
956
- s._sub._flags |= REACTIVE_SNAPSHOT_STALE;
1634
+ if (clearReask) sub._flags &= ~REACTIVE_REASK;
1635
+ // Missed-wake latch (#3037): this write is landing while the subscriber
1636
+ // is mid-recompute (a nested pull committing beneath its reads), and the
1637
+ // heap refuses RECOMPUTING nodes. A gen-current link means the pass
1638
+ // already validated this dep — the value it read is now stale — so latch
1639
+ // for recompute's tail to reschedule. Untouched links need no latch (the
1640
+ // pass either re-reads them fresh or trims them), and neither does the
1641
+ // tail link: it is the read IN FLIGHT — read() links before it pulls, so
1642
+ // this very commit is what that read returns.
1643
+ if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
1644
+ sub._flags |= REACTIVE_MISSED_WAKE;
1645
+ if (hasSnapshot && sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
1646
+ sub._flags |= REACTIVE_SNAPSHOT_STALE;
957
1647
  continue;
958
1648
  }
959
1649
  if (optimistic && sourceLane) {
960
- s._sub._flags |= REACTIVE_OPTIMISTIC_DIRTY;
961
- assignOrMergeLane(s._sub, sourceLane);
1650
+ sub._flags |= REACTIVE_OPTIMISTIC_DIRTY;
1651
+ assignOrMergeLane(sub, sourceLane);
962
1652
  } else if (optimistic) {
963
- s._sub._flags |= REACTIVE_OPTIMISTIC_DIRTY;
1653
+ sub._flags |= REACTIVE_OPTIMISTIC_DIRTY;
964
1654
  // No source lane means reversion - clear subscriber's lane so effects go to regular queue
965
- s._sub._optimisticLane = undefined;
1655
+ if (sub._x) sub._x._optimisticLane = undefined;
966
1656
  }
967
- enqueueSub(s._sub);
1657
+ enqueueSub(sub);
968
1658
  }
969
1659
  }
970
1660
  function commitPendingNode(n) {
@@ -974,7 +1664,7 @@ function commitPendingNode(n) {
974
1664
  n._value = n._pendingValue;
975
1665
  n._pendingValue = NOT_PENDING;
976
1666
  }
977
- if (n._pendingSignal || n._latestValueComputed) GlobalQueue._snapCompanions(n);
1667
+ if (n._config & CONFIG_HAS_COMPANIONS) GlobalQueue._snapCompanions(n);
978
1668
  return;
979
1669
  }
980
1670
  if (n._pendingValue !== NOT_PENDING) {
@@ -989,9 +1679,9 @@ function commitPendingNode(n) {
989
1679
  c._loading = false;
990
1680
  c._flags &= ~REACTIVE_MANUAL_WRITE;
991
1681
  if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
992
- if (c._pendingFirstChild !== null || c._pendingDisposal !== null)
1682
+ if (c._x != null && (c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null))
993
1683
  GlobalQueue._dispose(c, false, true);
994
- if (n._pendingSignal || n._latestValueComputed) GlobalQueue._snapCompanions(n);
1684
+ if (n._config & CONFIG_HAS_COMPANIONS) GlobalQueue._snapCompanions(n);
995
1685
  }
996
1686
  // Store commit hook (INTERNALS-STORE-STATE.md §3): installed by the store
997
1687
  // module at init (same treeshakeable pattern as _resolveOptimistic /
@@ -1145,18 +1835,18 @@ function runQueue$1(queue, type) {
1145
1835
  }
1146
1836
  function reporterBlocksSource(reporter, source) {
1147
1837
  if (reporter._flags & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
1148
- if (reporter._pendingSources?.has(source)) return true;
1838
+ if (reporter._x?._pendingSources?.has(source)) return true;
1149
1839
  for (let dep = reporter._deps; dep; dep = dep._nextDep) {
1150
1840
  let current = dep._dep;
1151
1841
  while (current) {
1152
1842
  if (current === source || current._firewall === source) return true;
1153
- current = current._parentSource;
1843
+ current = current._x?._parentSource;
1154
1844
  }
1155
1845
  }
1156
1846
  return !!(
1157
1847
  reporter._statusFlags & STATUS_PENDING &&
1158
- reporter._error instanceof NotReadyError &&
1159
- reporter._error.source === source
1848
+ reporter._x?._error instanceof NotReadyError &&
1849
+ reporter._x?._error.source === source
1160
1850
  );
1161
1851
  }
1162
1852
  function transitionComplete(transition) {
@@ -1173,7 +1863,7 @@ function transitionComplete(transition) {
1173
1863
  reporters.delete(reporter);
1174
1864
  }
1175
1865
  if (!hasLive) transition._asyncReporters.delete(source);
1176
- else if (source._statusFlags & STATUS_PENDING && source._error?.source === source) {
1866
+ else if (source._statusFlags & STATUS_PENDING && source._x?._error?.source === source) {
1177
1867
  done = false;
1178
1868
  break;
1179
1869
  }
@@ -1238,6 +1928,9 @@ function actualInsertIntoHeap(n, heap) {
1238
1928
  }
1239
1929
  function insertIntoHeap(n, heap) {
1240
1930
  let flags = n._flags;
1931
+ // RECOMPUTING refusals are not always losses: a genuinely missed wake (a
1932
+ // write to a link this pass already validated) is latched link-side in
1933
+ // insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
1241
1934
  if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
1242
1935
  if (flags & REACTIVE_CHECK) {
1243
1936
  n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
@@ -1295,8 +1988,11 @@ function markNode(el, newState = REACTIVE_DIRTY) {
1295
1988
  for (let link = el._subs; link !== null; link = link._nextSub) {
1296
1989
  markNode(link._sub, REACTIVE_CHECK);
1297
1990
  }
1298
- if (el._child !== null) {
1299
- for (let child = el._child; child !== null; child = child._nextChild) {
1991
+ // Firewall children (projection machinery only): gate the cold-extension
1992
+ // deref on the config bit markNode runs per sub edge per write, and an
1993
+ // unconditional _x chase here taxed every propagation (diamond -22%).
1994
+ if (el._config & CONFIG_FW_CHILDREN) {
1995
+ for (let child = el._x._child; child !== null; child = child._nextChild) {
1300
1996
  for (let link = child._subs; link !== null; link = link._nextSub) {
1301
1997
  markNode(link._sub, REACTIVE_CHECK);
1302
1998
  }
@@ -1357,17 +2053,14 @@ function markDisposal(el) {
1357
2053
  }
1358
2054
  }
1359
2055
  function dispose(node) {
1360
- // Leave every scheduler heap on disposal (mirrors `unobserved`): a node
1361
- // still queued here would be recomputed by the next flush, and recompute()
1362
- // rewriting `_flags` would clear REACTIVE_DISPOSEDresurrecting it (#2983).
1363
- deleteFromHeap(node, queueFor(node));
1364
- let toRemove = node._deps;
1365
- while (toRemove !== null) {
1366
- toRemove = unlinkSubs(toRemove);
1367
- }
1368
- node._deps = null;
1369
- node._depsTail = null;
1370
- disposeChildren(node, true);
2056
+ // Direct disposal is death, not dormancy: strip the observation lifecycle
2057
+ // so a later read freezes at the last committed value instead of
2058
+ // reawakening the node (#3024). The teardown itself (heap removal a node
2059
+ // left queued would be recomputed and resurrected by the next flush (#2983)
2060
+ // dep unlinking, child disposal) is exactly unobserved()'s body; only
2061
+ // this flag distinguishes death from dormancy.
2062
+ node._config &= ~CONFIG_AUTO_DISPOSE;
2063
+ unobserved(node);
1371
2064
  }
1372
2065
  function disposeChildren(node, self = false, zombie) {
1373
2066
  const flags = node._flags;
@@ -1380,33 +2073,36 @@ function disposeChildren(node, self = false, zombie) {
1380
2073
  // edge). Snap runs after the DISPOSED flag is set so the oracle reads
1381
2074
  // false, and notifies subscribers still watching the companion.
1382
2075
  const n = node;
1383
- if (n._pendingSignal || n._latestValueComputed) GlobalQueue._snapCompanions(n);
2076
+ if (n._x?._pendingSignal || n._x?._latestValueComputed) GlobalQueue._snapCompanions(n);
1384
2077
  }
1385
2078
  if (self && true) clearSignals(node);
1386
- if (self && node._fn) node._inFlight = null;
1387
- let child = zombie ? node._pendingFirstChild : node._firstChild;
2079
+ if (self && node._fn && node._x !== null) node._x._inFlight = null;
2080
+ let child = zombie ? (node._x?._pendingFirstChild ?? null) : node._firstChild;
1388
2081
  while (child) {
1389
2082
  const nextChild = child._nextSibling;
1390
2083
  const n = child;
2084
+ // Owner teardown is death regardless of the child's own lifecycle
2085
+ // (#3024): strip AUTO_DISPOSE so a post-disposal read freezes at the
2086
+ // last committed value instead of reawakening in a torn-down tree.
2087
+ // Runs before the recursion so already-dormant children (whose
2088
+ // disposeChildren call early-returns on REACTIVE_DISPOSED) die too.
2089
+ // Only unobserved()'s own node keeps its dormancy — it is never in
2090
+ // this loop; its children are rebuilt fresh on reawaken.
2091
+ n._config &= ~CONFIG_AUTO_DISPOSE;
1391
2092
  // Heap removal must not be gated on `_deps`: a dependency-free
1392
2093
  // computation queued by refresh() has a null dep list but still sits in
1393
2094
  // the dirty heap, and left there the post-disposal flush recomputes it —
1394
2095
  // recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
1395
2096
  // comes back to life (post-unmount runs, leaked cleanups, #2983).
1396
- if (n._flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT)) deleteFromHeap(n, queueFor(n));
1397
- if (n._deps) {
1398
- let toRemove = n._deps;
1399
- do {
1400
- toRemove = unlinkSubs(toRemove);
1401
- } while (toRemove !== null);
1402
- n._deps = null;
1403
- n._depsTail = null;
1404
- }
2097
+ // deleteFromHeap self-guards on the in-heap flags (and tolerates plain
2098
+ // Owners, whose _flags is undefined), so no gate here.
2099
+ deleteFromHeap(n, queueFor(n));
2100
+ clearDeps(n);
1405
2101
  disposeChildren(child, true);
1406
2102
  child = nextChild;
1407
2103
  }
1408
2104
  if (zombie) {
1409
- node._pendingFirstChild = null;
2105
+ if (node._x !== null) node._x._pendingFirstChild = null;
1410
2106
  } else {
1411
2107
  node._firstChild = null;
1412
2108
  node._childCount = 0;
@@ -1439,7 +2135,7 @@ function disposeChildren(node, self = false, zombie) {
1439
2135
  }
1440
2136
  }
1441
2137
  function runDisposal(node, zombie) {
1442
- let disposal = zombie ? node._pendingDisposal : node._disposal;
2138
+ let disposal = zombie ? node._x?._pendingDisposal : node._disposal;
1443
2139
  if (!disposal) return;
1444
2140
  if (Array.isArray(disposal)) {
1445
2141
  for (let i = 0; i < disposal.length; i++) {
@@ -1449,7 +2145,9 @@ function runDisposal(node, zombie) {
1449
2145
  } else {
1450
2146
  disposal.call(disposal);
1451
2147
  }
1452
- zombie ? (node._pendingDisposal = null) : (node._disposal = null);
2148
+ if (zombie) {
2149
+ if (node._x !== null) node._x._pendingDisposal = null;
2150
+ } else node._disposal = null;
1453
2151
  }
1454
2152
  function childId(owner, consume) {
1455
2153
  let counter = owner;
@@ -1589,8 +2287,7 @@ function createOwner(options) {
1589
2287
  _queue: parent?._queue ?? globalQueue,
1590
2288
  _context: parent?._context || defaultContext,
1591
2289
  _childCount: 0,
1592
- _pendingDisposal: null,
1593
- _pendingFirstChild: null,
2290
+ _x: null,
1594
2291
  _parent: parent,
1595
2292
  dispose: disposeRootSelf
1596
2293
  };
@@ -1649,6 +2346,7 @@ function createRoot(init, options) {
1649
2346
 
1650
2347
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
1651
2348
  function unlinkSubs(link) {
2349
+ unnoteGraphLink(link);
1652
2350
  const dep = link._dep;
1653
2351
  const nextDep = link._nextDep;
1654
2352
  const nextSub = link._nextSub;
@@ -1659,7 +2357,7 @@ function unlinkSubs(link) {
1659
2357
  else {
1660
2358
  dep._subs = nextSub;
1661
2359
  if (nextSub === null) {
1662
- dep._unobserved?.();
2360
+ dep._x?._unobserved?.();
1663
2361
  // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
1664
2362
  // A pending node is exempt: its in-flight async work (or the
1665
2363
  // transition holding it) is an observer — tearing down would orphan
@@ -1687,14 +2385,22 @@ function trimStaleDeps(el) {
1687
2385
  else el._deps = null;
1688
2386
  }
1689
2387
  }
1690
- function unobserved(el) {
1691
- deleteFromHeap(el, queueFor(el));
2388
+ // Shared by unobserved() and the disposeChildren child loop. The truthy guard
2389
+ // (not `!== null`) matters: plain Owners in a child chain have no _deps field,
2390
+ // and skipping early also avoids adding one (hidden-class churn) via the
2391
+ // null-out below.
2392
+ function clearDeps(el) {
1692
2393
  let dep = el._deps;
1693
- while (dep !== null) {
2394
+ if (!dep) return;
2395
+ do {
1694
2396
  dep = unlinkSubs(dep);
1695
- }
2397
+ } while (dep !== null);
1696
2398
  el._deps = null;
1697
2399
  el._depsTail = null;
2400
+ }
2401
+ function unobserved(el) {
2402
+ deleteFromHeap(el, queueFor(el));
2403
+ clearDeps(el);
1698
2404
  disposeChildren(el, true);
1699
2405
  }
1700
2406
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
@@ -1754,6 +2460,9 @@ function link(dep, sub, pendingObserver = false) {
1754
2460
  else sub._deps = newLink;
1755
2461
  if (prevSub !== null) prevSub._nextSub = newLink;
1756
2462
  else dep._subs = newLink;
2463
+ // New subscriber edge: staged-rewrite skips (§12d) must not miss it.
2464
+ bumpNotifyEpoch();
2465
+ noteGraphLink(dep, sub);
1757
2466
  }
1758
2467
 
1759
2468
  // The lazily-created Set is the ONE container for pending sources. Its
@@ -1762,18 +2471,18 @@ function link(dep, sub, pendingObserver = false) {
1762
2471
  // overlapping source landed beside the Set and removePendingSource refused
1763
2472
  // to clear it, stranding the Set members' pending forever (#2893).
1764
2473
  function addPendingSource(el, source) {
1765
- if (el._pendingSources?.has(source)) return false;
1766
- (el._pendingSources ??= new Set()).add(source);
2474
+ if (el._x?._pendingSources?.has(source)) return false;
2475
+ (ext(el)._pendingSources ??= new Set()).add(source);
1767
2476
  return true;
1768
2477
  }
1769
2478
  function removePendingSource(el, source) {
1770
- if (!el._pendingSources?.delete(source)) return false;
1771
- if (el._pendingSources.size === 0) el._pendingSources = undefined;
2479
+ if (!el._x?._pendingSources?.delete(source)) return false;
2480
+ if (el._x?._pendingSources.size === 0) if (el._x !== null) el._x._pendingSources = undefined;
1772
2481
  return true;
1773
2482
  }
1774
2483
  function clearPendingSources(el) {
1775
- el._pendingSources?.clear();
1776
- el._pendingSources = undefined;
2484
+ el._x?._pendingSources?.clear();
2485
+ if (el._x !== null) el._x._pendingSources = undefined;
1777
2486
  }
1778
2487
  // A rejection-pending only resolves through the settle sweep over the
1779
2488
  // SOURCE's subscribers, so it is retryable iff a tracked read created that
@@ -1783,7 +2492,7 @@ function clearPendingSources(el) {
1783
2492
  function retryReaches(el, source) {
1784
2493
  for (let d = el._deps; d; d = d._nextDep) {
1785
2494
  const dep = d._dep._firewall || d._dep;
1786
- if (dep === source || dep._pendingSources?.has(source)) return true;
2495
+ if (dep === source || dep._x?._pendingSources?.has(source)) return true;
1787
2496
  }
1788
2497
  return false;
1789
2498
  }
@@ -1795,7 +2504,7 @@ function retryReaches(el, source) {
1795
2504
  * transition, no lane registration. Commit #0 keeps serving.
1796
2505
  */
1797
2506
  function parkLoadingWindow(el, e) {
1798
- el._blocked = true;
2507
+ ext(el)._blocked = true;
1799
2508
  if (e.source) addPendingSource(el, e.source);
1800
2509
  // A settled error is the node's answer ("the error stays the answer until
1801
2510
  // this retry can actually run") — the park must not replace it: reads
@@ -1805,22 +2514,22 @@ function parkLoadingWindow(el, e) {
1805
2514
  }
1806
2515
  function setPendingError(el, source, error) {
1807
2516
  if (!source) {
1808
- el._error = null;
2517
+ if (el._x !== null) el._x._error = null;
1809
2518
  return;
1810
2519
  }
1811
2520
  if (error instanceof NotReadyError && error.source === source) {
1812
- el._error = error;
2521
+ ext(el)._error = error;
1813
2522
  return;
1814
2523
  }
1815
- const current = el._error;
2524
+ const current = el._x?._error;
1816
2525
  if (!(current instanceof NotReadyError) || current.source !== source) {
1817
- el._error = new NotReadyError(source);
2526
+ ext(el)._error = new NotReadyError(source);
1818
2527
  }
1819
2528
  }
1820
2529
  function forEachDependent(el, fn) {
1821
2530
  for (let s = el._subs; s !== null; s = s._nextSub) fn(s._sub, s);
1822
2531
  // `?? null`: affects() marks route plain signals (no `_child` slot) through here.
1823
- for (let child = el._child ?? null; child !== null; child = child._nextChild) {
2532
+ for (let child = el._x?._child ?? null; child !== null; child = child._nextChild) {
1824
2533
  for (let s = child._subs; s !== null; s = s._nextSub) fn(s._sub, s);
1825
2534
  }
1826
2535
  }
@@ -1879,7 +2588,7 @@ function settleErroredDependents(el, error) {
1879
2588
  const visit = node => {
1880
2589
  if (visited.has(node)) return;
1881
2590
  visited.add(node);
1882
- if (node._error === error) {
2591
+ if (node._x?._error === error) {
1883
2592
  enqueueSub(node);
1884
2593
  scheduled = true;
1885
2594
  }
@@ -1898,7 +2607,7 @@ function settlePendingSource(el) {
1898
2607
  if (visited.has(node) || !removePendingSource(node, el)) return;
1899
2608
  visited.add(node);
1900
2609
  node._time = clock;
1901
- const remaining = node._pendingSources?.values().next().value;
2610
+ const remaining = node._x?._pendingSources?.values().next().value;
1902
2611
  // STATUS_ERROR + pending sources only coexist via an errored loading
1903
2612
  // window's park (notifyStatus(STATUS_ERROR) clears pending sources
1904
2613
  // otherwise): the settled error stays the answer through the settle —
@@ -1912,11 +2621,11 @@ function settlePendingSource(el) {
1912
2621
  node._statusFlags &= ~STATUS_PENDING;
1913
2622
  if (!errored) setPendingError(node);
1914
2623
  updateCompanions !== null && updateCompanions(node);
1915
- if (node._blocked) {
2624
+ if (node._x?._blocked) {
1916
2625
  enqueueSub(node);
1917
2626
  scheduled = true;
1918
2627
  }
1919
- node._blocked = false;
2628
+ if (node._x !== null) node._x._blocked = false;
1920
2629
  // Fully settled with nobody watching: release candidate (#2934). Checked
1921
2630
  // again at release time — deferred so unobserved() can't unlink subs
1922
2631
  // lists this walk is still iterating.
@@ -1944,7 +2653,7 @@ function handleAsync(el, result, setter) {
1944
2653
  });
1945
2654
  }
1946
2655
  if (!thenable && !iterator) {
1947
- el._inFlight = null;
2656
+ if (el._x !== null) el._x._inFlight = null;
1948
2657
  // A sync landing is the first real answer for a loadingValue node.
1949
2658
  el._loading = false;
1950
2659
  return result;
@@ -1971,7 +2680,7 @@ function handleAsync(el, result, setter) {
1971
2680
  });
1972
2681
  throw new Error(message);
1973
2682
  }
1974
- el._inFlight = result;
2683
+ ext(el)._inFlight = result;
1975
2684
  let syncValue;
1976
2685
  // Settle-time transition re-entry. The loading rail is invisible to
1977
2686
  // transactions (#2933): a boundary-caught first load never registers as an
@@ -1998,7 +2707,7 @@ function handleAsync(el, result, setter) {
1998
2707
  globalQueue.initTransition(transition);
1999
2708
  };
2000
2709
  const handleError = error => {
2001
- if (el._inFlight !== result) return;
2710
+ if (el._x?._inFlight !== result) return;
2002
2711
  // NotReadyError from rejected promises should be treated as pending, not error
2003
2712
  let stillPending = error instanceof NotReadyError;
2004
2713
  // Dev-only authorship diagnostic (#2987): no edge means a post-`await`
@@ -2023,7 +2732,7 @@ function handleAsync(el, result, setter) {
2023
2732
  // serving commit #0 — same parking as recompute's catch for sync
2024
2733
  // dependency throws. The dead flight is released so the clock-gated
2025
2734
  // error-retry pull (updateIfNecessary) can also re-ask.
2026
- el._inFlight = null;
2735
+ if (el._x !== null) el._x._inFlight = null;
2027
2736
  parkLoadingWindow(el, error);
2028
2737
  el._time = clock;
2029
2738
  return;
@@ -2037,7 +2746,7 @@ function handleAsync(el, result, setter) {
2037
2746
  if (!stillPending) releaseSettledDependents(el);
2038
2747
  };
2039
2748
  const asyncWrite = (value, then) => {
2040
- if (el._inFlight !== result) return;
2749
+ if (el._x?._inFlight !== result) return;
2041
2750
  // If the node was dirtied by a newer write (optimistic override or regular),
2042
2751
  // skip this stale async result — the upcoming flush will recompute the node
2043
2752
  // with the new value, creating a fresh Promise that supersedes this one.
@@ -2048,10 +2757,14 @@ function handleAsync(el, result, setter) {
2048
2757
  clearStatus(el);
2049
2758
  const lane = resolveLane(el);
2050
2759
  if (lane) lane._pendingAsync.delete(el);
2760
+ // Attribution hook: lets the engine snapshot state before the landing
2761
+ // branches, so it can tell whether the plain path's setSignal committed a
2762
+ // change (and only then classify it as an async landing).
2763
+ if (attrHooks !== null) attrHooks.asyncStart(el);
2051
2764
  if (setter) {
2052
2765
  setter(value);
2053
2766
  if (wasUninitialized) clearStatus(el, true);
2054
- } else if (el._overrideValue !== undefined) {
2767
+ } else if (el._x?._overrideValue !== undefined) {
2055
2768
  // Optimistic node — resting OR covered by an active override — holds
2056
2769
  // through the shared pending-node path, exactly like a plain async memo,
2057
2770
  // so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
@@ -2071,7 +2784,10 @@ function handleAsync(el, result, setter) {
2071
2784
  // override every reader sees the override (A17), so waking subs would
2072
2785
  // re-show an unchanged view — the revert is the notification point.
2073
2786
  GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, value);
2074
- if (!hasActiveOverride$1(el)) insertSubs(el);
2787
+ if (!hasActiveOverride$1(el)) {
2788
+ if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
2789
+ insertSubs(el);
2790
+ }
2075
2791
  el._time = clock;
2076
2792
  } else if (lane) {
2077
2793
  // Route through lane's effect queue for independent flushing
@@ -2095,6 +2811,13 @@ function handleAsync(el, result, setter) {
2095
2811
  // rejection (#2837).
2096
2812
  notifyStatus(el, STATUS_ERROR, e);
2097
2813
  }
2814
+ // Attribution hook — unconditional, and OUTSIDE the try: rollup's
2815
+ // tryCatchDeoptimization retains anything referenced inside a try even
2816
+ // behind a folded true guard, so even a dev-only flag smuggled out of
2817
+ // the commit branch leaves prod residue (#2883). The engine instead
2818
+ // detects whether this landing committed by comparing the node against
2819
+ // its asyncStart snapshot (see attribution.ts).
2820
+ if (attrHooks !== null) attrHooks.asyncEnd(el, prevValue, value, true);
2098
2821
  } else {
2099
2822
  try {
2100
2823
  setSignal(el, () => value);
@@ -2103,6 +2826,11 @@ function handleAsync(el, result, setter) {
2103
2826
  // pre-commit failure here, and there is no user callsite to throw to.
2104
2827
  notifyStatus(el, STATUS_ERROR, e);
2105
2828
  }
2829
+ // Attribution hook: this path landed through setSignal, whose write
2830
+ // hook already saw any committed change — direct=false lets the engine
2831
+ // reclassify that write as an async landing iff it actually committed.
2832
+ // Outside the try (#2883 — see attribution-hooks.ts).
2833
+ if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, false);
2106
2834
  }
2107
2835
  // First real answer landing: the window closes when the answer becomes
2108
2836
  // OBSERVABLE. A direct commit is observable now; a transition-held write
@@ -2187,7 +2915,7 @@ function handleAsync(el, result, setter) {
2187
2915
  syncResult = r;
2188
2916
  resolved = true;
2189
2917
  if (r.done) completed = true;
2190
- } else if (el._inFlight !== result) {
2918
+ } else if (el._x?._inFlight !== result) {
2191
2919
  return;
2192
2920
  } else if (!r.done) {
2193
2921
  hadValue = true;
@@ -2208,7 +2936,7 @@ function handleAsync(el, result, setter) {
2208
2936
  if (isSync && initialRead) {
2209
2937
  syncError = e;
2210
2938
  rejected = true;
2211
- } else if (el._inFlight === result) {
2939
+ } else if (el._x?._inFlight === result) {
2212
2940
  completed = true;
2213
2941
  handleError(e);
2214
2942
  settleAutodispose();
@@ -2282,7 +3010,7 @@ function handleAsync(el, result, setter) {
2282
3010
  syncValue = v;
2283
3011
  resolved = true;
2284
3012
  } else if (
2285
- el._inFlight === result &&
3013
+ el._x?._inFlight === result &&
2286
3014
  !(el._flags & REACTIVE_DISPOSED) &&
2287
3015
  flattenIfIterable(v, registerDeferredClose)
2288
3016
  );
@@ -2339,20 +3067,24 @@ function handleAsync(el, result, setter) {
2339
3067
  return syncValue;
2340
3068
  }
2341
3069
  function clearStatus(el, clearUninitialized = false) {
2342
- if (el._pendingSources) clearPendingSources(el);
2343
- if (el._blocked) el._blocked = false;
3070
+ if (el._x?._pendingSources) clearPendingSources(el);
3071
+ if (el._x?._blocked) if (el._x !== null) el._x._blocked = false;
2344
3072
  // The pending window is over; its quiet classification dies with it.
2345
3073
  // (Unconditional: _reask is baked into the node literals, so this is a
2346
3074
  // plain store to an existing slot — no shape change.)
2347
- el._reask = false;
3075
+ if (el._x !== null) el._x._reask = false;
2348
3076
  el._statusFlags = clearUninitialized ? 0 : el._statusFlags & STATUS_UNINITIALIZED;
2349
- if (el._error) setPendingError(el);
3077
+ if (el._x?._error) setPendingError(el);
2350
3078
  // Update pending signal for isPending() reactivity (companions only exist
2351
3079
  // once the verdict layer created them, which installs the hooks).
2352
- if (el._pendingSignal || el._latestValueComputed) GlobalQueue._updatePendingSignal(el);
2353
- if (el._child && GlobalQueue._updateChildCompanions !== null)
3080
+ if (el._x?._pendingSignal || el._x?._latestValueComputed) GlobalQueue._updatePendingSignal(el);
3081
+ if (
3082
+ el._x?._child &&
3083
+ el._config & CONFIG_CHILD_COMPANIONS &&
3084
+ GlobalQueue._updateChildCompanions !== null
3085
+ )
2354
3086
  GlobalQueue._updateChildCompanions(el);
2355
- if (el._notifyStatus) el._notifyStatus();
3087
+ if (el._x?._notifyStatus) el._x._notifyStatus.call(el);
2356
3088
  }
2357
3089
  function notifyStatus(el, status, error, blockStatus, lane) {
2358
3090
  // Wrap regular errors to track source node
@@ -2366,7 +3098,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
2366
3098
  status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
2367
3099
  const isSource = pendingSource === el;
2368
3100
  const isOptimisticBoundary =
2369
- status === STATUS_PENDING && el._overrideValue !== undefined && !isSource;
3101
+ status === STATUS_PENDING && el._x?._overrideValue !== undefined && !isSource;
2370
3102
  const startsBlocking = isOptimisticBoundary && hasActiveOverride$1(el);
2371
3103
  if (!blockStatus) {
2372
3104
  if (status === STATUS_PENDING && pendingSource) {
@@ -2379,10 +3111,14 @@ function notifyStatus(el, status, error, blockStatus, lane) {
2379
3111
  clearPendingSources(el);
2380
3112
  el._statusFlags =
2381
3113
  status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
2382
- el._error = error;
3114
+ ext(el)._error = error;
2383
3115
  }
2384
3116
  GlobalQueue._updatePendingSignal !== null && GlobalQueue._updatePendingSignal(el);
2385
- if (el._child && GlobalQueue._updateChildCompanions !== null)
3117
+ if (
3118
+ el._x?._child &&
3119
+ el._config & CONFIG_CHILD_COMPANIONS &&
3120
+ GlobalQueue._updateChildCompanions !== null
3121
+ )
2386
3122
  GlobalQueue._updateChildCompanions(el);
2387
3123
  }
2388
3124
  if (lane && !blockStatus) {
@@ -2390,22 +3126,24 @@ function notifyStatus(el, status, error, blockStatus, lane) {
2390
3126
  }
2391
3127
  const downstreamBlockStatus = blockStatus || startsBlocking;
2392
3128
  const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
2393
- if (el._notifyStatus) {
3129
+ if (el._x?._notifyStatus) {
2394
3130
  if (blockStatus && status === STATUS_PENDING) {
2395
3131
  return;
2396
3132
  }
2397
3133
  if (downstreamBlockStatus) {
2398
- el._notifyStatus(status, error);
3134
+ el._x._notifyStatus.call(el, status, error);
2399
3135
  } else {
2400
- el._notifyStatus();
3136
+ el._x._notifyStatus.call(el);
2401
3137
  }
2402
3138
  return;
2403
3139
  }
2404
3140
  forEachDependent(el, (sub, link) => {
2405
3141
  sub._time = clock;
2406
3142
  if (
2407
- (status === STATUS_PENDING && pendingSource && !sub._pendingSources?.has(pendingSource)) ||
2408
- (status !== STATUS_PENDING && (sub._error !== error || sub._pendingSources))
3143
+ (status === STATUS_PENDING &&
3144
+ pendingSource &&
3145
+ !sub._x?._pendingSources?.has(pendingSource)) ||
3146
+ (status !== STATUS_PENDING && (sub._x?._error !== error || sub._x?._pendingSources))
2409
3147
  ) {
2410
3148
  // A pending-observer link is the subscription an `isPending` read created.
2411
3149
  // It exists so the observer re-runs when the source settles, but it must
@@ -2496,7 +3234,7 @@ function releaseSubtree(owner) {
2496
3234
  function clearSnapshots() {
2497
3235
  if (snapshotSources) {
2498
3236
  for (const source of snapshotSources) {
2499
- delete source._snapshotValue;
3237
+ delete source._x?._snapshotValue;
2500
3238
  // StoreNode targets share one pre-initialized hidden class (see
2501
3239
  // createStoreProxy) — assign undefined instead of deleting, and only
2502
3240
  // when present so signal-node sources don't grow the field.
@@ -2507,18 +3245,26 @@ function clearSnapshots() {
2507
3245
  snapshotCaptureActive = false;
2508
3246
  }
2509
3247
  function recompute(el, create = false) {
3248
+ // §12d: any recompute can clean a marked subscriber — invalidate skips.
3249
+ bumpNotifyEpoch();
2510
3250
  const isEffect = el._type;
3251
+ // Attribution hook: fired before this run touches the dep list — `_deps`
3252
+ // still holds the previous run's links (the subscriptions that could have
3253
+ // triggered this run, and the baseline for the engine's subscription diff).
3254
+ let devChanged = false;
3255
+ if (attrHooks !== null) attrHooks.recomputeStart(el, create);
2511
3256
  if (!create) {
2512
3257
  if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
2513
3258
  globalQueue.initTransition(el._transition);
2514
3259
  deleteFromHeap(el, queueFor(el));
2515
- el._inFlight = null;
3260
+ if (el._x !== null) el._x._inFlight = null;
2516
3261
  // Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
2517
3262
  if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
2518
3263
  else if (el._firstChild !== null || el._disposal !== null) {
2519
3264
  markDisposal(el);
2520
- el._pendingDisposal = el._disposal;
2521
- el._pendingFirstChild = el._firstChild;
3265
+ const x = ext(el);
3266
+ x._pendingDisposal = el._disposal;
3267
+ x._pendingFirstChild = el._firstChild;
2522
3268
  el._disposal = null;
2523
3269
  el._firstChild = null;
2524
3270
  el._childCount = 0;
@@ -2526,12 +3272,15 @@ function recompute(el, create = false) {
2526
3272
  } else clearSignals(el);
2527
3273
  }
2528
3274
  let isOptimisticDirty = !!(el._flags & REACTIVE_OPTIMISTIC_DIRTY);
2529
- const hasOverride = el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING;
3275
+ const hasOverride =
3276
+ (el._config & CONFIG_OPTIMISTIC) !== 0 &&
3277
+ el._x?._overrideValue !== NOT_PENDING &&
3278
+ el._x?._overrideValue !== undefined;
2530
3279
  const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
2531
3280
  // Outgoing error, captured before the compute clears status: if this run
2532
3281
  // recovers to an unchanged value, dependents still holding this object must
2533
3282
  // be swept (settleErroredDependents, #2949).
2534
- const outgoingError = el._statusFlags & STATUS_ERROR ? el._error : undefined;
3283
+ const outgoingError = el._statusFlags & STATUS_ERROR ? el._x?._error : undefined;
2535
3284
  // Re-ask classification lives in the verdict module; capture the flag before
2536
3285
  // the recompute wipes _flags below.
2537
3286
  const hadReask = (el._flags & REACTIVE_REASK) !== 0;
@@ -2548,6 +3297,7 @@ function recompute(el, create = false) {
2548
3297
  el._time = clock;
2549
3298
  let value = el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
2550
3299
  let oldHeight = el._height;
3300
+ let missedWake = false;
2551
3301
  let prevTracking = tracking;
2552
3302
  let prevLane = currentOptimisticLane;
2553
3303
  let prevStrictRead = false;
@@ -2595,13 +3345,13 @@ function recompute(el, create = false) {
2595
3345
  // with a setter callback). In that case, the outer `handleAsync` call below would
2596
3346
  // clobber the fresh subscription, so we skip it and let the internally-registered
2597
3347
  // iteration drive updates.
2598
- const prevInFlight = el._inFlight;
3348
+ const prevInFlight = el._x?._inFlight;
2599
3349
  const fnResult = el._fn(value);
2600
3350
  const isAsyncResult = typeof fnResult === "object" && fnResult !== null;
2601
- const inFlightChanged = el._inFlight !== prevInFlight;
3351
+ const inFlightChanged = el._x?._inFlight !== prevInFlight;
2602
3352
  value = inFlightChanged || !isAsyncResult ? fnResult : handleAsync(el, fnResult);
2603
3353
  if (!inFlightChanged && !isAsyncResult) {
2604
- el._inFlight = null;
3354
+ if (el._x !== null) el._x._inFlight = null;
2605
3355
  // A sync (non-object) return is the first real answer; async-shaped
2606
3356
  // results clear inside handleAsync at their own landing points, and a
2607
3357
  // self-registered flight (inFlightChanged — projections) clears when
@@ -2609,23 +3359,14 @@ function recompute(el, create = false) {
2609
3359
  el._loading = false;
2610
3360
  }
2611
3361
  }
2612
- // On a status-free node clearStatus is a guaranteed no-op: every branch
2613
- // in its body is gated on one of these fields, and with _statusFlags === 0
2614
- // the flags write (create or not) stores the 0 already there.
2615
- if (
2616
- el._statusFlags !== 0 ||
2617
- el._notifyStatus !== undefined ||
2618
- el._error ||
2619
- el._reask ||
2620
- el._blocked ||
2621
- el._pendingSources !== undefined ||
2622
- el._pendingSignal !== undefined ||
2623
- el._latestValueComputed !== undefined ||
2624
- el._child !== null
2625
- )
2626
- clearStatus(el, create);
2627
- // _optimisticLane is only ever assigned by engine paths.
2628
- if (el._optimisticLane) GlobalQueue._laneAsyncSettled(el);
3362
+ // On a status-free node clearStatus is a guaranteed no-op: every field
3363
+ // its body gates on is either _statusFlags or lives in the cold
3364
+ // extension no extension, no status to clear. (_x from an unrelated
3365
+ // installer just makes clearStatus a cheap re-verified no-op.)
3366
+ if (el._statusFlags !== 0 || el._x !== null) clearStatus(el, create);
3367
+ // _optimisticLane is only ever assigned by engine paths (CONFIG_HAS_LANE
3368
+ // is their sticky presence mark).
3369
+ if (el._config & CONFIG_HAS_LANE && el._x?._optimisticLane) GlobalQueue._laneAsyncSettled(el);
2629
3370
  } catch (e) {
2630
3371
  const notReady = e instanceof NotReadyError;
2631
3372
  if (notReady && el._loading) {
@@ -2643,7 +3384,7 @@ function recompute(el, create = false) {
2643
3384
  if (notReady && currentOptimisticLane) GlobalQueue._laneAsyncPending(el);
2644
3385
  let reaskChanged = false;
2645
3386
  if (notReady) {
2646
- el._blocked = true;
3387
+ ext(el)._blocked = true;
2647
3388
  if (GlobalQueue._applyReask !== null) reaskChanged = GlobalQueue._applyReask(el, hadReask);
2648
3389
  }
2649
3390
  notifyStatus(
@@ -2651,7 +3392,7 @@ function recompute(el, create = false) {
2651
3392
  notReady ? STATUS_PENDING : STATUS_ERROR,
2652
3393
  e,
2653
3394
  undefined,
2654
- notReady ? el._optimisticLane : undefined
3395
+ notReady ? el._x?._optimisticLane : undefined
2655
3396
  );
2656
3397
  if (reaskChanged) GlobalQueue._repollVerdicts(el);
2657
3398
  }
@@ -2660,13 +3401,19 @@ function recompute(el, create = false) {
2660
3401
  latestReadActive = prevLatestRead;
2661
3402
  strictRead = prevStrictRead;
2662
3403
  if (isStaleEffect) stale = prevStale;
3404
+ // Consume the missed-wake latch (#3037, set by insertSubs): a dep write
3405
+ // landed beneath this pass on a link it had already validated. The wipe
3406
+ // below must not key off DIRTY/CHECK — the read-time pull protocol
3407
+ // (markNode(c) in read()) marks the running node as part of ordinary
3408
+ // bookkeeping, and those marks are correctly discarded here.
3409
+ missedWake = (el._flags & REACTIVE_MISSED_WAKE) !== 0;
2663
3410
  el._flags = REACTIVE_NONE | (create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
2664
3411
  context = oldcontext;
2665
3412
  }
2666
- if (!el._error) {
3413
+ if (!el._x?._error) {
2667
3414
  trimStaleDeps(el);
2668
3415
  const compareValue = hasOverride
2669
- ? unwrapOverride(el._overrideValue)
3416
+ ? unwrapOverride(el._x?._overrideValue)
2670
3417
  : el._pendingValue === NOT_PENDING
2671
3418
  ? el._value
2672
3419
  : el._pendingValue;
@@ -2681,21 +3428,27 @@ function recompute(el, create = false) {
2681
3428
  // flush, bypassing every boundary and wedging the queue (#2837).
2682
3429
  notifyStatus(el, STATUS_ERROR, e);
2683
3430
  }
3431
+ // A committed derived change becomes a cause for this node's subscribers,
3432
+ // chaining their attribution through this node to the root write.
3433
+ if (attrHooks !== null) {
3434
+ devChanged = valueChanged && !el._x?._error;
3435
+ if (devChanged && !isEffect && !create) attrHooks.derivedChanged(el);
3436
+ }
2684
3437
  // Effects use `_equals: false` (no per-effect closure). The side effects that
2685
3438
  // the equals closure used to perform — flagging the effect dirty and enqueueing
2686
3439
  // its runner — happen here instead. `!create` matches the previous `initialized`
2687
3440
  // gate: the explicit recompute(node, true) inside effect() does not enqueue, so
2688
3441
  // effect() can call its runner synchronously for the first run.
2689
3442
  if (isEffect && valueChanged) {
2690
- el._modified = !el._error;
3443
+ el._modified = !el._x?._error;
2691
3444
  // Reuse one bound runner per effect — runEffect no-ops on a stale
2692
3445
  // `_modified`, so re-enqueueing the same function is harmless.
2693
3446
  if (!create)
2694
3447
  el._queue.enqueue(isEffect, (el._boundRunEffect ??= GlobalQueue._runEffect.bind(null, el)));
2695
3448
  }
2696
- if (el._error);
3449
+ if (el._x?._error);
2697
3450
  else if (valueChanged) {
2698
- const prevVisible = hasOverride ? el._overrideValue : undefined;
3451
+ const prevVisible = hasOverride ? el._x?._overrideValue : undefined;
2699
3452
  if (
2700
3453
  create ||
2701
3454
  // Plain sync flush (no transition on either side) commits effect
@@ -2704,6 +3457,13 @@ function recompute(el, create = false) {
2704
3457
  // paying it per effect on the plain path is pure overhead.
2705
3458
  (isEffect && (activeTransition !== el._transition || activeTransition === null)) ||
2706
3459
  isOptimisticDirty
3460
+ // NOTE (stage-3, 2026-08-21): a quiet-world MEMO direct-commit was
3461
+ // attempted here and REVERTED — memo staging is load-bearing beyond
3462
+ // transitions: mid-batch pulls (latest()/isPending()/read-triggered
3463
+ // recomputes before sources commit) must see the fresh value while
3464
+ // PLAIN reads stay committed until flush (#3009 purity). The pending
3465
+ // round-trip is that separation; it cannot be skipped on any path a
3466
+ // pull can reach.
2707
3467
  ) {
2708
3468
  el._value = value;
2709
3469
  // Lane-propagated correction: upstream data is fresh, correct the
@@ -2711,7 +3471,7 @@ function recompute(el, create = false) {
2711
3471
  // own reveal schedule; drop any superseded older hold so its queued
2712
3472
  // commit can't clobber the fresh value.
2713
3473
  if (hasOverride && isOptimisticDirty) {
2714
- el._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
3474
+ ext(el)._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
2715
3475
  el._pendingValue = NOT_PENDING;
2716
3476
  }
2717
3477
  } else {
@@ -2731,7 +3491,7 @@ function recompute(el, create = false) {
2731
3491
  // subscriber-less node has nothing to notify.
2732
3492
  if (
2733
3493
  el._subs !== null &&
2734
- (!hasOverride || isOptimisticDirty || el._overrideValue !== prevVisible)
3494
+ (!hasOverride || isOptimisticDirty || el._x?._overrideValue !== prevVisible)
2735
3495
  )
2736
3496
  insertSubs(el, isOptimisticDirty || hasOverride);
2737
3497
  } else if (hasOverride) {
@@ -2750,15 +3510,28 @@ function recompute(el, create = false) {
2750
3510
  // dependents still holding the propagated error consumed their dirty flag
2751
3511
  // in an errored run and may sit on stale commits (#2949). Changed-value
2752
3512
  // recoveries ride insertSubs above; a comparator throw re-errored the node
2753
- // (el._error re-set), so this only runs on a genuinely clean recovery.
2754
- if (outgoingError !== undefined && !valueChanged && !el._error)
3513
+ // (el._x?._error re-set), so this only runs on a genuinely clean recovery.
3514
+ if (outgoingError !== undefined && !valueChanged && !el._x?._error)
2755
3515
  settleErroredDependents(el, outgoingError);
2756
3516
  }
3517
+ // Attribution hook: fired before the lane restore so `currentOptimisticLane`
3518
+ // still reflects THIS run's posture. The facts distinguish an overlay
3519
+ // recompute (optimistic lane, transition replay, transition-held commit)
3520
+ // from a plain committed one — the engine must not blame overlay runs as
3521
+ // waste or double-count them against plain aggregates.
3522
+ if (attrHooks !== null)
3523
+ attrHooks.recomputeEnd(
3524
+ el,
3525
+ create,
3526
+ devChanged,
3527
+ isOptimisticDirty || currentOptimisticLane !== null,
3528
+ activeTransition !== null || el._transition !== null,
3529
+ el._pendingValue !== NOT_PENDING
3530
+ );
2757
3531
  currentOptimisticLane = prevLane;
2758
3532
  const needsPendingCommit =
2759
3533
  el._pendingValue !== NOT_PENDING ||
2760
- el._pendingFirstChild !== null ||
2761
- el._pendingDisposal !== null ||
3534
+ (el._x !== null && (el._x._pendingFirstChild !== null || el._x._pendingDisposal !== null)) ||
2762
3535
  (el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) !== 0;
2763
3536
  // Override-covered holds (hasOverride) always queue: their commit belongs
2764
3537
  // to their own transition's schedule (A18 re-rule) and is unobservable
@@ -2773,8 +3546,22 @@ function recompute(el, create = false) {
2773
3546
  isEffect &&
2774
3547
  activeTransition !== el._transition &&
2775
3548
  runInTransition(el._transition, () => recompute(el));
3549
+ // Missed-wake reschedule (see the finally above): values this pass read
3550
+ // before the nested commit are stale, so run again now that the heap will
3551
+ // accept the node. Equality gates stop same-value landings from cascading,
3552
+ // and a re-run only latches again if another nested commit changes a dep
3553
+ // beneath it — convergent unless deps genuinely keep changing.
3554
+ if (missedWake) {
3555
+ enqueueSub(el);
3556
+ schedule();
3557
+ }
2776
3558
  }
2777
3559
  function updateIfNecessary(el) {
3560
+ // Never re-enter a node that is currently computing: its dep bookkeeping
3561
+ // (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
3562
+ // A mid-pass mark stays latched for recompute's own tail to reschedule
3563
+ // (#3037); readers meanwhile serve the values the pass has so far.
3564
+ if (el._flags & REACTIVE_RECOMPUTING_DEPS) return;
2778
3565
  if (el._flags & REACTIVE_CHECK) {
2779
3566
  for (let d = el._deps; d; d = d._nextDep) {
2780
3567
  const dep1 = d._dep;
@@ -2789,7 +3576,7 @@ function updateIfNecessary(el) {
2789
3576
  }
2790
3577
  if (
2791
3578
  el._flags & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY) ||
2792
- (el._error && el._time < clock && !el._inFlight)
3579
+ (el._x?._error && el._time < clock && !el._x?._inFlight)
2793
3580
  ) {
2794
3581
  recompute(el);
2795
3582
  }
@@ -2811,7 +3598,6 @@ function computed(fn, options) {
2811
3598
  (options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0) |
2812
3599
  (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
2813
3600
  _equals: options?.equals != null ? options.equals : isEqual,
2814
- _unobserved: options?.unobserved,
2815
3601
  _disposal: null,
2816
3602
  _queue: context?._queue ?? globalQueue,
2817
3603
  _context: context?._context ?? defaultContext,
@@ -2819,7 +3605,6 @@ function computed(fn, options) {
2819
3605
  _fn: fn,
2820
3606
  _value: loading ? options.loadingValue : undefined,
2821
3607
  _height: 0,
2822
- _child: null,
2823
3608
  _nextHeap: undefined,
2824
3609
  _prevHeap: null,
2825
3610
  _deps: null,
@@ -2836,17 +3621,47 @@ function computed(fn, options) {
2836
3621
  _statusFlags: loading ? 0 : STATUS_UNINITIALIZED,
2837
3622
  _time: clock,
2838
3623
  _pendingValue: NOT_PENDING,
2839
- _pendingDisposal: null,
2840
- _pendingFirstChild: null,
2841
- _inFlight: null,
2842
3624
  _transition: null,
2843
- _reask: false,
2844
- _loading: loading
3625
+ _notifiedAt: -1,
3626
+ _loading: loading,
3627
+ // Cold machinery (async/transition/optimistic/verdict slots) lives one
3628
+ // hop away in the lazily-allocated extension — the core literal MUST
3629
+ // stay under V8's in-object boundary (§12: past ~39 fields every
3630
+ // allocation spills to a backing store and creation cost ~4x's).
3631
+ _x: null
2845
3632
  };
2846
3633
  self._name = options?.name ?? "computed";
3634
+ if (options?.unobserved) ext(self)._unobserved = options.unobserved;
2847
3635
  setupComputedNode(self, options);
2848
3636
  return self;
2849
3637
  }
3638
+ /** Lazily allocate a node's cold extension (ONE shape for signals and
3639
+ * computeds — `_x` access stays monomorphic). Installers write through
3640
+ * this; hot paths read `el._x?._field` gated by the _config presence bits.
3641
+ * Never call ext() just to store a field's default. */
3642
+ function ext(el) {
3643
+ return (el._x ??= {
3644
+ _overrideValue: undefined,
3645
+ _overrideOwner: undefined,
3646
+ _optimisticLane: undefined,
3647
+ _pendingSignal: undefined,
3648
+ _latestValueComputed: undefined,
3649
+ _parentSource: undefined,
3650
+ _affectsCount: 0,
3651
+ _inFlight: null,
3652
+ _error: undefined,
3653
+ _blocked: undefined,
3654
+ _pendingSources: undefined,
3655
+ _notifyStatus: undefined,
3656
+ _reask: false,
3657
+ _child: null,
3658
+ _unobserved: undefined,
3659
+ _snapshotValue: undefined,
3660
+ _pendingDisposal: null,
3661
+ _pendingFirstChild: null,
3662
+ _companionChildren: undefined
3663
+ });
3664
+ }
2850
3665
  /**
2851
3666
  * Build an Effect node with all effect-specific fields baked into a single object literal,
2852
3667
  * so V8 sees the full hidden class shape at construction time. Effects always run in lazy
@@ -2863,7 +3678,6 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
2863
3678
  (options?.sync ? CONFIG_SYNC : 0) |
2864
3679
  (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
2865
3680
  _equals: false,
2866
- _unobserved: options?.unobserved,
2867
3681
  _disposal: null,
2868
3682
  _queue: context?._queue ?? globalQueue,
2869
3683
  _context: context?._context ?? defaultContext,
@@ -2871,7 +3685,6 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
2871
3685
  _fn: fn,
2872
3686
  _value: undefined,
2873
3687
  _height: 0,
2874
- _child: null,
2875
3688
  _nextHeap: undefined,
2876
3689
  _prevHeap: null,
2877
3690
  _deps: null,
@@ -2887,11 +3700,8 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
2887
3700
  _statusFlags: STATUS_UNINITIALIZED,
2888
3701
  _time: clock,
2889
3702
  _pendingValue: NOT_PENDING,
2890
- _pendingDisposal: null,
2891
- _pendingFirstChild: null,
2892
- _inFlight: null,
2893
3703
  _transition: null,
2894
- _reask: false,
3704
+ _notifiedAt: -1,
2895
3705
  _loading: false,
2896
3706
  _modified: false,
2897
3707
  _prevValue: undefined,
@@ -2899,9 +3709,12 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
2899
3709
  _errorFn: errorFn,
2900
3710
  _cleanup: undefined,
2901
3711
  _type: type,
2902
- _notifyStatus: notifyStatus
3712
+ _x: null
2903
3713
  };
2904
3714
  self._name = options?.name ?? "effect";
3715
+ // Boundary effects carry a status channel; most effects never touch _x.
3716
+ if (notifyStatus !== undefined) ext(self)._notifyStatus = notifyStatus;
3717
+ if (options?.unobserved) ext(self)._unobserved = options.unobserved;
2905
3718
  setupComputedNode(self, lazyOptions);
2906
3719
  return self;
2907
3720
  }
@@ -2936,7 +3749,8 @@ function setupComputedNode(self, options) {
2936
3749
  !options?.lazy && recompute(self, true);
2937
3750
  if (snapshotCaptureActive && !options?.lazy) {
2938
3751
  if (!(self._statusFlags & STATUS_PENDING) && !(self._config & CONFIG_NO_SNAPSHOT)) {
2939
- self._snapshotValue = self._value === undefined ? NO_SNAPSHOT : self._value;
3752
+ ext(self)._snapshotValue = self._value === undefined ? NO_SNAPSHOT : self._value;
3753
+ self._config |= CONFIG_HAS_SNAPSHOT;
2940
3754
  snapshotSources.add(self);
2941
3755
  }
2942
3756
  }
@@ -2947,38 +3761,52 @@ function signal(v, options, firewall = null) {
2947
3761
  _config:
2948
3762
  (options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
2949
3763
  (options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0),
2950
- _unobserved: options?.unobserved,
2951
3764
  _value: v,
2952
3765
  _subs: null,
2953
3766
  _subsTail: null,
2954
3767
  _time: clock,
2955
3768
  _firewall: firewall,
2956
- _nextChild: firewall?._child || null,
2957
- _pendingValue: NOT_PENDING
3769
+ _nextChild: firewall?._x?._child || null,
3770
+ _pendingValue: NOT_PENDING,
3771
+ // Signal-literal diet (§12e): NO _time/_fn/_statusFlags slots. Stores
3772
+ // materialize one signal per touched leaf, so signal bytes are store
3773
+ // bytes. _time is write-only on signals (every read site is computed-
3774
+ // typed error-retry gating); _fn/_statusFlags read falsy-identically as
3775
+ // missing properties on the shared paths (undefined masks to 0).
3776
+ _transition: null,
3777
+ _notifiedAt: -1,
3778
+ _x: null
2958
3779
  };
2959
3780
  {
2960
3781
  s._name = options?.name ?? "signal";
2961
3782
  s._internal = !!firewall;
2962
3783
  }
2963
- firewall && (firewall._child = s);
3784
+ if (options?.unobserved) ext(s)._unobserved = options.unobserved;
3785
+ if (firewall) {
3786
+ ext(firewall)._child = s;
3787
+ firewall._config |= CONFIG_FW_CHILDREN;
3788
+ }
2964
3789
  if (
2965
3790
  snapshotCaptureActive &&
2966
3791
  !(s._config & CONFIG_NO_SNAPSHOT) &&
2967
3792
  !((firewall?._statusFlags ?? 0) & STATUS_PENDING)
2968
3793
  ) {
2969
- s._snapshotValue = v === undefined ? NO_SNAPSHOT : v;
3794
+ ext(s)._snapshotValue = v === undefined ? NO_SNAPSHOT : v;
3795
+ s._config |= CONFIG_HAS_SNAPSHOT;
2970
3796
  snapshotSources.add(s);
2971
3797
  }
2972
3798
  return s;
2973
3799
  }
2974
3800
  function optimisticSignal(v, options) {
2975
3801
  const s = signal(v, options);
2976
- s._overrideValue = NOT_PENDING;
3802
+ ext(s)._overrideValue = NOT_PENDING;
3803
+ s._config |= CONFIG_OPTIMISTIC;
2977
3804
  return s;
2978
3805
  }
2979
3806
  function optimisticComputed(fn, options) {
2980
3807
  const c = computed(fn, options);
2981
- c._overrideValue = NOT_PENDING;
3808
+ ext(c)._overrideValue = NOT_PENDING;
3809
+ c._config |= CONFIG_OPTIMISTIC;
2982
3810
  return c;
2983
3811
  }
2984
3812
  function isEqual(a, b) {
@@ -3041,7 +3869,13 @@ function prepareComputed(comp, refresh) {
3041
3869
  comp._flags &= ~REACTIVE_LAZY;
3042
3870
  recompute(comp, true);
3043
3871
  } else if (comp._flags & REACTIVE_DISPOSED) {
3044
- recompute(comp, true);
3872
+ // Two disposal lifecycles share the flag (#3024). Observation-lifecycle
3873
+ // nodes (CONFIG_AUTO_DISPOSE) are dormant — torn down by unobserved()
3874
+ // when the last subscriber left — and reads reawaken them; that is the
3875
+ // pay-for-use contract. Owner-lifecycle nodes are dead: recomputing would
3876
+ // re-run user code in a torn-down tree (and discard manual writes on
3877
+ // derived-writable signals), so reads return the last committed value.
3878
+ if (comp._config & CONFIG_AUTO_DISPOSE) recompute(comp, true);
3045
3879
  } else if (refresh) {
3046
3880
  updateIfNecessary(comp);
3047
3881
  }
@@ -3066,8 +3900,8 @@ function readNodeFast(el) {
3066
3900
  pendingCheckActive ||
3067
3901
  el._fn ||
3068
3902
  el._firewall ||
3069
- el._overrideValue !== undefined ||
3070
- el._snapshotValue !== undefined ||
3903
+ el._x?._overrideValue !== undefined ||
3904
+ el._x?._snapshotValue !== undefined ||
3071
3905
  activeTransition !== null ||
3072
3906
  currentOptimisticLane !== null ||
3073
3907
  snapshotCaptureActive ||
@@ -3107,8 +3941,8 @@ function read(el) {
3107
3941
  if (
3108
3942
  !computed._fn &&
3109
3943
  owner === el &&
3110
- el._overrideValue === undefined &&
3111
- el._snapshotValue === undefined &&
3944
+ el._x?._overrideValue === undefined &&
3945
+ el._x?._snapshotValue === undefined &&
3112
3946
  activeTransition === null &&
3113
3947
  currentOptimisticLane === null &&
3114
3948
  !snapshotCaptureActive &&
@@ -3168,13 +4002,23 @@ function read(el) {
3168
4002
  // active override throws.
3169
4003
  if (currentOptimisticLane === null || GlobalQueue._laneSuspends(owner)) {
3170
4004
  if (!tracking && el !== c) link(el, c);
3171
- throw owner._error;
4005
+ throw owner._x?._error;
3172
4006
  }
3173
- } else if (c && owner !== el && owner._statusFlags & STATUS_UNINITIALIZED) {
4007
+ } else if (c && owner._statusFlags & STATUS_UNINITIALIZED) {
4008
+ // A stale (render) reader of a node held pending in ANOTHER transition
4009
+ // normally keeps showing the node's committed value instead of
4010
+ // entangling the two transactions — but an uninitialized node has no
4011
+ // committed value to show. Suspend on it (firewall-backed store reads
4012
+ // always took this branch; plain memos now do too): the reader
4013
+ // registers as a reporter of that source, and its pending-node stamp
4014
+ // ties it to the active transaction, so the two transactions merge
4015
+ // when the source settles. Falling through served `undefined` as if
4016
+ // settled and stranded the reader outside both transactions, so it
4017
+ // never re-ran when either landed (#3043 port).
3174
4018
  if (!tracking && el !== c) link(el, c);
3175
- throw owner._error;
4019
+ throw owner._x?._error;
3176
4020
  } else if (!c && owner._statusFlags & STATUS_UNINITIALIZED) {
3177
- throw owner._error;
4021
+ throw owner._x?._error;
3178
4022
  }
3179
4023
  }
3180
4024
  // `owner` is the computed itself, or the firewall behind a store node —
@@ -3189,10 +4033,10 @@ function read(el) {
3189
4033
  if (tracking && !pendingCheckActive && owner._time < clock) {
3190
4034
  recompute(owner);
3191
4035
  return read(el);
3192
- } else throw owner._error;
4036
+ } else throw owner._x?._error;
3193
4037
  }
3194
4038
  if (snapshotCaptureActive && c && c._config & CONFIG_IN_SNAPSHOT_SCOPE) {
3195
- const sv = el._snapshotValue;
4039
+ const sv = el._x?._snapshotValue;
3196
4040
  if (sv !== undefined) {
3197
4041
  const snapshot = sv === NO_SNAPSHOT ? undefined : sv;
3198
4042
  const current = el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
@@ -3206,9 +4050,9 @@ function read(el) {
3206
4050
  ownerName: c?._name,
3207
4051
  nodeName: owner?._name
3208
4052
  });
3209
- if (el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING) {
4053
+ if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
3210
4054
  // A17: the override IS the value for every reader.
3211
- return unwrapOverride(el._overrideValue);
4055
+ return unwrapOverride(el._x?._overrideValue);
3212
4056
  }
3213
4057
  // Entanglement gate: a reader recomputing under an optimistic lane that reads
3214
4058
  // a pending mid-transition write sees the committed value. Projection-store
@@ -3300,9 +4144,10 @@ function setSignal(el, v) {
3300
4144
  globalQueue.initTransition(el._transition);
3301
4145
  // The optimistic write path lives with the engine: only optimisticSignal /
3302
4146
  // optimisticComputed callers and optimistic store nodes carry an
3303
- // _overrideValue slot, and every module that creates one installs the
3304
- // engine first.
3305
- if (el._overrideValue !== undefined && !projectionWriteActive)
4147
+ // _overrideValue slot (flagged by CONFIG_OPTIMISTIC a masked read of the
4148
+ // always-present config instead of a missing-property probe), and every
4149
+ // module that installs one installs the engine first.
4150
+ if (el._config & CONFIG_OPTIMISTIC && !projectionWriteActive)
3306
4151
  return GlobalQueue._optimisticWrite(el, v);
3307
4152
  const currentValue = el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
3308
4153
  if (typeof v === "function") v = v(currentValue);
@@ -3311,15 +4156,29 @@ function setSignal(el, v) {
3311
4156
  const valueChanged =
3312
4157
  !!(el._statusFlags & STATUS_UNINITIALIZED) || !el._equals || !el._equals(currentValue, v);
3313
4158
  if (!valueChanged) return v;
3314
- if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
4159
+ // Attribution hook: this committed write is where a re-run chain begins.
4160
+ if (attrHooks !== null) attrHooks.write(el, currentValue, v);
4161
+ const wasStaged = el._pendingValue !== NOT_PENDING;
4162
+ if (!wasStaged) queuePendingNode(el);
3315
4163
  el._pendingValue = v;
3316
4164
  // syncCompanions only pokes _pendingSignal/_latestValueComputed — with
3317
4165
  // neither companion present the call is a guaranteed no-op (companions are
3318
- // only ever created, never removed, and creating one installs the hook).
3319
- (el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
4166
+ // only ever created, never removed, and creating one installs the hook and
4167
+ // sets CONFIG_HAS_COMPANIONS one masked read replaces two optional-field
4168
+ // probes on every write).
4169
+ el._config & CONFIG_HAS_COMPANIONS &&
3320
4170
  GlobalQueue._syncCompanions !== null &&
3321
4171
  GlobalQueue._syncCompanions(el, v);
3322
- el._time = clock;
4172
+ // _time is a computed-only slot (§12e): writing it on a signal would fork
4173
+ // the lean shape. Every read site is computed-typed.
4174
+ if (el._fn !== undefined) el._time = clock;
4175
+ // Staged-rewrite fast path (§12d): a re-write to a node whose subscribers
4176
+ // were already walked — and where nothing has recomputed or linked since
4177
+ // (epoch) — re-stages the value and stops. The walk is idempotent (subs
4178
+ // marked, heap entries flag-guarded, effects queued once); lane and reask
4179
+ // contexts change what a walk MEANS, so they always walk.
4180
+ if (wasStaged && el._notifiedAt === notifyEpoch && currentOptimisticLane === null && !reaskArmed)
4181
+ return v;
3323
4182
  insertSubs(el);
3324
4183
  schedule();
3325
4184
  return v;
@@ -3338,6 +4197,7 @@ function suppressComputedRecompute(el) {
3338
4197
  schedule();
3339
4198
  }
3340
4199
  el._flags = (el._flags & -4) | REACTIVE_MANUAL_WRITE;
4200
+ el._manualWriteTime = clock;
3341
4201
  }
3342
4202
  /**
3343
4203
  * User-facing setter for the memo form of `createSignal(fn)`. Behaves like
@@ -3456,10 +4316,19 @@ function refresh(target) {
3456
4316
  });
3457
4317
  throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_REFRESH_MESSAGE);
3458
4318
  }
3459
- if (
3460
- typeof node._fn === "function" &&
3461
- !(node._flags & (REACTIVE_DISPOSED | REACTIVE_MANUAL_WRITE))
3462
- ) {
4319
+ if (typeof node._fn === "function" && !(node._flags & REACTIVE_DISPOSED)) {
4320
+ if (node._flags & REACTIVE_MANUAL_WRITE) {
4321
+ // A manual write in the CURRENT tick wins over the refresh (#2692).
4322
+ // A mask stamped in an earlier tick only survives because a
4323
+ // transaction (action) is holding the pending drain open; there the
4324
+ // refresh is a later, explicit re-ask and lifts the mask — otherwise
4325
+ // any setStore early in an action silently swallows every refresh()
4326
+ // for the rest of the transaction (#3026).
4327
+ if (node._manualWriteTime === clock) return;
4328
+ node._flags &= ~REACTIVE_MANUAL_WRITE;
4329
+ // No REASK below: the batch carries a manual value change, so the
4330
+ // recompute is not a quiet re-ask of an unchanged question.
4331
+ }
3463
4332
  // A refresh with no value-change dirt already queued is a re-ask of the
3464
4333
  // same question: mark it so the recompute classifies any resulting
3465
4334
  // pending window as quiet (not pending). If the node is already dirty
@@ -3467,11 +4336,14 @@ function refresh(target) {
3467
4336
  // REACTIVE_IN_HEAP counts as dirt: insertSubs schedules subscribers by
3468
4337
  // heap insertion alone (no DIRTY/CHECK flag), so a same-batch value
3469
4338
  // change followed by refresh() must not be laundered into a quiet re-ask.
3470
- if (!(node._flags & (REACTIVE_DIRTY | REACTIVE_CHECK | REACTIVE_IN_HEAP))) {
4339
+ else if (!(node._flags & (REACTIVE_DIRTY | REACTIVE_CHECK | REACTIVE_IN_HEAP))) {
3471
4340
  node._flags |= REACTIVE_REASK;
3472
4341
  armReaskClear();
3473
4342
  }
3474
4343
  node._flags = (node._flags & ~REACTIVE_CHECK) | REACTIVE_DIRTY;
4344
+ // A refresh() self-invalidation is a root cause too — the target's next
4345
+ // run has no changed dep to point at, so it points here instead.
4346
+ if (attrHooks !== null) attrHooks.refreshed(node);
3475
4347
  insertIntoHeap(node, queueFor(node));
3476
4348
  schedule();
3477
4349
  }
@@ -3631,8 +4503,8 @@ function isUndefined(value) {
3631
4503
  */
3632
4504
  /** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */
3633
4505
  function optimisticWrite(el, v) {
3634
- const hasOverride = el._overrideValue !== NOT_PENDING;
3635
- const currentValue = hasOverride ? unwrapOverride(el._overrideValue) : el._value;
4506
+ const hasOverride = el._x?._overrideValue !== NOT_PENDING;
4507
+ const currentValue = hasOverride ? unwrapOverride(el._x?._overrideValue) : el._value;
3636
4508
  if (typeof v === "function") v = v(currentValue);
3637
4509
  const valueChanged =
3638
4510
  !!(el._statusFlags & STATUS_UNINITIALIZED) || !el._equals || !el._equals(currentValue, v);
@@ -3653,19 +4525,20 @@ function optimisticWrite(el, v) {
3653
4525
  // Stamp ownership on the node (post-merge, so entangled writers share the
3654
4526
  // joint root). resolveTransition prefers this over the lane's _transition,
3655
4527
  // which a shared subscriber can merge across transactions (#2912).
3656
- el._overrideOwner = activeTransition;
4528
+ ext(el)._overrideOwner = activeTransition;
3657
4529
  const lane = getOrCreateLane(el);
3658
- el._optimisticLane = lane;
4530
+ ext(el)._optimisticLane = lane;
4531
+ el._config |= CONFIG_HAS_LANE;
3659
4532
  // Literal undefined must not land raw: the slot doubles as the optimistic
3660
4533
  // brand, and erasing it makes the write invisible and routes follow-up
3661
4534
  // writes off the optimistic path into permanent commits (#2898).
3662
- el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
4535
+ ext(el)._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
3663
4536
  // syncCompanions only pokes _pendingSignal/_latestValueComputed — with
3664
4537
  // neither companion present the call is a guaranteed no-op.
3665
- (el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
4538
+ (el._x?._pendingSignal !== undefined || el._x?._latestValueComputed !== undefined) &&
3666
4539
  GlobalQueue._syncCompanions !== null &&
3667
4540
  GlobalQueue._syncCompanions(el, v);
3668
- el._time = clock;
4541
+ if (el._fn !== undefined) el._time = clock; // §12e: computed-only slot
3669
4542
  insertSubs(el, true);
3670
4543
  schedule();
3671
4544
  return v;
@@ -3682,7 +4555,7 @@ function transitionBlocked(transition) {
3682
4555
  hasActiveOverride$1(node) &&
3683
4556
  "_statusFlags" in node &&
3684
4557
  node._statusFlags & STATUS_PENDING &&
3685
- node._error instanceof NotReadyError
4558
+ node._x?._error instanceof NotReadyError
3686
4559
  ) {
3687
4560
  return true;
3688
4561
  }
@@ -3696,26 +4569,26 @@ function resolveOptimisticNodes(nodes) {
3696
4569
  const len = nodes.length;
3697
4570
  for (let i = 0; i < len; i++) {
3698
4571
  const node = nodes[i];
3699
- node._optimisticLane = undefined;
4572
+ if (node._x !== null) node._x._optimisticLane = undefined;
3700
4573
  // Revert is a pure drop: there is no revert target to commit —
3701
4574
  // override-covered authoritative values hold in _pendingValue and
3702
4575
  // elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
3703
4576
  if (!(node._statusFlags & STATUS_PENDING)) node._statusFlags &= ~STATUS_UNINITIALIZED;
3704
- const prevOverride = node._overrideValue;
3705
- node._overrideValue = NOT_PENDING;
4577
+ const prevOverride = node._x?._overrideValue;
4578
+ ext(node)._overrideValue = NOT_PENDING;
3706
4579
  if (prevOverride !== NOT_PENDING && node._value !== unwrapOverride(prevOverride))
3707
4580
  insertSubs(node, true);
3708
4581
  node._transition = null;
3709
- node._overrideOwner = null;
4582
+ if (node._x !== null) node._x._overrideOwner = null;
3710
4583
  }
3711
4584
  // Settlement checkpoint (#2838): companions caught in this batch (or owned
3712
4585
  // by a node in it) re-derive from committed state, so verdicts survive the
3713
4586
  // transition that produced them (A19 — pending is a property of the data).
3714
4587
  for (let i = 0; i < len; i++) {
3715
4588
  const node = nodes[i];
3716
- if (node._pendingSignal || node._latestValueComputed) GlobalQueue._snapCompanions(node);
3717
- const owner = node._parentSource;
3718
- if (owner && (owner._pendingSignal === node || owner._latestValueComputed === node))
4589
+ if (node._x?._pendingSignal || node._x?._latestValueComputed) GlobalQueue._snapCompanions(node);
4590
+ const owner = node._x?._parentSource;
4591
+ if (owner && (owner._x?._pendingSignal === node || owner._x?._latestValueComputed === node))
3719
4592
  GlobalQueue._snapCompanions(owner);
3720
4593
  }
3721
4594
  nodes.splice(0, len);
@@ -3746,7 +4619,8 @@ function cleanupCompletedLanes(completingTransition) {
3746
4619
  if (lane._effectQueues[0].length) runQueue(lane._effectQueues[0], EFFECT_RENDER);
3747
4620
  if (lane._effectQueues[1].length) runQueue(lane._effectQueues[1], EFFECT_USER);
3748
4621
  }
3749
- if (lane._source._optimisticLane === lane) lane._source._optimisticLane = undefined;
4622
+ if (lane._source._x?._optimisticLane === lane)
4623
+ if (lane._source._x !== null) lane._source._x._optimisticLane = undefined;
3750
4624
  lane._pendingAsync.clear();
3751
4625
  lane._effectQueues[0].length = 0;
3752
4626
  lane._effectQueues[1].length = 0;
@@ -3759,7 +4633,7 @@ function laneSuspends(owner) {
3759
4633
  // Per-lane suspension: only throw if in same lane as pending async
3760
4634
  // AND the node doesn't have an active override (overrides are the visible value,
3761
4635
  // downstream in the lane should read the override, not throw)
3762
- const pendingLane = owner._optimisticLane;
4636
+ const pendingLane = owner._x?._optimisticLane;
3763
4637
  if (!pendingLane) return false;
3764
4638
  return findLane(pendingLane) === findLane(currentOptimisticLane) && !hasActiveOverride$1(owner);
3765
4639
  }
@@ -3786,12 +4660,12 @@ function gatedRead(el, owner, c) {
3786
4660
  */
3787
4661
  function laneReadsCommitted(el, owner, c) {
3788
4662
  if (
3789
- el._overrideValue !== undefined ||
3790
- !!el._optimisticLane ||
4663
+ el._x?._overrideValue !== undefined ||
4664
+ !!el._x?._optimisticLane ||
3791
4665
  !!(owner._statusFlags & STATUS_PENDING)
3792
4666
  )
3793
4667
  return true;
3794
- if (owner === el && stale && c._parentSource !== el) {
4668
+ if (owner === el && stale && c._x?._parentSource !== el) {
3795
4669
  // The committed view can hide a same-tick ambient write (a lane member —
3796
4670
  // even just an isPending companion flip — puts the reader "under a lane"
3797
4671
  // against unrelated plain writes). With no transaction the write commits
@@ -3827,10 +4701,10 @@ function recomputeLane(el, own) {
3827
4701
  !globalQueue._running &&
3828
4702
  !activeTransition &&
3829
4703
  !lane._transition &&
3830
- lane._source._parentSource !== undefined &&
3831
- el._overrideValue === undefined
4704
+ lane._source._x?._parentSource !== undefined &&
4705
+ el._x?._overrideValue === undefined
3832
4706
  ) {
3833
- el._optimisticLane = undefined;
4707
+ if (el._x !== null) el._x._optimisticLane = undefined;
3834
4708
  return false;
3835
4709
  }
3836
4710
  return lane;
@@ -3853,7 +4727,8 @@ function laneAsyncPending(el) {
3853
4727
  const lane = findLane(currentOptimisticLane);
3854
4728
  if (lane._source !== el) {
3855
4729
  lane._pendingAsync.add(el);
3856
- el._optimisticLane = lane;
4730
+ ext(el)._optimisticLane = lane;
4731
+ el._config |= CONFIG_HAS_LANE;
3857
4732
  GlobalQueue._updatePendingSignal !== null && GlobalQueue._updatePendingSignal(lane._source);
3858
4733
  }
3859
4734
  }
@@ -3902,18 +4777,43 @@ function installOptimisticEngine() {
3902
4777
  // same lanes, so the verdict layer brings the engine with it.
3903
4778
  installOptimisticEngine();
3904
4779
  let pendingProbe = null;
4780
+ /**
4781
+ * Probes whose verdict was suppressed by the fresh-read pairing rule while
4782
+ * the held write's fate was still undecided (see recordFreshRead /
4783
+ * wakeSuppressedProbes): held node → the wrapper computeds that probed it.
4784
+ * Entries die with the hold — the commit/revert snap clears them.
4785
+ */
4786
+ const suppressedProbes = new Map();
3905
4787
  /**
3906
4788
  * Get or create the pending signal for a node (lazy).
3907
4789
  * Used by isPending() to track pending state reactively.
3908
4790
  */
4791
+ /** #3038: register a companion-carrying firewall child on its firewall's
4792
+ * companion set and arm the post-recompute snap (CONFIG_CHILD_COMPANIONS is
4793
+ * the one-load gate at the call sites). The snap then iterates exactly the
4794
+ * children someone asked verdicts of — O(companions) — never the full
4795
+ * `_child` chain, which carries one node per materialized leaf (the
4796
+ * O(all-leaves-ever-read)-per-update pathology). Entries are permanent like
4797
+ * the companions themselves; a store with no leaf-level isPending()/latest()
4798
+ * reads never allocates the set or pays the walk. */
4799
+ function markFirewallChildCompanions(el) {
4800
+ const fw = el._firewall;
4801
+ if (!fw) return;
4802
+ fw._config |= CONFIG_CHILD_COMPANIONS;
4803
+ (ext(fw)._companionChildren ??= new Set()).add(el);
4804
+ }
3909
4805
  function getPendingSignal(el) {
3910
- if (!el._pendingSignal) {
4806
+ let ps = el._x?._pendingSignal;
4807
+ if (!ps) {
3911
4808
  // Start false, write true if pending - ensures reversion returns to false
3912
- el._pendingSignal = optimisticSignal(false, { ownedWrite: true });
3913
- el._pendingSignal._parentSource = el;
3914
- if (computePendingState(el)) setSignal(el._pendingSignal, true);
4809
+ ps = optimisticSignal(false, { ownedWrite: true });
4810
+ ext(el)._pendingSignal = ps;
4811
+ el._config |= CONFIG_HAS_COMPANIONS;
4812
+ markFirewallChildCompanions(el);
4813
+ ext(ps)._parentSource = el;
4814
+ if (computePendingState(el)) setSignal(ps, true);
3915
4815
  }
3916
- return el._pendingSignal;
4816
+ return ps;
3917
4817
  }
3918
4818
  function collectPendingSources(el) {
3919
4819
  if (!pendingProbe) return;
@@ -3942,7 +4842,7 @@ function witnessAffects(node) {
3942
4842
  * inherits the coverage it reports on.
3943
4843
  */
3944
4844
  function markWalk(el, seen) {
3945
- if (el._affectsCount) return true;
4845
+ if (el._x?._affectsCount) return true;
3946
4846
  // A real error outranks an inherited mark (A16/A24c): an errored node
3947
4847
  // answers probes with its error, not a coverage verdict, and coverage does
3948
4848
  // not flow through it — matching the rails' behavior, where propagation
@@ -3972,11 +4872,11 @@ function markCovered(el) {
3972
4872
  return activeAffectsMarks !== 0 && markWalk(el, new Set());
3973
4873
  }
3974
4874
  function quietPending(el) {
3975
- if (el._pendingSources) {
3976
- for (const source of el._pendingSources) if (!source._reask) return false;
4875
+ if (el._x?._pendingSources) {
4876
+ for (const source of el._x._pendingSources) if (!source._x?._reask) return false;
3977
4877
  return true;
3978
4878
  }
3979
- return el._reask;
4879
+ return el._x?._reask ?? false;
3980
4880
  }
3981
4881
  // NOTE: a loadingValue node's open loading window (_loading) is verdict-quiet
3982
4882
  // on purpose: commit #0 answers the question by declaration, so the window
@@ -4000,15 +4900,15 @@ function computePendingState(el) {
4000
4900
  // so the one walk covers direct marks, derivation, and companion chains.
4001
4901
  if (markCovered(el)) return true;
4002
4902
  const firewall = el._firewall;
4003
- if (el._parentSource) {
4004
- const parentNode = el._parentSource;
4903
+ if (el._x?._parentSource) {
4904
+ const parentNode = el._x?._parentSource;
4005
4905
  const parent = parentNode._firewall || parentNode;
4006
4906
  return newQuestionInFlight(parent);
4007
4907
  }
4008
4908
  if (firewall && el._pendingValue !== NOT_PENDING && !hasActiveOverride$1(el)) {
4009
4909
  return (
4010
4910
  !!(firewall._flags & REACTIVE_MANUAL_WRITE) ||
4011
- (!firewall._inFlight && !(firewall._statusFlags & STATUS_PENDING)) ||
4911
+ (!firewall._x?._inFlight && !(firewall._statusFlags & STATUS_PENDING)) ||
4012
4912
  (!!(firewall._statusFlags & STATUS_PENDING) && quietPending(firewall))
4013
4913
  );
4014
4914
  }
@@ -4022,25 +4922,25 @@ function computePendingState(el) {
4022
4922
  !comp._loading
4023
4923
  ) {
4024
4924
  if (hasActiveOverride$1(el))
4025
- return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._overrideValue));
4925
+ return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._x?._overrideValue));
4026
4926
  return true;
4027
4927
  }
4028
4928
  return newQuestionInFlight(comp);
4029
4929
  }
4030
4930
  function syncCompanions(el, value) {
4031
- if (el._pendingSignal) updatePendingSignal(el);
4032
- if (el._latestValueComputed) setSignal(el._latestValueComputed, value);
4931
+ if (el._x?._pendingSignal) updatePendingSignal(el);
4932
+ if (el._x?._latestValueComputed) setSignal(el._x?._latestValueComputed, value);
4033
4933
  }
4034
4934
  function updatePendingSignal(el) {
4035
- if (el._pendingSignal) {
4036
- setSignal(el._pendingSignal, computePendingState(el));
4935
+ if (el._x?._pendingSignal) {
4936
+ setSignal(el._x?._pendingSignal, computePendingState(el));
4037
4937
  }
4038
- if (el._latestValueComputed) updatePendingSignal(el._latestValueComputed);
4938
+ if (el._x?._latestValueComputed) updatePendingSignal(el._x?._latestValueComputed);
4039
4939
  }
4040
4940
  function updateChildCompanions(el) {
4041
- for (let child = el._child; child !== null; child = child._nextChild) {
4042
- if (child._pendingSignal || child._latestValueComputed) updatePendingSignal(child);
4043
- }
4941
+ const companions = el._x?._companionChildren;
4942
+ if (companions === undefined) return;
4943
+ for (const child of companions) updatePendingSignal(child);
4044
4944
  }
4045
4945
  /**
4046
4946
  * Re-derive every verdict companion downstream of `el` (subs + firewall
@@ -4057,30 +4957,65 @@ function repollDownstreamVerdicts(el, snap = false) {
4057
4957
  const visit = node => {
4058
4958
  if (visited.has(node)) return;
4059
4959
  visited.add(node);
4060
- if (node._pendingSignal || node._latestValueComputed) update(node);
4960
+ if (node._x?._pendingSignal || node._x?._latestValueComputed) update(node);
4061
4961
  for (let s = node._subs; s !== null; s = s._nextSub) visit(s._sub);
4062
- for (let child = node._child ?? null; child !== null; child = child._nextChild) {
4962
+ for (let child = node._x?._child ?? null; child !== null; child = child._nextChild) {
4063
4963
  visit(child);
4064
4964
  }
4065
4965
  };
4066
4966
  visit(el);
4067
4967
  }
4968
+ /**
4969
+ * The correction half of the provisional fresh-read suppression (see
4970
+ * collectPending): fired from the sanctioned async-registration site
4971
+ * (GlobalQueue.notify) when a transaction gains an in-flight async blocker.
4972
+ * Every probe that returned "not pending" purely because it read a held
4973
+ * value belonging to that transaction re-runs — its re-probe now sees the
4974
+ * live blocker through heldAwaitingAsync and lands the true verdict. The
4975
+ * wake mirrors a companion write's own notification (optimistic-dirty on the
4976
+ * companion's lane) so the corrected verdict commits and flushes immediately
4977
+ * instead of being held with the transaction it reports on.
4978
+ */
4979
+ function wakeSuppressedProbes(transition) {
4980
+ if (suppressedProbes.size === 0) return;
4981
+ let woke = false;
4982
+ for (const [node, probes] of suppressedProbes) {
4983
+ const nt = node._transition;
4984
+ const t = nt ? currentTransition(nt) : null;
4985
+ if (!t) {
4986
+ suppressedProbes.delete(node);
4987
+ continue;
4988
+ }
4989
+ if (t !== transition) continue;
4990
+ suppressedProbes.delete(node);
4991
+ const lane = node._x?._pendingSignal?._x?._optimisticLane;
4992
+ for (const p of probes) {
4993
+ if (p._flags & REACTIVE_DISPOSED) continue;
4994
+ p._flags |= REACTIVE_OPTIMISTIC_DIRTY;
4995
+ if (lane) assignOrMergeLane(p, lane);
4996
+ else if (p._x !== null) p._x._optimisticLane = undefined;
4997
+ enqueueSub(p);
4998
+ woke = true;
4999
+ }
5000
+ }
5001
+ if (woke) schedule();
5002
+ }
4068
5003
  function snapCompanionsToState(owner) {
4069
- const sig = owner._pendingSignal;
4070
- if (sig && (sig._overrideValue === undefined || sig._overrideValue === NOT_PENDING)) {
5004
+ suppressedProbes.size !== 0 && suppressedProbes.delete(owner);
5005
+ const sig = owner._x?._pendingSignal;
5006
+ if (sig && (sig._x?._overrideValue === undefined || sig._x?._overrideValue === NOT_PENDING)) {
4071
5007
  const pending = computePendingState(owner);
4072
5008
  if (sig._value !== pending || sig._pendingValue !== NOT_PENDING) {
4073
5009
  sig._value = pending;
4074
5010
  sig._pendingValue = NOT_PENDING;
4075
- sig._time = clock;
4076
5011
  insertSubs(sig);
4077
5012
  schedule();
4078
5013
  }
4079
5014
  }
4080
- const shadow = owner._latestValueComputed;
5015
+ const shadow = owner._x?._latestValueComputed;
4081
5016
  if (shadow && !(shadow._flags & REACTIVE_DISPOSED)) {
4082
5017
  if (
4083
- (shadow._overrideValue === undefined || shadow._overrideValue === NOT_PENDING) &&
5018
+ (shadow._x?._overrideValue === undefined || shadow._x?._overrideValue === NOT_PENDING) &&
4084
5019
  shadow._pendingValue === NOT_PENDING &&
4085
5020
  !Object.is(shadow._value, owner._value) &&
4086
5021
  !(shadow._flags & (REACTIVE_DIRTY | REACTIVE_CHECK))
@@ -4094,20 +5029,30 @@ function snapCompanionsToState(owner) {
4094
5029
  }
4095
5030
  }
4096
5031
  function getLatestValueComputed(el) {
4097
- if (!el._latestValueComputed) {
5032
+ let lvc = el._x?._latestValueComputed;
5033
+ if (!lvc) {
4098
5034
  const prevPending = latestReadActive;
4099
5035
  setLatestReadActive(false);
4100
5036
  const prevCheck = pendingCheckActive;
4101
5037
  setPendingCheckActive(false);
4102
5038
  const prevContext = context;
4103
5039
  setContextInternal(null); // Detach from owner so it isn't disposed with effects
4104
- el._latestValueComputed = optimisticComputed(() => read(el));
4105
- el._latestValueComputed._parentSource = el; // Parent-child lane relationship
5040
+ lvc = optimisticComputed(() => read(el));
5041
+ ext(el)._latestValueComputed = lvc;
5042
+ el._config |= CONFIG_HAS_COMPANIONS;
5043
+ markFirewallChildCompanions(el);
5044
+ ext(lvc)._parentSource = el; // Parent-child lane relationship
5045
+ // Backfill an in-flight write (mirrors getPendingSignal): the companion is
5046
+ // created lazily, possibly after the write was processed — syncCompanions
5047
+ // only pushes into companions that already exist, so the first latest()
5048
+ // read inside a held transition showed the committed value (#3041).
5049
+ if (el._pendingValue !== NOT_PENDING && !hasActiveOverride$1(el))
5050
+ setSignal(lvc, el._pendingValue);
4106
5051
  setContextInternal(prevContext);
4107
5052
  setPendingCheckActive(prevCheck);
4108
5053
  setLatestReadActive(prevPending);
4109
5054
  }
4110
- return el._latestValueComputed;
5055
+ return lvc;
4111
5056
  }
4112
5057
  /** The latest()-mode read path, installed as GlobalQueue._latestRead. */
4113
5058
  function latestRead(el) {
@@ -4115,8 +5060,8 @@ function latestRead(el) {
4115
5060
  const prevPending = latestReadActive;
4116
5061
  setLatestReadActive(false);
4117
5062
  const visibleValue =
4118
- el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING
4119
- ? unwrapOverride(el._overrideValue)
5063
+ el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING
5064
+ ? unwrapOverride(el._x?._overrideValue)
4120
5065
  : el._value;
4121
5066
  let value;
4122
5067
  try {
@@ -4142,8 +5087,8 @@ function latestRead(el) {
4142
5087
  setLatestReadActive(prevPending);
4143
5088
  }
4144
5089
  if (pendingComputed._statusFlags & STATUS_PENDING) return visibleValue;
4145
- if (stale && currentOptimisticLane && pendingComputed._optimisticLane) {
4146
- const pcLane = findLane(pendingComputed._optimisticLane);
5090
+ if (stale && currentOptimisticLane && pendingComputed._x?._optimisticLane) {
5091
+ const pcLane = findLane(pendingComputed._x?._optimisticLane);
4147
5092
  const curLane = findLane(currentOptimisticLane);
4148
5093
  if (pcLane !== curLane && pcLane._pendingAsync.size > 0) {
4149
5094
  return visibleValue;
@@ -4169,21 +5114,47 @@ function pendingCheckRead(el, c, owner, firewall) {
4169
5114
  if (c && ownerStatus & STATUS_PENDING && ownerStatus & STATUS_UNINITIALIZED) {
4170
5115
  if (tracking && el !== c) link(el, c);
4171
5116
  setPendingCheckActive(true);
4172
- throw owner._error;
5117
+ throw owner._x?._error;
4173
5118
  }
4174
5119
  collectPendingSources(el);
4175
5120
  if (firewall) collectPendingSources(firewall);
4176
5121
  setPendingCheckActive(true);
4177
5122
  }
5123
+ /**
5124
+ * A held node whose transaction still has an async question in flight. The
5125
+ * probe's fresh-read pairing rule (#2831 — "a reader that sees the fresh
5126
+ * value must not also be told it is pending") only applies to LANDED answers
5127
+ * awaiting reveal; while the answer is still computing, the fresh value the
5128
+ * reader saw is an input, and pending remains the truth for every reader
5129
+ * (#3028).
5130
+ */
5131
+ function heldAwaitingAsync(el) {
5132
+ const et = el._transition;
5133
+ const t = et ? currentTransition(et) : null;
5134
+ if (!t || t._done) return false;
5135
+ for (const [source, reporters] of t._asyncReporters) {
5136
+ if (
5137
+ reporters.size &&
5138
+ source._statusFlags & STATUS_PENDING &&
5139
+ source._x?._error?.source === source
5140
+ )
5141
+ return true;
5142
+ }
5143
+ return false;
5144
+ }
4178
5145
  function recordFreshRead(el, value) {
4179
- if (pendingProbe !== null && el._pendingValue !== NOT_PENDING && value === el._pendingValue)
5146
+ if (pendingProbe !== null && el._pendingValue !== NOT_PENDING && value === el._pendingValue) {
5147
+ if (heldAwaitingAsync(el)) return;
4180
5148
  pendingProbe.freshReads.add(el);
5149
+ }
4181
5150
  }
4182
5151
  function applyReask(el, hadReask) {
4183
5152
  const wasPending = !!(el._statusFlags & STATUS_PENDING);
4184
- const isReask = hadReask && !(wasPending && !el._reask);
4185
- const changed = wasPending && el._reask !== isReask;
4186
- el._reask = isReask;
5153
+ const isReask = hadReask && !(wasPending && !el._x?._reask);
5154
+ const changed = wasPending && (el._x?._reask ?? false) !== isReask;
5155
+ // Allocation-free for the quiet case: false is the extension default.
5156
+ if (isReask) ext(el)._reask = true;
5157
+ else if (el._x !== null) el._x._reask = false;
4187
5158
  return changed;
4188
5159
  }
4189
5160
  function latest(fn) {
@@ -4202,7 +5173,8 @@ function isPending(fn) {
4202
5173
  const probe = (pendingProbe = {
4203
5174
  found: false,
4204
5175
  sources: new Set(),
4205
- freshReads: new Set()
5176
+ freshReads: new Set(),
5177
+ suppressed: []
4206
5178
  });
4207
5179
  const collectPending = () => {
4208
5180
  setPendingCheckActive(false);
@@ -4210,12 +5182,27 @@ function isPending(fn) {
4210
5182
  setStrictRead(false);
4211
5183
  try {
4212
5184
  probe.sources.forEach(source => {
4213
- if (read(getPendingSignal(source)) && !probe.freshReads.has(source)) probe.found = true;
5185
+ if (read(getPendingSignal(source))) {
5186
+ if (!probe.freshReads.has(source)) probe.found = true;
5187
+ else probe.suppressed.push(source);
5188
+ }
4214
5189
  });
4215
5190
  } finally {
4216
5191
  setStrictRead(prevStrictRead);
4217
5192
  setPendingCheckActive(true);
4218
5193
  }
5194
+ // A "not pending" verdict that exists only because this reader saw the
5195
+ // fresh held value is provisional: if the write turns out NOT to commit
5196
+ // this flush (a downstream async pends and holds it), the suppression was
5197
+ // wrong and the wrapper must re-ask (#3028). Remember who to wake — the
5198
+ // async registration (GlobalQueue.notify) triggers wakeSuppressedProbes.
5199
+ if (!probe.found && probe.suppressed.length && context && typeof context._fn === "function") {
5200
+ for (const source of probe.suppressed) {
5201
+ let probes = suppressedProbes.get(source);
5202
+ if (!probes) suppressedProbes.set(source, (probes = new Set()));
5203
+ probes.add(context);
5204
+ }
5205
+ }
4219
5206
  };
4220
5207
  try {
4221
5208
  fn();
@@ -4247,6 +5234,7 @@ GlobalQueue._recordFresh = recordFreshRead;
4247
5234
  GlobalQueue._applyReask = applyReask;
4248
5235
  GlobalQueue._repollVerdicts = repollDownstreamVerdicts;
4249
5236
  GlobalQueue._witnessAffects = witnessAffects;
5237
+ GlobalQueue._wakeSuppressedProbes = wakeSuppressedProbes;
4250
5238
 
4251
5239
  /**
4252
5240
  * Effects are the leaf nodes of our reactive graph. When their sources change, they are
@@ -4286,7 +5274,7 @@ function effect(compute, effect, error, options) {
4286
5274
  function notifyEffectStatus(status, error) {
4287
5275
  // Use passed values if provided, otherwise read from node
4288
5276
  const actualStatus = status !== undefined ? status : this._statusFlags;
4289
- const actualError = error !== undefined ? error : this._error;
5277
+ const actualError = error !== undefined ? error : this._x?._error;
4290
5278
  if (actualStatus & STATUS_ERROR) {
4291
5279
  this._queue.notify(this, STATUS_PENDING, 0);
4292
5280
  if (this._type === EFFECT_USER) {
@@ -4345,7 +5333,7 @@ function runEffect(node) {
4345
5333
  // notifyEffectStatus, and a runner queued by an earlier valueChanged in the
4346
5334
  // same flush must not be hijacked by a later-arriving error status.
4347
5335
  if (node._statusFlags & STATUS_ERROR && node._type === EFFECT_USER) {
4348
- const err = unwrapStatusError(node._error);
5336
+ const err = unwrapStatusError(node._x?._error);
4349
5337
  node._prevValue = node._value;
4350
5338
  node._modified = false;
4351
5339
  try {
@@ -4382,7 +5370,7 @@ function runEffect(node) {
4382
5370
  // The final cleanup is invoked by disposeChildren at true disposal.
4383
5371
  node._cleanup = nextCleanup;
4384
5372
  } catch (error) {
4385
- node._error = new StatusError(node, error);
5373
+ ext(node)._error = new StatusError(node, error);
4386
5374
  node._statusFlags |= STATUS_ERROR;
4387
5375
  if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
4388
5376
  haltReactivity(error);
@@ -4433,11 +5421,11 @@ function trackedEffect(fn, options) {
4433
5421
  node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
4434
5422
  node._modified = true;
4435
5423
  node._type = EFFECT_TRACKED;
4436
- node._notifyStatus = (status, error) => {
5424
+ ext(node)._notifyStatus = (status, error) => {
4437
5425
  const actualStatus = status !== undefined ? status : node._statusFlags;
4438
5426
  if (actualStatus & STATUS_ERROR) {
4439
5427
  node._queue.notify(node, STATUS_PENDING, 0);
4440
- const err = error !== undefined ? error : node._error;
5428
+ const err = error !== undefined ? error : node._x?._error;
4441
5429
  if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
4442
5430
  haltReactivity(unwrapStatusError(err));
4443
5431
  throw err;
@@ -5207,7 +6195,7 @@ function inheritAffectsMarks(node, raw, property) {
5207
6195
  // A live scope exists, so affects.ts already installed the mark engine.
5208
6196
  for (const [carrier, entry] of affectsScopes) {
5209
6197
  if (
5210
- carrier._affectsCount &&
6198
+ carrier._x?._affectsCount &&
5211
6199
  entry.scope.has(raw) &&
5212
6200
  (entry.key === undefined || entry.key === property)
5213
6201
  ) {
@@ -5327,7 +6315,7 @@ function witnessAffectsMark(target, property) {
5327
6315
  // Callers guard on `pendingCheckActive`, which only flips inside
5328
6316
  // isPending() — the verdict layer is loaded and its hook installed.
5329
6317
  const own = target[STORE_NODE]?.[$AFFECTS];
5330
- if (own?._affectsCount) GlobalQueue._witnessAffects(own);
6318
+ if (own?._x?._affectsCount) GlobalQueue._witnessAffects(own);
5331
6319
  if (affectsScopes.size) {
5332
6320
  // Chained backings (§7b): a wrapper's STORE_VALUE can be another store's
5333
6321
  // proxy — marks cover by identity of the BASE raw, so resolve the chain
@@ -5336,7 +6324,7 @@ function witnessAffectsMark(target, property) {
5336
6324
  for (const [carrier, entry] of affectsScopes) {
5337
6325
  if (
5338
6326
  carrier !== own &&
5339
- carrier._affectsCount &&
6327
+ carrier._x?._affectsCount &&
5340
6328
  (entry.key === undefined || entry.key === property)
5341
6329
  ) {
5342
6330
  let r = raw;
@@ -5404,7 +6392,7 @@ function getStoreAffectsNodes(target, key) {
5404
6392
  * mid-window recomputes.
5405
6393
  */
5406
6394
  function markAffects(node) {
5407
- node._affectsCount = (node._affectsCount || 0) + 1;
6395
+ ext(node)._affectsCount = (node._x?._affectsCount || 0) + 1;
5408
6396
  shiftAffectsMarks(1);
5409
6397
  }
5410
6398
  /**
@@ -5420,7 +6408,7 @@ function markAffects(node) {
5420
6408
  * no visual change.
5421
6409
  */
5422
6410
  function notifyMarkBoundaries(node) {
5423
- if (!node._subs && !node._child) return;
6411
+ if (!node._subs && !node._x?._child) return;
5424
6412
  const error = new NotReadyError(node);
5425
6413
  error._markVisual = true;
5426
6414
  const visited = new Set();
@@ -5429,8 +6417,8 @@ function notifyMarkBoundaries(node) {
5429
6417
  visited.add(sub);
5430
6418
  // Display consumers (render effects, boundary computeds) act on the
5431
6419
  // notification; descent stops there, exactly like the status rails.
5432
- if (sub._notifyStatus) {
5433
- sub._notifyStatus(STATUS_PENDING, error);
6420
+ if (sub._x?._notifyStatus) {
6421
+ sub._x._notifyStatus.call(sub, STATUS_PENDING, error);
5434
6422
  return;
5435
6423
  }
5436
6424
  forEachDependent(sub, visit);
@@ -5463,8 +6451,8 @@ function registerAffectsMark(node) {
5463
6451
  */
5464
6452
  function releaseAffectsMark(node) {
5465
6453
  shiftAffectsMarks(-1);
5466
- node._affectsCount--;
5467
- if (!node._affectsCount) {
6454
+ node._x._affectsCount--;
6455
+ if (!node._x._affectsCount) {
5468
6456
  GlobalQueue._repollVerdicts !== null && GlobalQueue._repollVerdicts(node, true);
5469
6457
  GlobalQueue._releaseAffectsScope?.(node);
5470
6458
  }
@@ -5558,13 +6546,46 @@ function affects(target, key) {
5558
6546
  */
5559
6547
  // ---------------------------------------------------------------------------
5560
6548
  // wrap / dedupe
6549
+ /** Pre-shaped constructor for OBJECT proxy targets: V8 tips a bare `{}` into
6550
+ * dictionary mode once ~19 named properties are assigned onto it (the #3044
6551
+ * `ovl`/`del` fields crossed that line — every trap's field read became a
6552
+ * hash lookup, a 15% deep-dbmon tick regression). Declaring every field in a
6553
+ * constructor pre-allocates in-object slots so the map stays fast, with
6554
+ * headroom for future fields. The prototype is reset to `Object.prototype`
6555
+ * so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
6556
+ * plain object's. Array targets keep the bare-`[]` path — they must carry
6557
+ * the array exotic class for `Array.isArray(proxy)`, and arrays store named
6558
+ * fields off-object where this cliff does not apply. */
6559
+ function TargetShape() {
6560
+ this.v = undefined;
6561
+ this.ch = undefined;
6562
+ this.pb = undefined;
6563
+ this.n = undefined;
6564
+ this.h = undefined;
6565
+ this.k = undefined;
6566
+ this.dk = undefined;
6567
+ this.u = undefined;
6568
+ this.pk = undefined;
6569
+ this.px = undefined;
6570
+ this.d = undefined;
6571
+ this.a = undefined;
6572
+ this.sc = undefined;
6573
+ this.nc = undefined;
6574
+ this.adopted = undefined;
6575
+ this.fam = undefined;
6576
+ this.s = undefined;
6577
+ this.ovl = undefined;
6578
+ this.del = undefined;
6579
+ this.wk = undefined;
6580
+ }
6581
+ TargetShape.prototype = Object.prototype;
5561
6582
  function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
5562
6583
  // The proxy target carries the array exotic class when the value is an
5563
6584
  // array, so Array.isArray(proxy) is true; the fields live on it directly.
5564
6585
  // Direct field assignment in one fixed order (no Object.assign literal
5565
6586
  // copy): every target shares a hidden-class transition chain — createTarget
5566
6587
  // was the #2 store cost in the uibench creation profile.
5567
- const t = Array.isArray(value) ? [] : {};
6588
+ const t = Array.isArray(value) ? [] : new TargetShape();
5568
6589
  t.v = value;
5569
6590
  // Chained-backing flag (backing IS another store's proxy, §7b) — cached so
5570
6591
  // the hot read path never does a per-read symbol lookup on the backing.
@@ -5584,6 +6605,9 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
5584
6605
  t.adopted = false;
5585
6606
  t.fam = fam;
5586
6607
  t.s = false;
6608
+ t.ovl = false;
6609
+ t.del = null;
6610
+ t.wk = null;
5587
6611
  t.px = new Proxy(t, traps);
5588
6612
  // Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
5589
6613
  // proxy off looked-up targets as a field.
@@ -5610,7 +6634,13 @@ function wrapNext(value, parent = null, parentKey = null, fam = parent?.fam ?? n
5610
6634
  function unwrapValue(v) {
5611
6635
  if (v == null || typeof v !== "object") return v;
5612
6636
  const t = v[$TARGET];
5613
- if (t !== undefined && t.px === v && t.v !== undefined) return t.pb ?? t.v;
6637
+ if (t !== undefined && t.px === v && t.v !== undefined) {
6638
+ // A draft escaping into other storage must be a REAL container that
6639
+ // becomes this target's committed backing at fold (the shared-raw
6640
+ // contract) — a prototype overlay is neither.
6641
+ if (t.ovl) materializePB(t);
6642
+ return t.pb ?? t.v;
6643
+ }
5614
6644
  return v;
5615
6645
  }
5616
6646
  // ---------------------------------------------------------------------------
@@ -5622,13 +6652,19 @@ function getNode(target, key, current) {
5622
6652
  const created = (node = signal(
5623
6653
  current,
5624
6654
  {
6655
+ // Attribution-only: name store property nodes by path segment so
6656
+ // attribution chains and wide-scope warnings read "store.todos", not
6657
+ // "signal". Gated on the engine being installed — node creation is
6658
+ // the hottest store path, and the disabled cost must stay one null
6659
+ // check (nodes created before enable() stay generically named).
6660
+ name: attrHooks !== null ? "store." + String(key) : undefined,
5625
6661
  // Logical-slot equality: values resolving to the same child target
5626
6662
  // are the same slot (privatization/adoption swap raw identity without
5627
6663
  // changing the logical value — only changed leaves notify, R9).
5628
6664
  equals: (a, b) => isEqual(a, b) || sameLogicalSlot(target, a, b),
5629
6665
  unobserved() {
5630
6666
  // A live affects() mark keeps the node addressable (sweep parity).
5631
- if (created._affectsCount) return;
6667
+ if (created._x?._affectsCount) return;
5632
6668
  if (target.n && target.n[key] === created) {
5633
6669
  delete target.n[key];
5634
6670
  target.nc--;
@@ -5655,7 +6691,10 @@ function getNode(target, key, current) {
5655
6691
  created.pxv = undefined;
5656
6692
  // Optimistic families: arm the override slot — setSignal routes armed
5657
6693
  // nodes through the core engine (lanes, ownership, reverts all native).
5658
- if (target.fam?.opt) created._overrideValue = NOT_PENDING;
6694
+ if (target.fam?.opt) {
6695
+ ext(created)._overrideValue = NOT_PENDING;
6696
+ created._config |= CONFIG_OPTIMISTIC;
6697
+ }
5659
6698
  // A node born inside a live mark's identity scope inherits the mark
5660
6699
  // (the declaration walk could only cover nodes existing then).
5661
6700
  if (key !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(created, target.v, key);
@@ -5680,14 +6719,17 @@ function getHasNode(target, key, present) {
5680
6719
  {
5681
6720
  equals: isEqual,
5682
6721
  unobserved() {
5683
- if (created._affectsCount) return;
6722
+ if (created._x?._affectsCount) return;
5684
6723
  if (target.h && target.h[key] === created) delete target.h[key];
5685
6724
  }
5686
6725
  },
5687
6726
  target.fam?.node ?? undefined
5688
6727
  ));
5689
6728
  created._config |= CONFIG_OWNED_WRITE;
5690
- if (target.fam?.opt) created._overrideValue = NOT_PENDING;
6729
+ if (target.fam?.opt) {
6730
+ ext(created)._overrideValue = NOT_PENDING;
6731
+ created._config |= CONFIG_OPTIMISTIC;
6732
+ }
5691
6733
  if (affectsScopesLive()) inheritAffectsMarks(created, target.v, key);
5692
6734
  nodes[key] = node;
5693
6735
  markDescendants(target);
@@ -5708,7 +6750,10 @@ function getKeySetNode(target) {
5708
6750
  target.fam?.node ?? undefined
5709
6751
  ));
5710
6752
  created._config |= CONFIG_OWNED_WRITE;
5711
- if (target.fam?.opt) created._overrideValue = NOT_PENDING;
6753
+ if (target.fam?.opt) {
6754
+ ext(created)._overrideValue = NOT_PENDING;
6755
+ created._config |= CONFIG_OPTIMISTIC;
6756
+ }
5712
6757
  target.k = k;
5713
6758
  markDescendants(target);
5714
6759
  }
@@ -5728,7 +6773,10 @@ function getDeepNode(target) {
5728
6773
  target.fam?.node ?? undefined
5729
6774
  ));
5730
6775
  created._config |= CONFIG_OWNED_WRITE;
5731
- if (target.fam?.opt) created._overrideValue = NOT_PENDING;
6776
+ if (target.fam?.opt) {
6777
+ ext(created)._overrideValue = NOT_PENDING;
6778
+ created._config |= CONFIG_OPTIMISTIC;
6779
+ }
5732
6780
  if (affectsScopesLive()) inheritAffectsMarks(created, target.v, $TRACK);
5733
6781
  target.dk = dk;
5734
6782
  markDescendants(target);
@@ -5771,10 +6819,62 @@ function cloneRaw(source, t) {
5771
6819
  ? Object.defineProperties([], descs)
5772
6820
  : Object.create(Object.getPrototypeOf(source), descs);
5773
6821
  }
6822
+ /** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
6823
+ * returns true when the container is plain data (overlay-safe). */
6824
+ function scanAccessorsOnce(target) {
6825
+ const src = target.v;
6826
+ for (const key of Reflect.ownKeys(src)) {
6827
+ // Own keys shadow prototype accessors, so the lookups are exact here.
6828
+ if (lookupGetter.call(src, key) !== undefined || lookupSetter.call(src, key) !== undefined) {
6829
+ target.a = true;
6830
+ break;
6831
+ }
6832
+ }
6833
+ target.sc = true;
6834
+ return !target.a;
6835
+ }
6836
+ /** Downgrade a prototype-overlay pending backing to the clone path: builds
6837
+ * the real container (committed + overlay writes − deletes) that fold will
6838
+ * SWAP in as the committed backing, exactly as if the draft had started on
6839
+ * the clone path. Consumers that need a complete container (reconcile's
6840
+ * diff walks, drafts escaping into other storage) call this. */
6841
+ function materializePB(target) {
6842
+ if (!target.ovl) return;
6843
+ const proto = target.pb;
6844
+ const clone = cloneRaw(target.v, target);
6845
+ for (const key of Reflect.ownKeys(proto)) {
6846
+ const d = Object.getOwnPropertyDescriptor(proto, key);
6847
+ if (d.get || d.set || !d.enumerable || !d.writable || !d.configurable)
6848
+ Object.defineProperty(clone, key, d);
6849
+ else clone[key] = d.value;
6850
+ }
6851
+ if (target.del !== null) {
6852
+ for (const key of target.del) delete clone[key];
6853
+ target.del = null;
6854
+ }
6855
+ const map = target.fam?.map ?? storeNextLookup;
6856
+ map.delete(proto);
6857
+ ownedRaw.add(clone);
6858
+ map.set(clone, target);
6859
+ target.pb = clone;
6860
+ target.ovl = false;
6861
+ }
5774
6862
  function ensurePB(target) {
5775
6863
  let pb = target.pb;
5776
6864
  if (pb === null) {
5777
- pb = target.pb = cloneRaw(target.v, target);
6865
+ // Prototype-chain overlay (#3044): plain-data non-array containers
6866
+ // outside projection/optimistic families open drafts in O(1) — own keys
6867
+ // are the writes, reads fall through to committed. Everything else
6868
+ // (arrays: splice/length semantics; families: seeding/revert machinery;
6869
+ // accessor containers: live getters) keeps the descriptor clone.
6870
+ if (
6871
+ target.fam === null &&
6872
+ !Array.isArray(target.v) &&
6873
+ (target.sc ? !target.a : scanAccessorsOnce(target))
6874
+ ) {
6875
+ pb = target.pb = Object.create(target.v);
6876
+ target.ovl = true;
6877
+ } else pb = target.pb = cloneRaw(target.v, target);
5778
6878
  // Optimistic families: seed USER drafts from the OPTIMISTIC VIEW
5779
6879
  // (committed + active node overrides), so follow-up writes compose on
5780
6880
  // optimism instead of clobbering from base (#2951's compose half).
@@ -5786,14 +6886,14 @@ function ensurePB(target) {
5786
6886
  if (nodes !== null) {
5787
6887
  for (const key of Reflect.ownKeys(nodes)) {
5788
6888
  const node = nodes[key];
5789
- if (hasActiveOverride(node)) pb[key] = unwrapOverride(node._overrideValue);
6889
+ if (hasActiveOverride(node)) pb[key] = unwrapOverride(node._x?._overrideValue);
5790
6890
  }
5791
6891
  }
5792
6892
  const has = target.h;
5793
6893
  if (has !== null) {
5794
6894
  for (const key of Reflect.ownKeys(has)) {
5795
6895
  const node = has[key];
5796
- if (hasActiveOverride(node) && !unwrapOverride(node._overrideValue)) delete pb[key];
6896
+ if (hasActiveOverride(node) && !unwrapOverride(node._x?._overrideValue)) delete pb[key];
5797
6897
  }
5798
6898
  }
5799
6899
  }
@@ -5820,6 +6920,18 @@ function adoptPB(target, incoming, eager = false) {
5820
6920
  target.adopted = true;
5821
6921
  }
5822
6922
  target.pb = null;
6923
+ // Overlay and accessor-scan state describe the OUTGOING backing — a
6924
+ // swapped container must not inherit them: a stale `ovl` beside a nulled
6925
+ // pb crashes materializePB (unwrapValue consults ovl before the
6926
+ // null-coalesce), a stale `del` would read the adoptee's keys as deleted
6927
+ // in the next draft, and a stale plain-data verdict (`sc`/`a`) could
6928
+ // admit an accessor-bearing adoptee to the overlay path. Reset; the next
6929
+ // draft rescans once (#3044 audit follow-up).
6930
+ target.ovl = false;
6931
+ target.del = null;
6932
+ target.wk = null; // adoption supersedes any staged trap writes
6933
+ target.sc = false;
6934
+ target.a = false;
5823
6935
  target.v = incoming;
5824
6936
  target.ch = incoming[$TARGET] !== undefined;
5825
6937
  (target.fam?.map ?? storeNextLookup).set(incoming, target);
@@ -5863,9 +6975,17 @@ function drainFolds() {
5863
6975
  const pb = t.pb;
5864
6976
  const nodes = t.n;
5865
6977
  if (nodes !== null) {
5866
- for (const key of Reflect.ownKeys(nodes)) {
6978
+ // Only written keys can hold (their nodes took the setSignal); the
6979
+ // wk bound keeps this O(written) — see notifyWrites. Same fallback
6980
+ // rules as the notify (WK_ALL / accessors / non-plain prototypes).
6981
+ const wkh = t.wk;
6982
+ const keys =
6983
+ wkh === null || wkh === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb)
6984
+ ? Reflect.ownKeys(nodes)
6985
+ : wkh;
6986
+ for (const key of keys) {
5867
6987
  const node = nodes[key];
5868
- if (node._pendingValue !== NOT_PENDING) {
6988
+ if (node !== undefined && node._pendingValue !== NOT_PENDING) {
5869
6989
  held = true;
5870
6990
  break;
5871
6991
  }
@@ -5875,9 +6995,36 @@ function drainFolds() {
5875
6995
  foldOlds.set(t, old); // re-queue: commit happens when the hold settles
5876
6996
  continue;
5877
6997
  }
5878
- t.v = pb;
5879
- t.ch = false; // pb is always a plain clone
5880
- t.pb = null;
6998
+ if (t.ovl) {
6999
+ // Overlay flatten (#3044): apply this batch's writes onto an OWNED
7000
+ // committed backing in place — O(written), not O(container). The
7001
+ // backing keeps its identity, so the `t.v === old` gate below skips
7002
+ // path copying (the parent slot already points here) and the
7003
+ // adopted-notify (setter notifications happened at write time).
7004
+ // Unowned backings privatize first (clone once, parents re-slotted)
7005
+ // — the never-mutate-user-data contract holds.
7006
+ privatizeCommitted(t);
7007
+ const v = t.v;
7008
+ for (const key of Reflect.ownKeys(pb)) {
7009
+ const d = Object.getOwnPropertyDescriptor(pb, key);
7010
+ if (d.get || d.set || !d.enumerable || !d.writable || !d.configurable)
7011
+ Object.defineProperty(v, key, d);
7012
+ else v[key] = d.value;
7013
+ }
7014
+ if (t.del !== null) {
7015
+ for (const key of t.del) delete v[key];
7016
+ t.del = null;
7017
+ }
7018
+ (t.fam?.map ?? storeNextLookup).delete(pb);
7019
+ t.pb = null;
7020
+ t.ovl = false;
7021
+ t.wk = null; // written-keys window closes with the fold commit
7022
+ } else {
7023
+ t.v = pb;
7024
+ t.ch = false; // pb is always a plain clone
7025
+ t.pb = null;
7026
+ t.wk = null; // written-keys window closes with the fold commit
7027
+ }
5881
7028
  }
5882
7029
  if (t.v === old) continue; // adopted then re-adopted back, or no-op
5883
7030
  // Path copying (CAS: see the eager-fold twin above).
@@ -5900,8 +7047,19 @@ function drainFolds() {
5900
7047
  * "pending home = the node when a node exists"). Unobserved keys stay in the
5901
7048
  * pending backing and fold directly at commit.
5902
7049
  */
7050
+ /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
7051
+ * array length write implicitly deleted indices) — consumers full-scan. */
7052
+ const WK_ALL = new Set();
7053
+ /** Plain-prototype check for the written-keys bound: prototype getters on
7054
+ * class instances can derive from ANY field, so only plain-data containers
7055
+ * may bound the notify to written keys. Overlay pbs chain to the COMMITTED
7056
+ * object (#3044), so overlay plainness is judged on the committed proto. */
7057
+ const plainProto = o => {
7058
+ const p = Object.getPrototypeOf(o);
7059
+ return p === Object.prototype || p === Array.prototype || p === null;
7060
+ };
5903
7061
  function notifyWrites(t) {
5904
- const pb = t.pb;
7062
+ let pb = t.pb;
5905
7063
  if (pb === null) return;
5906
7064
  // Optimistic channel: user writes on an optimistic family become node-level
5907
7065
  // engine writes (armed nodes route setSignal through optimisticWrite) — the
@@ -5931,8 +7089,11 @@ function notifyWrites(t) {
5931
7089
  }
5932
7090
  const old = t.v;
5933
7091
  // Devtools mutation hook: full-key diff (dev-only cost) so unobserved
5934
- // writes report too, matching the legacy set-trap hook.
7092
+ // writes report too, matching the legacy set-trap hook. Overlay backings
7093
+ // materialize first so the diff walks a real container.
5935
7094
  if (DEV$1.hooks.onStoreNodeUpdate) {
7095
+ if (t.ovl) materializePB(t);
7096
+ pb = t.pb;
5936
7097
  for (const key of Reflect.ownKeys(pb)) {
5937
7098
  if (Array.isArray(pb) && key === "length") continue;
5938
7099
  const ov = old[key];
@@ -5945,9 +7106,20 @@ function notifyWrites(t) {
5945
7106
  }
5946
7107
  }
5947
7108
  const nodes = t.n;
7109
+ // Written-keys bound: trap writes record their keys, so the notify visits
7110
+ // O(written) nodes instead of every subscription on the record (a selection
7111
+ // map with thousands of per-key subscribers pays two visits per select,
7112
+ // not a full scan). Falls back to the full node scan when the bound can't
7113
+ // hold: no trap granularity (wk null), an array length write (WK_ALL —
7114
+ // implicit index deletes), accessors on the record (t.a — a getter node's
7115
+ // value can change when ANY key is written), or a non-plain prototype.
7116
+ const wk0 = t.wk;
7117
+ const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
5948
7118
  if (nodes !== null) {
5949
- for (const key of Reflect.ownKeys(nodes)) {
7119
+ const keys = writtenKeys ?? Reflect.ownKeys(nodes);
7120
+ for (const key of keys) {
5950
7121
  const node = nodes[key];
7122
+ if (node === undefined) continue;
5951
7123
  // Per-key accessor handling: the node's cached flag plus ONE getter
5952
7124
  // probe on the incoming side (getters arriving via merge/adoption).
5953
7125
  // Setter-only props read as data (value undefined) so lookupSetter is
@@ -5968,31 +7140,48 @@ function notifyWrites(t) {
5968
7140
  // projection recompute can run before the prior fold commits) — the
5969
7141
  // node's OWN current value is the true old side, and setSignal's
5970
7142
  // internal equality already checks exactly that.
5971
- const nv = pb[key];
7143
+ const nv = t.del !== null && t.del.has(key) ? undefined : pb[key];
5972
7144
  setSignal(node, () => nv);
5973
7145
  }
5974
7146
  }
5975
7147
  const has = t.h;
5976
7148
  if (has !== null) {
5977
- for (const key of Reflect.ownKeys(has)) setSignal(has[key], key in pb);
7149
+ for (const key of Reflect.ownKeys(has))
7150
+ setSignal(has[key], key in pb && !(t.del !== null && t.del.has(key)));
5978
7151
  }
5979
7152
  // Deep-witness (dk): setter writes must notify a deep() subscriber even on
5980
7153
  // keys with no node. O(pb keys) equality only when a witness exists.
5981
7154
  if (t.dk !== null) {
5982
- for (const key of Reflect.ownKeys(pb)) {
5983
- const nv = pb[key];
5984
- const ov = old[key];
5985
- if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
5986
- bumpDeep(t);
5987
- break;
7155
+ if (t.del !== null && t.del.size !== 0) bumpDeep(t);
7156
+ else
7157
+ for (const key of Reflect.ownKeys(pb)) {
7158
+ const nv = pb[key];
7159
+ const ov = old[key];
7160
+ if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
7161
+ bumpDeep(t);
7162
+ break;
7163
+ }
5988
7164
  }
5989
- }
5990
7165
  }
5991
7166
  if (t.k !== null) {
5992
- const changed =
5993
- Array.isArray(pb) && Array.isArray(old)
5994
- ? arrayStructureChanged(old, pb)
5995
- : membershipChanged(old, pb);
7167
+ let changed;
7168
+ if (t.ovl) {
7169
+ // Overlay membership: only NEW own keys or deletes can change it.
7170
+ changed = t.del !== null && t.del.size !== 0;
7171
+ if (!changed) {
7172
+ for (const key of Reflect.ownKeys(pb)) {
7173
+ if (!hasOwn.call(old, key)) {
7174
+ changed = true;
7175
+ break;
7176
+ }
7177
+ }
7178
+ }
7179
+ } else {
7180
+ changed =
7181
+ Array.isArray(pb) && Array.isArray(old)
7182
+ ? arrayStructureChanged(old, pb)
7183
+ : membershipChanged(old, pb);
7184
+ }
5996
7185
  if (changed) setSignal(t.k, v => v + 1);
5997
7186
  }
5998
7187
  // Projection backing folds split by channel (two pinned contracts):
@@ -6008,6 +7197,7 @@ function notifyWrites(t) {
6008
7197
  if (t.fam !== null && t.pb !== null && getWriteOverride()) {
6009
7198
  const oldBacking = t.v;
6010
7199
  t.pb = null;
7200
+ t.wk = null; // written-keys window closes with the eager fold
6011
7201
  t.v = pb;
6012
7202
  t.ch = false;
6013
7203
  if (t.u && t.u.v[t.pk] === oldBacking) {
@@ -6278,7 +7468,7 @@ function runAuthoritative(fn) {
6278
7468
  /** Active optimistic override on an armed node (armed slot idles at
6279
7469
  * NOT_PENDING; undefined = unarmed plain node). */
6280
7470
  function hasActiveOverride(node) {
6281
- return node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING;
7471
+ return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
6282
7472
  }
6283
7473
  /** Context-aware node view for reads outside tracking: active override >
6284
7474
  * held pending (owner context) > the BACKING value. Committed truth lives in
@@ -6289,7 +7479,7 @@ function hasActiveOverride(node) {
6289
7479
  * keys, which are served by the trap, not the node). */
6290
7480
  function nodeValue(node, backing) {
6291
7481
  const v = hasActiveOverride(node)
6292
- ? unwrapOverride(node._overrideValue)
7482
+ ? unwrapOverride(node._x?._overrideValue)
6293
7483
  : node._pendingValue !== NOT_PENDING && inOwnerContext()
6294
7484
  ? node._pendingValue
6295
7485
  : backing;
@@ -6325,7 +7515,8 @@ function serveDataKey(target, key, backingValue, src, node) {
6325
7515
  // #2951). Once ensurePB runs, the seeded clone carries the view.
6326
7516
  if (target.fam?.opt && target.pb === null) {
6327
7517
  const node = target.n?.[key];
6328
- if (node !== undefined && hasActiveOverride(node)) v = unwrapOverride(node._overrideValue);
7518
+ if (node !== undefined && hasActiveOverride(node))
7519
+ v = unwrapOverride(node._x?._overrideValue);
6329
7520
  }
6330
7521
  } else {
6331
7522
  if (node !== undefined) {
@@ -6368,6 +7559,15 @@ function serveDataKey(target, key, backingValue, src, node) {
6368
7559
  * link is what wakes async-memo readers when the landing writes values;
6369
7560
  * the firewall link rides the same read). */
6370
7561
  function firewallGate(target) {
7562
+ // Own-draft ops are exempt: an async derive's continuation (generator body
7563
+ // after an `await`/`yield`) runs OUTSIDE the sync write scope (inDraft is
7564
+ // already false), but its draft-proxy traps mark every op with the write
7565
+ // override. Those reads are the derive working its own draft (state.push
7566
+ // reading .length) — gating them throws NotReadyError back into the derive
7567
+ // itself, which the post-await read diagnostic (#2987) then escalates to a
7568
+ // reactivity halt. The gate exists for EXTERNAL readers (seed invisibility,
7569
+ // proj R23); the derive is the author.
7570
+ if (projectionWriteActive || getWriteOverride()) return;
6371
7571
  const fw = target.fam?.node;
6372
7572
  if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
6373
7573
  }
@@ -6399,6 +7599,12 @@ const traps = {
6399
7599
  if (pendingCheckActive) witnessAffectsMark(target, key);
6400
7600
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
6401
7601
  const src = readSource(target);
7602
+ // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
7603
+ // deleted keys are tracked aside and read as absent in the pending view.
7604
+ if (target.del !== null && src === target.pb && target.del.has(key)) {
7605
+ if (!inDraft(target) && getObserver() !== null) read(getNode(target, key, undefined));
7606
+ return undefined;
7607
+ }
6402
7608
  // Hot inline case: existing PLAIN node (non-accessor), unchained backing,
6403
7609
  // tracked read of a present data key — the dbmon/uibench effect re-read
6404
7610
  // shape. Skips serveDataKey's frame, the FORCE compare (only accessor
@@ -6439,14 +7645,21 @@ const traps = {
6439
7645
  // the node's cached flag; the first TRACKED read (which creates the
6440
7646
  // node) probes once — untracked node-less reads take the plain path,
6441
7647
  // where a raw-receiver getter still returns correct committed values.
7648
+ // Tracking suppression is PER-TARGET (inDraft), never global: `writing`
7649
+ // counts every open setter anywhere, and a projection derive runs its
7650
+ // whole body inside one — a global gate silently swallowed EXTERNAL
7651
+ // absent-key/accessor subscriptions for every store read during any
7652
+ // derive, leaving nested projections permanently dependency-less when
7653
+ // their sources hadn't materialized yet (#3037).
6442
7654
  const node0 = target.n?.[key];
6443
7655
  {
6444
7656
  const acc =
6445
7657
  node0 !== undefined
6446
7658
  ? node0.acc === true
6447
- : !writing && getObserver() !== null && isOwnAccessor(src, key);
7659
+ : !inDraft(target) && getObserver() !== null && isOwnAccessor(src, key);
6448
7660
  if (acc) {
6449
- if (!writing && getObserver() !== null) read(node0 ?? getNode(target, key, undefined));
7661
+ if (!inDraft(target) && getObserver() !== null)
7662
+ read(node0 ?? getNode(target, key, undefined));
6450
7663
  const v = Reflect.get(src, key, receiver);
6451
7664
  if (target.s) return serveShallow(target, key, v);
6452
7665
  return isWrappable(v) ? draftServe(target, wrapNext(v, target, key)) : v;
@@ -6455,19 +7668,27 @@ const traps = {
6455
7668
  // Plain-data fast path: no descriptor allocation per read.
6456
7669
  // Inherited pollution keys are never served (core R30) — checked before
6457
7670
  // the proto-function branch can leak `constructor`. Interned-string
6458
- // compares beat a Set hash on this per-read path.
7671
+ // compares beat a Set hash on this per-read path. Overlay pending
7672
+ // backings chain to the committed backing, so "own in the view" means
7673
+ // own on either layer (ownInView) — a genuine prototype method is one
7674
+ // that is own on NEITHER.
7675
+ const viewOvl = target.ovl && src === target.pb;
6459
7676
  if (
6460
7677
  (key === "constructor" || key === "__proto__" || key === "prototype") &&
6461
- !hasOwn.call(src, key)
7678
+ !hasOwn.call(src, key) &&
7679
+ !(viewOvl && hasOwn.call(target.v, key))
6462
7680
  )
6463
7681
  return undefined;
6464
7682
  let v = src[key];
6465
- if (v === undefined ? !hasOwn.call(src, key) : false) {
7683
+ if (
7684
+ v === undefined ? !hasOwn.call(src, key) && !(viewOvl && hasOwn.call(target.v, key)) : false
7685
+ ) {
6466
7686
  // Inherited: prototype getters/methods run with the proxy receiver.
6467
7687
  v = Reflect.get(src, key, receiver);
6468
7688
  if (typeof v === "function") return v; // proto methods untracked
6469
- // Reading a currently-absent own key subscribes to it (R12).
6470
- if (v === undefined && !writing) {
7689
+ // Reading a currently-absent own key subscribes to it (R12) — for any
7690
+ // target OUTSIDE its own draft scope, even mid-setter (#3037, above).
7691
+ if (v === undefined && !inDraft(target)) {
6471
7692
  if (getObserver() !== null) read(getNode(target, key, undefined));
6472
7693
  const node = target.n?.[key];
6473
7694
  if (node) {
@@ -6477,12 +7698,18 @@ const traps = {
6477
7698
  }
6478
7699
  } else if (v === undefined && inDraft(target) && target.fam?.opt && target.pb === null) {
6479
7700
  const node = target.n?.[key];
6480
- if (node !== undefined && hasActiveOverride(node)) v = unwrapOverride(node._overrideValue);
7701
+ if (node !== undefined && hasActiveOverride(node))
7702
+ v = unwrapOverride(node._x?._overrideValue);
6481
7703
  }
6482
7704
  if (target.s) return serveShallow(target, key, v);
6483
7705
  return isWrappable(v) ? draftServe(target, wrapNext(v, target, key)) : v;
6484
7706
  }
6485
- if (typeof v === "function" && !hasOwn.call(src, key)) return v; // proto method
7707
+ if (
7708
+ typeof v === "function" &&
7709
+ !hasOwn.call(src, key) &&
7710
+ !(viewOvl && hasOwn.call(target.v, key))
7711
+ )
7712
+ return v; // proto method
6486
7713
  return serveDataKey(target, key, v, src, node0);
6487
7714
  },
6488
7715
  has(target, key) {
@@ -6491,6 +7718,8 @@ const traps = {
6491
7718
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
6492
7719
  const src = readSource(target);
6493
7720
  let present = key in src;
7721
+ // Overlay deletes read as absent in the pending view (#3044).
7722
+ if (present && target.del !== null && src === target.pb && target.del.has(key)) present = false;
6494
7723
  if (!inDraft(target)) {
6495
7724
  if (getObserver() !== null) {
6496
7725
  const node = getHasNode(target, key, present);
@@ -6499,12 +7728,12 @@ const traps = {
6499
7728
  } else {
6500
7729
  const node = target.h?.[key];
6501
7730
  if (node !== undefined && hasActiveOverride(node))
6502
- present = !!unwrapOverride(node._overrideValue);
7731
+ present = !!unwrapOverride(node._x?._overrideValue);
6503
7732
  }
6504
7733
  } else if (target.fam?.opt && target.pb === null) {
6505
7734
  const node = target.h?.[key];
6506
7735
  if (node !== undefined && hasActiveOverride(node))
6507
- present = !!unwrapOverride(node._overrideValue);
7736
+ present = !!unwrapOverride(node._x?._overrideValue);
6508
7737
  }
6509
7738
  return present;
6510
7739
  },
@@ -6512,7 +7741,18 @@ const traps = {
6512
7741
  if (pendingCheckActive) witnessAffectsMark(target);
6513
7742
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
6514
7743
  if (!inDraft(target) && getObserver() !== null) read(getKeySetNode(target));
6515
- const keys = Reflect.ownKeys(readSource(target));
7744
+ const src = readSource(target);
7745
+ let keys;
7746
+ if (target.ovl && src === target.pb) {
7747
+ // Overlay merge (#3044): committed keys in their order, then this
7748
+ // batch's NEW keys, minus deletes.
7749
+ keys = Reflect.ownKeys(target.v);
7750
+ const del = target.del;
7751
+ if (del !== null && del.size !== 0) keys = keys.filter(key => !del.has(key));
7752
+ for (const key of Reflect.ownKeys(src)) {
7753
+ if (!hasOwn.call(target.v, key)) keys.push(key);
7754
+ }
7755
+ } else keys = Reflect.ownKeys(src);
6516
7756
  // Optimistic membership overlay: presence-node overrides add/remove keys
6517
7757
  // (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
6518
7758
  // Draft reads before the first write overlay too (pb, once created, is
@@ -6523,7 +7763,7 @@ const traps = {
6523
7763
  const node = target.h[key];
6524
7764
  if (!hasActiveOverride(node)) continue;
6525
7765
  set ??= new Set(keys);
6526
- if (unwrapOverride(node._overrideValue)) set.add(key);
7766
+ if (unwrapOverride(node._x?._overrideValue)) set.add(key);
6527
7767
  else set.delete(key);
6528
7768
  }
6529
7769
  if (set !== null) return [...set];
@@ -6531,11 +7771,18 @@ const traps = {
6531
7771
  return keys;
6532
7772
  },
6533
7773
  getOwnPropertyDescriptor(target, key) {
6534
- const desc = Object.getOwnPropertyDescriptor(readSource(target), key);
7774
+ const srcD = readSource(target);
7775
+ let desc = Object.getOwnPropertyDescriptor(srcD, key);
7776
+ // Overlay (#3044): unwritten keys live on the committed backing;
7777
+ // deleted keys are absent from the pending view.
7778
+ if (target.ovl && srcD === target.pb) {
7779
+ if (target.del !== null && target.del.has(key)) return undefined;
7780
+ if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
7781
+ }
6535
7782
  if (target.fam?.opt && !inDraft(target)) {
6536
7783
  const node = target.h?.[key];
6537
7784
  if (node !== undefined && hasActiveOverride(node)) {
6538
- if (!unwrapOverride(node._overrideValue)) return undefined; // opt delete
7785
+ if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
6539
7786
  if (desc === undefined) {
6540
7787
  const vn = target.n?.[key];
6541
7788
  return {
@@ -6562,24 +7809,50 @@ const traps = {
6562
7809
  const override = !draft && getWriteOverride();
6563
7810
  if (!draft && !override) return true;
6564
7811
  if (key === "__proto__") return true; // pollution guard (core R30)
7812
+ // Unwrap BEFORE ensurePB: unwrapValue materializes a self-referencing
7813
+ // draft's overlay (replacing target.pb), so a pb local captured earlier
7814
+ // would be the abandoned overlay and the write would vanish.
7815
+ // Shallow slots store what was written VERBATIM — another store's proxy
7816
+ // passes through by reference (#2932; markRawOne skips proxies), while
7817
+ // deep stores unwrap to raw backings.
7818
+ const uv = target.s ? value : unwrapValue(value);
6565
7819
  const pb = ensurePB(target);
6566
7820
  pendingNotify.add(target);
7821
+ // Array length writes implicitly delete indices — the written-keys bound
7822
+ // can't see them, so poison to the full scan for this batch. Index
7823
+ // writes implicitly GROW length, so arrays always record it alongside.
7824
+ if (Array.isArray(pb)) {
7825
+ if (key === "length") target.wk = WK_ALL;
7826
+ else if (target.wk !== WK_ALL) {
7827
+ const wk = (target.wk ??= new Set());
7828
+ wk.add(key);
7829
+ wk.add("length");
7830
+ }
7831
+ } else if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
6567
7832
  // Own data keys literally named "prototype"/"constructor" land as data —
6568
7833
  // defineProperty sidesteps a proto-chain setter named the same.
6569
7834
  if (UNSAFE_KEYS.has(key)) {
6570
7835
  Object.defineProperty(pb, key, {
6571
- value: unwrapValue(value),
7836
+ value: uv,
6572
7837
  writable: true,
6573
7838
  enumerable: true,
6574
7839
  configurable: true
6575
7840
  });
7841
+ if (target.del !== null) target.del.delete(key);
6576
7842
  return true;
6577
7843
  }
6578
- // Shallow slots store what was written VERBATIM another store's proxy
6579
- // passes through by reference (#2932; markRawOne skips proxies), while
6580
- // deep stores unwrap to raw backings.
6581
- const uv = target.s ? value : unwrapValue(value);
6582
- pb[key] = uv;
7844
+ // Overlay first-write DEFINES the own key: assignment through the proto
7845
+ // chain would reject on a non-writable committed property (the clone
7846
+ // path normalized descriptors for exactly this — R51 parity).
7847
+ if (target.ovl && !hasOwn.call(pb, key)) {
7848
+ Object.defineProperty(pb, key, {
7849
+ value: uv,
7850
+ writable: true,
7851
+ enumerable: true,
7852
+ configurable: true
7853
+ });
7854
+ } else pb[key] = uv;
7855
+ if (target.del !== null) target.del.delete(key);
6583
7856
  // Shallow ingest: written records are sticky raw-marked (one entity is
6584
7857
  // never both deep-wrapped and raw — R41/#2932, shared invariant).
6585
7858
  if (target.s && uv !== null && typeof uv === "object") markRawOne(uv);
@@ -6594,10 +7867,13 @@ const traps = {
6594
7867
  if (!draft && !override) return true;
6595
7868
  if (key === "__proto__") return true;
6596
7869
  if (desc.get || desc.set) target.a = true;
7870
+ // Unwrap before ensurePB (see the set trap: self-reference materializes).
7871
+ if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
6597
7872
  const pb = ensurePB(target);
6598
7873
  pendingNotify.add(target);
6599
- if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
7874
+ if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
6600
7875
  Object.defineProperty(pb, key, desc);
7876
+ if (target.del !== null) target.del.delete(key);
6601
7877
  if (override) notifyWrites(target);
6602
7878
  return true;
6603
7879
  },
@@ -6607,7 +7883,11 @@ const traps = {
6607
7883
  if (!draft && !override) return true;
6608
7884
  const pb = ensurePB(target);
6609
7885
  pendingNotify.add(target);
7886
+ if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
6610
7887
  delete pb[key];
7888
+ // A prototype overlay cannot shadow a delete of a committed key —
7889
+ // record it aside (#3044); reads/has/ownKeys/commit consult the set.
7890
+ if (target.ovl && hasOwn.call(target.v, key)) (target.del ??= new Set()).add(key);
6611
7891
  if (override) notifyWrites(target);
6612
7892
  return true;
6613
7893
  }
@@ -6749,6 +8029,9 @@ function snapshotWalk(value, seen, fam) {
6749
8029
  if (t === undefined) break;
6750
8030
  if (t.fam !== null) fam = t.fam;
6751
8031
  if (t.fam?.opt === true) (optOwners ??= []).push(t);
8032
+ // Snapshot runs mid-flush (tracked memos execute before commit), so a
8033
+ // pending prototype overlay must present as a REAL merged container.
8034
+ if (t.ovl) materializePB(t);
6752
8035
  const backing = t.pb ?? t.v;
6753
8036
  if (backing === src) break;
6754
8037
  src = backing;
@@ -6848,6 +8131,10 @@ function reconcileNextState(value, state, key, replace = false) {
6848
8131
  if (state == null) throw new Error("Cannot reconcile null or undefined state");
6849
8132
  const t = state?.[$TARGET];
6850
8133
  if (t === undefined || t.px !== state) throw new Error("reconcile target is not a store proxy");
8134
+ // Reconcile's diff walks need a REAL pending container — a prototype
8135
+ // overlay (#3044) materializes to the clone path first (edge: reconcile
8136
+ // inside a setter that already wrote this target).
8137
+ if (t.ovl) materializePB(t);
6851
8138
  let keyFn = key === null ? null : typeof key === "string" ? item => item?.[key] : key;
6852
8139
  // §7b chained backing: a projection derive returning a LIVE store proxy
6853
8140
  // adopts the proxy itself as the backing — reads flow through the inner
@@ -7293,7 +8580,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
7293
8580
  : null;
7294
8581
  const draft = new Proxy(
7295
8582
  wrappedStore,
7296
- createWriteTraps(() => !settled || owner._inFlight === result, onDraftWrite)
8583
+ createWriteTraps(() => !settled || owner._x?._inFlight === result, onDraftWrite)
7297
8584
  );
7298
8585
  storeSetterNext(
7299
8586
  draft,
@@ -7374,10 +8661,15 @@ function familyHasLiveOverrides(fam) {
7374
8661
  if (bucket === null) continue;
7375
8662
  for (const key of Reflect.ownKeys(bucket)) {
7376
8663
  const node = bucket[key];
7377
- if (node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING) return true;
8664
+ if (node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING)
8665
+ return true;
7378
8666
  }
7379
8667
  }
7380
- if (t.k !== null && t.k._overrideValue !== undefined && t.k._overrideValue !== NOT_PENDING)
8668
+ if (
8669
+ t.k !== null &&
8670
+ t.k._x?._overrideValue !== undefined &&
8671
+ t.k._x?._overrideValue !== NOT_PENDING
8672
+ )
7381
8673
  return true;
7382
8674
  }
7383
8675
  overlaid.clear(); // nothing live — drop the bookkeeping
@@ -7451,13 +8743,13 @@ function notifyOptimisticWrites(t, pb) {
7451
8743
  const visible = (key, fallback) => {
7452
8744
  const node = t.n?.[key];
7453
8745
  return node !== undefined && hasActiveOverride(node)
7454
- ? unwrapOverride(node._overrideValue)
8746
+ ? unwrapOverride(node._x?._overrideValue)
7455
8747
  : fallback;
7456
8748
  };
7457
8749
  const visiblePresent = key => {
7458
8750
  const node = t.h?.[key];
7459
8751
  return node !== undefined && hasActiveOverride(node)
7460
- ? !!unwrapOverride(node._overrideValue)
8752
+ ? !!unwrapOverride(node._x?._overrideValue)
7461
8753
  : key in old;
7462
8754
  };
7463
8755
  let structural = false;
@@ -7518,14 +8810,18 @@ function consumeOverridesNext(fam) {
7518
8810
  for (const t of overlaid) {
7519
8811
  const drop = (node, committed) => {
7520
8812
  if (!hasActiveOverride(node)) return;
7521
- const prev = unwrapOverride(node._overrideValue);
8813
+ const prev = unwrapOverride(node._x?._overrideValue);
7522
8814
  // Full legacy reset (clearOptimisticOverride parity): the landing is
7523
8815
  // authoritative NOW — fold committed into the node directly instead
7524
8816
  // of riding a transaction's commit (whose queues may be stashed with
7525
8817
  // the transaction parked; the wake would strand until it settles).
7526
- node._overrideValue = NOT_PENDING;
7527
- node._overrideOwner = null;
7528
- node._optimisticLane = undefined;
8818
+ ext(node)._overrideValue = NOT_PENDING;
8819
+ node._config |= CONFIG_OPTIMISTIC;
8820
+ const nx = node._x;
8821
+ if (nx) {
8822
+ nx._overrideOwner = null;
8823
+ nx._optimisticLane = undefined;
8824
+ }
7529
8825
  node._pendingValue = NOT_PENDING;
7530
8826
  node._value = committed;
7531
8827
  if (!node._equals || !node._equals(prev, committed)) {
@@ -7561,9 +8857,13 @@ function consumeOverridesNext(fam) {
7561
8857
  for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
7562
8858
  }
7563
8859
  if (t.k !== null && hasActiveOverride(t.k)) {
7564
- t.k._overrideValue = NOT_PENDING;
7565
- t.k._overrideOwner = null;
7566
- t.k._optimisticLane = undefined;
8860
+ ext(t.k)._overrideValue = NOT_PENDING;
8861
+ t.k._config |= CONFIG_OPTIMISTIC;
8862
+ const kx = t.k._x;
8863
+ if (kx) {
8864
+ kx._overrideOwner = null;
8865
+ kx._optimisticLane = undefined;
8866
+ }
7567
8867
  insertSubs(t.k, true);
7568
8868
  schedule();
7569
8869
  }
@@ -7583,7 +8883,7 @@ function optimisticView(t, src) {
7583
8883
  for (const key of Reflect.ownKeys(nodes)) {
7584
8884
  const node = nodes[key];
7585
8885
  if (!hasActiveOverride(node)) continue;
7586
- const ov = unwrapOverride(node._overrideValue);
8886
+ const ov = unwrapOverride(node._x?._overrideValue);
7587
8887
  if (key === "length" && Array.isArray(src)) {
7588
8888
  if (src.length !== ov) ensure().length = ov;
7589
8889
  } else if (!isEqual(src[key], ov)) ensure()[key] = ov;
@@ -7594,7 +8894,7 @@ function optimisticView(t, src) {
7594
8894
  for (const key of Reflect.ownKeys(has)) {
7595
8895
  const node = has[key];
7596
8896
  if (!hasActiveOverride(node)) continue;
7597
- const present = !!unwrapOverride(node._overrideValue);
8897
+ const present = !!unwrapOverride(node._x?._overrideValue);
7598
8898
  if (!present && key in (out ?? src)) delete ensure()[key];
7599
8899
  }
7600
8900
  }
@@ -8322,10 +9622,10 @@ function compare(key, a, b) {
8322
9622
 
8323
9623
  function boundaryComputed(fn, propagationMask) {
8324
9624
  const node = computed(fn, { lazy: true });
8325
- node._notifyStatus = (status, error) => {
9625
+ ext(node)._notifyStatus = (status, error) => {
8326
9626
  // Use passed values if provided, otherwise read from node
8327
9627
  const flags = status !== undefined ? status : node._statusFlags;
8328
- const actualError = error !== undefined ? error : node._error;
9628
+ const actualError = error !== undefined ? error : node._x?._error;
8329
9629
  // Notify both status dimensions like a render effect does; the queue chain
8330
9630
  // consumes this boundary's own type and forwards the remainder upward until
8331
9631
  // a boundary that handles it is found.
@@ -8338,8 +9638,8 @@ function boundaryComputed(fn, propagationMask) {
8338
9638
  const foreign = flags & ~node._propagationMask & (STATUS_PENDING | STATUS_ERROR);
8339
9639
  if (foreign) {
8340
9640
  node._statusFlags &= ~foreign;
8341
- if (node._error === actualError && !(node._statusFlags & (STATUS_PENDING | STATUS_ERROR)))
8342
- node._error = undefined;
9641
+ if (node._x?._error === actualError && !(node._statusFlags & (STATUS_PENDING | STATUS_ERROR)))
9642
+ if (node._x !== null) node._x._error = undefined;
8343
9643
  }
8344
9644
  // An ERROR the chain could not deliver to any boundary is uncaught. The
8345
9645
  // scrub above already removed it from reader-visible state, so without
@@ -8579,13 +9879,13 @@ class CollectionQueue extends Queue {
8579
9879
  return super.notify(node, type, flags, error);
8580
9880
  if (flags & this._collectionType) {
8581
9881
  this._pending = true;
8582
- const source = error?.source || node._error?.source;
9882
+ const source = error?.source || node._x?._error?.source;
8583
9883
  if (source) {
8584
9884
  const wasEmpty = this._sources.size === 0;
8585
9885
  this._sources.add(source);
8586
9886
  if (wasEmpty) setSignal(this._disabled, true);
8587
9887
  if (this._collectionType & STATUS_ERROR) {
8588
- setSignal(this._error, unwrapStatusError(source._error));
9888
+ setSignal(this._error, unwrapStatusError(source._x?._error));
8589
9889
  }
8590
9890
  }
8591
9891
  }
@@ -8600,7 +9900,7 @@ class CollectionQueue extends Queue {
8600
9900
  // sweep (finalizePureQueue after mark release) re-runs this check.
8601
9901
  if (
8602
9902
  source._flags & REACTIVE_DISPOSED ||
8603
- (!source._affectsCount &&
9903
+ (!source._x?._affectsCount &&
8604
9904
  !(source._statusFlags & this._collectionType) &&
8605
9905
  !(this._collectionType & STATUS_ERROR && source._statusFlags & STATUS_PENDING))
8606
9906
  )
@@ -8661,7 +9961,7 @@ function createCollectionBoundary(type, fn, fallback, onFn) {
8661
9961
  else throw e;
8662
9962
  }
8663
9963
  queue._pending =
8664
- pending || !!(tree._statusFlags & type) || tree._error instanceof NotReadyError;
9964
+ pending || !!(tree._statusFlags & type) || tree._x?._error instanceof NotReadyError;
8665
9965
  });
8666
9966
  const controller =
8667
9967
  _revealUsed && type === STATUS_PENDING ? getContext(RevealControllerContext) : null;