@solidjs/signals 2.0.0-rc.1 → 2.0.0-rc.3
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 +1779 -383
- package/dist/node.cjs +2058 -1359
- 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 +93 -68
- package/dist/prod/core/owner.js +97 -96
- package/dist/prod/core/scheduler.js +243 -194
- package/dist/prod/core/verdict.js +191 -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 +109 -47
- 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/invariants.d.ts +1 -1
- 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/invariants.d.cts +1 -1
- 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 +15 -15
- package/dist/types/store/optimistic.d.ts +0 -45
- package/dist/types/store/projection.d.ts +0 -70
- package/dist/types/store/reconcile.d.ts +0 -46
- package/dist/types-cjs/store/optimistic.d.cts +0 -45
- package/dist/types-cjs/store/projection.d.cts +0 -70
- package/dist/types-cjs/store/reconcile.d.cts +0 -46
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 => !/(?:^|[/\\])(?:packages[/\\])?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,11 +4503,20 @@ 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
|
-
!!(el._statusFlags & STATUS_UNINITIALIZED) ||
|
|
4510
|
+
!!(el._statusFlags & STATUS_UNINITIALIZED) ||
|
|
4511
|
+
// A dirty node's _value is stale (its queued recompute hasn't run — e.g.
|
|
4512
|
+
// a latest() shadow marked by the previous landing's companion snap), so
|
|
4513
|
+
// equality against it must not swallow the write. Without this, a sync
|
|
4514
|
+
// push returning the shadow to that stale value was dropped, the snap
|
|
4515
|
+
// recompute then committed the parent's old value, and the banner showed
|
|
4516
|
+
// the previous transition's target (#3041 follow-up).
|
|
4517
|
+
!!((el._flags ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) ||
|
|
4518
|
+
!el._equals ||
|
|
4519
|
+
!el._equals(currentValue, v);
|
|
3639
4520
|
if (!valueChanged) {
|
|
3640
4521
|
// Same-value write with an active override still entangles the current
|
|
3641
4522
|
// action's transition — the hold must outlast all overlapping actions.
|
|
@@ -3653,19 +4534,20 @@ function optimisticWrite(el, v) {
|
|
|
3653
4534
|
// Stamp ownership on the node (post-merge, so entangled writers share the
|
|
3654
4535
|
// joint root). resolveTransition prefers this over the lane's _transition,
|
|
3655
4536
|
// which a shared subscriber can merge across transactions (#2912).
|
|
3656
|
-
el._overrideOwner = activeTransition;
|
|
4537
|
+
ext(el)._overrideOwner = activeTransition;
|
|
3657
4538
|
const lane = getOrCreateLane(el);
|
|
3658
|
-
el._optimisticLane = lane;
|
|
4539
|
+
ext(el)._optimisticLane = lane;
|
|
4540
|
+
el._config |= CONFIG_HAS_LANE;
|
|
3659
4541
|
// Literal undefined must not land raw: the slot doubles as the optimistic
|
|
3660
4542
|
// brand, and erasing it makes the write invisible and routes follow-up
|
|
3661
4543
|
// writes off the optimistic path into permanent commits (#2898).
|
|
3662
|
-
el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
|
|
4544
|
+
ext(el)._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
|
|
3663
4545
|
// syncCompanions only pokes _pendingSignal/_latestValueComputed — with
|
|
3664
4546
|
// neither companion present the call is a guaranteed no-op.
|
|
3665
|
-
(el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
|
|
4547
|
+
(el._x?._pendingSignal !== undefined || el._x?._latestValueComputed !== undefined) &&
|
|
3666
4548
|
GlobalQueue._syncCompanions !== null &&
|
|
3667
4549
|
GlobalQueue._syncCompanions(el, v);
|
|
3668
|
-
el._time = clock;
|
|
4550
|
+
if (el._fn !== undefined) el._time = clock; // §12e: computed-only slot
|
|
3669
4551
|
insertSubs(el, true);
|
|
3670
4552
|
schedule();
|
|
3671
4553
|
return v;
|
|
@@ -3682,7 +4564,7 @@ function transitionBlocked(transition) {
|
|
|
3682
4564
|
hasActiveOverride$1(node) &&
|
|
3683
4565
|
"_statusFlags" in node &&
|
|
3684
4566
|
node._statusFlags & STATUS_PENDING &&
|
|
3685
|
-
node._error instanceof NotReadyError
|
|
4567
|
+
node._x?._error instanceof NotReadyError
|
|
3686
4568
|
) {
|
|
3687
4569
|
return true;
|
|
3688
4570
|
}
|
|
@@ -3696,26 +4578,26 @@ function resolveOptimisticNodes(nodes) {
|
|
|
3696
4578
|
const len = nodes.length;
|
|
3697
4579
|
for (let i = 0; i < len; i++) {
|
|
3698
4580
|
const node = nodes[i];
|
|
3699
|
-
node._optimisticLane = undefined;
|
|
4581
|
+
if (node._x !== null) node._x._optimisticLane = undefined;
|
|
3700
4582
|
// Revert is a pure drop: there is no revert target to commit —
|
|
3701
4583
|
// override-covered authoritative values hold in _pendingValue and
|
|
3702
4584
|
// elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
|
|
3703
4585
|
if (!(node._statusFlags & STATUS_PENDING)) node._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
3704
|
-
const prevOverride = node._overrideValue;
|
|
3705
|
-
node._overrideValue = NOT_PENDING;
|
|
4586
|
+
const prevOverride = node._x?._overrideValue;
|
|
4587
|
+
ext(node)._overrideValue = NOT_PENDING;
|
|
3706
4588
|
if (prevOverride !== NOT_PENDING && node._value !== unwrapOverride(prevOverride))
|
|
3707
4589
|
insertSubs(node, true);
|
|
3708
4590
|
node._transition = null;
|
|
3709
|
-
node._overrideOwner = null;
|
|
4591
|
+
if (node._x !== null) node._x._overrideOwner = null;
|
|
3710
4592
|
}
|
|
3711
4593
|
// Settlement checkpoint (#2838): companions caught in this batch (or owned
|
|
3712
4594
|
// by a node in it) re-derive from committed state, so verdicts survive the
|
|
3713
4595
|
// transition that produced them (A19 — pending is a property of the data).
|
|
3714
4596
|
for (let i = 0; i < len; i++) {
|
|
3715
4597
|
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))
|
|
4598
|
+
if (node._x?._pendingSignal || node._x?._latestValueComputed) GlobalQueue._snapCompanions(node);
|
|
4599
|
+
const owner = node._x?._parentSource;
|
|
4600
|
+
if (owner && (owner._x?._pendingSignal === node || owner._x?._latestValueComputed === node))
|
|
3719
4601
|
GlobalQueue._snapCompanions(owner);
|
|
3720
4602
|
}
|
|
3721
4603
|
nodes.splice(0, len);
|
|
@@ -3746,7 +4628,8 @@ function cleanupCompletedLanes(completingTransition) {
|
|
|
3746
4628
|
if (lane._effectQueues[0].length) runQueue(lane._effectQueues[0], EFFECT_RENDER);
|
|
3747
4629
|
if (lane._effectQueues[1].length) runQueue(lane._effectQueues[1], EFFECT_USER);
|
|
3748
4630
|
}
|
|
3749
|
-
if (lane._source._optimisticLane === lane)
|
|
4631
|
+
if (lane._source._x?._optimisticLane === lane)
|
|
4632
|
+
if (lane._source._x !== null) lane._source._x._optimisticLane = undefined;
|
|
3750
4633
|
lane._pendingAsync.clear();
|
|
3751
4634
|
lane._effectQueues[0].length = 0;
|
|
3752
4635
|
lane._effectQueues[1].length = 0;
|
|
@@ -3759,7 +4642,7 @@ function laneSuspends(owner) {
|
|
|
3759
4642
|
// Per-lane suspension: only throw if in same lane as pending async
|
|
3760
4643
|
// AND the node doesn't have an active override (overrides are the visible value,
|
|
3761
4644
|
// downstream in the lane should read the override, not throw)
|
|
3762
|
-
const pendingLane = owner._optimisticLane;
|
|
4645
|
+
const pendingLane = owner._x?._optimisticLane;
|
|
3763
4646
|
if (!pendingLane) return false;
|
|
3764
4647
|
return findLane(pendingLane) === findLane(currentOptimisticLane) && !hasActiveOverride$1(owner);
|
|
3765
4648
|
}
|
|
@@ -3786,21 +4669,36 @@ function gatedRead(el, owner, c) {
|
|
|
3786
4669
|
*/
|
|
3787
4670
|
function laneReadsCommitted(el, owner, c) {
|
|
3788
4671
|
if (
|
|
3789
|
-
el._overrideValue !== undefined ||
|
|
3790
|
-
!!el._optimisticLane ||
|
|
4672
|
+
el._x?._overrideValue !== undefined ||
|
|
4673
|
+
!!el._x?._optimisticLane ||
|
|
3791
4674
|
!!(owner._statusFlags & STATUS_PENDING)
|
|
3792
|
-
)
|
|
4675
|
+
) {
|
|
4676
|
+
// The committed view hides a staged in-flight value that will promote
|
|
4677
|
+
// silently (commitPendingNode never re-notifies). gatedRead records plain
|
|
4678
|
+
// signals for replay at commit; async memos are excluded from it by the
|
|
4679
|
+
// `_fn` check and reach here instead — a lane-assigned source whose async
|
|
4680
|
+
// already settled (laneAsyncSettled keeps _optimisticLane) served its
|
|
4681
|
+
// committed value to a reader that never re-ran after the landing, so a
|
|
4682
|
+
// pending-gated branch stayed one value behind permanently (#3041
|
|
4683
|
+
// follow-up). Record the reader under the same replay contract.
|
|
4684
|
+
if (el._pendingValue !== NOT_PENDING)
|
|
4685
|
+
(activeTransition ?? globalQueue._batch)._gatedSubs.add(c);
|
|
3793
4686
|
return true;
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
//
|
|
3797
|
-
//
|
|
3798
|
-
//
|
|
3799
|
-
// replay at commit — the same
|
|
3800
|
-
//
|
|
3801
|
-
//
|
|
3802
|
-
|
|
3803
|
-
|
|
4687
|
+
}
|
|
4688
|
+
if (owner === el && stale && c._x?._parentSource !== el) {
|
|
4689
|
+
// The committed view can hide a staged write (a lane member — even just
|
|
4690
|
+
// an isPending companion flip — puts the reader "under a lane"). The
|
|
4691
|
+
// staged value commits with no re-delivery (commitPendingNode never
|
|
4692
|
+
// re-notifies), so record the reader for replay at commit — the same
|
|
4693
|
+
// contract gatedRead provides (#2963). gatedRead itself only covers
|
|
4694
|
+
// signal reads where the reading computed differs from the source; the
|
|
4695
|
+
// owner === el memo/self read lands here instead. With a transaction
|
|
4696
|
+
// active the staged value promotes silently at ITS landing, so record
|
|
4697
|
+
// into the transaction (#3041 follow-up: a pending-gated branch that
|
|
4698
|
+
// first read its async source during the landing flush stayed one value
|
|
4699
|
+
// behind permanently); with none, into the ambient batch.
|
|
4700
|
+
if (el._pendingValue !== NOT_PENDING)
|
|
4701
|
+
(activeTransition ?? globalQueue._batch)._gatedSubs.add(c);
|
|
3804
4702
|
return true;
|
|
3805
4703
|
}
|
|
3806
4704
|
return false;
|
|
@@ -3827,10 +4725,10 @@ function recomputeLane(el, own) {
|
|
|
3827
4725
|
!globalQueue._running &&
|
|
3828
4726
|
!activeTransition &&
|
|
3829
4727
|
!lane._transition &&
|
|
3830
|
-
lane._source._parentSource !== undefined &&
|
|
3831
|
-
el._overrideValue === undefined
|
|
4728
|
+
lane._source._x?._parentSource !== undefined &&
|
|
4729
|
+
el._x?._overrideValue === undefined
|
|
3832
4730
|
) {
|
|
3833
|
-
el._optimisticLane = undefined;
|
|
4731
|
+
if (el._x !== null) el._x._optimisticLane = undefined;
|
|
3834
4732
|
return false;
|
|
3835
4733
|
}
|
|
3836
4734
|
return lane;
|
|
@@ -3853,7 +4751,8 @@ function laneAsyncPending(el) {
|
|
|
3853
4751
|
const lane = findLane(currentOptimisticLane);
|
|
3854
4752
|
if (lane._source !== el) {
|
|
3855
4753
|
lane._pendingAsync.add(el);
|
|
3856
|
-
el._optimisticLane = lane;
|
|
4754
|
+
ext(el)._optimisticLane = lane;
|
|
4755
|
+
el._config |= CONFIG_HAS_LANE;
|
|
3857
4756
|
GlobalQueue._updatePendingSignal !== null && GlobalQueue._updatePendingSignal(lane._source);
|
|
3858
4757
|
}
|
|
3859
4758
|
}
|
|
@@ -3902,18 +4801,43 @@ function installOptimisticEngine() {
|
|
|
3902
4801
|
// same lanes, so the verdict layer brings the engine with it.
|
|
3903
4802
|
installOptimisticEngine();
|
|
3904
4803
|
let pendingProbe = null;
|
|
4804
|
+
/**
|
|
4805
|
+
* Probes whose verdict was suppressed by the fresh-read pairing rule while
|
|
4806
|
+
* the held write's fate was still undecided (see recordFreshRead /
|
|
4807
|
+
* wakeSuppressedProbes): held node → the wrapper computeds that probed it.
|
|
4808
|
+
* Entries die with the hold — the commit/revert snap clears them.
|
|
4809
|
+
*/
|
|
4810
|
+
const suppressedProbes = new Map();
|
|
3905
4811
|
/**
|
|
3906
4812
|
* Get or create the pending signal for a node (lazy).
|
|
3907
4813
|
* Used by isPending() to track pending state reactively.
|
|
3908
4814
|
*/
|
|
4815
|
+
/** #3038: register a companion-carrying firewall child on its firewall's
|
|
4816
|
+
* companion set and arm the post-recompute snap (CONFIG_CHILD_COMPANIONS is
|
|
4817
|
+
* the one-load gate at the call sites). The snap then iterates exactly the
|
|
4818
|
+
* children someone asked verdicts of — O(companions) — never the full
|
|
4819
|
+
* `_child` chain, which carries one node per materialized leaf (the
|
|
4820
|
+
* O(all-leaves-ever-read)-per-update pathology). Entries are permanent like
|
|
4821
|
+
* the companions themselves; a store with no leaf-level isPending()/latest()
|
|
4822
|
+
* reads never allocates the set or pays the walk. */
|
|
4823
|
+
function markFirewallChildCompanions(el) {
|
|
4824
|
+
const fw = el._firewall;
|
|
4825
|
+
if (!fw) return;
|
|
4826
|
+
fw._config |= CONFIG_CHILD_COMPANIONS;
|
|
4827
|
+
(ext(fw)._companionChildren ??= new Set()).add(el);
|
|
4828
|
+
}
|
|
3909
4829
|
function getPendingSignal(el) {
|
|
3910
|
-
|
|
4830
|
+
let ps = el._x?._pendingSignal;
|
|
4831
|
+
if (!ps) {
|
|
3911
4832
|
// Start false, write true if pending - ensures reversion returns to false
|
|
3912
|
-
|
|
3913
|
-
el._pendingSignal
|
|
3914
|
-
|
|
4833
|
+
ps = optimisticSignal(false, { ownedWrite: true });
|
|
4834
|
+
ext(el)._pendingSignal = ps;
|
|
4835
|
+
el._config |= CONFIG_HAS_COMPANIONS;
|
|
4836
|
+
markFirewallChildCompanions(el);
|
|
4837
|
+
ext(ps)._parentSource = el;
|
|
4838
|
+
if (computePendingState(el)) setSignal(ps, true);
|
|
3915
4839
|
}
|
|
3916
|
-
return
|
|
4840
|
+
return ps;
|
|
3917
4841
|
}
|
|
3918
4842
|
function collectPendingSources(el) {
|
|
3919
4843
|
if (!pendingProbe) return;
|
|
@@ -3942,7 +4866,7 @@ function witnessAffects(node) {
|
|
|
3942
4866
|
* inherits the coverage it reports on.
|
|
3943
4867
|
*/
|
|
3944
4868
|
function markWalk(el, seen) {
|
|
3945
|
-
if (el._affectsCount) return true;
|
|
4869
|
+
if (el._x?._affectsCount) return true;
|
|
3946
4870
|
// A real error outranks an inherited mark (A16/A24c): an errored node
|
|
3947
4871
|
// answers probes with its error, not a coverage verdict, and coverage does
|
|
3948
4872
|
// not flow through it — matching the rails' behavior, where propagation
|
|
@@ -3972,11 +4896,11 @@ function markCovered(el) {
|
|
|
3972
4896
|
return activeAffectsMarks !== 0 && markWalk(el, new Set());
|
|
3973
4897
|
}
|
|
3974
4898
|
function quietPending(el) {
|
|
3975
|
-
if (el._pendingSources) {
|
|
3976
|
-
for (const source of el._pendingSources) if (!source._reask) return false;
|
|
4899
|
+
if (el._x?._pendingSources) {
|
|
4900
|
+
for (const source of el._x._pendingSources) if (!source._x?._reask) return false;
|
|
3977
4901
|
return true;
|
|
3978
4902
|
}
|
|
3979
|
-
return el._reask;
|
|
4903
|
+
return el._x?._reask ?? false;
|
|
3980
4904
|
}
|
|
3981
4905
|
// NOTE: a loadingValue node's open loading window (_loading) is verdict-quiet
|
|
3982
4906
|
// on purpose: commit #0 answers the question by declaration, so the window
|
|
@@ -4000,15 +4924,15 @@ function computePendingState(el) {
|
|
|
4000
4924
|
// so the one walk covers direct marks, derivation, and companion chains.
|
|
4001
4925
|
if (markCovered(el)) return true;
|
|
4002
4926
|
const firewall = el._firewall;
|
|
4003
|
-
if (el._parentSource) {
|
|
4004
|
-
const parentNode = el._parentSource;
|
|
4927
|
+
if (el._x?._parentSource) {
|
|
4928
|
+
const parentNode = el._x?._parentSource;
|
|
4005
4929
|
const parent = parentNode._firewall || parentNode;
|
|
4006
4930
|
return newQuestionInFlight(parent);
|
|
4007
4931
|
}
|
|
4008
4932
|
if (firewall && el._pendingValue !== NOT_PENDING && !hasActiveOverride$1(el)) {
|
|
4009
4933
|
return (
|
|
4010
4934
|
!!(firewall._flags & REACTIVE_MANUAL_WRITE) ||
|
|
4011
|
-
(!firewall._inFlight && !(firewall._statusFlags & STATUS_PENDING)) ||
|
|
4935
|
+
(!firewall._x?._inFlight && !(firewall._statusFlags & STATUS_PENDING)) ||
|
|
4012
4936
|
(!!(firewall._statusFlags & STATUS_PENDING) && quietPending(firewall))
|
|
4013
4937
|
);
|
|
4014
4938
|
}
|
|
@@ -4022,25 +4946,25 @@ function computePendingState(el) {
|
|
|
4022
4946
|
!comp._loading
|
|
4023
4947
|
) {
|
|
4024
4948
|
if (hasActiveOverride$1(el))
|
|
4025
|
-
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._overrideValue));
|
|
4949
|
+
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._x?._overrideValue));
|
|
4026
4950
|
return true;
|
|
4027
4951
|
}
|
|
4028
4952
|
return newQuestionInFlight(comp);
|
|
4029
4953
|
}
|
|
4030
4954
|
function syncCompanions(el, value) {
|
|
4031
|
-
if (el._pendingSignal) updatePendingSignal(el);
|
|
4032
|
-
if (el._latestValueComputed) setSignal(el._latestValueComputed, value);
|
|
4955
|
+
if (el._x?._pendingSignal) updatePendingSignal(el);
|
|
4956
|
+
if (el._x?._latestValueComputed) setSignal(el._x?._latestValueComputed, value);
|
|
4033
4957
|
}
|
|
4034
4958
|
function updatePendingSignal(el) {
|
|
4035
|
-
if (el._pendingSignal) {
|
|
4036
|
-
setSignal(el._pendingSignal, computePendingState(el));
|
|
4959
|
+
if (el._x?._pendingSignal) {
|
|
4960
|
+
setSignal(el._x?._pendingSignal, computePendingState(el));
|
|
4037
4961
|
}
|
|
4038
|
-
if (el._latestValueComputed) updatePendingSignal(el._latestValueComputed);
|
|
4962
|
+
if (el._x?._latestValueComputed) updatePendingSignal(el._x?._latestValueComputed);
|
|
4039
4963
|
}
|
|
4040
4964
|
function updateChildCompanions(el) {
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4965
|
+
const companions = el._x?._companionChildren;
|
|
4966
|
+
if (companions === undefined) return;
|
|
4967
|
+
for (const child of companions) updatePendingSignal(child);
|
|
4044
4968
|
}
|
|
4045
4969
|
/**
|
|
4046
4970
|
* Re-derive every verdict companion downstream of `el` (subs + firewall
|
|
@@ -4057,30 +4981,65 @@ function repollDownstreamVerdicts(el, snap = false) {
|
|
|
4057
4981
|
const visit = node => {
|
|
4058
4982
|
if (visited.has(node)) return;
|
|
4059
4983
|
visited.add(node);
|
|
4060
|
-
if (node._pendingSignal || node._latestValueComputed) update(node);
|
|
4984
|
+
if (node._x?._pendingSignal || node._x?._latestValueComputed) update(node);
|
|
4061
4985
|
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) {
|
|
4986
|
+
for (let child = node._x?._child ?? null; child !== null; child = child._nextChild) {
|
|
4063
4987
|
visit(child);
|
|
4064
4988
|
}
|
|
4065
4989
|
};
|
|
4066
4990
|
visit(el);
|
|
4067
4991
|
}
|
|
4992
|
+
/**
|
|
4993
|
+
* The correction half of the provisional fresh-read suppression (see
|
|
4994
|
+
* collectPending): fired from the sanctioned async-registration site
|
|
4995
|
+
* (GlobalQueue.notify) when a transaction gains an in-flight async blocker.
|
|
4996
|
+
* Every probe that returned "not pending" purely because it read a held
|
|
4997
|
+
* value belonging to that transaction re-runs — its re-probe now sees the
|
|
4998
|
+
* live blocker through heldAwaitingAsync and lands the true verdict. The
|
|
4999
|
+
* wake mirrors a companion write's own notification (optimistic-dirty on the
|
|
5000
|
+
* companion's lane) so the corrected verdict commits and flushes immediately
|
|
5001
|
+
* instead of being held with the transaction it reports on.
|
|
5002
|
+
*/
|
|
5003
|
+
function wakeSuppressedProbes(transition) {
|
|
5004
|
+
if (suppressedProbes.size === 0) return;
|
|
5005
|
+
let woke = false;
|
|
5006
|
+
for (const [node, probes] of suppressedProbes) {
|
|
5007
|
+
const nt = node._transition;
|
|
5008
|
+
const t = nt ? currentTransition(nt) : null;
|
|
5009
|
+
if (!t) {
|
|
5010
|
+
suppressedProbes.delete(node);
|
|
5011
|
+
continue;
|
|
5012
|
+
}
|
|
5013
|
+
if (t !== transition) continue;
|
|
5014
|
+
suppressedProbes.delete(node);
|
|
5015
|
+
const lane = node._x?._pendingSignal?._x?._optimisticLane;
|
|
5016
|
+
for (const p of probes) {
|
|
5017
|
+
if (p._flags & REACTIVE_DISPOSED) continue;
|
|
5018
|
+
p._flags |= REACTIVE_OPTIMISTIC_DIRTY;
|
|
5019
|
+
if (lane) assignOrMergeLane(p, lane);
|
|
5020
|
+
else if (p._x !== null) p._x._optimisticLane = undefined;
|
|
5021
|
+
enqueueSub(p);
|
|
5022
|
+
woke = true;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
if (woke) schedule();
|
|
5026
|
+
}
|
|
4068
5027
|
function snapCompanionsToState(owner) {
|
|
4069
|
-
|
|
4070
|
-
|
|
5028
|
+
suppressedProbes.size !== 0 && suppressedProbes.delete(owner);
|
|
5029
|
+
const sig = owner._x?._pendingSignal;
|
|
5030
|
+
if (sig && (sig._x?._overrideValue === undefined || sig._x?._overrideValue === NOT_PENDING)) {
|
|
4071
5031
|
const pending = computePendingState(owner);
|
|
4072
5032
|
if (sig._value !== pending || sig._pendingValue !== NOT_PENDING) {
|
|
4073
5033
|
sig._value = pending;
|
|
4074
5034
|
sig._pendingValue = NOT_PENDING;
|
|
4075
|
-
sig._time = clock;
|
|
4076
5035
|
insertSubs(sig);
|
|
4077
5036
|
schedule();
|
|
4078
5037
|
}
|
|
4079
5038
|
}
|
|
4080
|
-
const shadow = owner._latestValueComputed;
|
|
5039
|
+
const shadow = owner._x?._latestValueComputed;
|
|
4081
5040
|
if (shadow && !(shadow._flags & REACTIVE_DISPOSED)) {
|
|
4082
5041
|
if (
|
|
4083
|
-
(shadow._overrideValue === undefined || shadow._overrideValue === NOT_PENDING) &&
|
|
5042
|
+
(shadow._x?._overrideValue === undefined || shadow._x?._overrideValue === NOT_PENDING) &&
|
|
4084
5043
|
shadow._pendingValue === NOT_PENDING &&
|
|
4085
5044
|
!Object.is(shadow._value, owner._value) &&
|
|
4086
5045
|
!(shadow._flags & (REACTIVE_DIRTY | REACTIVE_CHECK))
|
|
@@ -4094,20 +5053,37 @@ function snapCompanionsToState(owner) {
|
|
|
4094
5053
|
}
|
|
4095
5054
|
}
|
|
4096
5055
|
function getLatestValueComputed(el) {
|
|
4097
|
-
|
|
5056
|
+
let lvc = el._x?._latestValueComputed;
|
|
5057
|
+
// A shadow disposed while unobserved (its gated reader unmounted at a
|
|
5058
|
+
// landing) is a corpse: sync writes into it equality-swallow against its
|
|
5059
|
+
// frozen _value, and a later read revives it via recompute — clearing
|
|
5060
|
+
// DISPOSED and re-deriving from the committed view, so the banner showed
|
|
5061
|
+
// the previous transition's target (#3041 follow-up). Treat it as absent;
|
|
5062
|
+
// recreation backfills from the in-flight write below.
|
|
5063
|
+
if (lvc && lvc._flags & REACTIVE_DISPOSED) lvc = undefined;
|
|
5064
|
+
if (!lvc) {
|
|
4098
5065
|
const prevPending = latestReadActive;
|
|
4099
5066
|
setLatestReadActive(false);
|
|
4100
5067
|
const prevCheck = pendingCheckActive;
|
|
4101
5068
|
setPendingCheckActive(false);
|
|
4102
5069
|
const prevContext = context;
|
|
4103
5070
|
setContextInternal(null); // Detach from owner so it isn't disposed with effects
|
|
4104
|
-
|
|
4105
|
-
el._latestValueComputed
|
|
5071
|
+
lvc = optimisticComputed(() => read(el));
|
|
5072
|
+
ext(el)._latestValueComputed = lvc;
|
|
5073
|
+
el._config |= CONFIG_HAS_COMPANIONS;
|
|
5074
|
+
markFirewallChildCompanions(el);
|
|
5075
|
+
ext(lvc)._parentSource = el; // Parent-child lane relationship
|
|
5076
|
+
// Backfill an in-flight write (mirrors getPendingSignal): the companion is
|
|
5077
|
+
// created lazily, possibly after the write was processed — syncCompanions
|
|
5078
|
+
// only pushes into companions that already exist, so the first latest()
|
|
5079
|
+
// read inside a held transition showed the committed value (#3041).
|
|
5080
|
+
if (el._pendingValue !== NOT_PENDING && !hasActiveOverride$1(el))
|
|
5081
|
+
setSignal(lvc, el._pendingValue);
|
|
4106
5082
|
setContextInternal(prevContext);
|
|
4107
5083
|
setPendingCheckActive(prevCheck);
|
|
4108
5084
|
setLatestReadActive(prevPending);
|
|
4109
5085
|
}
|
|
4110
|
-
return
|
|
5086
|
+
return lvc;
|
|
4111
5087
|
}
|
|
4112
5088
|
/** The latest()-mode read path, installed as GlobalQueue._latestRead. */
|
|
4113
5089
|
function latestRead(el) {
|
|
@@ -4115,8 +5091,8 @@ function latestRead(el) {
|
|
|
4115
5091
|
const prevPending = latestReadActive;
|
|
4116
5092
|
setLatestReadActive(false);
|
|
4117
5093
|
const visibleValue =
|
|
4118
|
-
el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING
|
|
4119
|
-
? unwrapOverride(el._overrideValue)
|
|
5094
|
+
el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING
|
|
5095
|
+
? unwrapOverride(el._x?._overrideValue)
|
|
4120
5096
|
: el._value;
|
|
4121
5097
|
let value;
|
|
4122
5098
|
try {
|
|
@@ -4142,8 +5118,8 @@ function latestRead(el) {
|
|
|
4142
5118
|
setLatestReadActive(prevPending);
|
|
4143
5119
|
}
|
|
4144
5120
|
if (pendingComputed._statusFlags & STATUS_PENDING) return visibleValue;
|
|
4145
|
-
if (stale && currentOptimisticLane && pendingComputed._optimisticLane) {
|
|
4146
|
-
const pcLane = findLane(pendingComputed._optimisticLane);
|
|
5121
|
+
if (stale && currentOptimisticLane && pendingComputed._x?._optimisticLane) {
|
|
5122
|
+
const pcLane = findLane(pendingComputed._x?._optimisticLane);
|
|
4147
5123
|
const curLane = findLane(currentOptimisticLane);
|
|
4148
5124
|
if (pcLane !== curLane && pcLane._pendingAsync.size > 0) {
|
|
4149
5125
|
return visibleValue;
|
|
@@ -4169,21 +5145,47 @@ function pendingCheckRead(el, c, owner, firewall) {
|
|
|
4169
5145
|
if (c && ownerStatus & STATUS_PENDING && ownerStatus & STATUS_UNINITIALIZED) {
|
|
4170
5146
|
if (tracking && el !== c) link(el, c);
|
|
4171
5147
|
setPendingCheckActive(true);
|
|
4172
|
-
throw owner._error;
|
|
5148
|
+
throw owner._x?._error;
|
|
4173
5149
|
}
|
|
4174
5150
|
collectPendingSources(el);
|
|
4175
5151
|
if (firewall) collectPendingSources(firewall);
|
|
4176
5152
|
setPendingCheckActive(true);
|
|
4177
5153
|
}
|
|
5154
|
+
/**
|
|
5155
|
+
* A held node whose transaction still has an async question in flight. The
|
|
5156
|
+
* probe's fresh-read pairing rule (#2831 — "a reader that sees the fresh
|
|
5157
|
+
* value must not also be told it is pending") only applies to LANDED answers
|
|
5158
|
+
* awaiting reveal; while the answer is still computing, the fresh value the
|
|
5159
|
+
* reader saw is an input, and pending remains the truth for every reader
|
|
5160
|
+
* (#3028).
|
|
5161
|
+
*/
|
|
5162
|
+
function heldAwaitingAsync(el) {
|
|
5163
|
+
const et = el._transition;
|
|
5164
|
+
const t = et ? currentTransition(et) : null;
|
|
5165
|
+
if (!t || t._done) return false;
|
|
5166
|
+
for (const [source, reporters] of t._asyncReporters) {
|
|
5167
|
+
if (
|
|
5168
|
+
reporters.size &&
|
|
5169
|
+
source._statusFlags & STATUS_PENDING &&
|
|
5170
|
+
source._x?._error?.source === source
|
|
5171
|
+
)
|
|
5172
|
+
return true;
|
|
5173
|
+
}
|
|
5174
|
+
return false;
|
|
5175
|
+
}
|
|
4178
5176
|
function recordFreshRead(el, value) {
|
|
4179
|
-
if (pendingProbe !== null && el._pendingValue !== NOT_PENDING && value === el._pendingValue)
|
|
5177
|
+
if (pendingProbe !== null && el._pendingValue !== NOT_PENDING && value === el._pendingValue) {
|
|
5178
|
+
if (heldAwaitingAsync(el)) return;
|
|
4180
5179
|
pendingProbe.freshReads.add(el);
|
|
5180
|
+
}
|
|
4181
5181
|
}
|
|
4182
5182
|
function applyReask(el, hadReask) {
|
|
4183
5183
|
const wasPending = !!(el._statusFlags & STATUS_PENDING);
|
|
4184
|
-
const isReask = hadReask && !(wasPending && !el._reask);
|
|
4185
|
-
const changed = wasPending && el._reask !== isReask;
|
|
4186
|
-
|
|
5184
|
+
const isReask = hadReask && !(wasPending && !el._x?._reask);
|
|
5185
|
+
const changed = wasPending && (el._x?._reask ?? false) !== isReask;
|
|
5186
|
+
// Allocation-free for the quiet case: false is the extension default.
|
|
5187
|
+
if (isReask) ext(el)._reask = true;
|
|
5188
|
+
else if (el._x !== null) el._x._reask = false;
|
|
4187
5189
|
return changed;
|
|
4188
5190
|
}
|
|
4189
5191
|
function latest(fn) {
|
|
@@ -4202,7 +5204,8 @@ function isPending(fn) {
|
|
|
4202
5204
|
const probe = (pendingProbe = {
|
|
4203
5205
|
found: false,
|
|
4204
5206
|
sources: new Set(),
|
|
4205
|
-
freshReads: new Set()
|
|
5207
|
+
freshReads: new Set(),
|
|
5208
|
+
suppressed: []
|
|
4206
5209
|
});
|
|
4207
5210
|
const collectPending = () => {
|
|
4208
5211
|
setPendingCheckActive(false);
|
|
@@ -4210,12 +5213,27 @@ function isPending(fn) {
|
|
|
4210
5213
|
setStrictRead(false);
|
|
4211
5214
|
try {
|
|
4212
5215
|
probe.sources.forEach(source => {
|
|
4213
|
-
if (read(getPendingSignal(source))
|
|
5216
|
+
if (read(getPendingSignal(source))) {
|
|
5217
|
+
if (!probe.freshReads.has(source)) probe.found = true;
|
|
5218
|
+
else probe.suppressed.push(source);
|
|
5219
|
+
}
|
|
4214
5220
|
});
|
|
4215
5221
|
} finally {
|
|
4216
5222
|
setStrictRead(prevStrictRead);
|
|
4217
5223
|
setPendingCheckActive(true);
|
|
4218
5224
|
}
|
|
5225
|
+
// A "not pending" verdict that exists only because this reader saw the
|
|
5226
|
+
// fresh held value is provisional: if the write turns out NOT to commit
|
|
5227
|
+
// this flush (a downstream async pends and holds it), the suppression was
|
|
5228
|
+
// wrong and the wrapper must re-ask (#3028). Remember who to wake — the
|
|
5229
|
+
// async registration (GlobalQueue.notify) triggers wakeSuppressedProbes.
|
|
5230
|
+
if (!probe.found && probe.suppressed.length && context && typeof context._fn === "function") {
|
|
5231
|
+
for (const source of probe.suppressed) {
|
|
5232
|
+
let probes = suppressedProbes.get(source);
|
|
5233
|
+
if (!probes) suppressedProbes.set(source, (probes = new Set()));
|
|
5234
|
+
probes.add(context);
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
4219
5237
|
};
|
|
4220
5238
|
try {
|
|
4221
5239
|
fn();
|
|
@@ -4247,6 +5265,7 @@ GlobalQueue._recordFresh = recordFreshRead;
|
|
|
4247
5265
|
GlobalQueue._applyReask = applyReask;
|
|
4248
5266
|
GlobalQueue._repollVerdicts = repollDownstreamVerdicts;
|
|
4249
5267
|
GlobalQueue._witnessAffects = witnessAffects;
|
|
5268
|
+
GlobalQueue._wakeSuppressedProbes = wakeSuppressedProbes;
|
|
4250
5269
|
|
|
4251
5270
|
/**
|
|
4252
5271
|
* Effects are the leaf nodes of our reactive graph. When their sources change, they are
|
|
@@ -4286,7 +5305,7 @@ function effect(compute, effect, error, options) {
|
|
|
4286
5305
|
function notifyEffectStatus(status, error) {
|
|
4287
5306
|
// Use passed values if provided, otherwise read from node
|
|
4288
5307
|
const actualStatus = status !== undefined ? status : this._statusFlags;
|
|
4289
|
-
const actualError = error !== undefined ? error : this._error;
|
|
5308
|
+
const actualError = error !== undefined ? error : this._x?._error;
|
|
4290
5309
|
if (actualStatus & STATUS_ERROR) {
|
|
4291
5310
|
this._queue.notify(this, STATUS_PENDING, 0);
|
|
4292
5311
|
if (this._type === EFFECT_USER) {
|
|
@@ -4345,7 +5364,7 @@ function runEffect(node) {
|
|
|
4345
5364
|
// notifyEffectStatus, and a runner queued by an earlier valueChanged in the
|
|
4346
5365
|
// same flush must not be hijacked by a later-arriving error status.
|
|
4347
5366
|
if (node._statusFlags & STATUS_ERROR && node._type === EFFECT_USER) {
|
|
4348
|
-
const err = unwrapStatusError(node._error);
|
|
5367
|
+
const err = unwrapStatusError(node._x?._error);
|
|
4349
5368
|
node._prevValue = node._value;
|
|
4350
5369
|
node._modified = false;
|
|
4351
5370
|
try {
|
|
@@ -4382,7 +5401,7 @@ function runEffect(node) {
|
|
|
4382
5401
|
// The final cleanup is invoked by disposeChildren at true disposal.
|
|
4383
5402
|
node._cleanup = nextCleanup;
|
|
4384
5403
|
} catch (error) {
|
|
4385
|
-
node._error = new StatusError(node, error);
|
|
5404
|
+
ext(node)._error = new StatusError(node, error);
|
|
4386
5405
|
node._statusFlags |= STATUS_ERROR;
|
|
4387
5406
|
if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
|
|
4388
5407
|
haltReactivity(error);
|
|
@@ -4433,11 +5452,11 @@ function trackedEffect(fn, options) {
|
|
|
4433
5452
|
node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
|
|
4434
5453
|
node._modified = true;
|
|
4435
5454
|
node._type = EFFECT_TRACKED;
|
|
4436
|
-
node._notifyStatus = (status, error) => {
|
|
5455
|
+
ext(node)._notifyStatus = (status, error) => {
|
|
4437
5456
|
const actualStatus = status !== undefined ? status : node._statusFlags;
|
|
4438
5457
|
if (actualStatus & STATUS_ERROR) {
|
|
4439
5458
|
node._queue.notify(node, STATUS_PENDING, 0);
|
|
4440
|
-
const err = error !== undefined ? error : node._error;
|
|
5459
|
+
const err = error !== undefined ? error : node._x?._error;
|
|
4441
5460
|
if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
|
|
4442
5461
|
haltReactivity(unwrapStatusError(err));
|
|
4443
5462
|
throw err;
|
|
@@ -5207,7 +6226,7 @@ function inheritAffectsMarks(node, raw, property) {
|
|
|
5207
6226
|
// A live scope exists, so affects.ts already installed the mark engine.
|
|
5208
6227
|
for (const [carrier, entry] of affectsScopes) {
|
|
5209
6228
|
if (
|
|
5210
|
-
carrier._affectsCount &&
|
|
6229
|
+
carrier._x?._affectsCount &&
|
|
5211
6230
|
entry.scope.has(raw) &&
|
|
5212
6231
|
(entry.key === undefined || entry.key === property)
|
|
5213
6232
|
) {
|
|
@@ -5327,7 +6346,7 @@ function witnessAffectsMark(target, property) {
|
|
|
5327
6346
|
// Callers guard on `pendingCheckActive`, which only flips inside
|
|
5328
6347
|
// isPending() — the verdict layer is loaded and its hook installed.
|
|
5329
6348
|
const own = target[STORE_NODE]?.[$AFFECTS];
|
|
5330
|
-
if (own?._affectsCount) GlobalQueue._witnessAffects(own);
|
|
6349
|
+
if (own?._x?._affectsCount) GlobalQueue._witnessAffects(own);
|
|
5331
6350
|
if (affectsScopes.size) {
|
|
5332
6351
|
// Chained backings (§7b): a wrapper's STORE_VALUE can be another store's
|
|
5333
6352
|
// proxy — marks cover by identity of the BASE raw, so resolve the chain
|
|
@@ -5336,7 +6355,7 @@ function witnessAffectsMark(target, property) {
|
|
|
5336
6355
|
for (const [carrier, entry] of affectsScopes) {
|
|
5337
6356
|
if (
|
|
5338
6357
|
carrier !== own &&
|
|
5339
|
-
carrier._affectsCount &&
|
|
6358
|
+
carrier._x?._affectsCount &&
|
|
5340
6359
|
(entry.key === undefined || entry.key === property)
|
|
5341
6360
|
) {
|
|
5342
6361
|
let r = raw;
|
|
@@ -5404,7 +6423,7 @@ function getStoreAffectsNodes(target, key) {
|
|
|
5404
6423
|
* mid-window recomputes.
|
|
5405
6424
|
*/
|
|
5406
6425
|
function markAffects(node) {
|
|
5407
|
-
node._affectsCount = (node._affectsCount || 0) + 1;
|
|
6426
|
+
ext(node)._affectsCount = (node._x?._affectsCount || 0) + 1;
|
|
5408
6427
|
shiftAffectsMarks(1);
|
|
5409
6428
|
}
|
|
5410
6429
|
/**
|
|
@@ -5420,7 +6439,7 @@ function markAffects(node) {
|
|
|
5420
6439
|
* no visual change.
|
|
5421
6440
|
*/
|
|
5422
6441
|
function notifyMarkBoundaries(node) {
|
|
5423
|
-
if (!node._subs && !node._child) return;
|
|
6442
|
+
if (!node._subs && !node._x?._child) return;
|
|
5424
6443
|
const error = new NotReadyError(node);
|
|
5425
6444
|
error._markVisual = true;
|
|
5426
6445
|
const visited = new Set();
|
|
@@ -5429,8 +6448,8 @@ function notifyMarkBoundaries(node) {
|
|
|
5429
6448
|
visited.add(sub);
|
|
5430
6449
|
// Display consumers (render effects, boundary computeds) act on the
|
|
5431
6450
|
// notification; descent stops there, exactly like the status rails.
|
|
5432
|
-
if (sub._notifyStatus) {
|
|
5433
|
-
sub._notifyStatus(STATUS_PENDING, error);
|
|
6451
|
+
if (sub._x?._notifyStatus) {
|
|
6452
|
+
sub._x._notifyStatus.call(sub, STATUS_PENDING, error);
|
|
5434
6453
|
return;
|
|
5435
6454
|
}
|
|
5436
6455
|
forEachDependent(sub, visit);
|
|
@@ -5463,8 +6482,8 @@ function registerAffectsMark(node) {
|
|
|
5463
6482
|
*/
|
|
5464
6483
|
function releaseAffectsMark(node) {
|
|
5465
6484
|
shiftAffectsMarks(-1);
|
|
5466
|
-
node._affectsCount--;
|
|
5467
|
-
if (!node._affectsCount) {
|
|
6485
|
+
node._x._affectsCount--;
|
|
6486
|
+
if (!node._x._affectsCount) {
|
|
5468
6487
|
GlobalQueue._repollVerdicts !== null && GlobalQueue._repollVerdicts(node, true);
|
|
5469
6488
|
GlobalQueue._releaseAffectsScope?.(node);
|
|
5470
6489
|
}
|
|
@@ -5558,13 +6577,46 @@ function affects(target, key) {
|
|
|
5558
6577
|
*/
|
|
5559
6578
|
// ---------------------------------------------------------------------------
|
|
5560
6579
|
// wrap / dedupe
|
|
6580
|
+
/** Pre-shaped constructor for OBJECT proxy targets: V8 tips a bare `{}` into
|
|
6581
|
+
* dictionary mode once ~19 named properties are assigned onto it (the #3044
|
|
6582
|
+
* `ovl`/`del` fields crossed that line — every trap's field read became a
|
|
6583
|
+
* hash lookup, a 15% deep-dbmon tick regression). Declaring every field in a
|
|
6584
|
+
* constructor pre-allocates in-object slots so the map stays fast, with
|
|
6585
|
+
* headroom for future fields. The prototype is reset to `Object.prototype`
|
|
6586
|
+
* so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
|
|
6587
|
+
* plain object's. Array targets keep the bare-`[]` path — they must carry
|
|
6588
|
+
* the array exotic class for `Array.isArray(proxy)`, and arrays store named
|
|
6589
|
+
* fields off-object where this cliff does not apply. */
|
|
6590
|
+
function TargetShape() {
|
|
6591
|
+
this.v = undefined;
|
|
6592
|
+
this.ch = undefined;
|
|
6593
|
+
this.pb = undefined;
|
|
6594
|
+
this.n = undefined;
|
|
6595
|
+
this.h = undefined;
|
|
6596
|
+
this.k = undefined;
|
|
6597
|
+
this.dk = undefined;
|
|
6598
|
+
this.u = undefined;
|
|
6599
|
+
this.pk = undefined;
|
|
6600
|
+
this.px = undefined;
|
|
6601
|
+
this.d = undefined;
|
|
6602
|
+
this.a = undefined;
|
|
6603
|
+
this.sc = undefined;
|
|
6604
|
+
this.nc = undefined;
|
|
6605
|
+
this.adopted = undefined;
|
|
6606
|
+
this.fam = undefined;
|
|
6607
|
+
this.s = undefined;
|
|
6608
|
+
this.ovl = undefined;
|
|
6609
|
+
this.del = undefined;
|
|
6610
|
+
this.wk = undefined;
|
|
6611
|
+
}
|
|
6612
|
+
TargetShape.prototype = Object.prototype;
|
|
5561
6613
|
function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
5562
6614
|
// The proxy target carries the array exotic class when the value is an
|
|
5563
6615
|
// array, so Array.isArray(proxy) is true; the fields live on it directly.
|
|
5564
6616
|
// Direct field assignment in one fixed order (no Object.assign literal
|
|
5565
6617
|
// copy): every target shares a hidden-class transition chain — createTarget
|
|
5566
6618
|
// was the #2 store cost in the uibench creation profile.
|
|
5567
|
-
const t = Array.isArray(value) ? [] :
|
|
6619
|
+
const t = Array.isArray(value) ? [] : new TargetShape();
|
|
5568
6620
|
t.v = value;
|
|
5569
6621
|
// Chained-backing flag (backing IS another store's proxy, §7b) — cached so
|
|
5570
6622
|
// the hot read path never does a per-read symbol lookup on the backing.
|
|
@@ -5584,6 +6636,9 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
5584
6636
|
t.adopted = false;
|
|
5585
6637
|
t.fam = fam;
|
|
5586
6638
|
t.s = false;
|
|
6639
|
+
t.ovl = false;
|
|
6640
|
+
t.del = null;
|
|
6641
|
+
t.wk = null;
|
|
5587
6642
|
t.px = new Proxy(t, traps);
|
|
5588
6643
|
// Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
|
|
5589
6644
|
// proxy off looked-up targets as a field.
|
|
@@ -5610,7 +6665,13 @@ function wrapNext(value, parent = null, parentKey = null, fam = parent?.fam ?? n
|
|
|
5610
6665
|
function unwrapValue(v) {
|
|
5611
6666
|
if (v == null || typeof v !== "object") return v;
|
|
5612
6667
|
const t = v[$TARGET];
|
|
5613
|
-
if (t !== undefined && t.px === v && t.v !== undefined)
|
|
6668
|
+
if (t !== undefined && t.px === v && t.v !== undefined) {
|
|
6669
|
+
// A draft escaping into other storage must be a REAL container that
|
|
6670
|
+
// becomes this target's committed backing at fold (the shared-raw
|
|
6671
|
+
// contract) — a prototype overlay is neither.
|
|
6672
|
+
if (t.ovl) materializePB(t);
|
|
6673
|
+
return t.pb ?? t.v;
|
|
6674
|
+
}
|
|
5614
6675
|
return v;
|
|
5615
6676
|
}
|
|
5616
6677
|
// ---------------------------------------------------------------------------
|
|
@@ -5622,13 +6683,19 @@ function getNode(target, key, current) {
|
|
|
5622
6683
|
const created = (node = signal(
|
|
5623
6684
|
current,
|
|
5624
6685
|
{
|
|
6686
|
+
// Attribution-only: name store property nodes by path segment so
|
|
6687
|
+
// attribution chains and wide-scope warnings read "store.todos", not
|
|
6688
|
+
// "signal". Gated on the engine being installed — node creation is
|
|
6689
|
+
// the hottest store path, and the disabled cost must stay one null
|
|
6690
|
+
// check (nodes created before enable() stay generically named).
|
|
6691
|
+
name: attrHooks !== null ? "store." + String(key) : undefined,
|
|
5625
6692
|
// Logical-slot equality: values resolving to the same child target
|
|
5626
6693
|
// are the same slot (privatization/adoption swap raw identity without
|
|
5627
6694
|
// changing the logical value — only changed leaves notify, R9).
|
|
5628
6695
|
equals: (a, b) => isEqual(a, b) || sameLogicalSlot(target, a, b),
|
|
5629
6696
|
unobserved() {
|
|
5630
6697
|
// A live affects() mark keeps the node addressable (sweep parity).
|
|
5631
|
-
if (created._affectsCount) return;
|
|
6698
|
+
if (created._x?._affectsCount) return;
|
|
5632
6699
|
if (target.n && target.n[key] === created) {
|
|
5633
6700
|
delete target.n[key];
|
|
5634
6701
|
target.nc--;
|
|
@@ -5655,7 +6722,10 @@ function getNode(target, key, current) {
|
|
|
5655
6722
|
created.pxv = undefined;
|
|
5656
6723
|
// Optimistic families: arm the override slot — setSignal routes armed
|
|
5657
6724
|
// nodes through the core engine (lanes, ownership, reverts all native).
|
|
5658
|
-
if (target.fam?.opt)
|
|
6725
|
+
if (target.fam?.opt) {
|
|
6726
|
+
ext(created)._overrideValue = NOT_PENDING;
|
|
6727
|
+
created._config |= CONFIG_OPTIMISTIC;
|
|
6728
|
+
}
|
|
5659
6729
|
// A node born inside a live mark's identity scope inherits the mark
|
|
5660
6730
|
// (the declaration walk could only cover nodes existing then).
|
|
5661
6731
|
if (key !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(created, target.v, key);
|
|
@@ -5680,14 +6750,17 @@ function getHasNode(target, key, present) {
|
|
|
5680
6750
|
{
|
|
5681
6751
|
equals: isEqual,
|
|
5682
6752
|
unobserved() {
|
|
5683
|
-
if (created._affectsCount) return;
|
|
6753
|
+
if (created._x?._affectsCount) return;
|
|
5684
6754
|
if (target.h && target.h[key] === created) delete target.h[key];
|
|
5685
6755
|
}
|
|
5686
6756
|
},
|
|
5687
6757
|
target.fam?.node ?? undefined
|
|
5688
6758
|
));
|
|
5689
6759
|
created._config |= CONFIG_OWNED_WRITE;
|
|
5690
|
-
if (target.fam?.opt)
|
|
6760
|
+
if (target.fam?.opt) {
|
|
6761
|
+
ext(created)._overrideValue = NOT_PENDING;
|
|
6762
|
+
created._config |= CONFIG_OPTIMISTIC;
|
|
6763
|
+
}
|
|
5691
6764
|
if (affectsScopesLive()) inheritAffectsMarks(created, target.v, key);
|
|
5692
6765
|
nodes[key] = node;
|
|
5693
6766
|
markDescendants(target);
|
|
@@ -5708,7 +6781,10 @@ function getKeySetNode(target) {
|
|
|
5708
6781
|
target.fam?.node ?? undefined
|
|
5709
6782
|
));
|
|
5710
6783
|
created._config |= CONFIG_OWNED_WRITE;
|
|
5711
|
-
if (target.fam?.opt)
|
|
6784
|
+
if (target.fam?.opt) {
|
|
6785
|
+
ext(created)._overrideValue = NOT_PENDING;
|
|
6786
|
+
created._config |= CONFIG_OPTIMISTIC;
|
|
6787
|
+
}
|
|
5712
6788
|
target.k = k;
|
|
5713
6789
|
markDescendants(target);
|
|
5714
6790
|
}
|
|
@@ -5728,7 +6804,10 @@ function getDeepNode(target) {
|
|
|
5728
6804
|
target.fam?.node ?? undefined
|
|
5729
6805
|
));
|
|
5730
6806
|
created._config |= CONFIG_OWNED_WRITE;
|
|
5731
|
-
if (target.fam?.opt)
|
|
6807
|
+
if (target.fam?.opt) {
|
|
6808
|
+
ext(created)._overrideValue = NOT_PENDING;
|
|
6809
|
+
created._config |= CONFIG_OPTIMISTIC;
|
|
6810
|
+
}
|
|
5732
6811
|
if (affectsScopesLive()) inheritAffectsMarks(created, target.v, $TRACK);
|
|
5733
6812
|
target.dk = dk;
|
|
5734
6813
|
markDescendants(target);
|
|
@@ -5771,10 +6850,62 @@ function cloneRaw(source, t) {
|
|
|
5771
6850
|
? Object.defineProperties([], descs)
|
|
5772
6851
|
: Object.create(Object.getPrototypeOf(source), descs);
|
|
5773
6852
|
}
|
|
6853
|
+
/** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
|
|
6854
|
+
* returns true when the container is plain data (overlay-safe). */
|
|
6855
|
+
function scanAccessorsOnce(target) {
|
|
6856
|
+
const src = target.v;
|
|
6857
|
+
for (const key of Reflect.ownKeys(src)) {
|
|
6858
|
+
// Own keys shadow prototype accessors, so the lookups are exact here.
|
|
6859
|
+
if (lookupGetter.call(src, key) !== undefined || lookupSetter.call(src, key) !== undefined) {
|
|
6860
|
+
target.a = true;
|
|
6861
|
+
break;
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
target.sc = true;
|
|
6865
|
+
return !target.a;
|
|
6866
|
+
}
|
|
6867
|
+
/** Downgrade a prototype-overlay pending backing to the clone path: builds
|
|
6868
|
+
* the real container (committed + overlay writes − deletes) that fold will
|
|
6869
|
+
* SWAP in as the committed backing, exactly as if the draft had started on
|
|
6870
|
+
* the clone path. Consumers that need a complete container (reconcile's
|
|
6871
|
+
* diff walks, drafts escaping into other storage) call this. */
|
|
6872
|
+
function materializePB(target) {
|
|
6873
|
+
if (!target.ovl) return;
|
|
6874
|
+
const proto = target.pb;
|
|
6875
|
+
const clone = cloneRaw(target.v, target);
|
|
6876
|
+
for (const key of Reflect.ownKeys(proto)) {
|
|
6877
|
+
const d = Object.getOwnPropertyDescriptor(proto, key);
|
|
6878
|
+
if (d.get || d.set || !d.enumerable || !d.writable || !d.configurable)
|
|
6879
|
+
Object.defineProperty(clone, key, d);
|
|
6880
|
+
else clone[key] = d.value;
|
|
6881
|
+
}
|
|
6882
|
+
if (target.del !== null) {
|
|
6883
|
+
for (const key of target.del) delete clone[key];
|
|
6884
|
+
target.del = null;
|
|
6885
|
+
}
|
|
6886
|
+
const map = target.fam?.map ?? storeNextLookup;
|
|
6887
|
+
map.delete(proto);
|
|
6888
|
+
ownedRaw.add(clone);
|
|
6889
|
+
map.set(clone, target);
|
|
6890
|
+
target.pb = clone;
|
|
6891
|
+
target.ovl = false;
|
|
6892
|
+
}
|
|
5774
6893
|
function ensurePB(target) {
|
|
5775
6894
|
let pb = target.pb;
|
|
5776
6895
|
if (pb === null) {
|
|
5777
|
-
|
|
6896
|
+
// Prototype-chain overlay (#3044): plain-data non-array containers
|
|
6897
|
+
// outside projection/optimistic families open drafts in O(1) — own keys
|
|
6898
|
+
// are the writes, reads fall through to committed. Everything else
|
|
6899
|
+
// (arrays: splice/length semantics; families: seeding/revert machinery;
|
|
6900
|
+
// accessor containers: live getters) keeps the descriptor clone.
|
|
6901
|
+
if (
|
|
6902
|
+
target.fam === null &&
|
|
6903
|
+
!Array.isArray(target.v) &&
|
|
6904
|
+
(target.sc ? !target.a : scanAccessorsOnce(target))
|
|
6905
|
+
) {
|
|
6906
|
+
pb = target.pb = Object.create(target.v);
|
|
6907
|
+
target.ovl = true;
|
|
6908
|
+
} else pb = target.pb = cloneRaw(target.v, target);
|
|
5778
6909
|
// Optimistic families: seed USER drafts from the OPTIMISTIC VIEW
|
|
5779
6910
|
// (committed + active node overrides), so follow-up writes compose on
|
|
5780
6911
|
// optimism instead of clobbering from base (#2951's compose half).
|
|
@@ -5786,14 +6917,14 @@ function ensurePB(target) {
|
|
|
5786
6917
|
if (nodes !== null) {
|
|
5787
6918
|
for (const key of Reflect.ownKeys(nodes)) {
|
|
5788
6919
|
const node = nodes[key];
|
|
5789
|
-
if (hasActiveOverride(node)) pb[key] = unwrapOverride(node._overrideValue);
|
|
6920
|
+
if (hasActiveOverride(node)) pb[key] = unwrapOverride(node._x?._overrideValue);
|
|
5790
6921
|
}
|
|
5791
6922
|
}
|
|
5792
6923
|
const has = target.h;
|
|
5793
6924
|
if (has !== null) {
|
|
5794
6925
|
for (const key of Reflect.ownKeys(has)) {
|
|
5795
6926
|
const node = has[key];
|
|
5796
|
-
if (hasActiveOverride(node) && !unwrapOverride(node._overrideValue)) delete pb[key];
|
|
6927
|
+
if (hasActiveOverride(node) && !unwrapOverride(node._x?._overrideValue)) delete pb[key];
|
|
5797
6928
|
}
|
|
5798
6929
|
}
|
|
5799
6930
|
}
|
|
@@ -5820,6 +6951,18 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
5820
6951
|
target.adopted = true;
|
|
5821
6952
|
}
|
|
5822
6953
|
target.pb = null;
|
|
6954
|
+
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
6955
|
+
// swapped container must not inherit them: a stale `ovl` beside a nulled
|
|
6956
|
+
// pb crashes materializePB (unwrapValue consults ovl before the
|
|
6957
|
+
// null-coalesce), a stale `del` would read the adoptee's keys as deleted
|
|
6958
|
+
// in the next draft, and a stale plain-data verdict (`sc`/`a`) could
|
|
6959
|
+
// admit an accessor-bearing adoptee to the overlay path. Reset; the next
|
|
6960
|
+
// draft rescans once (#3044 audit follow-up).
|
|
6961
|
+
target.ovl = false;
|
|
6962
|
+
target.del = null;
|
|
6963
|
+
target.wk = null; // adoption supersedes any staged trap writes
|
|
6964
|
+
target.sc = false;
|
|
6965
|
+
target.a = false;
|
|
5823
6966
|
target.v = incoming;
|
|
5824
6967
|
target.ch = incoming[$TARGET] !== undefined;
|
|
5825
6968
|
(target.fam?.map ?? storeNextLookup).set(incoming, target);
|
|
@@ -5863,9 +7006,17 @@ function drainFolds() {
|
|
|
5863
7006
|
const pb = t.pb;
|
|
5864
7007
|
const nodes = t.n;
|
|
5865
7008
|
if (nodes !== null) {
|
|
5866
|
-
|
|
7009
|
+
// Only written keys can hold (their nodes took the setSignal); the
|
|
7010
|
+
// wk bound keeps this O(written) — see notifyWrites. Same fallback
|
|
7011
|
+
// rules as the notify (WK_ALL / accessors / non-plain prototypes).
|
|
7012
|
+
const wkh = t.wk;
|
|
7013
|
+
const keys =
|
|
7014
|
+
wkh === null || wkh === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb)
|
|
7015
|
+
? Reflect.ownKeys(nodes)
|
|
7016
|
+
: wkh;
|
|
7017
|
+
for (const key of keys) {
|
|
5867
7018
|
const node = nodes[key];
|
|
5868
|
-
if (node._pendingValue !== NOT_PENDING) {
|
|
7019
|
+
if (node !== undefined && node._pendingValue !== NOT_PENDING) {
|
|
5869
7020
|
held = true;
|
|
5870
7021
|
break;
|
|
5871
7022
|
}
|
|
@@ -5875,9 +7026,36 @@ function drainFolds() {
|
|
|
5875
7026
|
foldOlds.set(t, old); // re-queue: commit happens when the hold settles
|
|
5876
7027
|
continue;
|
|
5877
7028
|
}
|
|
5878
|
-
t.
|
|
5879
|
-
|
|
5880
|
-
|
|
7029
|
+
if (t.ovl) {
|
|
7030
|
+
// Overlay flatten (#3044): apply this batch's writes onto an OWNED
|
|
7031
|
+
// committed backing in place — O(written), not O(container). The
|
|
7032
|
+
// backing keeps its identity, so the `t.v === old` gate below skips
|
|
7033
|
+
// path copying (the parent slot already points here) and the
|
|
7034
|
+
// adopted-notify (setter notifications happened at write time).
|
|
7035
|
+
// Unowned backings privatize first (clone once, parents re-slotted)
|
|
7036
|
+
// — the never-mutate-user-data contract holds.
|
|
7037
|
+
privatizeCommitted(t);
|
|
7038
|
+
const v = t.v;
|
|
7039
|
+
for (const key of Reflect.ownKeys(pb)) {
|
|
7040
|
+
const d = Object.getOwnPropertyDescriptor(pb, key);
|
|
7041
|
+
if (d.get || d.set || !d.enumerable || !d.writable || !d.configurable)
|
|
7042
|
+
Object.defineProperty(v, key, d);
|
|
7043
|
+
else v[key] = d.value;
|
|
7044
|
+
}
|
|
7045
|
+
if (t.del !== null) {
|
|
7046
|
+
for (const key of t.del) delete v[key];
|
|
7047
|
+
t.del = null;
|
|
7048
|
+
}
|
|
7049
|
+
(t.fam?.map ?? storeNextLookup).delete(pb);
|
|
7050
|
+
t.pb = null;
|
|
7051
|
+
t.ovl = false;
|
|
7052
|
+
t.wk = null; // written-keys window closes with the fold commit
|
|
7053
|
+
} else {
|
|
7054
|
+
t.v = pb;
|
|
7055
|
+
t.ch = false; // pb is always a plain clone
|
|
7056
|
+
t.pb = null;
|
|
7057
|
+
t.wk = null; // written-keys window closes with the fold commit
|
|
7058
|
+
}
|
|
5881
7059
|
}
|
|
5882
7060
|
if (t.v === old) continue; // adopted then re-adopted back, or no-op
|
|
5883
7061
|
// Path copying (CAS: see the eager-fold twin above).
|
|
@@ -5900,8 +7078,19 @@ function drainFolds() {
|
|
|
5900
7078
|
* "pending home = the node when a node exists"). Unobserved keys stay in the
|
|
5901
7079
|
* pending backing and fold directly at commit.
|
|
5902
7080
|
*/
|
|
7081
|
+
/** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
|
|
7082
|
+
* array length write implicitly deleted indices) — consumers full-scan. */
|
|
7083
|
+
const WK_ALL = new Set();
|
|
7084
|
+
/** Plain-prototype check for the written-keys bound: prototype getters on
|
|
7085
|
+
* class instances can derive from ANY field, so only plain-data containers
|
|
7086
|
+
* may bound the notify to written keys. Overlay pbs chain to the COMMITTED
|
|
7087
|
+
* object (#3044), so overlay plainness is judged on the committed proto. */
|
|
7088
|
+
const plainProto = o => {
|
|
7089
|
+
const p = Object.getPrototypeOf(o);
|
|
7090
|
+
return p === Object.prototype || p === Array.prototype || p === null;
|
|
7091
|
+
};
|
|
5903
7092
|
function notifyWrites(t) {
|
|
5904
|
-
|
|
7093
|
+
let pb = t.pb;
|
|
5905
7094
|
if (pb === null) return;
|
|
5906
7095
|
// Optimistic channel: user writes on an optimistic family become node-level
|
|
5907
7096
|
// engine writes (armed nodes route setSignal through optimisticWrite) — the
|
|
@@ -5931,8 +7120,11 @@ function notifyWrites(t) {
|
|
|
5931
7120
|
}
|
|
5932
7121
|
const old = t.v;
|
|
5933
7122
|
// Devtools mutation hook: full-key diff (dev-only cost) so unobserved
|
|
5934
|
-
// writes report too, matching the legacy set-trap hook.
|
|
7123
|
+
// writes report too, matching the legacy set-trap hook. Overlay backings
|
|
7124
|
+
// materialize first so the diff walks a real container.
|
|
5935
7125
|
if (DEV$1.hooks.onStoreNodeUpdate) {
|
|
7126
|
+
if (t.ovl) materializePB(t);
|
|
7127
|
+
pb = t.pb;
|
|
5936
7128
|
for (const key of Reflect.ownKeys(pb)) {
|
|
5937
7129
|
if (Array.isArray(pb) && key === "length") continue;
|
|
5938
7130
|
const ov = old[key];
|
|
@@ -5945,9 +7137,20 @@ function notifyWrites(t) {
|
|
|
5945
7137
|
}
|
|
5946
7138
|
}
|
|
5947
7139
|
const nodes = t.n;
|
|
7140
|
+
// Written-keys bound: trap writes record their keys, so the notify visits
|
|
7141
|
+
// O(written) nodes instead of every subscription on the record (a selection
|
|
7142
|
+
// map with thousands of per-key subscribers pays two visits per select,
|
|
7143
|
+
// not a full scan). Falls back to the full node scan when the bound can't
|
|
7144
|
+
// hold: no trap granularity (wk null), an array length write (WK_ALL —
|
|
7145
|
+
// implicit index deletes), accessors on the record (t.a — a getter node's
|
|
7146
|
+
// value can change when ANY key is written), or a non-plain prototype.
|
|
7147
|
+
const wk0 = t.wk;
|
|
7148
|
+
const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
|
|
5948
7149
|
if (nodes !== null) {
|
|
5949
|
-
|
|
7150
|
+
const keys = writtenKeys ?? Reflect.ownKeys(nodes);
|
|
7151
|
+
for (const key of keys) {
|
|
5950
7152
|
const node = nodes[key];
|
|
7153
|
+
if (node === undefined) continue;
|
|
5951
7154
|
// Per-key accessor handling: the node's cached flag plus ONE getter
|
|
5952
7155
|
// probe on the incoming side (getters arriving via merge/adoption).
|
|
5953
7156
|
// Setter-only props read as data (value undefined) so lookupSetter is
|
|
@@ -5968,31 +7171,48 @@ function notifyWrites(t) {
|
|
|
5968
7171
|
// projection recompute can run before the prior fold commits) — the
|
|
5969
7172
|
// node's OWN current value is the true old side, and setSignal's
|
|
5970
7173
|
// internal equality already checks exactly that.
|
|
5971
|
-
const nv = pb[key];
|
|
7174
|
+
const nv = t.del !== null && t.del.has(key) ? undefined : pb[key];
|
|
5972
7175
|
setSignal(node, () => nv);
|
|
5973
7176
|
}
|
|
5974
7177
|
}
|
|
5975
7178
|
const has = t.h;
|
|
5976
7179
|
if (has !== null) {
|
|
5977
|
-
for (const key of Reflect.ownKeys(has))
|
|
7180
|
+
for (const key of Reflect.ownKeys(has))
|
|
7181
|
+
setSignal(has[key], key in pb && !(t.del !== null && t.del.has(key)));
|
|
5978
7182
|
}
|
|
5979
7183
|
// Deep-witness (dk): setter writes must notify a deep() subscriber even on
|
|
5980
7184
|
// keys with no node. O(pb keys) equality only when a witness exists.
|
|
5981
7185
|
if (t.dk !== null) {
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
const
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
7186
|
+
if (t.del !== null && t.del.size !== 0) bumpDeep(t);
|
|
7187
|
+
else
|
|
7188
|
+
for (const key of Reflect.ownKeys(pb)) {
|
|
7189
|
+
const nv = pb[key];
|
|
7190
|
+
const ov = old[key];
|
|
7191
|
+
if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
|
|
7192
|
+
bumpDeep(t);
|
|
7193
|
+
break;
|
|
7194
|
+
}
|
|
5988
7195
|
}
|
|
5989
|
-
}
|
|
5990
7196
|
}
|
|
5991
7197
|
if (t.k !== null) {
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
7198
|
+
let changed;
|
|
7199
|
+
if (t.ovl) {
|
|
7200
|
+
// Overlay membership: only NEW own keys or deletes can change it.
|
|
7201
|
+
changed = t.del !== null && t.del.size !== 0;
|
|
7202
|
+
if (!changed) {
|
|
7203
|
+
for (const key of Reflect.ownKeys(pb)) {
|
|
7204
|
+
if (!hasOwn.call(old, key)) {
|
|
7205
|
+
changed = true;
|
|
7206
|
+
break;
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
} else {
|
|
7211
|
+
changed =
|
|
7212
|
+
Array.isArray(pb) && Array.isArray(old)
|
|
7213
|
+
? arrayStructureChanged(old, pb)
|
|
7214
|
+
: membershipChanged(old, pb);
|
|
7215
|
+
}
|
|
5996
7216
|
if (changed) setSignal(t.k, v => v + 1);
|
|
5997
7217
|
}
|
|
5998
7218
|
// Projection backing folds split by channel (two pinned contracts):
|
|
@@ -6008,6 +7228,7 @@ function notifyWrites(t) {
|
|
|
6008
7228
|
if (t.fam !== null && t.pb !== null && getWriteOverride()) {
|
|
6009
7229
|
const oldBacking = t.v;
|
|
6010
7230
|
t.pb = null;
|
|
7231
|
+
t.wk = null; // written-keys window closes with the eager fold
|
|
6011
7232
|
t.v = pb;
|
|
6012
7233
|
t.ch = false;
|
|
6013
7234
|
if (t.u && t.u.v[t.pk] === oldBacking) {
|
|
@@ -6278,7 +7499,7 @@ function runAuthoritative(fn) {
|
|
|
6278
7499
|
/** Active optimistic override on an armed node (armed slot idles at
|
|
6279
7500
|
* NOT_PENDING; undefined = unarmed plain node). */
|
|
6280
7501
|
function hasActiveOverride(node) {
|
|
6281
|
-
return node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING;
|
|
7502
|
+
return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
|
|
6282
7503
|
}
|
|
6283
7504
|
/** Context-aware node view for reads outside tracking: active override >
|
|
6284
7505
|
* held pending (owner context) > the BACKING value. Committed truth lives in
|
|
@@ -6289,7 +7510,7 @@ function hasActiveOverride(node) {
|
|
|
6289
7510
|
* keys, which are served by the trap, not the node). */
|
|
6290
7511
|
function nodeValue(node, backing) {
|
|
6291
7512
|
const v = hasActiveOverride(node)
|
|
6292
|
-
? unwrapOverride(node._overrideValue)
|
|
7513
|
+
? unwrapOverride(node._x?._overrideValue)
|
|
6293
7514
|
: node._pendingValue !== NOT_PENDING && inOwnerContext()
|
|
6294
7515
|
? node._pendingValue
|
|
6295
7516
|
: backing;
|
|
@@ -6325,7 +7546,8 @@ function serveDataKey(target, key, backingValue, src, node) {
|
|
|
6325
7546
|
// #2951). Once ensurePB runs, the seeded clone carries the view.
|
|
6326
7547
|
if (target.fam?.opt && target.pb === null) {
|
|
6327
7548
|
const node = target.n?.[key];
|
|
6328
|
-
if (node !== undefined && hasActiveOverride(node))
|
|
7549
|
+
if (node !== undefined && hasActiveOverride(node))
|
|
7550
|
+
v = unwrapOverride(node._x?._overrideValue);
|
|
6329
7551
|
}
|
|
6330
7552
|
} else {
|
|
6331
7553
|
if (node !== undefined) {
|
|
@@ -6368,6 +7590,15 @@ function serveDataKey(target, key, backingValue, src, node) {
|
|
|
6368
7590
|
* link is what wakes async-memo readers when the landing writes values;
|
|
6369
7591
|
* the firewall link rides the same read). */
|
|
6370
7592
|
function firewallGate(target) {
|
|
7593
|
+
// Own-draft ops are exempt: an async derive's continuation (generator body
|
|
7594
|
+
// after an `await`/`yield`) runs OUTSIDE the sync write scope (inDraft is
|
|
7595
|
+
// already false), but its draft-proxy traps mark every op with the write
|
|
7596
|
+
// override. Those reads are the derive working its own draft (state.push
|
|
7597
|
+
// reading .length) — gating them throws NotReadyError back into the derive
|
|
7598
|
+
// itself, which the post-await read diagnostic (#2987) then escalates to a
|
|
7599
|
+
// reactivity halt. The gate exists for EXTERNAL readers (seed invisibility,
|
|
7600
|
+
// proj R23); the derive is the author.
|
|
7601
|
+
if (projectionWriteActive || getWriteOverride()) return;
|
|
6371
7602
|
const fw = target.fam?.node;
|
|
6372
7603
|
if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
|
|
6373
7604
|
}
|
|
@@ -6399,6 +7630,12 @@ const traps = {
|
|
|
6399
7630
|
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
6400
7631
|
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
6401
7632
|
const src = readSource(target);
|
|
7633
|
+
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
7634
|
+
// deleted keys are tracked aside and read as absent in the pending view.
|
|
7635
|
+
if (target.del !== null && src === target.pb && target.del.has(key)) {
|
|
7636
|
+
if (!inDraft(target) && getObserver() !== null) read(getNode(target, key, undefined));
|
|
7637
|
+
return undefined;
|
|
7638
|
+
}
|
|
6402
7639
|
// Hot inline case: existing PLAIN node (non-accessor), unchained backing,
|
|
6403
7640
|
// tracked read of a present data key — the dbmon/uibench effect re-read
|
|
6404
7641
|
// shape. Skips serveDataKey's frame, the FORCE compare (only accessor
|
|
@@ -6439,14 +7676,21 @@ const traps = {
|
|
|
6439
7676
|
// the node's cached flag; the first TRACKED read (which creates the
|
|
6440
7677
|
// node) probes once — untracked node-less reads take the plain path,
|
|
6441
7678
|
// where a raw-receiver getter still returns correct committed values.
|
|
7679
|
+
// Tracking suppression is PER-TARGET (inDraft), never global: `writing`
|
|
7680
|
+
// counts every open setter anywhere, and a projection derive runs its
|
|
7681
|
+
// whole body inside one — a global gate silently swallowed EXTERNAL
|
|
7682
|
+
// absent-key/accessor subscriptions for every store read during any
|
|
7683
|
+
// derive, leaving nested projections permanently dependency-less when
|
|
7684
|
+
// their sources hadn't materialized yet (#3037).
|
|
6442
7685
|
const node0 = target.n?.[key];
|
|
6443
7686
|
{
|
|
6444
7687
|
const acc =
|
|
6445
7688
|
node0 !== undefined
|
|
6446
7689
|
? node0.acc === true
|
|
6447
|
-
: !
|
|
7690
|
+
: !inDraft(target) && getObserver() !== null && isOwnAccessor(src, key);
|
|
6448
7691
|
if (acc) {
|
|
6449
|
-
if (!
|
|
7692
|
+
if (!inDraft(target) && getObserver() !== null)
|
|
7693
|
+
read(node0 ?? getNode(target, key, undefined));
|
|
6450
7694
|
const v = Reflect.get(src, key, receiver);
|
|
6451
7695
|
if (target.s) return serveShallow(target, key, v);
|
|
6452
7696
|
return isWrappable(v) ? draftServe(target, wrapNext(v, target, key)) : v;
|
|
@@ -6455,19 +7699,27 @@ const traps = {
|
|
|
6455
7699
|
// Plain-data fast path: no descriptor allocation per read.
|
|
6456
7700
|
// Inherited pollution keys are never served (core R30) — checked before
|
|
6457
7701
|
// the proto-function branch can leak `constructor`. Interned-string
|
|
6458
|
-
// compares beat a Set hash on this per-read path.
|
|
7702
|
+
// compares beat a Set hash on this per-read path. Overlay pending
|
|
7703
|
+
// backings chain to the committed backing, so "own in the view" means
|
|
7704
|
+
// own on either layer (ownInView) — a genuine prototype method is one
|
|
7705
|
+
// that is own on NEITHER.
|
|
7706
|
+
const viewOvl = target.ovl && src === target.pb;
|
|
6459
7707
|
if (
|
|
6460
7708
|
(key === "constructor" || key === "__proto__" || key === "prototype") &&
|
|
6461
|
-
!hasOwn.call(src, key)
|
|
7709
|
+
!hasOwn.call(src, key) &&
|
|
7710
|
+
!(viewOvl && hasOwn.call(target.v, key))
|
|
6462
7711
|
)
|
|
6463
7712
|
return undefined;
|
|
6464
7713
|
let v = src[key];
|
|
6465
|
-
if (
|
|
7714
|
+
if (
|
|
7715
|
+
v === undefined ? !hasOwn.call(src, key) && !(viewOvl && hasOwn.call(target.v, key)) : false
|
|
7716
|
+
) {
|
|
6466
7717
|
// Inherited: prototype getters/methods run with the proxy receiver.
|
|
6467
7718
|
v = Reflect.get(src, key, receiver);
|
|
6468
7719
|
if (typeof v === "function") return v; // proto methods untracked
|
|
6469
|
-
// Reading a currently-absent own key subscribes to it (R12)
|
|
6470
|
-
|
|
7720
|
+
// Reading a currently-absent own key subscribes to it (R12) — for any
|
|
7721
|
+
// target OUTSIDE its own draft scope, even mid-setter (#3037, above).
|
|
7722
|
+
if (v === undefined && !inDraft(target)) {
|
|
6471
7723
|
if (getObserver() !== null) read(getNode(target, key, undefined));
|
|
6472
7724
|
const node = target.n?.[key];
|
|
6473
7725
|
if (node) {
|
|
@@ -6477,12 +7729,18 @@ const traps = {
|
|
|
6477
7729
|
}
|
|
6478
7730
|
} else if (v === undefined && inDraft(target) && target.fam?.opt && target.pb === null) {
|
|
6479
7731
|
const node = target.n?.[key];
|
|
6480
|
-
if (node !== undefined && hasActiveOverride(node))
|
|
7732
|
+
if (node !== undefined && hasActiveOverride(node))
|
|
7733
|
+
v = unwrapOverride(node._x?._overrideValue);
|
|
6481
7734
|
}
|
|
6482
7735
|
if (target.s) return serveShallow(target, key, v);
|
|
6483
7736
|
return isWrappable(v) ? draftServe(target, wrapNext(v, target, key)) : v;
|
|
6484
7737
|
}
|
|
6485
|
-
if (
|
|
7738
|
+
if (
|
|
7739
|
+
typeof v === "function" &&
|
|
7740
|
+
!hasOwn.call(src, key) &&
|
|
7741
|
+
!(viewOvl && hasOwn.call(target.v, key))
|
|
7742
|
+
)
|
|
7743
|
+
return v; // proto method
|
|
6486
7744
|
return serveDataKey(target, key, v, src, node0);
|
|
6487
7745
|
},
|
|
6488
7746
|
has(target, key) {
|
|
@@ -6491,6 +7749,8 @@ const traps = {
|
|
|
6491
7749
|
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
6492
7750
|
const src = readSource(target);
|
|
6493
7751
|
let present = key in src;
|
|
7752
|
+
// Overlay deletes read as absent in the pending view (#3044).
|
|
7753
|
+
if (present && target.del !== null && src === target.pb && target.del.has(key)) present = false;
|
|
6494
7754
|
if (!inDraft(target)) {
|
|
6495
7755
|
if (getObserver() !== null) {
|
|
6496
7756
|
const node = getHasNode(target, key, present);
|
|
@@ -6499,12 +7759,12 @@ const traps = {
|
|
|
6499
7759
|
} else {
|
|
6500
7760
|
const node = target.h?.[key];
|
|
6501
7761
|
if (node !== undefined && hasActiveOverride(node))
|
|
6502
|
-
present = !!unwrapOverride(node._overrideValue);
|
|
7762
|
+
present = !!unwrapOverride(node._x?._overrideValue);
|
|
6503
7763
|
}
|
|
6504
7764
|
} else if (target.fam?.opt && target.pb === null) {
|
|
6505
7765
|
const node = target.h?.[key];
|
|
6506
7766
|
if (node !== undefined && hasActiveOverride(node))
|
|
6507
|
-
present = !!unwrapOverride(node._overrideValue);
|
|
7767
|
+
present = !!unwrapOverride(node._x?._overrideValue);
|
|
6508
7768
|
}
|
|
6509
7769
|
return present;
|
|
6510
7770
|
},
|
|
@@ -6512,7 +7772,18 @@ const traps = {
|
|
|
6512
7772
|
if (pendingCheckActive) witnessAffectsMark(target);
|
|
6513
7773
|
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
6514
7774
|
if (!inDraft(target) && getObserver() !== null) read(getKeySetNode(target));
|
|
6515
|
-
const
|
|
7775
|
+
const src = readSource(target);
|
|
7776
|
+
let keys;
|
|
7777
|
+
if (target.ovl && src === target.pb) {
|
|
7778
|
+
// Overlay merge (#3044): committed keys in their order, then this
|
|
7779
|
+
// batch's NEW keys, minus deletes.
|
|
7780
|
+
keys = Reflect.ownKeys(target.v);
|
|
7781
|
+
const del = target.del;
|
|
7782
|
+
if (del !== null && del.size !== 0) keys = keys.filter(key => !del.has(key));
|
|
7783
|
+
for (const key of Reflect.ownKeys(src)) {
|
|
7784
|
+
if (!hasOwn.call(target.v, key)) keys.push(key);
|
|
7785
|
+
}
|
|
7786
|
+
} else keys = Reflect.ownKeys(src);
|
|
6516
7787
|
// Optimistic membership overlay: presence-node overrides add/remove keys
|
|
6517
7788
|
// (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
|
|
6518
7789
|
// Draft reads before the first write overlay too (pb, once created, is
|
|
@@ -6523,7 +7794,7 @@ const traps = {
|
|
|
6523
7794
|
const node = target.h[key];
|
|
6524
7795
|
if (!hasActiveOverride(node)) continue;
|
|
6525
7796
|
set ??= new Set(keys);
|
|
6526
|
-
if (unwrapOverride(node._overrideValue)) set.add(key);
|
|
7797
|
+
if (unwrapOverride(node._x?._overrideValue)) set.add(key);
|
|
6527
7798
|
else set.delete(key);
|
|
6528
7799
|
}
|
|
6529
7800
|
if (set !== null) return [...set];
|
|
@@ -6531,11 +7802,18 @@ const traps = {
|
|
|
6531
7802
|
return keys;
|
|
6532
7803
|
},
|
|
6533
7804
|
getOwnPropertyDescriptor(target, key) {
|
|
6534
|
-
const
|
|
7805
|
+
const srcD = readSource(target);
|
|
7806
|
+
let desc = Object.getOwnPropertyDescriptor(srcD, key);
|
|
7807
|
+
// Overlay (#3044): unwritten keys live on the committed backing;
|
|
7808
|
+
// deleted keys are absent from the pending view.
|
|
7809
|
+
if (target.ovl && srcD === target.pb) {
|
|
7810
|
+
if (target.del !== null && target.del.has(key)) return undefined;
|
|
7811
|
+
if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
|
|
7812
|
+
}
|
|
6535
7813
|
if (target.fam?.opt && !inDraft(target)) {
|
|
6536
7814
|
const node = target.h?.[key];
|
|
6537
7815
|
if (node !== undefined && hasActiveOverride(node)) {
|
|
6538
|
-
if (!unwrapOverride(node._overrideValue)) return undefined; // opt delete
|
|
7816
|
+
if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
|
|
6539
7817
|
if (desc === undefined) {
|
|
6540
7818
|
const vn = target.n?.[key];
|
|
6541
7819
|
return {
|
|
@@ -6562,24 +7840,50 @@ const traps = {
|
|
|
6562
7840
|
const override = !draft && getWriteOverride();
|
|
6563
7841
|
if (!draft && !override) return true;
|
|
6564
7842
|
if (key === "__proto__") return true; // pollution guard (core R30)
|
|
7843
|
+
// Unwrap BEFORE ensurePB: unwrapValue materializes a self-referencing
|
|
7844
|
+
// draft's overlay (replacing target.pb), so a pb local captured earlier
|
|
7845
|
+
// would be the abandoned overlay and the write would vanish.
|
|
7846
|
+
// Shallow slots store what was written VERBATIM — another store's proxy
|
|
7847
|
+
// passes through by reference (#2932; markRawOne skips proxies), while
|
|
7848
|
+
// deep stores unwrap to raw backings.
|
|
7849
|
+
const uv = target.s ? value : unwrapValue(value);
|
|
6565
7850
|
const pb = ensurePB(target);
|
|
6566
7851
|
pendingNotify.add(target);
|
|
7852
|
+
// Array length writes implicitly delete indices — the written-keys bound
|
|
7853
|
+
// can't see them, so poison to the full scan for this batch. Index
|
|
7854
|
+
// writes implicitly GROW length, so arrays always record it alongside.
|
|
7855
|
+
if (Array.isArray(pb)) {
|
|
7856
|
+
if (key === "length") target.wk = WK_ALL;
|
|
7857
|
+
else if (target.wk !== WK_ALL) {
|
|
7858
|
+
const wk = (target.wk ??= new Set());
|
|
7859
|
+
wk.add(key);
|
|
7860
|
+
wk.add("length");
|
|
7861
|
+
}
|
|
7862
|
+
} else if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
|
|
6567
7863
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
6568
7864
|
// defineProperty sidesteps a proto-chain setter named the same.
|
|
6569
7865
|
if (UNSAFE_KEYS.has(key)) {
|
|
6570
7866
|
Object.defineProperty(pb, key, {
|
|
6571
|
-
value:
|
|
7867
|
+
value: uv,
|
|
6572
7868
|
writable: true,
|
|
6573
7869
|
enumerable: true,
|
|
6574
7870
|
configurable: true
|
|
6575
7871
|
});
|
|
7872
|
+
if (target.del !== null) target.del.delete(key);
|
|
6576
7873
|
return true;
|
|
6577
7874
|
}
|
|
6578
|
-
//
|
|
6579
|
-
//
|
|
6580
|
-
//
|
|
6581
|
-
|
|
6582
|
-
|
|
7875
|
+
// Overlay first-write DEFINES the own key: assignment through the proto
|
|
7876
|
+
// chain would reject on a non-writable committed property (the clone
|
|
7877
|
+
// path normalized descriptors for exactly this — R51 parity).
|
|
7878
|
+
if (target.ovl && !hasOwn.call(pb, key)) {
|
|
7879
|
+
Object.defineProperty(pb, key, {
|
|
7880
|
+
value: uv,
|
|
7881
|
+
writable: true,
|
|
7882
|
+
enumerable: true,
|
|
7883
|
+
configurable: true
|
|
7884
|
+
});
|
|
7885
|
+
} else pb[key] = uv;
|
|
7886
|
+
if (target.del !== null) target.del.delete(key);
|
|
6583
7887
|
// Shallow ingest: written records are sticky raw-marked (one entity is
|
|
6584
7888
|
// never both deep-wrapped and raw — R41/#2932, shared invariant).
|
|
6585
7889
|
if (target.s && uv !== null && typeof uv === "object") markRawOne(uv);
|
|
@@ -6594,10 +7898,13 @@ const traps = {
|
|
|
6594
7898
|
if (!draft && !override) return true;
|
|
6595
7899
|
if (key === "__proto__") return true;
|
|
6596
7900
|
if (desc.get || desc.set) target.a = true;
|
|
7901
|
+
// Unwrap before ensurePB (see the set trap: self-reference materializes).
|
|
7902
|
+
if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
|
|
6597
7903
|
const pb = ensurePB(target);
|
|
6598
7904
|
pendingNotify.add(target);
|
|
6599
|
-
if (
|
|
7905
|
+
if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
|
|
6600
7906
|
Object.defineProperty(pb, key, desc);
|
|
7907
|
+
if (target.del !== null) target.del.delete(key);
|
|
6601
7908
|
if (override) notifyWrites(target);
|
|
6602
7909
|
return true;
|
|
6603
7910
|
},
|
|
@@ -6607,7 +7914,11 @@ const traps = {
|
|
|
6607
7914
|
if (!draft && !override) return true;
|
|
6608
7915
|
const pb = ensurePB(target);
|
|
6609
7916
|
pendingNotify.add(target);
|
|
7917
|
+
if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
|
|
6610
7918
|
delete pb[key];
|
|
7919
|
+
// A prototype overlay cannot shadow a delete of a committed key —
|
|
7920
|
+
// record it aside (#3044); reads/has/ownKeys/commit consult the set.
|
|
7921
|
+
if (target.ovl && hasOwn.call(target.v, key)) (target.del ??= new Set()).add(key);
|
|
6611
7922
|
if (override) notifyWrites(target);
|
|
6612
7923
|
return true;
|
|
6613
7924
|
}
|
|
@@ -6749,6 +8060,9 @@ function snapshotWalk(value, seen, fam) {
|
|
|
6749
8060
|
if (t === undefined) break;
|
|
6750
8061
|
if (t.fam !== null) fam = t.fam;
|
|
6751
8062
|
if (t.fam?.opt === true) (optOwners ??= []).push(t);
|
|
8063
|
+
// Snapshot runs mid-flush (tracked memos execute before commit), so a
|
|
8064
|
+
// pending prototype overlay must present as a REAL merged container.
|
|
8065
|
+
if (t.ovl) materializePB(t);
|
|
6752
8066
|
const backing = t.pb ?? t.v;
|
|
6753
8067
|
if (backing === src) break;
|
|
6754
8068
|
src = backing;
|
|
@@ -6848,6 +8162,10 @@ function reconcileNextState(value, state, key, replace = false) {
|
|
|
6848
8162
|
if (state == null) throw new Error("Cannot reconcile null or undefined state");
|
|
6849
8163
|
const t = state?.[$TARGET];
|
|
6850
8164
|
if (t === undefined || t.px !== state) throw new Error("reconcile target is not a store proxy");
|
|
8165
|
+
// Reconcile's diff walks need a REAL pending container — a prototype
|
|
8166
|
+
// overlay (#3044) materializes to the clone path first (edge: reconcile
|
|
8167
|
+
// inside a setter that already wrote this target).
|
|
8168
|
+
if (t.ovl) materializePB(t);
|
|
6851
8169
|
let keyFn = key === null ? null : typeof key === "string" ? item => item?.[key] : key;
|
|
6852
8170
|
// §7b chained backing: a projection derive returning a LIVE store proxy
|
|
6853
8171
|
// adopts the proxy itself as the backing — reads flow through the inner
|
|
@@ -7176,10 +8494,29 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
7176
8494
|
* primitives; the generic draft write-traps are reused from the legacy
|
|
7177
8495
|
* module unchanged.
|
|
7178
8496
|
*/
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
8497
|
+
/**
|
|
8498
|
+
* Wrap a store proxy as a projection DRAFT: every operation carries the write
|
|
8499
|
+
* override (the derive is the author — its ops must not hit the §6c firewall
|
|
8500
|
+
* gate, even in a continuation after an `await`/`yield` where the sync write
|
|
8501
|
+
* scope has closed).
|
|
8502
|
+
*
|
|
8503
|
+
* FAKE TARGET, not the store proxy itself (#3060): after a proxy trap
|
|
8504
|
+
* returns, the engine runs spec invariant validation against the proxy's
|
|
8505
|
+
* TARGET — [[OwnPropertyKeys]] after ownKeys, [[GetOwnProperty]] after
|
|
8506
|
+
* set/getOwnPropertyDescriptor/defineProperty. With the store proxy as
|
|
8507
|
+
* target those checks re-enter the store's traps OUTSIDE the override
|
|
8508
|
+
* bracket (the trap's finally has already run), so `Object.keys(state)` in
|
|
8509
|
+
* a derive continuation fired the firewall gate and re-threw the
|
|
8510
|
+
* projection's own pending NotReadyError into the derive. A dummy of
|
|
8511
|
+
* matching kind (array/object, same trick as the store's own TargetShape)
|
|
8512
|
+
* keeps invariant validation away from the store entirely; the traps
|
|
8513
|
+
* forward to the closed-over inner proxy inside the bracket.
|
|
8514
|
+
*
|
|
8515
|
+
* Save/restore projectionWriteActive, never hard-reset: the draft can be
|
|
8516
|
+
* driven from inside an enclosing authoritative-write scope (next-store
|
|
8517
|
+
* optimistic derives), and a hard `false` would clobber it mid-derive.
|
|
8518
|
+
*/
|
|
8519
|
+
function wrapDraft(inner, isActive, onDraftWrite) {
|
|
7183
8520
|
const traps = {
|
|
7184
8521
|
get(_, prop) {
|
|
7185
8522
|
let value;
|
|
@@ -7187,13 +8524,15 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
7187
8524
|
setWriteOverride(true);
|
|
7188
8525
|
setProjectionWriteActive(true);
|
|
7189
8526
|
try {
|
|
7190
|
-
value =
|
|
8527
|
+
value = inner[prop];
|
|
7191
8528
|
} finally {
|
|
7192
8529
|
setWriteOverride(false);
|
|
7193
8530
|
setProjectionWriteActive(was);
|
|
7194
8531
|
}
|
|
7195
8532
|
if (prop === $TARGET) return value;
|
|
7196
|
-
return typeof value === "object" && value !== null
|
|
8533
|
+
return typeof value === "object" && value !== null
|
|
8534
|
+
? wrapDraft(value, isActive, onDraftWrite)
|
|
8535
|
+
: value;
|
|
7197
8536
|
},
|
|
7198
8537
|
has(_, prop) {
|
|
7199
8538
|
let value;
|
|
@@ -7201,7 +8540,7 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
7201
8540
|
setWriteOverride(true);
|
|
7202
8541
|
setProjectionWriteActive(true);
|
|
7203
8542
|
try {
|
|
7204
|
-
value = prop in
|
|
8543
|
+
value = prop in inner;
|
|
7205
8544
|
} finally {
|
|
7206
8545
|
setWriteOverride(false);
|
|
7207
8546
|
setProjectionWriteActive(was);
|
|
@@ -7214,7 +8553,7 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
7214
8553
|
setWriteOverride(true);
|
|
7215
8554
|
setProjectionWriteActive(true);
|
|
7216
8555
|
try {
|
|
7217
|
-
|
|
8556
|
+
inner[prop] = value;
|
|
7218
8557
|
onDraftWrite?.();
|
|
7219
8558
|
} finally {
|
|
7220
8559
|
setWriteOverride(false);
|
|
@@ -7228,7 +8567,49 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
7228
8567
|
setWriteOverride(true);
|
|
7229
8568
|
setProjectionWriteActive(true);
|
|
7230
8569
|
try {
|
|
7231
|
-
delete
|
|
8570
|
+
delete inner[prop];
|
|
8571
|
+
onDraftWrite?.();
|
|
8572
|
+
} finally {
|
|
8573
|
+
setWriteOverride(false);
|
|
8574
|
+
setProjectionWriteActive(was);
|
|
8575
|
+
}
|
|
8576
|
+
return true;
|
|
8577
|
+
},
|
|
8578
|
+
ownKeys() {
|
|
8579
|
+
const was = projectionWriteActive;
|
|
8580
|
+
setWriteOverride(true);
|
|
8581
|
+
setProjectionWriteActive(true);
|
|
8582
|
+
try {
|
|
8583
|
+
return Reflect.ownKeys(inner);
|
|
8584
|
+
} finally {
|
|
8585
|
+
setWriteOverride(false);
|
|
8586
|
+
setProjectionWriteActive(was);
|
|
8587
|
+
}
|
|
8588
|
+
},
|
|
8589
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
8590
|
+
let d;
|
|
8591
|
+
const was = projectionWriteActive;
|
|
8592
|
+
setWriteOverride(true);
|
|
8593
|
+
setProjectionWriteActive(true);
|
|
8594
|
+
try {
|
|
8595
|
+
d = Reflect.getOwnPropertyDescriptor(inner, prop);
|
|
8596
|
+
} finally {
|
|
8597
|
+
setWriteOverride(false);
|
|
8598
|
+
setProjectionWriteActive(was);
|
|
8599
|
+
}
|
|
8600
|
+
// The dummy target doesn't hold the key, so a non-configurable report
|
|
8601
|
+
// would violate the proxy invariant. Store descriptors are already
|
|
8602
|
+
// normalized configurable; enforce it for raw leaves too.
|
|
8603
|
+
if (d) d.configurable = true;
|
|
8604
|
+
return d;
|
|
8605
|
+
},
|
|
8606
|
+
defineProperty(_, prop, desc) {
|
|
8607
|
+
if (isActive && !isActive()) return true;
|
|
8608
|
+
const was = projectionWriteActive;
|
|
8609
|
+
setWriteOverride(true);
|
|
8610
|
+
setProjectionWriteActive(true);
|
|
8611
|
+
try {
|
|
8612
|
+
Reflect.defineProperty(inner, prop, desc);
|
|
7232
8613
|
onDraftWrite?.();
|
|
7233
8614
|
} finally {
|
|
7234
8615
|
setWriteOverride(false);
|
|
@@ -7237,7 +8618,8 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
7237
8618
|
return true;
|
|
7238
8619
|
}
|
|
7239
8620
|
};
|
|
7240
|
-
|
|
8621
|
+
// Matching-kind dummy so Array.isArray(draft) answers like the store.
|
|
8622
|
+
return new Proxy(Array.isArray(inner) ? [] : {}, traps);
|
|
7241
8623
|
}
|
|
7242
8624
|
function createProjectionNextInternal(fn, seed, options) {
|
|
7243
8625
|
const fam = {
|
|
@@ -7291,9 +8673,10 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
7291
8673
|
const shadow = owner._loading
|
|
7292
8674
|
? JSON.parse(JSON.stringify(wrappedStore[$TARGET][STORE_VALUE]))
|
|
7293
8675
|
: null;
|
|
7294
|
-
const draft =
|
|
8676
|
+
const draft = wrapDraft(
|
|
7295
8677
|
wrappedStore,
|
|
7296
|
-
|
|
8678
|
+
() => !settled || owner._x?._inFlight === result,
|
|
8679
|
+
onDraftWrite
|
|
7297
8680
|
);
|
|
7298
8681
|
storeSetterNext(
|
|
7299
8682
|
draft,
|
|
@@ -7374,10 +8757,15 @@ function familyHasLiveOverrides(fam) {
|
|
|
7374
8757
|
if (bucket === null) continue;
|
|
7375
8758
|
for (const key of Reflect.ownKeys(bucket)) {
|
|
7376
8759
|
const node = bucket[key];
|
|
7377
|
-
if (node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING)
|
|
8760
|
+
if (node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING)
|
|
8761
|
+
return true;
|
|
7378
8762
|
}
|
|
7379
8763
|
}
|
|
7380
|
-
if (
|
|
8764
|
+
if (
|
|
8765
|
+
t.k !== null &&
|
|
8766
|
+
t.k._x?._overrideValue !== undefined &&
|
|
8767
|
+
t.k._x?._overrideValue !== NOT_PENDING
|
|
8768
|
+
)
|
|
7381
8769
|
return true;
|
|
7382
8770
|
}
|
|
7383
8771
|
overlaid.clear(); // nothing live — drop the bookkeeping
|
|
@@ -7451,13 +8839,13 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
7451
8839
|
const visible = (key, fallback) => {
|
|
7452
8840
|
const node = t.n?.[key];
|
|
7453
8841
|
return node !== undefined && hasActiveOverride(node)
|
|
7454
|
-
? unwrapOverride(node._overrideValue)
|
|
8842
|
+
? unwrapOverride(node._x?._overrideValue)
|
|
7455
8843
|
: fallback;
|
|
7456
8844
|
};
|
|
7457
8845
|
const visiblePresent = key => {
|
|
7458
8846
|
const node = t.h?.[key];
|
|
7459
8847
|
return node !== undefined && hasActiveOverride(node)
|
|
7460
|
-
? !!unwrapOverride(node._overrideValue)
|
|
8848
|
+
? !!unwrapOverride(node._x?._overrideValue)
|
|
7461
8849
|
: key in old;
|
|
7462
8850
|
};
|
|
7463
8851
|
let structural = false;
|
|
@@ -7518,14 +8906,18 @@ function consumeOverridesNext(fam) {
|
|
|
7518
8906
|
for (const t of overlaid) {
|
|
7519
8907
|
const drop = (node, committed) => {
|
|
7520
8908
|
if (!hasActiveOverride(node)) return;
|
|
7521
|
-
const prev = unwrapOverride(node._overrideValue);
|
|
8909
|
+
const prev = unwrapOverride(node._x?._overrideValue);
|
|
7522
8910
|
// Full legacy reset (clearOptimisticOverride parity): the landing is
|
|
7523
8911
|
// authoritative NOW — fold committed into the node directly instead
|
|
7524
8912
|
// of riding a transaction's commit (whose queues may be stashed with
|
|
7525
8913
|
// the transaction parked; the wake would strand until it settles).
|
|
7526
|
-
node._overrideValue = NOT_PENDING;
|
|
7527
|
-
node.
|
|
7528
|
-
|
|
8914
|
+
ext(node)._overrideValue = NOT_PENDING;
|
|
8915
|
+
node._config |= CONFIG_OPTIMISTIC;
|
|
8916
|
+
const nx = node._x;
|
|
8917
|
+
if (nx) {
|
|
8918
|
+
nx._overrideOwner = null;
|
|
8919
|
+
nx._optimisticLane = undefined;
|
|
8920
|
+
}
|
|
7529
8921
|
node._pendingValue = NOT_PENDING;
|
|
7530
8922
|
node._value = committed;
|
|
7531
8923
|
if (!node._equals || !node._equals(prev, committed)) {
|
|
@@ -7561,9 +8953,13 @@ function consumeOverridesNext(fam) {
|
|
|
7561
8953
|
for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
|
|
7562
8954
|
}
|
|
7563
8955
|
if (t.k !== null && hasActiveOverride(t.k)) {
|
|
7564
|
-
t.k._overrideValue = NOT_PENDING;
|
|
7565
|
-
t.k.
|
|
7566
|
-
t.k.
|
|
8956
|
+
ext(t.k)._overrideValue = NOT_PENDING;
|
|
8957
|
+
t.k._config |= CONFIG_OPTIMISTIC;
|
|
8958
|
+
const kx = t.k._x;
|
|
8959
|
+
if (kx) {
|
|
8960
|
+
kx._overrideOwner = null;
|
|
8961
|
+
kx._optimisticLane = undefined;
|
|
8962
|
+
}
|
|
7567
8963
|
insertSubs(t.k, true);
|
|
7568
8964
|
schedule();
|
|
7569
8965
|
}
|
|
@@ -7583,7 +8979,7 @@ function optimisticView(t, src) {
|
|
|
7583
8979
|
for (const key of Reflect.ownKeys(nodes)) {
|
|
7584
8980
|
const node = nodes[key];
|
|
7585
8981
|
if (!hasActiveOverride(node)) continue;
|
|
7586
|
-
const ov = unwrapOverride(node._overrideValue);
|
|
8982
|
+
const ov = unwrapOverride(node._x?._overrideValue);
|
|
7587
8983
|
if (key === "length" && Array.isArray(src)) {
|
|
7588
8984
|
if (src.length !== ov) ensure().length = ov;
|
|
7589
8985
|
} else if (!isEqual(src[key], ov)) ensure()[key] = ov;
|
|
@@ -7594,7 +8990,7 @@ function optimisticView(t, src) {
|
|
|
7594
8990
|
for (const key of Reflect.ownKeys(has)) {
|
|
7595
8991
|
const node = has[key];
|
|
7596
8992
|
if (!hasActiveOverride(node)) continue;
|
|
7597
|
-
const present = !!unwrapOverride(node._overrideValue);
|
|
8993
|
+
const present = !!unwrapOverride(node._x?._overrideValue);
|
|
7598
8994
|
if (!present && key in (out ?? src)) delete ensure()[key];
|
|
7599
8995
|
}
|
|
7600
8996
|
}
|
|
@@ -8322,10 +9718,10 @@ function compare(key, a, b) {
|
|
|
8322
9718
|
|
|
8323
9719
|
function boundaryComputed(fn, propagationMask) {
|
|
8324
9720
|
const node = computed(fn, { lazy: true });
|
|
8325
|
-
node._notifyStatus = (status, error) => {
|
|
9721
|
+
ext(node)._notifyStatus = (status, error) => {
|
|
8326
9722
|
// Use passed values if provided, otherwise read from node
|
|
8327
9723
|
const flags = status !== undefined ? status : node._statusFlags;
|
|
8328
|
-
const actualError = error !== undefined ? error : node._error;
|
|
9724
|
+
const actualError = error !== undefined ? error : node._x?._error;
|
|
8329
9725
|
// Notify both status dimensions like a render effect does; the queue chain
|
|
8330
9726
|
// consumes this boundary's own type and forwards the remainder upward until
|
|
8331
9727
|
// a boundary that handles it is found.
|
|
@@ -8338,8 +9734,8 @@ function boundaryComputed(fn, propagationMask) {
|
|
|
8338
9734
|
const foreign = flags & ~node._propagationMask & (STATUS_PENDING | STATUS_ERROR);
|
|
8339
9735
|
if (foreign) {
|
|
8340
9736
|
node._statusFlags &= ~foreign;
|
|
8341
|
-
if (node._error === actualError && !(node._statusFlags & (STATUS_PENDING | STATUS_ERROR)))
|
|
8342
|
-
node._error = undefined;
|
|
9737
|
+
if (node._x?._error === actualError && !(node._statusFlags & (STATUS_PENDING | STATUS_ERROR)))
|
|
9738
|
+
if (node._x !== null) node._x._error = undefined;
|
|
8343
9739
|
}
|
|
8344
9740
|
// An ERROR the chain could not deliver to any boundary is uncaught. The
|
|
8345
9741
|
// scrub above already removed it from reader-visible state, so without
|
|
@@ -8579,13 +9975,13 @@ class CollectionQueue extends Queue {
|
|
|
8579
9975
|
return super.notify(node, type, flags, error);
|
|
8580
9976
|
if (flags & this._collectionType) {
|
|
8581
9977
|
this._pending = true;
|
|
8582
|
-
const source = error?.source || node._error?.source;
|
|
9978
|
+
const source = error?.source || node._x?._error?.source;
|
|
8583
9979
|
if (source) {
|
|
8584
9980
|
const wasEmpty = this._sources.size === 0;
|
|
8585
9981
|
this._sources.add(source);
|
|
8586
9982
|
if (wasEmpty) setSignal(this._disabled, true);
|
|
8587
9983
|
if (this._collectionType & STATUS_ERROR) {
|
|
8588
|
-
setSignal(this._error, unwrapStatusError(source._error));
|
|
9984
|
+
setSignal(this._error, unwrapStatusError(source._x?._error));
|
|
8589
9985
|
}
|
|
8590
9986
|
}
|
|
8591
9987
|
}
|
|
@@ -8600,7 +9996,7 @@ class CollectionQueue extends Queue {
|
|
|
8600
9996
|
// sweep (finalizePureQueue after mark release) re-runs this check.
|
|
8601
9997
|
if (
|
|
8602
9998
|
source._flags & REACTIVE_DISPOSED ||
|
|
8603
|
-
(!source._affectsCount &&
|
|
9999
|
+
(!source._x?._affectsCount &&
|
|
8604
10000
|
!(source._statusFlags & this._collectionType) &&
|
|
8605
10001
|
!(this._collectionType & STATUS_ERROR && source._statusFlags & STATUS_PENDING))
|
|
8606
10002
|
)
|
|
@@ -8661,7 +10057,7 @@ function createCollectionBoundary(type, fn, fallback, onFn) {
|
|
|
8661
10057
|
else throw e;
|
|
8662
10058
|
}
|
|
8663
10059
|
queue._pending =
|
|
8664
|
-
pending || !!(tree._statusFlags & type) || tree._error instanceof NotReadyError;
|
|
10060
|
+
pending || !!(tree._statusFlags & type) || tree._x?._error instanceof NotReadyError;
|
|
8665
10061
|
});
|
|
8666
10062
|
const controller =
|
|
8667
10063
|
_revealUsed && type === STATUS_PENDING ? getContext(RevealControllerContext) : null;
|