@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.
- package/dist/dev.js +1661 -361
- package/dist/node.cjs +1939 -1331
- package/dist/prod/affects.js +16 -16
- package/dist/prod/boundaries.js +90 -90
- package/dist/prod/core/action.js +3 -3
- package/dist/prod/core/async.js +74 -72
- package/dist/prod/core/constants.js +39 -1
- package/dist/prod/core/core.js +332 -224
- package/dist/prod/core/effect.js +56 -56
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +65 -54
- package/dist/prod/core/heap.js +52 -44
- package/dist/prod/core/invariants.js +3 -2
- package/dist/prod/core/lanes.js +41 -34
- package/dist/prod/core/optimistic.js +63 -60
- package/dist/prod/core/owner.js +97 -96
- package/dist/prod/core/scheduler.js +243 -194
- package/dist/prod/core/verdict.js +184 -77
- package/dist/prod/map.js +104 -104
- package/dist/prod/signals.js +1 -1
- package/dist/prod/store/next/optimistic.js +31 -23
- package/dist/prod/store/next/projection.js +3 -3
- package/dist/prod/store/next/reconcile.js +78 -74
- package/dist/prod/store/next/store.js +357 -90
- package/dist/prod/store/store.js +7 -7
- package/dist/types/core/attribution-hooks.d.ts +52 -0
- package/dist/types/core/attribution.d.ts +186 -0
- package/dist/types/core/constants.d.ts +26 -0
- package/dist/types/core/core.d.ts +8 -1
- package/dist/types/core/dev.d.ts +20 -3
- package/dist/types/core/graph.d.ts +1 -0
- package/dist/types/core/lanes.d.ts +4 -16
- package/dist/types/core/scheduler.d.ts +8 -0
- package/dist/types/core/types.d.ts +85 -41
- package/dist/types/store/next/projection.d.ts +0 -16
- package/dist/types/store/next/store.d.ts +6 -0
- package/dist/types/store/next/target.d.ts +18 -0
- package/dist/types-cjs/core/attribution-hooks.d.cts +52 -0
- package/dist/types-cjs/core/attribution.d.cts +186 -0
- package/dist/types-cjs/core/constants.d.cts +26 -0
- package/dist/types-cjs/core/core.d.cts +8 -1
- package/dist/types-cjs/core/dev.d.cts +20 -3
- package/dist/types-cjs/core/graph.d.cts +1 -0
- package/dist/types-cjs/core/lanes.d.cts +4 -16
- package/dist/types-cjs/core/scheduler.d.cts +8 -0
- package/dist/types-cjs/core/types.d.cts +85 -41
- package/dist/types-cjs/store/next/projection.d.cts +0 -16
- package/dist/types-cjs/store/next/store.d.cts +6 -0
- package/dist/types-cjs/store/next/target.d.cts +18 -0
- 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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
949
|
-
|
|
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)
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
961
|
-
assignOrMergeLane(
|
|
1650
|
+
sub._flags |= REACTIVE_OPTIMISTIC_DIRTY;
|
|
1651
|
+
assignOrMergeLane(sub, sourceLane);
|
|
962
1652
|
} else if (optimistic) {
|
|
963
|
-
|
|
1653
|
+
sub._flags |= REACTIVE_OPTIMISTIC_DIRTY;
|
|
964
1654
|
// No source lane means reversion - clear subscriber's lane so effects go to regular queue
|
|
965
|
-
|
|
1655
|
+
if (sub._x) sub._x._optimisticLane = undefined;
|
|
966
1656
|
}
|
|
967
|
-
enqueueSub(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1299
|
-
|
|
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
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1691
|
-
|
|
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
|
-
|
|
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))
|
|
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 (
|
|
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 (
|
|
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 &&
|
|
2408
|
-
|
|
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
|
-
|
|
2521
|
-
|
|
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 =
|
|
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
|
|
2613
|
-
//
|
|
2614
|
-
//
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3304
|
-
//
|
|
3305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3461
|
-
|
|
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)
|
|
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
|
-
|
|
4806
|
+
let ps = el._x?._pendingSignal;
|
|
4807
|
+
if (!ps) {
|
|
3911
4808
|
// Start false, write true if pending - ensures reversion returns to false
|
|
3912
|
-
|
|
3913
|
-
el._pendingSignal
|
|
3914
|
-
|
|
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
|
|
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
|
-
|
|
4042
|
-
|
|
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
|
-
|
|
4070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4105
|
-
el._latestValueComputed
|
|
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
|
|
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
|
-
|
|
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))
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
5879
|
-
|
|
5880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
5983
|
-
|
|
5984
|
-
const
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
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
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
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))
|
|
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
|
-
: !
|
|
7659
|
+
: !inDraft(target) && getObserver() !== null && isOwnAccessor(src, key);
|
|
6448
7660
|
if (acc) {
|
|
6449
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
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))
|
|
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 (
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
6579
|
-
//
|
|
6580
|
-
//
|
|
6581
|
-
|
|
6582
|
-
|
|
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 (
|
|
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)
|
|
8664
|
+
if (node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING)
|
|
8665
|
+
return true;
|
|
7378
8666
|
}
|
|
7379
8667
|
}
|
|
7380
|
-
if (
|
|
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.
|
|
7528
|
-
|
|
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.
|
|
7566
|
-
t.k.
|
|
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;
|