@solidjs/signals 2.0.0-rc.3 → 2.0.0-rc.5
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 +2494 -271
- package/dist/node.cjs +3691 -1595
- package/dist/prod/affects.js +13 -12
- package/dist/prod/boundaries.js +43 -35
- package/dist/prod/core/action.js +3 -3
- package/dist/prod/core/async.js +137 -106
- package/dist/prod/core/constants.js +55 -1
- package/dist/prod/core/core.js +354 -247
- package/dist/prod/core/effect.js +47 -50
- package/dist/prod/core/error.js +13 -1
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +88 -52
- package/dist/prod/core/heap.js +38 -38
- package/dist/prod/core/lanes.js +34 -34
- package/dist/prod/core/optimistic.js +61 -58
- package/dist/prod/core/owner.js +38 -38
- package/dist/prod/core/scheduler.js +401 -174
- package/dist/prod/core/verdict.js +132 -65
- package/dist/prod/index.js +7 -3
- package/dist/prod/map.js +106 -106
- package/dist/prod/signals.js +253 -25
- package/dist/prod/store/index.js +2 -0
- package/dist/prod/store/next/optimistic.js +314 -121
- package/dist/prod/store/next/patch-hooks.js +13 -0
- package/dist/prod/store/next/patch.js +614 -0
- package/dist/prod/store/next/projection.js +23 -19
- package/dist/prod/store/next/reconcile.js +307 -120
- package/dist/prod/store/next/store.js +440 -117
- package/dist/prod/store/next/target.js +13 -4
- package/dist/prod/store/store.js +5 -5
- package/dist/types/core/async.d.ts +2 -0
- package/dist/types/core/attribution.d.ts +9 -4
- package/dist/types/core/constants.d.ts +54 -0
- package/dist/types/core/core.d.ts +34 -21
- package/dist/types/core/dev.d.ts +8 -0
- package/dist/types/core/error.d.ts +9 -0
- package/dist/types/core/graph.d.ts +22 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/scheduler.d.ts +46 -0
- package/dist/types/core/types.d.ts +12 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/signals.d.ts +108 -0
- package/dist/types/store/index.d.ts +2 -0
- package/dist/types/store/next/optimistic.d.ts +13 -10
- package/dist/types/store/next/patch-hooks.d.ts +41 -0
- package/dist/types/store/next/patch.d.ts +91 -0
- package/dist/types/store/next/projection.d.ts +1 -1
- package/dist/types/store/next/reconcile.d.ts +14 -0
- package/dist/types/store/next/store.d.ts +52 -2
- package/dist/types/store/next/target.d.ts +73 -8
- package/dist/types-cjs/core/async.d.cts +2 -0
- package/dist/types-cjs/core/attribution.d.cts +9 -4
- package/dist/types-cjs/core/constants.d.cts +54 -0
- package/dist/types-cjs/core/core.d.cts +34 -21
- package/dist/types-cjs/core/dev.d.cts +8 -0
- package/dist/types-cjs/core/error.d.cts +9 -0
- package/dist/types-cjs/core/graph.d.cts +22 -0
- package/dist/types-cjs/core/index.d.cts +2 -2
- package/dist/types-cjs/core/scheduler.d.cts +46 -0
- package/dist/types-cjs/core/types.d.cts +12 -0
- package/dist/types-cjs/index.d.cts +3 -3
- package/dist/types-cjs/signals.d.cts +108 -0
- package/dist/types-cjs/store/index.d.cts +2 -0
- package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
- package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
- package/dist/types-cjs/store/next/patch.d.cts +91 -0
- package/dist/types-cjs/store/next/projection.d.cts +1 -1
- package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
- package/dist/types-cjs/store/next/store.d.cts +52 -2
- package/dist/types-cjs/store/next/target.d.cts +73 -8
- package/package.json +2 -2
package/dist/dev.js
CHANGED
|
@@ -42,6 +42,18 @@ class StatusError extends Error {
|
|
|
42
42
|
function unwrapStatusError(error) {
|
|
43
43
|
return error instanceof StatusError ? error.cause : error;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Rejection value of `until(fn, { timeout })` when the predicate does not turn
|
|
47
|
+
* truthy within the window. Inside an `action()`, the rejection is thrown back
|
|
48
|
+
* in at the `yield` point — catchable there, or the action fails and its
|
|
49
|
+
* optimistic state reverts.
|
|
50
|
+
*/
|
|
51
|
+
class TimeoutError extends Error {
|
|
52
|
+
constructor(message = "Timed out waiting for condition") {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "TimeoutError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
class NoOwnerError extends Error {
|
|
46
58
|
constructor() {
|
|
47
59
|
super("Context can only be accessed under a reactive root.");
|
|
@@ -117,6 +129,60 @@ const CONFIG_CHILD_COMPANIONS = 1 << 11;
|
|
|
117
129
|
* moved into the cold extension (§12), and an unconditional `_x` deref per
|
|
118
130
|
* marked node measurably taxed the propagation hot path (diamond -22%). */
|
|
119
131
|
const CONFIG_FW_CHILDREN = 1 << 12;
|
|
132
|
+
/** Authoritative-view reader (`until()`): while this node computes, reads
|
|
133
|
+
* dodge active optimistic OVERRIDES only — the predicate must observe
|
|
134
|
+
* arriving truth, never the caller's own tentative writes (which would
|
|
135
|
+
* trivially satisfy it). Everything else reads normally, INCLUDING
|
|
136
|
+
* transition-staged `_pendingValue`: staged data is authoritative (optimism
|
|
137
|
+
* lives only in override slots), and a hold that refused staged reads would
|
|
138
|
+
* deadlock on data the open transaction itself is holding (a refresh the
|
|
139
|
+
* action issued lands staged and cannot commit until the hold releases).
|
|
140
|
+
* read() checks the bit on the reading computation (`context`) directly — no
|
|
141
|
+
* ambient flag — so a shared computed the predicate pulls recomputes as
|
|
142
|
+
* itself (no bit) under the normal view, and its cache never forks. */
|
|
143
|
+
const CONFIG_AUTHORITATIVE_READ = 1 << 13;
|
|
144
|
+
/** Sticky mark: an authoritative-view reader read this node PAST an active
|
|
145
|
+
* override. The ack shape — an authoritative arrival EQUAL to the override —
|
|
146
|
+
* rides paths that are deliberately silent under A17 (every ordinary reader
|
|
147
|
+
* sees the override, so an equal landing changes nothing for them). A marked
|
|
148
|
+
* node notifies those readers on such paths anyway, so the landed truth is
|
|
149
|
+
* seen without re-firing ordinary subscribers. Never cleared — only nodes an
|
|
150
|
+
* until() predicate observed mid-override pay. */
|
|
151
|
+
const CONFIG_AUTHORITATIVE_OBSERVED = 1 << 14;
|
|
152
|
+
/** Promise-delivery effect (resolve()/until()): commits its computed value
|
|
153
|
+
* directly even when recomputing under its own held transition. These
|
|
154
|
+
* effects deliver applies on a microtask (#2930) instead of the stashed
|
|
155
|
+
* effect queues, so the value must ride the same immediate schedule — a
|
|
156
|
+
* staged value with an immediate apply delivers stale state (resolve) or
|
|
157
|
+
* deadlocks the hold (until). Safe because the node is a private leaf: no
|
|
158
|
+
* subscriber reads an effect's value, only its own apply does. */
|
|
159
|
+
const CONFIG_DIRECT_COMMIT = 1 << 15;
|
|
160
|
+
/** Fresh-pull reader (awaitable `refresh()`'s waiter effect): a read of a
|
|
161
|
+
* dirty source recomputes it inline even when the height gate defers to the
|
|
162
|
+
* flush. Closes the same-flush ordering race where a waiter created
|
|
163
|
+
* alongside a refresh() mark read the PRE-re-ask value as settled and
|
|
164
|
+
* delivered stale; with the pull, the waiter either parks on the re-ask's
|
|
165
|
+
* pending window (async — woken by the settle walk, which runs on every
|
|
166
|
+
* landing including equal-value ones) or serves its sync answer. resolve()
|
|
167
|
+
* deliberately keeps that race — its contract is "first settled value"
|
|
168
|
+
* (#2930), not "next quiescent state". */
|
|
169
|
+
const CONFIG_FRESH_READ = 1 << 16;
|
|
170
|
+
/** HELD truth (#3164): this node's staged `_pendingValue` is confirming
|
|
171
|
+
* truth riding a transaction that retains optimism, revealed only at that
|
|
172
|
+
* transaction's settle. Two arming sites, one meaning: the store fold
|
|
173
|
+
* (a landing staged into the retaining transaction) and until()'s
|
|
174
|
+
* flip-entanglement (a foreign carrier's staged write, stolen when it
|
|
175
|
+
* flipped the awaited predicate truthy). Until the reveal, ordinary
|
|
176
|
+
* readers — lane and speculative recomputes included — keep committed:
|
|
177
|
+
* the staging notified subscribers as a plain write, so without the mask
|
|
178
|
+
* a mid-hold recompute composes live optimism with the confirming truth,
|
|
179
|
+
* a frame no timeline contains (GabbeV's union tear). Authoritative
|
|
180
|
+
* readers (until()'s predicate) and latest() tunnel through — the
|
|
181
|
+
* exemption that keeps holds deadlock-free. Override-covered nodes never
|
|
182
|
+
* arm: the override is their display and its revert their notification
|
|
183
|
+
* (A17). Cleared at commit (the commit IS the reveal); subscribers masked
|
|
184
|
+
* during the hold are woken by finalizePureQueue's post-revert pass. */
|
|
185
|
+
const CONFIG_HELD_TRUTH = 1 << 17;
|
|
120
186
|
const STATUS_PENDING = 1 << 0;
|
|
121
187
|
const STATUS_ERROR = 1 << 1;
|
|
122
188
|
const STATUS_UNINITIALIZED = 1 << 2;
|
|
@@ -600,6 +666,18 @@ const engineHooks = {
|
|
|
600
666
|
const totalMs = now() - frame.start;
|
|
601
667
|
if (frames.length > 0) frames[frames.length - 1].childMs += totalMs;
|
|
602
668
|
const selfMs = Math.max(0, totalMs - frame.childMs);
|
|
669
|
+
// Effect-output honesty: effects run with `_equals: false`, so core
|
|
670
|
+
// reports EVERY effect recompute as changed — which made effect waste
|
|
671
|
+
// invisible to costs() (and compiled JSX bindings are effects: the
|
|
672
|
+
// fan-out waste a naive selected-row produces is all effects). The
|
|
673
|
+
// engine re-derives the fact from its own snapshot: an identical
|
|
674
|
+
// committed compute output is an unchanged run. `undefined` outputs are
|
|
675
|
+
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
676
|
+
// identity of `undefined` proves nothing.
|
|
677
|
+
if (changed && frame.causes !== null && el._type) {
|
|
678
|
+
const committed = el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
|
|
679
|
+
if (committed !== undefined && committed === frame.prevValue) changed = false;
|
|
680
|
+
}
|
|
603
681
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
604
682
|
// committed change. The fresh value sits in `_pendingValue` for held
|
|
605
683
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
@@ -708,11 +786,17 @@ const hooks = {};
|
|
|
708
786
|
const diagnosticListeners = new Set();
|
|
709
787
|
const diagnosticCaptures = new Set();
|
|
710
788
|
let diagnosticSequence = 0;
|
|
789
|
+
let consoleFooter;
|
|
790
|
+
const footeredCodes = new Set();
|
|
711
791
|
const diagnostics = {
|
|
712
792
|
subscribe(listener) {
|
|
713
793
|
diagnosticListeners.add(listener);
|
|
714
794
|
return () => diagnosticListeners.delete(listener);
|
|
715
795
|
},
|
|
796
|
+
setConsoleFooter(footer) {
|
|
797
|
+
consoleFooter = footer;
|
|
798
|
+
footeredCodes.clear();
|
|
799
|
+
},
|
|
716
800
|
capture() {
|
|
717
801
|
const events = [];
|
|
718
802
|
diagnosticCaptures.add(events);
|
|
@@ -752,6 +836,13 @@ function emitDiagnostic(event) {
|
|
|
752
836
|
};
|
|
753
837
|
for (const listener of diagnosticListeners) listener(entry);
|
|
754
838
|
for (const capture of diagnosticCaptures) capture.push(entry);
|
|
839
|
+
if (consoleFooter && !footeredCodes.has(entry.code)) {
|
|
840
|
+
footeredCodes.add(entry.code);
|
|
841
|
+
const footer = consoleFooter(entry);
|
|
842
|
+
// Call sites console.warn/error their message after emitDiagnostic
|
|
843
|
+
// returns; a microtask lands the footer right below that report.
|
|
844
|
+
if (footer) queueMicrotask(() => console.warn(footer));
|
|
845
|
+
}
|
|
755
846
|
return entry;
|
|
756
847
|
}
|
|
757
848
|
/**
|
|
@@ -1097,7 +1188,7 @@ function cancelZombieRecompute(el) {
|
|
|
1097
1188
|
}
|
|
1098
1189
|
let clock = 0;
|
|
1099
1190
|
let activeTransition = null;
|
|
1100
|
-
let scheduled = false;
|
|
1191
|
+
let scheduled$1 = false;
|
|
1101
1192
|
let halted = false;
|
|
1102
1193
|
let haltNotified = false;
|
|
1103
1194
|
let syncDepth = 0;
|
|
@@ -1207,6 +1298,27 @@ function mergeTransitionState(target, outgoing) {
|
|
|
1207
1298
|
outgoing._affectsNodes.length = 0;
|
|
1208
1299
|
}
|
|
1209
1300
|
for (const store of outgoing._optimisticStores) target._optimisticStores.add(store);
|
|
1301
|
+
// Patch-channel stash (store/next/patch.ts): entries held for the outgoing
|
|
1302
|
+
// transition must ride the merge like every other per-transition
|
|
1303
|
+
// collection — releaseBatch only reads the COMMITTING transition's stash,
|
|
1304
|
+
// so a stranded sidecar would silently drop its patches. Move (don't
|
|
1305
|
+
// copy), same aliasing rule as the collections above. The field is an
|
|
1306
|
+
// expando so this module stays free of patch imports (pay-for-use).
|
|
1307
|
+
const heldPatches = outgoing._heldPatches;
|
|
1308
|
+
if (heldPatches !== undefined) {
|
|
1309
|
+
outgoing._heldPatches = undefined;
|
|
1310
|
+
let dest = target._heldPatches;
|
|
1311
|
+
if (dest !== undefined) dest.push(...heldPatches);
|
|
1312
|
+
else dest = target._heldPatches = heldPatches;
|
|
1313
|
+
// Retarget the entries' coalescing stamps to the surviving stash
|
|
1314
|
+
// (opaque backref contract with store/next/patch.ts): without this a
|
|
1315
|
+
// post-merge emission misses the stamp and pushes a SECOND entry —
|
|
1316
|
+
// the record's patch applies twice at commit (re-audit 5, P1-2).
|
|
1317
|
+
for (let i = 0; i < heldPatches.length; i++) {
|
|
1318
|
+
const pc = heldPatches[i].pc;
|
|
1319
|
+
if (pc !== undefined && pc.qe === heldPatches[i]) pc.qa = dest;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1210
1322
|
for (const [source, reporters] of outgoing._asyncReporters) {
|
|
1211
1323
|
let targetReporters = target._asyncReporters.get(source);
|
|
1212
1324
|
if (!targetReporters) target._asyncReporters.set(source, (targetReporters = new Set()));
|
|
@@ -1214,13 +1326,114 @@ function mergeTransitionState(target, outgoing) {
|
|
|
1214
1326
|
}
|
|
1215
1327
|
for (const sub of outgoing._gatedSubs) target._gatedSubs.add(sub);
|
|
1216
1328
|
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Flip-entanglement (#3164 follow-up): `until()` is a declaration of
|
|
1331
|
+
* relatedness — the predicate names the condition that confirms the awaiting
|
|
1332
|
+
* transaction. When the predicate settles truthy, every live foreign
|
|
1333
|
+
* transition whose staged write it read IS the confirming event by the
|
|
1334
|
+
* user's own definition, so it merges into the awaiting transaction and
|
|
1335
|
+
* reveals at the joint settle — the cross-primitive twin of the family fold
|
|
1336
|
+
* (a landing on an optimism-carrying family joins the retaining
|
|
1337
|
+
* transaction). Non-flipping updates never pass through here: falsy
|
|
1338
|
+
* evaluations don't entangle, so unrelated traffic on the watched sources
|
|
1339
|
+
* reveals freely on its own schedule.
|
|
1340
|
+
*
|
|
1341
|
+
* Runs inside the predicate's compute (pure phase) — the confirming
|
|
1342
|
+
* transition's stamps are still live and its commit decision hasn't run, so
|
|
1343
|
+
* the merge lands before any reveal. Only the tree-shaken graphs that call
|
|
1344
|
+
* `until()` retain this.
|
|
1345
|
+
*/
|
|
1346
|
+
function entangleConfirmingTransitions(obs, target) {
|
|
1347
|
+
target = currentTransition(target);
|
|
1348
|
+
if (target._done === true) return;
|
|
1349
|
+
// The confirming evidence is a dep whose value is STAGED (pending,
|
|
1350
|
+
// uncommitted) at flip evaluation — committed deps are public already and
|
|
1351
|
+
// carry nothing to entangle. A staged dep lives in one of two carriers: a
|
|
1352
|
+
// stamped transition, or the queue's current batch (ambient registrations
|
|
1353
|
+
// don't stamp; "ambient work IS a transaction" — the batch is the
|
|
1354
|
+
// carrier). The entangle STEALS the carrier's staged cargo — its pending
|
|
1355
|
+
// nodes move (re-stamped) into the awaiting transaction and reveal at its
|
|
1356
|
+
// settle — but never the carrier itself: its async reporters, actions,
|
|
1357
|
+
// and stashes are its own future (a live stream's flight must not chain
|
|
1358
|
+
// the awaiting transaction to landings that haven't happened; a merged
|
|
1359
|
+
// reporter deadlocked exactly that way).
|
|
1360
|
+
let stole = false;
|
|
1361
|
+
for (let l = obs._deps; l !== null; l = l._nextDep) {
|
|
1362
|
+
const dep = l._dep;
|
|
1363
|
+
if (dep._pendingValue !== NOT_PENDING) {
|
|
1364
|
+
const stamp = dep._transition;
|
|
1365
|
+
const t = stamp != null ? currentTransition(stamp) : null;
|
|
1366
|
+
// Skip the awaiting transaction's own cargo (t === target: a
|
|
1367
|
+
// fold-staged landing or a write the action itself issued — hold and
|
|
1368
|
+
// reveal already correct) and dead carriers. Ambient-batch staging
|
|
1369
|
+
// (t === null) must leave the batch NOW — it commits at this flush's
|
|
1370
|
+
// end, which would reveal the confirmation under the live optimism
|
|
1371
|
+
// it just confirmed.
|
|
1372
|
+
const carrier =
|
|
1373
|
+
t === null
|
|
1374
|
+
? currentBatch._pendingNodes
|
|
1375
|
+
: t !== target && t._done !== true
|
|
1376
|
+
? t._pendingNodes
|
|
1377
|
+
: null;
|
|
1378
|
+
if (carrier !== null) stole = stealEntangledCargo(carrier, target) || stole;
|
|
1379
|
+
}
|
|
1380
|
+
if (l === obs._depsTail) break;
|
|
1381
|
+
}
|
|
1382
|
+
// The steal never activates the awaiting transaction: the predicate can
|
|
1383
|
+
// flip inside another transaction's finalize heap, and adopting the queue
|
|
1384
|
+
// batch there hands the stolen cargo to that finalize's commit sweep — a
|
|
1385
|
+
// premature reveal at a foreign settle. Subscribers that computed against
|
|
1386
|
+
// the pre-steal world were re-dirtied by the steal itself, so this
|
|
1387
|
+
// flush's applies paint the masked (mid-hold) view; the cargo commits at
|
|
1388
|
+
// the awaiting transaction's own settle.
|
|
1389
|
+
}
|
|
1390
|
+
/** Move a confirming carrier's staged nodes into the awaiting transaction:
|
|
1391
|
+
* re-stamp and arm the held-truth mask (override-covered nodes skip it —
|
|
1392
|
+
* the override already hides their staged value per A17, and is usually
|
|
1393
|
+
* the very optimism this confirmation settles); the mask's commit
|
|
1394
|
+
* registers the settle-side post-revert wake. The carrier's array is
|
|
1395
|
+
* emptied so its own commit point commits none of the stolen cargo.
|
|
1396
|
+
*
|
|
1397
|
+
* EFFECT subs of stolen nodes re-run: any that recomputed against the
|
|
1398
|
+
* staging BEFORE the steal (the carrier's landing notified them as a plain
|
|
1399
|
+
* write) hold a private torn result — override composed with confirming
|
|
1400
|
+
* truth — that the next paint gate (stash-point lane run, a foreign
|
|
1401
|
+
* flush's completion drain) would show. Re-running them under the mask
|
|
1402
|
+
* re-derives the mid-hold view in this same heap pass. PURE computeds are
|
|
1403
|
+
* deliberately NOT re-run: a torn staged value of theirs is itself stolen
|
|
1404
|
+
* cargo — masked at read, so ordinary readers already serve their
|
|
1405
|
+
* committed value — while re-running them would re-derive the OLD world
|
|
1406
|
+
* and re-stage it over the held truth. The reveal re-notifies
|
|
1407
|
+
* (commitPendingNodes), which is when they re-derive for real. */
|
|
1408
|
+
function stealEntangledCargo(carrier, target) {
|
|
1409
|
+
if (carrier === target._pendingNodes || carrier.length === 0) return false;
|
|
1410
|
+
for (let i = 0; i < carrier.length; i++) {
|
|
1411
|
+
const node = carrier[i];
|
|
1412
|
+
node._transition = target;
|
|
1413
|
+
target._pendingNodes.push(node);
|
|
1414
|
+
// Override-covered nodes stay silent AND unmasked: the override is the
|
|
1415
|
+
// display (A17 — its staging never notified, its revert will), so their
|
|
1416
|
+
// subs saw nothing and re-running one would break the silence with a
|
|
1417
|
+
// duplicate fire of an unchanged view.
|
|
1418
|
+
if (!hasActiveOverride$1(node)) {
|
|
1419
|
+
node._config |= CONFIG_HELD_TRUTH;
|
|
1420
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
1421
|
+
const sub = s._sub;
|
|
1422
|
+
if (sub._type && !(sub._config & CONFIG_AUTHORITATIVE_READ)) enqueueSub(sub);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
carrier.length = 0;
|
|
1427
|
+
transitions.add(target);
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1217
1430
|
function schedule() {
|
|
1218
1431
|
if (halted) {
|
|
1219
1432
|
notifyHalted();
|
|
1220
1433
|
return;
|
|
1221
1434
|
}
|
|
1222
|
-
if (scheduled) return;
|
|
1223
|
-
scheduled = true;
|
|
1435
|
+
if (scheduled$1) return;
|
|
1436
|
+
scheduled$1 = true;
|
|
1224
1437
|
if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);
|
|
1225
1438
|
}
|
|
1226
1439
|
/**
|
|
@@ -1403,11 +1616,19 @@ class GlobalQueue extends Queue {
|
|
|
1403
1616
|
static _transitionBlocked = null;
|
|
1404
1617
|
static _cleanupLanes = null;
|
|
1405
1618
|
static _runLaneEffects = null;
|
|
1619
|
+
/** Patch-channel optimistic drain (next/patch.ts): optimistic emissions
|
|
1620
|
+
* apply at lane-effect timing — visible in flight, unlike the regular
|
|
1621
|
+
* effect queues an action stashes. Injected; null when unused. */
|
|
1622
|
+
static _drainPatchOptimistic = null;
|
|
1406
1623
|
static _gatedRead = null;
|
|
1407
1624
|
static _laneSuspends = null;
|
|
1408
1625
|
static _laneReadsCommitted = null;
|
|
1409
1626
|
static _recomputeLane = null;
|
|
1410
1627
|
static _laneAsyncPending = null;
|
|
1628
|
+
/** Authoritative-view reader wakeup (until()): installed at first until() call.
|
|
1629
|
+
* Call sites are gated by CONFIG_AUTHORITATIVE_OBSERVED, which only until()'s
|
|
1630
|
+
* carve-out read can set, so `!` invocations are safe once the gate holds. */
|
|
1631
|
+
static _notifyAuthoritativeObservers = null;
|
|
1411
1632
|
static _laneAsyncSettled = null;
|
|
1412
1633
|
static _trackOptimisticStore = null;
|
|
1413
1634
|
flush() {
|
|
@@ -1415,6 +1636,10 @@ class GlobalQueue extends Queue {
|
|
|
1415
1636
|
this._running = true;
|
|
1416
1637
|
try {
|
|
1417
1638
|
if (true) devCheckFlushStart();
|
|
1639
|
+
// Before runHeap for the same reason as the fast drain above; late
|
|
1640
|
+
// subscribers (an effect reading a swept memo this flush) revive it,
|
|
1641
|
+
// which is the pay-for-use contract.
|
|
1642
|
+
sweepDormant();
|
|
1418
1643
|
runHeap(dirtyQueue, GlobalQueue._update);
|
|
1419
1644
|
if (activeTransition) {
|
|
1420
1645
|
const isComplete = transitionComplete(activeTransition);
|
|
@@ -1449,7 +1674,7 @@ class GlobalQueue extends Queue {
|
|
|
1449
1674
|
// A kept ambient batch may hold pending nodes (#2916): stay
|
|
1450
1675
|
// scheduled so the outer drain loop commits them via the plain
|
|
1451
1676
|
// flush path instead of leaving them until the next natural flush.
|
|
1452
|
-
scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
1677
|
+
scheduled$1 = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
1453
1678
|
reassignPendingTransition(stashedTransition._pendingNodes);
|
|
1454
1679
|
activeTransition = null;
|
|
1455
1680
|
finalizePureQueue(null, true);
|
|
@@ -1489,7 +1714,7 @@ class GlobalQueue extends Queue {
|
|
|
1489
1714
|
}
|
|
1490
1715
|
clock++;
|
|
1491
1716
|
// Check if finalization added items to the heap (from optimistic reversion)
|
|
1492
|
-
scheduled = dirtyQueue._max >= dirtyQueue._min;
|
|
1717
|
+
scheduled$1 = dirtyQueue._max >= dirtyQueue._min;
|
|
1493
1718
|
// Run lane effects first (for ready lanes), then regular effects
|
|
1494
1719
|
activeLanes.size && GlobalQueue._runLaneEffects(EFFECT_RENDER);
|
|
1495
1720
|
this.run(EFFECT_RENDER);
|
|
@@ -1506,7 +1731,7 @@ class GlobalQueue extends Queue {
|
|
|
1506
1731
|
}
|
|
1507
1732
|
if (
|
|
1508
1733
|
true &&
|
|
1509
|
-
!scheduled &&
|
|
1734
|
+
!scheduled$1 &&
|
|
1510
1735
|
!activeTransition &&
|
|
1511
1736
|
transitions.size === 0 &&
|
|
1512
1737
|
activeLanes.size === 0
|
|
@@ -1547,8 +1772,17 @@ class GlobalQueue extends Queue {
|
|
|
1547
1772
|
return false;
|
|
1548
1773
|
}
|
|
1549
1774
|
initTransition(transition) {
|
|
1550
|
-
if (transition)
|
|
1551
|
-
|
|
1775
|
+
if (transition) {
|
|
1776
|
+
transition = currentTransition(transition);
|
|
1777
|
+
// A finished transaction cannot be re-entered: its state is committed
|
|
1778
|
+
// or reverted, so "rejoining" it (A26) is meaningless and re-activating
|
|
1779
|
+
// it spins the drain loop (#3140). The refusal must be a bare return —
|
|
1780
|
+
// redirecting the caller to a fresh batch would re-arm the loop with a
|
|
1781
|
+
// new transaction identity each pass. Stamps are cleared at commit, so
|
|
1782
|
+
// this is a belt for paths that hand over a chased-dead reference
|
|
1783
|
+
// (merged chains, async settles racing completion).
|
|
1784
|
+
if (transition._done === true || transition === activeTransition) return;
|
|
1785
|
+
}
|
|
1552
1786
|
if (!transition && activeTransition && activeTransition._time === clock) return;
|
|
1553
1787
|
if (!activeTransition) {
|
|
1554
1788
|
activeTransition = transition ?? createBatch();
|
|
@@ -1592,11 +1826,25 @@ class GlobalQueue extends Queue {
|
|
|
1592
1826
|
for (const lane of activeLanes) {
|
|
1593
1827
|
if (!lane._transition) lane._transition = activeTransition;
|
|
1594
1828
|
}
|
|
1829
|
+
// A transaction's ambient window is one flush. Entering must therefore
|
|
1830
|
+
// guarantee a flush: a transaction opened with no writes (an action whose
|
|
1831
|
+
// first statements only await) otherwise leaves activeTransition and the
|
|
1832
|
+
// adopted batch armed across the async gap, and the next unrelated work
|
|
1833
|
+
// to arrive — an optimistic store's authoritative landing, a plain async
|
|
1834
|
+
// settle — is adopted into a transaction it has nothing to do with
|
|
1835
|
+
// (#3141). The scheduled flush parks the incomplete transaction through
|
|
1836
|
+
// the normal machinery and detaches the ambient slots first.
|
|
1837
|
+
schedule();
|
|
1595
1838
|
}
|
|
1596
1839
|
}
|
|
1597
1840
|
function queuePendingNode(node) {
|
|
1841
|
+
lastStagedNodeName = node._name ?? null;
|
|
1598
1842
|
currentBatch._pendingNodes.push(node);
|
|
1599
1843
|
}
|
|
1844
|
+
// Dev-only attribution for the flush loop guard (#3140): when the guard
|
|
1845
|
+
// trips, naming what the loop kept chewing on lets the app author attribute
|
|
1846
|
+
// the runaway without patching dist.
|
|
1847
|
+
let lastStagedNodeName = null;
|
|
1600
1848
|
// Sticky: flips true on the first refresh() ever (the only setter of
|
|
1601
1849
|
// REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
|
|
1602
1850
|
// clear entirely in apps that never refresh.
|
|
@@ -1692,13 +1940,48 @@ let storeCommitHook = null;
|
|
|
1692
1940
|
function setStoreCommitHook(fn) {
|
|
1693
1941
|
storeCommitHook = fn;
|
|
1694
1942
|
}
|
|
1943
|
+
/** Patch-channel release hook (next/patch.ts): transition-stamped patch
|
|
1944
|
+
* emissions are released when THEIR batch commits. Transitions never
|
|
1945
|
+
* abort: failed actions still commit (only optimistic overrides revert),
|
|
1946
|
+
* and merged-away transitions hand their stash to the survivor
|
|
1947
|
+
* (mergeTransitionState) — every stash drains exactly once. Injected like
|
|
1948
|
+
* storeCommitHook to stay tree-shakeable. */
|
|
1949
|
+
let patchCommitHook = null;
|
|
1950
|
+
function setPatchCommitHook(fn) {
|
|
1951
|
+
patchCommitHook = fn;
|
|
1952
|
+
}
|
|
1953
|
+
/** Held truth committed this finalize, awaiting its post-revert wake (see
|
|
1954
|
+
* finalizePureQueue): the commit IS the reveal, but subscribers must not
|
|
1955
|
+
* re-derive until the settling transaction's optimistic overrides have
|
|
1956
|
+
* reverted — a commit-time wake recomputes them in the window where
|
|
1957
|
+
* confirming truth is committed and the override still displays, a torn
|
|
1958
|
+
* frame no timeline contains. */
|
|
1959
|
+
const heldRevealed = [];
|
|
1695
1960
|
function commitPendingNodes() {
|
|
1696
1961
|
const pendingNodes = currentBatch._pendingNodes;
|
|
1697
1962
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
1698
|
-
|
|
1963
|
+
const node = pendingNodes[i];
|
|
1964
|
+
commitPendingNode(node);
|
|
1965
|
+
// The stamp dies with the commit (#3143) — symmetric with
|
|
1966
|
+
// resolveOptimisticNodes clearing optimistic stamps. A stamp outliving
|
|
1967
|
+
// its transaction let any later write (even a value-equal no-op, which
|
|
1968
|
+
// re-opens before the equality bail) resurrect the finished transaction;
|
|
1969
|
+
// a boundary flag rewritten every finalize pass then spun the drain loop
|
|
1970
|
+
// forever (#3140). The held-truth mark dies the same death — the commit
|
|
1971
|
+
// IS the reveal — but its wake defers to the post-revert pass: ordinary
|
|
1972
|
+
// subscribers were masked to committed all hold (some re-derived against
|
|
1973
|
+
// that old view and cached it), and commits are otherwise silent
|
|
1974
|
+
// (staging already notified), so without a wake they'd hold the old
|
|
1975
|
+
// world forever.
|
|
1976
|
+
node._transition = null;
|
|
1977
|
+
if (node._config & CONFIG_HELD_TRUTH) {
|
|
1978
|
+
node._config &= ~CONFIG_HELD_TRUTH;
|
|
1979
|
+
heldRevealed.push(node);
|
|
1980
|
+
}
|
|
1699
1981
|
}
|
|
1700
1982
|
pendingNodes.length = 0;
|
|
1701
1983
|
storeCommitHook?.();
|
|
1984
|
+
patchCommitHook?.(currentBatch);
|
|
1702
1985
|
}
|
|
1703
1986
|
function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
1704
1987
|
// For incomplete transitions, skip pending resolution and optimistic reversion
|
|
@@ -1746,6 +2029,22 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
1746
2029
|
// completing transition scopes the clear to its own layer keys (#2899).
|
|
1747
2030
|
if (batch._optimisticStores.size)
|
|
1748
2031
|
GlobalQueue._clearOptimisticStores(batch._optimisticStores, completingTransition);
|
|
2032
|
+
// Held-truth reveal wake (#3164), post-revert by construction: this
|
|
2033
|
+
// finalize committed confirming truth whose subscribers were masked all
|
|
2034
|
+
// hold — some re-derived against the committed view (the staging, or a
|
|
2035
|
+
// confirming carrier's landing, notified them as a plain write) and
|
|
2036
|
+
// cached it, and stash-restored applies may carry those torn values.
|
|
2037
|
+
// Waking and recomputing HERE — after _resolveOptimistic and the store
|
|
2038
|
+
// clears above — means every apply paints the settled view; a wake at
|
|
2039
|
+
// commit time would recompute them in the window where truth is
|
|
2040
|
+
// committed but the settling transaction's overrides still display.
|
|
2041
|
+
if (heldRevealed.length !== 0) {
|
|
2042
|
+
while (heldRevealed.length) insertSubs(heldRevealed.pop());
|
|
2043
|
+
if (dirtyQueue._max >= dirtyQueue._min) {
|
|
2044
|
+
runHeap(dirtyQueue, GlobalQueue._update);
|
|
2045
|
+
commitPendingNodes();
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
1749
2048
|
sweepTransientStoreNodes();
|
|
1750
2049
|
// Lanes only enter activeLanes through the engine's getOrCreateLane.
|
|
1751
2050
|
if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
|
|
@@ -1825,8 +2124,21 @@ function flush(fn) {
|
|
|
1825
2124
|
let count = 0;
|
|
1826
2125
|
// `flush()` is an explicit drain point, so it must also process an active
|
|
1827
2126
|
// transition even if no microtask was scheduled for it yet.
|
|
1828
|
-
while (scheduled || activeTransition) {
|
|
1829
|
-
if (++count === 1e5)
|
|
2127
|
+
while (scheduled$1 || activeTransition) {
|
|
2128
|
+
if (++count === 1e5) {
|
|
2129
|
+
// Attribution beats a bare guard (#3140): say what kept the loop alive.
|
|
2130
|
+
// A completed transition being re-activated reads `done=true` here —
|
|
2131
|
+
// the corpse-revival signature — while application-driven runaways
|
|
2132
|
+
// (#2843) usually show staged work naming the culprit node.
|
|
2133
|
+
const t = activeTransition;
|
|
2134
|
+
throw new Error(
|
|
2135
|
+
`Potential Infinite Loop Detected. Kept alive by ${scheduled$1 ? "scheduled work" : "an active transition"}${
|
|
2136
|
+
t
|
|
2137
|
+
? `; transition: done=${t._done === true}, pending=${t._pendingNodes.length}, optimistic=${t._optimisticNodes.length}, asyncReporters=${t._asyncReporters.size}`
|
|
2138
|
+
: ""
|
|
2139
|
+
}${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
1830
2142
|
globalQueue.flush();
|
|
1831
2143
|
}
|
|
1832
2144
|
}
|
|
@@ -1888,6 +2200,29 @@ function runInTransition(transition, fn) {
|
|
|
1888
2200
|
activeTransition = prevTransition;
|
|
1889
2201
|
}
|
|
1890
2202
|
}
|
|
2203
|
+
/** Run `fn` with `transition` as BOTH the ambient transaction and the
|
|
2204
|
+
* registration batch, restoring both after. runInTransition alone is not
|
|
2205
|
+
* enough for code that WRITES on behalf of a transaction from inside someone
|
|
2206
|
+
* else's window (optimistic replay re-arming a still-open action's edits
|
|
2207
|
+
* during a landing commit, #3123): registrations route through the queue's
|
|
2208
|
+
* batch pointer, and a bare activeTransition swap leaves them in the ambient
|
|
2209
|
+
* batch — a plain batch "completes" at the next flush and reverts optimistic
|
|
2210
|
+
* registrations that were supposed to live with the transaction.
|
|
2211
|
+
* initTransition is the wrong tool here: it MERGES the currently ambient
|
|
2212
|
+
* transaction into the target, entangling whatever the interrupted window
|
|
2213
|
+
* belonged to. */
|
|
2214
|
+
function runAsTransitionBatch(transition, fn) {
|
|
2215
|
+
const prevTransition = activeTransition;
|
|
2216
|
+
const prevBatch = globalQueue._batch;
|
|
2217
|
+
try {
|
|
2218
|
+
activeTransition = currentTransition(transition);
|
|
2219
|
+
currentBatch = globalQueue._batch = activeTransition;
|
|
2220
|
+
return fn();
|
|
2221
|
+
} finally {
|
|
2222
|
+
activeTransition = prevTransition;
|
|
2223
|
+
currentBatch = globalQueue._batch = prevBatch;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1891
2226
|
|
|
1892
2227
|
/** The queue a node belongs to, picked from its own zombie flag. */
|
|
1893
2228
|
function queueFor(n) {
|
|
@@ -2363,7 +2698,7 @@ function unlinkSubs(link) {
|
|
|
2363
2698
|
// transition holding it) is an observer — tearing down would orphan
|
|
2364
2699
|
// the work and re-execute it on the next read. The settle path runs
|
|
2365
2700
|
// this same last-one-out check when that observer releases (the
|
|
2366
|
-
// untracked-read
|
|
2701
|
+
// untracked-read dormancy sweep guards on pending identically).
|
|
2367
2702
|
const c = dep;
|
|
2368
2703
|
c._fn &&
|
|
2369
2704
|
c._config & CONFIG_AUTO_DISPOSE &&
|
|
@@ -2403,6 +2738,46 @@ function unobserved(el) {
|
|
|
2403
2738
|
clearDeps(el);
|
|
2404
2739
|
disposeChildren(el, true);
|
|
2405
2740
|
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Deferred dormancy for never-observed auto-dispose computeds (#3078).
|
|
2743
|
+
*
|
|
2744
|
+
* An untracked top-level read of a subscriber-less observation-lifecycle memo
|
|
2745
|
+
* used to call unobserved() inline at the end of read(). That kept the leak
|
|
2746
|
+
* closed (the compute links the memo into its deps' sub lists — without a
|
|
2747
|
+
* teardown point a never-observed memo is retained by its sources forever;
|
|
2748
|
+
* upstream alien-signals has exactly this retention), but it made reads
|
|
2749
|
+
* destructive: each read disposed the node, the next read revived it with a
|
|
2750
|
+
* full recompute in whatever ambient transition/lane context happened to be
|
|
2751
|
+
* current, so consecutive reads could return different answers with no write
|
|
2752
|
+
* in between.
|
|
2753
|
+
*
|
|
2754
|
+
* Instead, reads queue the node here and the scheduler sweeps at the top of
|
|
2755
|
+
* the next flush (before runHeap, so a same-tick dirtying is reclaimed
|
|
2756
|
+
* instead of recomputed). Reads become idempotent within a tick (the node
|
|
2757
|
+
* stays alive and serves its cache, uniform with observed memos) while
|
|
2758
|
+
* reclamation still happens within one microtask — the enqueue site arms
|
|
2759
|
+
* schedule(), so a flush is guaranteed even when no other work is queued.
|
|
2760
|
+
*/
|
|
2761
|
+
const dormantNodes = new Set();
|
|
2762
|
+
function sweepDormant() {
|
|
2763
|
+
if (dormantNodes.size === 0) return;
|
|
2764
|
+
for (const el of dormantNodes) {
|
|
2765
|
+
// Re-validate at sweep time: the node may have gained a subscriber (its
|
|
2766
|
+
// lifecycle is the unlinkSubs cascade now), gone pending (in-flight async
|
|
2767
|
+
// is an observer; the settle path re-runs last-one-out), lost its
|
|
2768
|
+
// AUTO_DISPOSE bit (owner teardown strips it, #3024), or already been
|
|
2769
|
+
// torn down.
|
|
2770
|
+
if (
|
|
2771
|
+
!el._subs &&
|
|
2772
|
+
el._config & CONFIG_AUTO_DISPOSE &&
|
|
2773
|
+
!(el._statusFlags & STATUS_PENDING) &&
|
|
2774
|
+
!(el._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
2775
|
+
) {
|
|
2776
|
+
unobserved(el);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
dormantNodes.clear();
|
|
2780
|
+
}
|
|
2406
2781
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
2407
2782
|
function link(dep, sub, pendingObserver = false) {
|
|
2408
2783
|
// Repeat touches within one pass AND-combine `_pendingObserver`: a probe
|
|
@@ -2616,11 +2991,11 @@ function settlePendingSource(el) {
|
|
|
2616
2991
|
const errored = node._statusFlags & STATUS_ERROR;
|
|
2617
2992
|
if (remaining) {
|
|
2618
2993
|
if (!errored) setPendingError(node, remaining);
|
|
2619
|
-
updateCompanions
|
|
2994
|
+
updateCompanions?.(node);
|
|
2620
2995
|
} else {
|
|
2621
2996
|
node._statusFlags &= ~STATUS_PENDING;
|
|
2622
2997
|
if (!errored) setPendingError(node);
|
|
2623
|
-
updateCompanions
|
|
2998
|
+
updateCompanions?.(node);
|
|
2624
2999
|
if (node._x?._blocked) {
|
|
2625
3000
|
enqueueSub(node);
|
|
2626
3001
|
scheduled = true;
|
|
@@ -2643,6 +3018,14 @@ function settlePendingSource(el) {
|
|
|
2643
3018
|
function isThenable(value) {
|
|
2644
3019
|
return value != null && typeof value === "object" && typeof value.then === "function";
|
|
2645
3020
|
}
|
|
3021
|
+
/** Fire and clear a node's iterator-flight cancellation hook (#3122). */
|
|
3022
|
+
function releaseFlightTeardown(el) {
|
|
3023
|
+
const teardown = el._x?._flightTeardown;
|
|
3024
|
+
if (teardown != null) {
|
|
3025
|
+
el._x._flightTeardown = null;
|
|
3026
|
+
teardown();
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
2646
3029
|
function handleAsync(el, result, setter) {
|
|
2647
3030
|
let iterator = false;
|
|
2648
3031
|
let thenable = false;
|
|
@@ -2680,6 +3063,11 @@ function handleAsync(el, result, setter) {
|
|
|
2680
3063
|
});
|
|
2681
3064
|
throw new Error(message);
|
|
2682
3065
|
}
|
|
3066
|
+
// Flight replacement relies on recompute's supersede release for iterator
|
|
3067
|
+
// teardown (#3122): every handleAsync call — including the projection
|
|
3068
|
+
// self-registration — runs during a recompute of `el`, which has already
|
|
3069
|
+
// fired _flightTeardown. A future non-recompute registration path must
|
|
3070
|
+
// release it here before overwriting _inFlight.
|
|
2683
3071
|
ext(el)._inFlight = result;
|
|
2684
3072
|
let syncValue;
|
|
2685
3073
|
// Settle-time transition re-entry. The loading rail is invisible to
|
|
@@ -2783,10 +3171,21 @@ function handleAsync(el, result, setter) {
|
|
|
2783
3171
|
// only notified when the hold is visible to them: under an active
|
|
2784
3172
|
// override every reader sees the override (A17), so waking subs would
|
|
2785
3173
|
// re-show an unchanged view — the revert is the notification point.
|
|
2786
|
-
GlobalQueue._syncCompanions
|
|
3174
|
+
GlobalQueue._syncCompanions?.(el, value);
|
|
2787
3175
|
if (!hasActiveOverride$1(el)) {
|
|
2788
3176
|
if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
|
|
2789
3177
|
insertSubs(el);
|
|
3178
|
+
} else if (el._config & CONFIG_AUTHORITATIVE_OBSERVED) {
|
|
3179
|
+
// A17 silence is stated over ordinary readers; an authoritative-view
|
|
3180
|
+
// reader (until()'s predicate) observed this node PAST its override
|
|
3181
|
+
// and is waiting for exactly this staged truth. Without the wake the
|
|
3182
|
+
// hold deadlocks: the landing waits on the transaction, the
|
|
3183
|
+
// transaction on the action, the action on an until() that was never
|
|
3184
|
+
// re-notified (#3164). Same selective wake as the equal-landing
|
|
3185
|
+
// branch in recompute(). Optional call: the bit implies the
|
|
3186
|
+
// optimistic engine WAS consulted, but the hook only installs with
|
|
3187
|
+
// it — a bare-core build must not crash here.
|
|
3188
|
+
GlobalQueue._notifyAuthoritativeObservers?.(el);
|
|
2790
3189
|
}
|
|
2791
3190
|
el._time = clock;
|
|
2792
3191
|
} else if (lane) {
|
|
@@ -2801,7 +3200,7 @@ function handleAsync(el, result, setter) {
|
|
|
2801
3200
|
// The latest() shadow write gives latest() effects independent lanes; the
|
|
2802
3201
|
// _pendingSignal update is a no-op repeat of the clearStatus() call above
|
|
2803
3202
|
// (computePendingState doesn't read _value).
|
|
2804
|
-
GlobalQueue._syncCompanions
|
|
3203
|
+
GlobalQueue._syncCompanions?.(el, value);
|
|
2805
3204
|
insertSubs(el, true);
|
|
2806
3205
|
}
|
|
2807
3206
|
} catch (e) {
|
|
@@ -2883,6 +3282,11 @@ function handleAsync(el, result, setter) {
|
|
|
2883
3282
|
} catch {}
|
|
2884
3283
|
};
|
|
2885
3284
|
registerClose ? registerClose(close) : cleanup(close);
|
|
3285
|
+
// Flight-identity cancellation (#3122): the registration above is the
|
|
3286
|
+
// owner-death backstop, but its disposal list can be zombie-deferred
|
|
3287
|
+
// until the SUPERSEDING flight settles. The teardown slot fires at the
|
|
3288
|
+
// _inFlight release sites so supersede stops this stream immediately.
|
|
3289
|
+
ext(el)._flightTeardown = close;
|
|
2886
3290
|
// Release check before each next pull: an unobserved lazy node must tear
|
|
2887
3291
|
// down (its close above runs via disposal, closing the iterator) instead
|
|
2888
3292
|
// of pumping the stream forever with zero subscribers (#2935).
|
|
@@ -3084,7 +3488,8 @@ function clearStatus(el, clearUninitialized = false) {
|
|
|
3084
3488
|
GlobalQueue._updateChildCompanions !== null
|
|
3085
3489
|
)
|
|
3086
3490
|
GlobalQueue._updateChildCompanions(el);
|
|
3087
|
-
|
|
3491
|
+
const notify = statusNotifierOf(el);
|
|
3492
|
+
if (notify) notify.call(el);
|
|
3088
3493
|
}
|
|
3089
3494
|
function notifyStatus(el, status, error, blockStatus, lane) {
|
|
3090
3495
|
// Wrap regular errors to track source node
|
|
@@ -3113,7 +3518,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3113
3518
|
status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
|
|
3114
3519
|
ext(el)._error = error;
|
|
3115
3520
|
}
|
|
3116
|
-
GlobalQueue._updatePendingSignal
|
|
3521
|
+
GlobalQueue._updatePendingSignal?.(el);
|
|
3117
3522
|
if (
|
|
3118
3523
|
el._x?._child &&
|
|
3119
3524
|
el._config & CONFIG_CHILD_COMPANIONS &&
|
|
@@ -3126,14 +3531,15 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3126
3531
|
}
|
|
3127
3532
|
const downstreamBlockStatus = blockStatus || startsBlocking;
|
|
3128
3533
|
const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
|
|
3129
|
-
|
|
3534
|
+
const elNotify = statusNotifierOf(el);
|
|
3535
|
+
if (elNotify) {
|
|
3130
3536
|
if (blockStatus && status === STATUS_PENDING) {
|
|
3131
3537
|
return;
|
|
3132
3538
|
}
|
|
3133
3539
|
if (downstreamBlockStatus) {
|
|
3134
|
-
|
|
3540
|
+
elNotify.call(el, status, error);
|
|
3135
3541
|
} else {
|
|
3136
|
-
|
|
3542
|
+
elNotify.call(el);
|
|
3137
3543
|
}
|
|
3138
3544
|
return;
|
|
3139
3545
|
}
|
|
@@ -3257,7 +3663,15 @@ function recompute(el, create = false) {
|
|
|
3257
3663
|
if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
|
|
3258
3664
|
globalQueue.initTransition(el._transition);
|
|
3259
3665
|
deleteFromHeap(el, queueFor(el));
|
|
3260
|
-
if (el._x !== null)
|
|
3666
|
+
if (el._x !== null) {
|
|
3667
|
+
el._x._inFlight = null;
|
|
3668
|
+
// Supersede is where an iterator flight dies (#3122): close it now.
|
|
3669
|
+
// Its cleanup(close) registration may sit in a zombie-deferred
|
|
3670
|
+
// disposal list that a held transition only drains when the
|
|
3671
|
+
// SUPERSEDING flight settles — cancellation must not wait for the
|
|
3672
|
+
// work that replaced it. Idempotent with the cleanup-channel close.
|
|
3673
|
+
releaseFlightTeardown(el);
|
|
3674
|
+
}
|
|
3261
3675
|
// Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
|
|
3262
3676
|
if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
|
|
3263
3677
|
else if (el._firstChild !== null || el._disposal !== null) {
|
|
@@ -3281,6 +3695,14 @@ function recompute(el, create = false) {
|
|
|
3281
3695
|
// recovers to an unchanged value, dependents still holding this object must
|
|
3282
3696
|
// be swept (settleErroredDependents, #2949).
|
|
3283
3697
|
const outgoingError = el._statusFlags & STATUS_ERROR ? el._x?._error : undefined;
|
|
3698
|
+
// Pending SOURCE-hood, captured before the compute clears status: a node
|
|
3699
|
+
// whose own flight parked dependents self-registers in _pendingSources
|
|
3700
|
+
// (notifyStatus, isSource). If this recompute supersedes that flight and
|
|
3701
|
+
// settles synchronously, those dependents settle HERE — asyncWrite's
|
|
3702
|
+
// settlePendingSource walk never runs for a landing that was preempted
|
|
3703
|
+
// (#3181).
|
|
3704
|
+
const wasPendingSource =
|
|
3705
|
+
(el._statusFlags & STATUS_PENDING) !== 0 && el._x?._pendingSources?.has(el) === true;
|
|
3284
3706
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
3285
3707
|
// the recompute wipes _flags below.
|
|
3286
3708
|
const hadReask = (el._flags & REACTIVE_REASK) !== 0;
|
|
@@ -3455,7 +3877,14 @@ function recompute(el, create = false) {
|
|
|
3455
3877
|
// values directly — the pending round-trip (queuePendingNode +
|
|
3456
3878
|
// commitPendingNodes) exists to sequence transition reveals, and
|
|
3457
3879
|
// paying it per effect on the plain path is pure overhead.
|
|
3458
|
-
|
|
3880
|
+
// DIRECT_COMMIT effects (resolve/until) commit directly even under
|
|
3881
|
+
// their own held transition: their applies deliver on a microtask,
|
|
3882
|
+
// not the stashed queues, so a staged value would hand the immediate
|
|
3883
|
+
// apply stale state — see CONFIG_DIRECT_COMMIT.
|
|
3884
|
+
(isEffect &&
|
|
3885
|
+
(activeTransition !== el._transition ||
|
|
3886
|
+
activeTransition === null ||
|
|
3887
|
+
el._config & CONFIG_DIRECT_COMMIT)) ||
|
|
3459
3888
|
isOptimisticDirty
|
|
3460
3889
|
// NOTE (stage-3, 2026-08-21): a quiet-world MEMO direct-commit was
|
|
3461
3890
|
// attempted here and REVERTED — memo staging is load-bearing beyond
|
|
@@ -3501,6 +3930,12 @@ function recompute(el, create = false) {
|
|
|
3501
3930
|
if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
|
|
3502
3931
|
el._pendingValue = value;
|
|
3503
3932
|
if (wasLoading) el._loading = true; // see the held branch above (#2990)
|
|
3933
|
+
// A authoritative-view reader (until()) observed this node past its
|
|
3934
|
+
// override — and "authoritative arrival equal to the override" is
|
|
3935
|
+
// exactly the acknowledgment it waits for. Wake those readers only;
|
|
3936
|
+
// A17 silence holds for every ordinary subscriber. (Hook installed by
|
|
3937
|
+
// until(), the only setter of the gating bit.)
|
|
3938
|
+
if (el._config & CONFIG_AUTHORITATIVE_OBSERVED) GlobalQueue._notifyAuthoritativeObservers(el);
|
|
3504
3939
|
} else if (el._height != oldHeight) {
|
|
3505
3940
|
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
3506
3941
|
insertIntoHeapHeight(s._sub, queueFor(s._sub));
|
|
@@ -3513,6 +3948,17 @@ function recompute(el, create = false) {
|
|
|
3513
3948
|
// (el._x?._error re-set), so this only runs on a genuinely clean recovery.
|
|
3514
3949
|
if (outgoingError !== undefined && !valueChanged && !el._x?._error)
|
|
3515
3950
|
settleErroredDependents(el, outgoingError);
|
|
3951
|
+
// Pending twin of the sweep above (#3181): a flight superseded by this
|
|
3952
|
+
// synchronous settle leaves every registered dependent still flagged
|
|
3953
|
+
// STATUS_PENDING with a pending source that will never land — asyncWrite
|
|
3954
|
+
// owns the walk only for flights that actually land. An unchanged value
|
|
3955
|
+
// is the dangerous shape (a projection reconciling in place: a memo over
|
|
3956
|
+
// it re-throws its cached NotReadyError forever, and every reader that
|
|
3957
|
+
// suspended through that memo re-parks on the dead source), but the walk
|
|
3958
|
+
// runs on the changed shape too, exactly as the landing path does —
|
|
3959
|
+
// insertSubs notifies value SUBSCRIBERS, not pending REGISTRANTS, and
|
|
3960
|
+
// the two sets only partially overlap.
|
|
3961
|
+
if (wasPendingSource && !(el._statusFlags & STATUS_PENDING)) settlePendingSource(el);
|
|
3516
3962
|
}
|
|
3517
3963
|
// Attribution hook: fired before the lane restore so `currentOptimisticLane`
|
|
3518
3964
|
// still reflects THIS run's posture. The facts distinguish an overlay
|
|
@@ -3561,7 +4007,9 @@ function updateIfNecessary(el) {
|
|
|
3561
4007
|
// (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
|
|
3562
4008
|
// A mid-pass mark stays latched for recompute's own tail to reschedule
|
|
3563
4009
|
// (#3037); readers meanwhile serve the values the pass has so far.
|
|
3564
|
-
|
|
4010
|
+
// Never recompute a DISPOSED node either: recompute rewrites _flags and
|
|
4011
|
+
// would resurrect it (#2983) — readers serve its last value.
|
|
4012
|
+
if (el._flags & (REACTIVE_RECOMPUTING_DEPS | REACTIVE_DISPOSED)) return;
|
|
3565
4013
|
if (el._flags & REACTIVE_CHECK) {
|
|
3566
4014
|
for (let d = el._deps; d; d = d._nextDep) {
|
|
3567
4015
|
const dep1 = d._dep;
|
|
@@ -3597,7 +4045,7 @@ function computed(fn, options) {
|
|
|
3597
4045
|
(options?.sync ? CONFIG_SYNC : 0) |
|
|
3598
4046
|
(options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0) |
|
|
3599
4047
|
(snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
3600
|
-
_equals: options?.equals
|
|
4048
|
+
_equals: options?.equals ?? isEqual,
|
|
3601
4049
|
_disposal: null,
|
|
3602
4050
|
_queue: context?._queue ?? globalQueue,
|
|
3603
4051
|
_context: context?._context ?? defaultContext,
|
|
@@ -3649,6 +4097,7 @@ function ext(el) {
|
|
|
3649
4097
|
_parentSource: undefined,
|
|
3650
4098
|
_affectsCount: 0,
|
|
3651
4099
|
_inFlight: null,
|
|
4100
|
+
_flightTeardown: null,
|
|
3652
4101
|
_error: undefined,
|
|
3653
4102
|
_blocked: undefined,
|
|
3654
4103
|
_pendingSources: undefined,
|
|
@@ -3668,7 +4117,7 @@ function ext(el) {
|
|
|
3668
4117
|
* mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip
|
|
3669
4118
|
* the auto-dispose CONFIG bit (effect() previously cleared it post-construction).
|
|
3670
4119
|
*/
|
|
3671
|
-
function createEffectNode(fn, effectFn, errorFn, type,
|
|
4120
|
+
function createEffectNode(fn, effectFn, errorFn, type, options) {
|
|
3672
4121
|
const transparent = options?.transparent ?? false;
|
|
3673
4122
|
const self = {
|
|
3674
4123
|
id: inheritId(options, transparent, context),
|
|
@@ -3676,6 +4125,7 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
|
|
|
3676
4125
|
(transparent ? CONFIG_TRANSPARENT : 0) |
|
|
3677
4126
|
(options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
|
|
3678
4127
|
(options?.sync ? CONFIG_SYNC : 0) |
|
|
4128
|
+
(options?._extraConfig ?? 0) |
|
|
3679
4129
|
(snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
3680
4130
|
_equals: false,
|
|
3681
4131
|
_disposal: null,
|
|
@@ -3712,12 +4162,35 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
|
|
|
3712
4162
|
_x: null
|
|
3713
4163
|
};
|
|
3714
4164
|
self._name = options?.name ?? "effect";
|
|
3715
|
-
//
|
|
3716
|
-
|
|
4165
|
+
// Effects dispatch status through the SHARED notifier (statusNotifierOf,
|
|
4166
|
+
// keyed off _type) — storing it per node forced a full NodeExtension
|
|
4167
|
+
// allocation on EVERY effect at creation (an alloc + 19 field stores,
|
|
4168
|
+
// +23% effect creation, caught by the creation benches). Only genuinely
|
|
4169
|
+
// per-node channels (boundaries) live on _x.
|
|
3717
4170
|
if (options?.unobserved) ext(self)._unobserved = options.unobserved;
|
|
3718
4171
|
setupComputedNode(self, lazyOptions);
|
|
3719
4172
|
return self;
|
|
3720
4173
|
}
|
|
4174
|
+
/**
|
|
4175
|
+
* The shared status notifier for effect nodes, installed once by effect.ts
|
|
4176
|
+
* at module evaluation (`this`-dispatched — one function serves every
|
|
4177
|
+
* effect, so nodes never store it). Boundary computeds keep their own
|
|
4178
|
+
* per-node channel on `_x._notifyStatus`, which takes precedence.
|
|
4179
|
+
*/
|
|
4180
|
+
let effectStatusNotify = null;
|
|
4181
|
+
function setEffectStatusNotify(fn) {
|
|
4182
|
+
effectStatusNotify = fn;
|
|
4183
|
+
}
|
|
4184
|
+
/** Resolve a node's status notifier: an own `_x` channel (boundaries) wins;
|
|
4185
|
+
* effect nodes (`_type` — EFFECT_PURE is 0, and only effect literals carry
|
|
4186
|
+
* the field) fall back to the shared notifier. Presence doubles as the
|
|
4187
|
+
* "display consumer" membership test in the status walks, exactly as the
|
|
4188
|
+
* per-node field did when every effect carried one. */
|
|
4189
|
+
function statusNotifierOf(el) {
|
|
4190
|
+
const own = el._x?._notifyStatus;
|
|
4191
|
+
if (own !== undefined) return own;
|
|
4192
|
+
return el._type ? (effectStatusNotify ?? undefined) : undefined;
|
|
4193
|
+
}
|
|
3721
4194
|
const lazyOptions = { lazy: true };
|
|
3722
4195
|
function setupComputedNode(self, options) {
|
|
3723
4196
|
self._prevHeap = self;
|
|
@@ -3757,7 +4230,7 @@ function setupComputedNode(self, options) {
|
|
|
3757
4230
|
}
|
|
3758
4231
|
function signal(v, options, firewall = null) {
|
|
3759
4232
|
const s = {
|
|
3760
|
-
_equals: options?.equals
|
|
4233
|
+
_equals: options?.equals ?? isEqual,
|
|
3761
4234
|
_config:
|
|
3762
4235
|
(options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
|
|
3763
4236
|
(options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0),
|
|
@@ -3894,6 +4367,35 @@ const READ_SLOW = Symbol("read-slow");
|
|
|
3894
4367
|
* snapshot / transition / lane / dev-strictRead state all take the full
|
|
3895
4368
|
* resolution. Anything slow returns READ_SLOW; the caller then calls read().
|
|
3896
4369
|
*/
|
|
4370
|
+
/**
|
|
4371
|
+
* Wake only authoritative-view readers (until() predicates) subscribed to `el`.
|
|
4372
|
+
* The A17-silent ack paths — an authoritative arrival equal to the active
|
|
4373
|
+
* override — use this so the predicate re-evaluates without re-firing
|
|
4374
|
+
* ordinary subscribers whose visible (override) value did not change.
|
|
4375
|
+
* Pay-for-use: reached through GlobalQueue._notifyAuthoritativeObservers,
|
|
4376
|
+
* installed at first until() call — apps that never use until() shake it.
|
|
4377
|
+
*/
|
|
4378
|
+
function notifyAuthoritativeObservers(el) {
|
|
4379
|
+
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
4380
|
+
const sub = s._sub;
|
|
4381
|
+
if (!(sub._config & CONFIG_AUTHORITATIVE_READ)) continue;
|
|
4382
|
+
// Missed-wake latch (#3037), same contract as insertSubs: the reader may
|
|
4383
|
+
// itself have pulled this recompute (updateIfNecessary from its own
|
|
4384
|
+
// read), and the heap refuses RECOMPUTING nodes — latch so recompute's
|
|
4385
|
+
// tail reschedules it with the staged value visible.
|
|
4386
|
+
if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
|
|
4387
|
+
sub._flags |= REACTIVE_MISSED_WAKE;
|
|
4388
|
+
enqueueSub(sub);
|
|
4389
|
+
}
|
|
4390
|
+
schedule();
|
|
4391
|
+
}
|
|
4392
|
+
/** Installs the until() machinery hook. Idempotent; called by until() before
|
|
4393
|
+
* any authoritative-view read happens (same late-binding contract as the
|
|
4394
|
+
* optimistic engine). */
|
|
4395
|
+
function installAuthoritativeRead() {
|
|
4396
|
+
if (GlobalQueue._notifyAuthoritativeObservers === null)
|
|
4397
|
+
GlobalQueue._notifyAuthoritativeObservers = notifyAuthoritativeObservers;
|
|
4398
|
+
}
|
|
3897
4399
|
function readNodeFast(el) {
|
|
3898
4400
|
if (
|
|
3899
4401
|
latestReadActive ||
|
|
@@ -3973,6 +4475,13 @@ function read(el) {
|
|
|
3973
4475
|
markHeap(elQueue);
|
|
3974
4476
|
updateIfNecessary(owner);
|
|
3975
4477
|
}
|
|
4478
|
+
// Fresh-pull readers (awaitable refresh's waiter) recompute a dirty
|
|
4479
|
+
// source inline even when the height gate defers to the flush: the
|
|
4480
|
+
// waiter must park on the re-ask's window (or serve its sync answer),
|
|
4481
|
+
// never read the PRE-re-ask value as settled. Self-guarded: a clean
|
|
4482
|
+
// node no-ops and updateIfNecessary refuses disposed nodes (#2983) —
|
|
4483
|
+
// a dead target serves its last value, which is already quiescent.
|
|
4484
|
+
else if (c._config & CONFIG_FRESH_READ) updateIfNecessary(owner);
|
|
3976
4485
|
const height = owner._height;
|
|
3977
4486
|
// parent check is shallow, might need to be recursive
|
|
3978
4487
|
if (height >= c._height && el._parent !== c) {
|
|
@@ -4051,8 +4560,18 @@ function read(el) {
|
|
|
4051
4560
|
nodeName: owner?._name
|
|
4052
4561
|
});
|
|
4053
4562
|
if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
|
|
4054
|
-
// A17: the override IS the value for every reader
|
|
4055
|
-
|
|
4563
|
+
// A17: the override IS the value for every reader — except an authoritative
|
|
4564
|
+
// reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
|
|
4565
|
+
// observe independently-arriving truth, and serving it the caller's own
|
|
4566
|
+
// tentative write would trivially satisfy the predicate. The bit is checked
|
|
4567
|
+
// on the reading computation itself, so a shared computed the predicate
|
|
4568
|
+
// pulls recomputes as ITSELF (context = the memo, no bit) under the normal
|
|
4569
|
+
// view. Fall through to normal value selection (staged `_pendingValue` is
|
|
4570
|
+
// authoritative — optimism never lives there); the sticky mark makes the
|
|
4571
|
+
// A17-silent "landing equals override" paths notify this node's subs so
|
|
4572
|
+
// the reader re-runs when truth arrives.
|
|
4573
|
+
if (!(c && c._config & CONFIG_AUTHORITATIVE_READ)) return unwrapOverride(el._x?._overrideValue);
|
|
4574
|
+
el._config |= CONFIG_AUTHORITATIVE_OBSERVED;
|
|
4056
4575
|
}
|
|
4057
4576
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
4058
4577
|
// a pending mid-transition write sees the committed value. Projection-store
|
|
@@ -4080,7 +4599,18 @@ function read(el) {
|
|
|
4080
4599
|
(currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
|
|
4081
4600
|
el._pendingValue === NOT_PENDING ||
|
|
4082
4601
|
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
4083
|
-
(stale && el._transition && activeTransition !== el._transition)
|
|
4602
|
+
(stale && el._transition && activeTransition !== el._transition) ||
|
|
4603
|
+
// A17 for HELD truth (#3164, see CONFIG_HELD_TRUTH): staged confirming
|
|
4604
|
+
// truth — fold-staged onto an armed family, or entangle-stolen by an
|
|
4605
|
+
// awaited until() — is masked from ordinary readers until its
|
|
4606
|
+
// transaction's reveal; the retaining transaction's own speculative
|
|
4607
|
+
// recomputes included (partial override coverage would otherwise
|
|
4608
|
+
// compose override + staged truth into a state no timeline contains).
|
|
4609
|
+
// Authoritative readers (until()'s predicate) and latest() see the
|
|
4610
|
+
// staged truth — the tunnel that keeps the hold deadlock-free.
|
|
4611
|
+
(el._config & CONFIG_HELD_TRUTH &&
|
|
4612
|
+
!latestReadActive &&
|
|
4613
|
+
!(c._config & CONFIG_AUTHORITATIVE_READ))
|
|
4084
4614
|
? el._value
|
|
4085
4615
|
: el._pendingValue;
|
|
4086
4616
|
// Record that this isPending() probe observed the fresh pending value, so
|
|
@@ -4094,7 +4624,14 @@ function read(el) {
|
|
|
4094
4624
|
!(owner._statusFlags & STATUS_PENDING) &&
|
|
4095
4625
|
!el._subs
|
|
4096
4626
|
) {
|
|
4097
|
-
unobserved(
|
|
4627
|
+
// Deferred, not inline (#3078): an inline unobserved() here made untracked
|
|
4628
|
+
// reads destructive — dispose on this read, full revival recompute on the
|
|
4629
|
+
// next — so consecutive reads could answer differently with no write in
|
|
4630
|
+
// between (the revival samples the ambient transition/lane context).
|
|
4631
|
+
// The sweep at flush finalization re-validates and reclaims; schedule()
|
|
4632
|
+
// guarantees that flush happens even if nothing else is queued.
|
|
4633
|
+
dormantNodes.add(el);
|
|
4634
|
+
schedule();
|
|
4098
4635
|
}
|
|
4099
4636
|
return value;
|
|
4100
4637
|
}
|
|
@@ -4118,9 +4655,17 @@ function devGuardStoreSetterWrite() {
|
|
|
4118
4655
|
ownerName: context._name,
|
|
4119
4656
|
data: { operation: "setStore" }
|
|
4120
4657
|
});
|
|
4121
|
-
|
|
4658
|
+
// the owner name reaches the THROWN message too, not just the
|
|
4659
|
+
// diagnostics channel apps don't subscribe to by default (#3157)
|
|
4660
|
+
throw new Error(ownedScopeWriteMessage(context));
|
|
4122
4661
|
}
|
|
4123
4662
|
}
|
|
4663
|
+
function ownedScopeWriteMessage(owner) {
|
|
4664
|
+
const name = owner._name;
|
|
4665
|
+
return name
|
|
4666
|
+
? `${REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE} (in ${name})`
|
|
4667
|
+
: REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE;
|
|
4668
|
+
}
|
|
4124
4669
|
function setSignal(el, v) {
|
|
4125
4670
|
if (
|
|
4126
4671
|
!(el._config & CONFIG_OWNED_WRITE) &&
|
|
@@ -4138,7 +4683,7 @@ function setSignal(el, v) {
|
|
|
4138
4683
|
nodeName: el._name,
|
|
4139
4684
|
data: { operation: "setSignal" }
|
|
4140
4685
|
});
|
|
4141
|
-
throw new Error(
|
|
4686
|
+
throw new Error(ownedScopeWriteMessage(context));
|
|
4142
4687
|
}
|
|
4143
4688
|
if (el._transition && activeTransition !== el._transition)
|
|
4144
4689
|
globalQueue.initTransition(el._transition);
|
|
@@ -4264,41 +4809,13 @@ function staleValues(fn, set = true) {
|
|
|
4264
4809
|
}
|
|
4265
4810
|
}
|
|
4266
4811
|
/**
|
|
4267
|
-
*
|
|
4268
|
-
*
|
|
4269
|
-
*
|
|
4270
|
-
*
|
|
4271
|
-
*
|
|
4272
|
-
* write-like invalidation operation: it does not read the target's value, and
|
|
4273
|
-
* refreshing a plain signal accessor is a no-op.
|
|
4274
|
-
*
|
|
4275
|
-
* Use it to invalidate cached async values (e.g. force a re-fetch) without
|
|
4276
|
-
* tearing the consumer down.
|
|
4277
|
-
*
|
|
4278
|
-
* @example
|
|
4279
|
-
* ```ts
|
|
4280
|
-
* const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
|
|
4281
|
-
*
|
|
4282
|
-
* // Re-fetch on demand
|
|
4283
|
-
* <button onClick={() => refresh(user)}>Reload</button>
|
|
4284
|
-
* ```
|
|
4812
|
+
* Core marking half of `refresh()` (the public wrapper lives in signals.ts —
|
|
4813
|
+
* it validates the target, marks through here, then builds the quiescence
|
|
4814
|
+
* promise on the resolve()/until() effect machinery). Flags the node's next
|
|
4815
|
+
* recompute as a quiet re-ask and schedules it; no-ops for non-derived or
|
|
4816
|
+
* disposed targets and for same-tick manual writes.
|
|
4285
4817
|
*/
|
|
4286
|
-
function
|
|
4287
|
-
const node = target?.[$REFRESH];
|
|
4288
|
-
if (!node) {
|
|
4289
|
-
{
|
|
4290
|
-
const message =
|
|
4291
|
-
"[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
|
|
4292
|
-
"Pass the original source target, not a wrapper function or derived property read.";
|
|
4293
|
-
emitDiagnostic({
|
|
4294
|
-
code: "INVALID_REFRESH_TARGET",
|
|
4295
|
-
kind: "write",
|
|
4296
|
-
severity: "error",
|
|
4297
|
-
message
|
|
4298
|
-
});
|
|
4299
|
-
throw new Error(message);
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4818
|
+
function markRefresh(node) {
|
|
4302
4819
|
if (
|
|
4303
4820
|
context &&
|
|
4304
4821
|
!((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
|
|
@@ -4617,6 +5134,9 @@ function runLaneEffects(type) {
|
|
|
4617
5134
|
runQueue(effects, type);
|
|
4618
5135
|
}
|
|
4619
5136
|
}
|
|
5137
|
+
// Optimistic patch applications ride the same visibility slot as lane
|
|
5138
|
+
// effects (in-flight DOM updates); no-op unless patches registered.
|
|
5139
|
+
if (type === EFFECT_RENDER) GlobalQueue._drainPatchOptimistic?.();
|
|
4620
5140
|
}
|
|
4621
5141
|
function cleanupCompletedLanes(completingTransition) {
|
|
4622
5142
|
for (const lane of activeLanes) {
|
|
@@ -5107,7 +5627,22 @@ function latestRead(el) {
|
|
|
5107
5627
|
!(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
5108
5628
|
) {
|
|
5109
5629
|
markHeap(queue);
|
|
5110
|
-
|
|
5630
|
+
// Suspend probe collection during the pull (mirrors pendingCheckRead's
|
|
5631
|
+
// prepare): a probe through latest() answers for the SHADOW — the
|
|
5632
|
+
// read() dispatch collects it deliberately, so the verdict reflects
|
|
5633
|
+
// async still in flight for the latest view, not the parent's held
|
|
5634
|
+
// write. A stale shadow recomputing HERE ran its `read(parent)` with
|
|
5635
|
+
// the probe still live and collected the parent too, so the verdict
|
|
5636
|
+
// depended on whether anything had pulled the shadow current earlier
|
|
5637
|
+
// in the tick (#3104: reading latest(m) flipped a later
|
|
5638
|
+
// latest(() => isPending(x)) from true to false).
|
|
5639
|
+
const prevCheck = pendingCheckActive;
|
|
5640
|
+
setPendingCheckActive(false);
|
|
5641
|
+
try {
|
|
5642
|
+
prepareComputed(pendingComputed, true);
|
|
5643
|
+
} finally {
|
|
5644
|
+
setPendingCheckActive(prevCheck);
|
|
5645
|
+
}
|
|
5111
5646
|
}
|
|
5112
5647
|
value = read(pendingComputed);
|
|
5113
5648
|
} catch (e) {
|
|
@@ -5137,12 +5672,42 @@ function latestRead(el) {
|
|
|
5137
5672
|
return pendingComputed._pendingValue;
|
|
5138
5673
|
return value;
|
|
5139
5674
|
}
|
|
5675
|
+
/**
|
|
5676
|
+
* A latest() shadow that is uninitialized only because it was CREATED during
|
|
5677
|
+
* an active flight — its parent source already has a committed value, so
|
|
5678
|
+
* latest() serves that as the visible value and a tracked reader has
|
|
5679
|
+
* something to pair a verdict with (#3166). Same parent resolution as
|
|
5680
|
+
* computePendingState. The pending signal companion also carries
|
|
5681
|
+
* `_parentSource` but is a plain signal (no `_fn`) that never goes pending,
|
|
5682
|
+
* so the `_fn` check is belt-and-braces for this call site.
|
|
5683
|
+
*/
|
|
5684
|
+
function latestShadowWithInitializedParent(owner) {
|
|
5685
|
+
if (typeof owner._fn !== "function") return false;
|
|
5686
|
+
const parentNode = owner._x?._parentSource;
|
|
5687
|
+
if (parentNode === undefined) return false;
|
|
5688
|
+
const parent = parentNode._firewall || parentNode;
|
|
5689
|
+
return !(parent._statusFlags & STATUS_UNINITIALIZED);
|
|
5690
|
+
}
|
|
5140
5691
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
5141
5692
|
function pendingCheckRead(el, c, owner, firewall) {
|
|
5142
5693
|
setPendingCheckActive(false);
|
|
5143
5694
|
if (typeof el._fn === "function") prepareComputed(el, true);
|
|
5144
5695
|
const ownerStatus = owner._statusFlags;
|
|
5145
|
-
if (
|
|
5696
|
+
if (
|
|
5697
|
+
c &&
|
|
5698
|
+
ownerStatus & STATUS_PENDING &&
|
|
5699
|
+
ownerStatus & STATUS_UNINITIALIZED &&
|
|
5700
|
+
// The suspend-throw is for a genuinely-first-load source: the tracked
|
|
5701
|
+
// reader has nothing to pair a verdict with, so it parks on the source.
|
|
5702
|
+
// A latest() SHADOW created lazily mid-flight is born uninitialized even
|
|
5703
|
+
// though its parent has a committed value latest() will serve — throwing
|
|
5704
|
+
// here (swallowed by latestRead's fallback) dropped the shadow from the
|
|
5705
|
+
// probe, so a tracked latest(isPending()) probe created during a
|
|
5706
|
+
// new-question flight cached `false` for that whole flight (#3166).
|
|
5707
|
+
// Defer to the PARENT's initialization state and fall through to normal
|
|
5708
|
+
// collection; the plain pending throw downstream still links the reader.
|
|
5709
|
+
!latestShadowWithInitializedParent(owner)
|
|
5710
|
+
) {
|
|
5146
5711
|
if (tracking && el !== c) link(el, c);
|
|
5147
5712
|
setPendingCheckActive(true);
|
|
5148
5713
|
throw owner._x?._error;
|
|
@@ -5161,8 +5726,22 @@ function pendingCheckRead(el, c, owner, firewall) {
|
|
|
5161
5726
|
*/
|
|
5162
5727
|
function heldAwaitingAsync(el) {
|
|
5163
5728
|
const et = el._transition;
|
|
5164
|
-
const t = et ? currentTransition(et) :
|
|
5729
|
+
const t = et ? currentTransition(et) : activeTransition;
|
|
5165
5730
|
if (!t || t._done) return false;
|
|
5731
|
+
// A plain staged write (a signal/store leaf — no _fn) held while an action
|
|
5732
|
+
// is still running is an INPUT to a computation still in flight (#3078):
|
|
5733
|
+
// the pairing rule must not suppress the verdict, or a memo recomputing
|
|
5734
|
+
// mid-action reads the staged value, gets told "not pending", and
|
|
5735
|
+
// disagrees with a direct isPending() probe for the whole action window.
|
|
5736
|
+
// A computed's staged value is the opposite case — a LANDED answer
|
|
5737
|
+
// awaiting reveal — where the pairing rule stands even inside an open
|
|
5738
|
+
// action (#2831: a reader that saw the new value must not also see
|
|
5739
|
+
// pending); still-computing answers are covered by the reporter scan.
|
|
5740
|
+
if (t._actions.length && !el._fn) return true;
|
|
5741
|
+
// A node not yet stamped with a transition only qualifies through the
|
|
5742
|
+
// action check above; the reporter scan below is for transition-held
|
|
5743
|
+
// writes whose source async is still computing.
|
|
5744
|
+
if (!et) return false;
|
|
5166
5745
|
for (const [source, reporters] of t._asyncReporters) {
|
|
5167
5746
|
if (
|
|
5168
5747
|
reporters.size &&
|
|
@@ -5209,6 +5788,17 @@ function isPending(fn) {
|
|
|
5209
5788
|
});
|
|
5210
5789
|
const collectPending = () => {
|
|
5211
5790
|
setPendingCheckActive(false);
|
|
5791
|
+
// Companion reads are mode-neutral plumbing: under an outer latest()
|
|
5792
|
+
// (isPending inside a latest window — #3104's memo shape) leaving latest
|
|
5793
|
+
// mode active dispatched these reads through latestRead, which built a
|
|
5794
|
+
// SHADOW OF THE PENDING SIGNAL itself. The next updatePendingSignal then
|
|
5795
|
+
// wrote that companion-on-companion from inside a recompute
|
|
5796
|
+
// (syncCompanions → setSignal on a shadow created without ownedWrite)
|
|
5797
|
+
// and halted dev with the owned-scope write guard. The creation paths
|
|
5798
|
+
// (getLatestValueComputed / getPendingSignal) already suspend both
|
|
5799
|
+
// modes; this read site must too.
|
|
5800
|
+
const prevLatest = latestReadActive;
|
|
5801
|
+
setLatestReadActive(false);
|
|
5212
5802
|
const prevStrictRead = strictRead;
|
|
5213
5803
|
setStrictRead(false);
|
|
5214
5804
|
try {
|
|
@@ -5220,6 +5810,7 @@ function isPending(fn) {
|
|
|
5220
5810
|
});
|
|
5221
5811
|
} finally {
|
|
5222
5812
|
setStrictRead(prevStrictRead);
|
|
5813
|
+
setLatestReadActive(prevLatest);
|
|
5223
5814
|
setPendingCheckActive(true);
|
|
5224
5815
|
}
|
|
5225
5816
|
// A "not pending" verdict that exists only because this reader saw the
|
|
@@ -5279,7 +5870,6 @@ function effect(compute, effect, error, options) {
|
|
|
5279
5870
|
effect,
|
|
5280
5871
|
error,
|
|
5281
5872
|
isUser ? EFFECT_USER : EFFECT_RENDER,
|
|
5282
|
-
notifyEffectStatus,
|
|
5283
5873
|
options
|
|
5284
5874
|
);
|
|
5285
5875
|
recompute(node, true);
|
|
@@ -5452,17 +6042,9 @@ function trackedEffect(fn, options) {
|
|
|
5452
6042
|
node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
|
|
5453
6043
|
node._modified = true;
|
|
5454
6044
|
node._type = EFFECT_TRACKED;
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
node._queue.notify(node, STATUS_PENDING, 0);
|
|
5459
|
-
const err = error !== undefined ? error : node._x?._error;
|
|
5460
|
-
if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
|
|
5461
|
-
haltReactivity(unwrapStatusError(err));
|
|
5462
|
-
throw err;
|
|
5463
|
-
}
|
|
5464
|
-
}
|
|
5465
|
-
};
|
|
6045
|
+
// Status dispatch rides the SHARED notifier (statusNotifierOf keys off
|
|
6046
|
+
// _type): its error arm is behavior-identical to the closure that used to
|
|
6047
|
+
// live here, without the per-node NodeExtension allocation.
|
|
5466
6048
|
node._run = run;
|
|
5467
6049
|
node._queue.enqueue(EFFECT_USER, run);
|
|
5468
6050
|
if (!node._parent) {
|
|
@@ -5480,6 +6062,10 @@ function trackedEffect(fn, options) {
|
|
|
5480
6062
|
console.warn(message);
|
|
5481
6063
|
}
|
|
5482
6064
|
}
|
|
6065
|
+
// Install the shared effect status notifier (statusNotifierOf serves it to
|
|
6066
|
+
// every effect node) — module-scope: any bundle that creates effects
|
|
6067
|
+
// evaluates this module.
|
|
6068
|
+
setEffectStatusNotify(notifyEffectStatus);
|
|
5483
6069
|
|
|
5484
6070
|
const ACTION_CALLED_IN_OWNED_SCOPE_MESSAGE =
|
|
5485
6071
|
"[ACTION_CALLED_IN_OWNED_SCOPE] Calling an action inside an owned scope (component, computation) is not allowed. " +
|
|
@@ -5948,8 +6534,256 @@ function resolve(fn) {
|
|
|
5948
6534
|
rej(err);
|
|
5949
6535
|
dispose();
|
|
5950
6536
|
},
|
|
5951
|
-
|
|
6537
|
+
// DIRECT_COMMIT: a source settling INTO the held transaction (e.g. a
|
|
6538
|
+
// refresh this action issued) stages its landing; the effect's own
|
|
6539
|
+
// recompute must not stage too, or the microtask apply reads the
|
|
6540
|
+
// stale mainline value and resolves with old data.
|
|
6541
|
+
{ user: true, _extraConfig: CONFIG_DIRECT_COMMIT }
|
|
6542
|
+
);
|
|
6543
|
+
});
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
/**
|
|
6547
|
+
* Invalidates one reactive source, forcing it to re-execute even if its inputs
|
|
6548
|
+
* haven't changed, and returns a promise for the target's NEXT QUIESCENT
|
|
6549
|
+
* STATE — the re-ask (and anything that supersedes it) has settled.
|
|
6550
|
+
*
|
|
6551
|
+
* Pass either a Solid-created accessor or a projected store created from
|
|
6552
|
+
* `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
|
|
6553
|
+
* write-like invalidation operation: it does not read the target's value, and
|
|
6554
|
+
* refreshing a plain signal accessor is a no-op that resolves immediately.
|
|
6555
|
+
*
|
|
6556
|
+
* The returned promise is safe to ignore (fire-and-forget refresh is
|
|
6557
|
+
* unchanged, and a failed refetch will not surface an unhandled rejection).
|
|
6558
|
+
* Awaiting it gives imperative flows the settle point without a reactive
|
|
6559
|
+
* read:
|
|
6560
|
+
* - Accessor targets resolve with the settled value; store targets resolve
|
|
6561
|
+
* with the store node passed (reads through it are fresh after the await).
|
|
6562
|
+
* - A failed re-ask rejects with the error (inside an action's generator,
|
|
6563
|
+
* `yield refresh(x)` throws back at the yield point and the action reverts
|
|
6564
|
+
* like any other failure).
|
|
6565
|
+
* - Semantics are quiescence, not flight identity: if another refresh (or
|
|
6566
|
+
* any invalidation) supersedes this one mid-flight, the promise waits for
|
|
6567
|
+
* — and delivers — whatever finally lands.
|
|
6568
|
+
* - Inside an action, truth landing into the held transaction is STAGED;
|
|
6569
|
+
* the promise still settles then (matching `resolve()`/`until()`, #2930)
|
|
6570
|
+
* and delivers the staged value — the caller's own optimistic override is
|
|
6571
|
+
* never the delivered value.
|
|
6572
|
+
* - The re-ask itself stays verdict-quiet exactly as before: `isPending`
|
|
6573
|
+
* does not flip for a bare refresh (pair with `affects()` for a visible
|
|
6574
|
+
* pending window).
|
|
6575
|
+
*
|
|
6576
|
+
* @example
|
|
6577
|
+
* ```ts
|
|
6578
|
+
* const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
|
|
6579
|
+
*
|
|
6580
|
+
* // Fire-and-forget re-fetch
|
|
6581
|
+
* <button onClick={() => refresh(user)}>Reload</button>;
|
|
6582
|
+
*
|
|
6583
|
+
* // Imperative settle point
|
|
6584
|
+
* const fresh = await refresh(user);
|
|
6585
|
+
* ```
|
|
6586
|
+
*/
|
|
6587
|
+
function refresh(target) {
|
|
6588
|
+
const node = target?.[$REFRESH];
|
|
6589
|
+
if (!node) {
|
|
6590
|
+
{
|
|
6591
|
+
const message =
|
|
6592
|
+
"[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
|
|
6593
|
+
"Pass the original source target, not a wrapper function or derived property read.";
|
|
6594
|
+
emitDiagnostic({
|
|
6595
|
+
code: "INVALID_REFRESH_TARGET",
|
|
6596
|
+
kind: "write",
|
|
6597
|
+
severity: "error",
|
|
6598
|
+
message
|
|
6599
|
+
});
|
|
6600
|
+
throw new Error(message);
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
// Mark now, watch on a microtask. The waiter is resolve()'s machinery with
|
|
6604
|
+
// two extra reader bits, but it must NOT compute at call time (effects
|
|
6605
|
+
// recompute eagerly on creation): same-tick refreshes coalesce into ONE
|
|
6606
|
+
// re-ask only because every mark lands before anything pulls, and eager
|
|
6607
|
+
// per-call pulls turned three refreshes into three fetches. Deferred, the
|
|
6608
|
+
// waiter's first read sees the coalesced state: FRESH_READ pulls the node
|
|
6609
|
+
// through recompute if it is still dirty (self-deduping — a clean node
|
|
6610
|
+
// no-ops, so N waiters cost one pull; this also closes the race where a
|
|
6611
|
+
// waiter reads the PRE-re-ask value as settled and delivers stale), after
|
|
6612
|
+
// which the read either parks on the re-ask's pending window (async — the
|
|
6613
|
+
// settle walk re-runs it on every landing, equal-value and
|
|
6614
|
+
// staged-under-hold included, and a rejection arrives through the effect's
|
|
6615
|
+
// error channel) or serves the sync answer. AUTHORITATIVE_READ keeps an
|
|
6616
|
+
// action's own optimistic override out of the delivered value. resolve()'s
|
|
6617
|
+
// own eager compute is untouched: created after a refresh it still settles
|
|
6618
|
+
// stale-while-revalidate (#2930) — its contract is "first settled value",
|
|
6619
|
+
// not "next quiescent state".
|
|
6620
|
+
markRefresh(node);
|
|
6621
|
+
const promise = new Promise((res, rej) => {
|
|
6622
|
+
queueMicrotask(() => {
|
|
6623
|
+
// No createRoot: the microtask has no ambient owner, so the effect is
|
|
6624
|
+
// naturally detached, and settle disposes the node directly — the root
|
|
6625
|
+
// added ~560B of otherwise-shakeable machinery for nothing but the
|
|
6626
|
+
// dev-mode NO_OWNER_EFFECT warning, so dev keeps a root husk purely to
|
|
6627
|
+
// stay quiet. The waiter swaps in its microtask queue during its own
|
|
6628
|
+
// first compute (before the initial apply enqueue), replacing the
|
|
6629
|
+
// root-owner plumbing.
|
|
6630
|
+
// Typed as the effect node, not Owner: the capture runs inside the
|
|
6631
|
+
// effect's own compute, where the ambient owner IS the effect —
|
|
6632
|
+
// exactly what dispose() takes.
|
|
6633
|
+
let waiter = null;
|
|
6634
|
+
const make = () =>
|
|
6635
|
+
effect(
|
|
6636
|
+
() => {
|
|
6637
|
+
if (waiter === null) {
|
|
6638
|
+
waiter = getOwner();
|
|
6639
|
+
const queue = new MicrotaskQueue();
|
|
6640
|
+
queue._parent = waiter._queue;
|
|
6641
|
+
waiter._queue = queue;
|
|
6642
|
+
}
|
|
6643
|
+
return read(node);
|
|
6644
|
+
},
|
|
6645
|
+
value => {
|
|
6646
|
+
res(typeof target === "function" ? value : target);
|
|
6647
|
+
dispose(waiter);
|
|
6648
|
+
},
|
|
6649
|
+
err => {
|
|
6650
|
+
rej(err);
|
|
6651
|
+
dispose(waiter);
|
|
6652
|
+
},
|
|
6653
|
+
{
|
|
6654
|
+
user: true,
|
|
6655
|
+
_extraConfig: CONFIG_DIRECT_COMMIT | CONFIG_AUTHORITATIVE_READ | CONFIG_FRESH_READ
|
|
6656
|
+
}
|
|
6657
|
+
);
|
|
6658
|
+
createRoot(make);
|
|
6659
|
+
});
|
|
6660
|
+
});
|
|
6661
|
+
// Fire-and-forget refresh must not turn a failed refetch into an unhandled
|
|
6662
|
+
// rejection; awaiting callers attach their own handlers to `promise`.
|
|
6663
|
+
promise.catch(() => {});
|
|
6664
|
+
return promise;
|
|
6665
|
+
}
|
|
6666
|
+
/**
|
|
6667
|
+
* Awaits a reactive predicate and resolves the first time it settles *truthy*,
|
|
6668
|
+
* with that (narrowed) value. Falsy results and pending async reads both mean
|
|
6669
|
+
* "not yet": the subscription stays live and re-evaluates as sources change.
|
|
6670
|
+
* If the predicate settles with an error — a throw, or an async source that
|
|
6671
|
+
* rejects — the promise rejects with it, as do timeout and abort.
|
|
6672
|
+
*
|
|
6673
|
+
* Where {@link resolve} answers "what is this value" (first settled value,
|
|
6674
|
+
* whatever it is), `until` answers "when does the world confirm this
|
|
6675
|
+
* condition". The difference matters inside an `action()`: `yield until(...)`
|
|
6676
|
+
* holds the action's transaction — and any optimistic state riding it — open
|
|
6677
|
+
* until the condition is independently true.
|
|
6678
|
+
*
|
|
6679
|
+
* To make that sound, `until`'s predicate reads the AUTHORITATIVE view — and
|
|
6680
|
+
* this is the one read-semantics difference from `resolve`, which reads the
|
|
6681
|
+
* normal (transaction's own) view where overrides are visible:
|
|
6682
|
+
*
|
|
6683
|
+
* - **Optimistic overrides are invisible** to the predicate. Your own
|
|
6684
|
+
* tentative write can never satisfy your own ack, even on the
|
|
6685
|
+
* single-primitive shape where the optimistic store IS the live-fed store.
|
|
6686
|
+
* (Derived computeds serve their normal cached values — express the
|
|
6687
|
+
* condition over sources of truth, not derived views of the overlay.)
|
|
6688
|
+
* - **Everything else reads normally, including uncommitted transition-staged
|
|
6689
|
+
* data.** Real data is real wherever it currently lives. This is
|
|
6690
|
+
* load-bearing, not a loophole: truth that arrives *into* the open
|
|
6691
|
+
* transaction (a `refresh()` this action issued, an entangled landing)
|
|
6692
|
+
* stages and cannot commit until the hold releases — a predicate that
|
|
6693
|
+
* refused staged reads would deadlock on the very data it is waiting for.
|
|
6694
|
+
*
|
|
6695
|
+
* This is the acknowledgment mechanism for mutations confirmed on a live data
|
|
6696
|
+
* channel (sockets, subscriptions, live queries) rather than by the mutation's
|
|
6697
|
+
* own response: correlate by a client-generated id or version in the predicate,
|
|
6698
|
+
* and let truth arrive however it arrives — push, refetch, or another tab.
|
|
6699
|
+
*
|
|
6700
|
+
* Failure composes with action semantics: a rejection is thrown back into the
|
|
6701
|
+
* generator at the `yield` point — catchable there, or the action fails and
|
|
6702
|
+
* its optimistic state reverts.
|
|
6703
|
+
*
|
|
6704
|
+
* Must be called *outside* a tracking scope.
|
|
6705
|
+
*
|
|
6706
|
+
* @example
|
|
6707
|
+
* ```ts
|
|
6708
|
+
* const send = action(async function* (text: string) {
|
|
6709
|
+
* const clientId = crypto.randomUUID();
|
|
6710
|
+
* setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
|
|
6711
|
+
* await socket.send({ clientId, text }); // fire-and-forget transport
|
|
6712
|
+
* // Hold until the live source echoes the write (authoritative view —
|
|
6713
|
+
* // the optimistic row above cannot satisfy this):
|
|
6714
|
+
* yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
|
|
6715
|
+
* });
|
|
6716
|
+
* ```
|
|
6717
|
+
*
|
|
6718
|
+
* @param fn a reactive predicate over authoritative state
|
|
6719
|
+
* @param options optional `timeout` (ms) and abort `signal`
|
|
6720
|
+
*/
|
|
6721
|
+
function until(fn, options) {
|
|
6722
|
+
if (getObserver()) {
|
|
6723
|
+
throw new Error(
|
|
6724
|
+
"Cannot call until inside a reactive scope; await it from an action or another imperative scope."
|
|
6725
|
+
);
|
|
6726
|
+
}
|
|
6727
|
+
// Late-bind the wakeup hook for the A17-silent ack paths (pay-for-use:
|
|
6728
|
+
// apps that never call until() never retain it).
|
|
6729
|
+
installAuthoritativeRead();
|
|
6730
|
+
// Flip-entanglement (#3164 follow-up): the transaction this until() holds
|
|
6731
|
+
// open (the action's, when yielded from one). The predicate is the user's
|
|
6732
|
+
// declaration of what confirms it — when a foreign transition's staged
|
|
6733
|
+
// write flips it truthy, that transition merges here and reveals at the
|
|
6734
|
+
// joint settle instead of painting the confirmation under live optimism.
|
|
6735
|
+
const awaiting = activeTransition;
|
|
6736
|
+
return new Promise((res, rej) => {
|
|
6737
|
+
const signal = options?.signal;
|
|
6738
|
+
if (signal?.aborted) return rej(signal.reason);
|
|
6739
|
+
createRoot(dispose => {
|
|
6740
|
+
// Same delivery contract as resolve() (#2930): effect applies ride a
|
|
6741
|
+
// microtask so the promise can settle while the transaction the caller
|
|
6742
|
+
// yielded it into is still open — that transaction being open is the
|
|
6743
|
+
// entire point of the hold.
|
|
6744
|
+
const owner = getOwner();
|
|
6745
|
+
const queue = new MicrotaskQueue();
|
|
6746
|
+
queue._parent = owner._queue;
|
|
6747
|
+
owner._queue = queue;
|
|
6748
|
+
let timer;
|
|
6749
|
+
let onAbort;
|
|
6750
|
+
const settle = fire => {
|
|
6751
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
6752
|
+
if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
|
|
6753
|
+
fire();
|
|
6754
|
+
dispose();
|
|
6755
|
+
};
|
|
6756
|
+
effect(
|
|
6757
|
+
awaiting === null
|
|
6758
|
+
? fn
|
|
6759
|
+
: () => {
|
|
6760
|
+
const value = fn();
|
|
6761
|
+
// Runs inside the compute (pure phase): the confirming
|
|
6762
|
+
// transition's stamps are live and its commit hasn't run, so
|
|
6763
|
+
// the merge lands before any reveal. Falsy evaluations skip —
|
|
6764
|
+
// non-flipping updates were never named as the confirmation.
|
|
6765
|
+
if (value) entangleConfirmingTransitions(getObserver(), awaiting);
|
|
6766
|
+
return value;
|
|
6767
|
+
},
|
|
6768
|
+
value => {
|
|
6769
|
+
// Falsy is "not yet": keep the subscription live and wait for the
|
|
6770
|
+
// next evaluation. Only a truthy settled value resolves.
|
|
6771
|
+
if (value) settle(() => res(value));
|
|
6772
|
+
},
|
|
6773
|
+
err => settle(() => rej(err)),
|
|
6774
|
+
// AUTHORITATIVE_READ: overrides invisible to the predicate.
|
|
6775
|
+
// DIRECT_COMMIT: truth that stages into the held transaction (a
|
|
6776
|
+
// refresh the action issued) must flow through to the microtask
|
|
6777
|
+
// apply — a staged effect value would deadlock the hold on data
|
|
6778
|
+
// the hold itself is keeping uncommitted.
|
|
6779
|
+
{ user: true, _extraConfig: CONFIG_AUTHORITATIVE_READ | CONFIG_DIRECT_COMMIT }
|
|
5952
6780
|
);
|
|
6781
|
+
if (options?.timeout !== undefined)
|
|
6782
|
+
timer = setTimeout(() => settle(() => rej(new TimeoutError())), options.timeout);
|
|
6783
|
+
if (signal !== undefined) {
|
|
6784
|
+
onAbort = () => settle(() => rej(signal.reason));
|
|
6785
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6786
|
+
}
|
|
5953
6787
|
});
|
|
5954
6788
|
});
|
|
5955
6789
|
}
|
|
@@ -6100,6 +6934,15 @@ let optHooks = null;
|
|
|
6100
6934
|
function setOptHooks(h) {
|
|
6101
6935
|
optHooks = h;
|
|
6102
6936
|
}
|
|
6937
|
+
/** Sticky descendants flag walk (§6d): reconcile's keyed pruning descends
|
|
6938
|
+
* only where subscriptions exist at/below. Nodes AND patches count. */
|
|
6939
|
+
function markDescendants(target) {
|
|
6940
|
+
let t = target;
|
|
6941
|
+
while (t && !t.d) {
|
|
6942
|
+
t.d = true;
|
|
6943
|
+
t = t.u;
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6103
6946
|
|
|
6104
6947
|
/**
|
|
6105
6948
|
* Brand symbols used internally by the store proxy / projection plumbing.
|
|
@@ -6448,8 +7291,9 @@ function notifyMarkBoundaries(node) {
|
|
|
6448
7291
|
visited.add(sub);
|
|
6449
7292
|
// Display consumers (render effects, boundary computeds) act on the
|
|
6450
7293
|
// notification; descent stops there, exactly like the status rails.
|
|
6451
|
-
|
|
6452
|
-
|
|
7294
|
+
const notify = statusNotifierOf(sub);
|
|
7295
|
+
if (notify) {
|
|
7296
|
+
notify.call(sub, STATUS_PENDING, error);
|
|
6453
7297
|
return;
|
|
6454
7298
|
}
|
|
6455
7299
|
forEachDependent(sub, visit);
|
|
@@ -6557,6 +7401,15 @@ function affects(target, key) {
|
|
|
6557
7401
|
}
|
|
6558
7402
|
}
|
|
6559
7403
|
|
|
7404
|
+
let patchHooks = null;
|
|
7405
|
+
let rowHooks = null;
|
|
7406
|
+
function installPatchHooks(hooks) {
|
|
7407
|
+
patchHooks = hooks;
|
|
7408
|
+
}
|
|
7409
|
+
function installRowHooks(hooks) {
|
|
7410
|
+
rowHooks = hooks;
|
|
7411
|
+
}
|
|
7412
|
+
|
|
6560
7413
|
/**
|
|
6561
7414
|
* Store rewrite — increment 2: plain deep stores with pending-backing writes.
|
|
6562
7415
|
* Contract: INTERNALS-STORE-STATE.md.
|
|
@@ -6585,8 +7438,13 @@ function affects(target, key) {
|
|
|
6585
7438
|
* headroom for future fields. The prototype is reset to `Object.prototype`
|
|
6586
7439
|
* so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
|
|
6587
7440
|
* plain object's. Array targets keep the bare-`[]` path — they must carry
|
|
6588
|
-
* the array exotic class for `Array.isArray(proxy)
|
|
6589
|
-
*
|
|
7441
|
+
* the array exotic class for `Array.isArray(proxy)`.
|
|
7442
|
+
*
|
|
7443
|
+
* ARRAY SHAPE RULE: arrays normalize their named properties to dictionary
|
|
7444
|
+
* mode as the count grows (V8 13.x: counts ≡ 0 mod 3 from 18 up), so the
|
|
7445
|
+
* target's named field count is capped at 20 — write-side patch-channel
|
|
7446
|
+
* state lives inside the single `pc` extension (see target.ts), never as
|
|
7447
|
+
* new named fields here. */
|
|
6590
7448
|
function TargetShape() {
|
|
6591
7449
|
this.v = undefined;
|
|
6592
7450
|
this.ch = undefined;
|
|
@@ -6607,9 +7465,15 @@ function TargetShape() {
|
|
|
6607
7465
|
this.s = undefined;
|
|
6608
7466
|
this.ovl = undefined;
|
|
6609
7467
|
this.del = undefined;
|
|
6610
|
-
this.
|
|
7468
|
+
this.pc = undefined;
|
|
7469
|
+
this.hv = undefined;
|
|
7470
|
+
this.ht = undefined;
|
|
6611
7471
|
}
|
|
6612
7472
|
TargetShape.prototype = Object.prototype;
|
|
7473
|
+
/** Lazily allocate the patch-channel extension (one literal shape). */
|
|
7474
|
+
function pcOf(t) {
|
|
7475
|
+
return t.pc ?? (t.pc = { sp: null, p: null, ro: null, wk: null, qa: null, qe: null });
|
|
7476
|
+
}
|
|
6613
7477
|
function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
6614
7478
|
// The proxy target carries the array exotic class when the value is an
|
|
6615
7479
|
// array, so Array.isArray(proxy) is true; the fields live on it directly.
|
|
@@ -6626,6 +7490,7 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
6626
7490
|
t.h = null;
|
|
6627
7491
|
t.k = null;
|
|
6628
7492
|
t.dk = null;
|
|
7493
|
+
t.pc = null;
|
|
6629
7494
|
t.u = parent;
|
|
6630
7495
|
t.pk = parentKey;
|
|
6631
7496
|
t.px = null;
|
|
@@ -6638,7 +7503,8 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
6638
7503
|
t.s = false;
|
|
6639
7504
|
t.ovl = false;
|
|
6640
7505
|
t.del = null;
|
|
6641
|
-
t.
|
|
7506
|
+
t.hv = null;
|
|
7507
|
+
t.ht = null;
|
|
6642
7508
|
t.px = new Proxy(t, traps);
|
|
6643
7509
|
// Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
|
|
6644
7510
|
// proxy off looked-up targets as a field.
|
|
@@ -6819,13 +7685,6 @@ function getDeepNode(target) {
|
|
|
6819
7685
|
function bumpDeep(t) {
|
|
6820
7686
|
if (t.dk !== null) setSignal(t.dk, 1);
|
|
6821
7687
|
}
|
|
6822
|
-
function markDescendants(target) {
|
|
6823
|
-
let t = target;
|
|
6824
|
-
while (t && !t.d) {
|
|
6825
|
-
t.d = true;
|
|
6826
|
-
t = t.u;
|
|
6827
|
-
}
|
|
6828
|
-
}
|
|
6829
7688
|
// ---------------------------------------------------------------------------
|
|
6830
7689
|
// pending backing + fold (the single mutation point)
|
|
6831
7690
|
/** target → committed backing at batch start (the fold diff's old side). */
|
|
@@ -6850,6 +7709,12 @@ function cloneRaw(source, t) {
|
|
|
6850
7709
|
? Object.defineProperties([], descs)
|
|
6851
7710
|
: Object.create(Object.getPrototypeOf(source), descs);
|
|
6852
7711
|
}
|
|
7712
|
+
/** Scanned plainness for patch admission (patchableRaw): runs the one-time
|
|
7713
|
+
* accessor scan if it hasn't happened yet — the sticky `a` flag alone is not
|
|
7714
|
+
* trustworthy before a scan (it starts false and is discovered lazily). */
|
|
7715
|
+
function targetIsPlain(target) {
|
|
7716
|
+
return target.sc ? !target.a : scanAccessorsOnce(target);
|
|
7717
|
+
}
|
|
6853
7718
|
/** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
|
|
6854
7719
|
* returns true when the container is plain data (overlay-safe). */
|
|
6855
7720
|
function scanAccessorsOnce(target) {
|
|
@@ -6892,6 +7757,28 @@ function materializePB(target) {
|
|
|
6892
7757
|
}
|
|
6893
7758
|
function ensurePB(target) {
|
|
6894
7759
|
let pb = target.pb;
|
|
7760
|
+
// Truth-staged backing hand-off (#3164 fold): a TENTATIVE draft opening on
|
|
7761
|
+
// a target whose pending backing is truth-staged (a landing folded into a
|
|
7762
|
+
// retaining transaction — it carries a foldBatches stamp) must not share
|
|
7763
|
+
// the container. Tentative writes would pollute staged truth, and the
|
|
7764
|
+
// tentative discard (notifyOptimisticWrites nulls pb) would destroy the
|
|
7765
|
+
// landing. Park the staged backing and open a fresh draft seeded from the
|
|
7766
|
+
// optimistic view below; the tentative discard restores it. The
|
|
7767
|
+
// tentativePBs guard scopes this to draft OPEN: the draft's own backing
|
|
7768
|
+
// (foldBatches-stamped by its first write when an action's transition is
|
|
7769
|
+
// ambient) must not be parked by its own later writes.
|
|
7770
|
+
if (
|
|
7771
|
+
pb !== null &&
|
|
7772
|
+
!tentativePBs.has(pb) &&
|
|
7773
|
+
target.fam?.opt === true &&
|
|
7774
|
+
!projectionWriteActive &&
|
|
7775
|
+
!getWriteOverride() &&
|
|
7776
|
+
foldBatches.has(target)
|
|
7777
|
+
) {
|
|
7778
|
+
stagedTruthPB.set(target, pb);
|
|
7779
|
+
pb = target.pb = null;
|
|
7780
|
+
}
|
|
7781
|
+
if (activeTransition !== null) foldBatches.set(target, activeTransition);
|
|
6895
7782
|
if (pb === null) {
|
|
6896
7783
|
// Prototype-chain overlay (#3044): plain-data non-array containers
|
|
6897
7784
|
// outside projection/optimistic families open drafts in O(1) — own keys
|
|
@@ -6913,6 +7800,7 @@ function ensurePB(target) {
|
|
|
6913
7800
|
// seed from committed truth — seeding overrides there would fold a lane
|
|
6914
7801
|
// value into the committed home ("authority wins at reveal" would break).
|
|
6915
7802
|
if (target.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
|
|
7803
|
+
tentativePBs.add(pb);
|
|
6916
7804
|
const nodes = target.n;
|
|
6917
7805
|
if (nodes !== null) {
|
|
6918
7806
|
for (const key of Reflect.ownKeys(nodes)) {
|
|
@@ -6934,6 +7822,25 @@ function ensurePB(target) {
|
|
|
6934
7822
|
}
|
|
6935
7823
|
return pb;
|
|
6936
7824
|
}
|
|
7825
|
+
/** Sentinel holder for `t.ht`: a latest()-pull staged this adoption outside
|
|
7826
|
+
* any transition — the hold lasts until the fold commit (drainFolds). */
|
|
7827
|
+
const PLAIN_HOLD = Symbol("plainHold");
|
|
7828
|
+
/** True while a latest() read is pulling the projection computed up to date
|
|
7829
|
+
* (see the get trap): adoptions landing during the pull are speculative
|
|
7830
|
+
* against the un-flushed batch and stage a held view. (Not injectable — the
|
|
7831
|
+
* derived createStore overload retains projection machinery in every store
|
|
7832
|
+
* bundle, see treeshake.test.ts.) */
|
|
7833
|
+
let latestPullActive = false;
|
|
7834
|
+
/** Resolve the held committed view (#3074): answers the masked old backing
|
|
7835
|
+
* while the hold is live, and lazily clears a hold whose transition has
|
|
7836
|
+
* committed (transitions merge — resolve through currentTransition, same as
|
|
7837
|
+
* foldHeld's node stamps). */
|
|
7838
|
+
function heldMaskView(t) {
|
|
7839
|
+
const ht = t.ht;
|
|
7840
|
+
if (ht === null) return null;
|
|
7841
|
+
if (ht !== PLAIN_HOLD && currentTransition(ht)?._done === true) return (t.ht = t.hv = null);
|
|
7842
|
+
return t.hv;
|
|
7843
|
+
}
|
|
6937
7844
|
/**
|
|
6938
7845
|
* Adoption (2026-08-16c): the incoming object becomes the committed backing
|
|
6939
7846
|
* IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
|
|
@@ -6949,6 +7856,20 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
6949
7856
|
if (!eager) {
|
|
6950
7857
|
queueFold(target); // records the pre-batch old before we swap
|
|
6951
7858
|
target.adopted = true;
|
|
7859
|
+
// #3074/#3075: a projection recompute deriving from uncommitted inputs
|
|
7860
|
+
// swaps the backing SPECULATIVELY — committed-visibility readers must
|
|
7861
|
+
// keep the pre-hold view until the hold resolves (a source held by a
|
|
7862
|
+
// live transition, or a latest()-pull ahead of the flush). Post-await
|
|
7863
|
+
// landings (write-override) stay immediately visible — landed truth —
|
|
7864
|
+
// and clear any hold; optimistic families ride the lane machinery.
|
|
7865
|
+
if (target.fam?.opt !== true) {
|
|
7866
|
+
if (getWriteOverride()) {
|
|
7867
|
+
target.ht = target.hv = null;
|
|
7868
|
+
} else if (activeTransition !== null || latestPullActive) {
|
|
7869
|
+
if (heldMaskView(target) === null) target.hv = target.v;
|
|
7870
|
+
target.ht = activeTransition ?? PLAIN_HOLD;
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
6952
7873
|
}
|
|
6953
7874
|
target.pb = null;
|
|
6954
7875
|
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
@@ -6960,24 +7881,54 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
6960
7881
|
// draft rescans once (#3044 audit follow-up).
|
|
6961
7882
|
target.ovl = false;
|
|
6962
7883
|
target.del = null;
|
|
6963
|
-
target.wk = null; // adoption supersedes any staged trap writes
|
|
6964
7884
|
target.sc = false;
|
|
6965
7885
|
target.a = false;
|
|
7886
|
+
if (target.pc !== null) target.pc.wk = null; // adoption supersedes staged trap writes
|
|
6966
7887
|
target.v = incoming;
|
|
6967
7888
|
target.ch = incoming[$TARGET] !== undefined;
|
|
6968
7889
|
(target.fam?.map ?? storeNextLookup).set(incoming, target);
|
|
6969
7890
|
}
|
|
7891
|
+
/** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
|
|
7892
|
+
* array length write implicitly deleted indices) — consumers full-scan. */
|
|
7893
|
+
const WK_ALL = new Set();
|
|
7894
|
+
const plainProto = o => {
|
|
7895
|
+
const p = Object.getPrototypeOf(o);
|
|
7896
|
+
return p === Object.prototype || p === Array.prototype || p === null;
|
|
7897
|
+
};
|
|
6970
7898
|
function queueFold(target) {
|
|
6971
7899
|
if (foldOlds.has(target)) return;
|
|
6972
|
-
if (
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
7900
|
+
if (!hookInstalled) {
|
|
7901
|
+
hookInstalled = true;
|
|
7902
|
+
setStoreCommitHook(drainFolds);
|
|
7903
|
+
}
|
|
7904
|
+
// Always arm — "map non-empty ⇒ drain scheduled" is NOT an invariant: a
|
|
7905
|
+
// held re-queue, or an incomplete-transition flush (which skips
|
|
7906
|
+
// commitPendingNodes entirely), leaves entries behind after `scheduled`
|
|
7907
|
+
// was consumed. A size-gated arm then strands every LATER fold — queued
|
|
7908
|
+
// silently, never drained, committed base frozen at stale state while its
|
|
7909
|
+
// nodes commit (#3089). schedule() early-returns when already armed.
|
|
7910
|
+
schedule();
|
|
6979
7911
|
foldOlds.set(target, target.v);
|
|
6980
7912
|
}
|
|
7913
|
+
/** Fold write-attribution (#3089): a draft written while a transition is
|
|
7914
|
+
* active belongs to that transition — its fold must not commit before the
|
|
7915
|
+
* transition settles. Observed keys already defer through the held check in
|
|
7916
|
+
* drainFolds (their nodes carry _pendingValue); this write-time stamp is the
|
|
7917
|
+
* equivalent hold for UNOBSERVED keys, which have no node to consult.
|
|
7918
|
+
* Refreshed on every write; resolved through currentTransition at drain
|
|
7919
|
+
* (transitions merge — same rule as heldMaskView). */
|
|
7920
|
+
const foldBatches = new WeakMap();
|
|
7921
|
+
/** Parked truth-staged pending backings (#3164 fold): a tentative draft that
|
|
7922
|
+
* opens while a folded landing's backing is live moves the staged container
|
|
7923
|
+
* here (see ensurePB); the tentative discard in notifyOptimisticWrites
|
|
7924
|
+
* restores it in place of the usual null. */
|
|
7925
|
+
const stagedTruthPB = new WeakMap();
|
|
7926
|
+
/** Backings opened by TENTATIVE drafts (optimistic user setters): ensurePB's
|
|
7927
|
+
* truth-park must not fire against the draft's own container on its second
|
|
7928
|
+
* and later writes (the first write stamps foldBatches whenever an action's
|
|
7929
|
+
* transition is ambient). Entries die with their draft — tentative backings
|
|
7930
|
+
* are consumed at setter exit. */
|
|
7931
|
+
const tentativePBs = new WeakSet();
|
|
6981
7932
|
/** Committed-time privatization for parent-chain slot updates (path copying). */
|
|
6982
7933
|
function privatizeCommitted(target) {
|
|
6983
7934
|
if (ownedRaw.has(target.v)) return;
|
|
@@ -6997,7 +7948,27 @@ function drainFolds() {
|
|
|
6997
7948
|
const entries = [...foldOlds];
|
|
6998
7949
|
foldOlds.clear();
|
|
6999
7950
|
for (const [t, old] of entries) {
|
|
7951
|
+
// A latest()-pull staging holds only until the fold commit: this flush
|
|
7952
|
+
// is committing the batch the pull ran ahead of. Transition holds stay —
|
|
7953
|
+
// they clear when their transition is done (heldMaskView).
|
|
7954
|
+
if (t.ht === PLAIN_HOLD) t.ht = t.hv = null;
|
|
7955
|
+
// Eager (write-override) family folds swap pb -> v at notifyWrites'
|
|
7956
|
+
// tail: by the time this drain runs they carry no pb, and their
|
|
7957
|
+
// structural ops must emit at the fold-commit site below (the clone
|
|
7958
|
+
// branch never sees them). Re-audit blocker 4.
|
|
7959
|
+
const foldedEager = t.pb === null;
|
|
7000
7960
|
if (t.pb !== null) {
|
|
7961
|
+
// #3089: a fold written under a still-running transition defers to
|
|
7962
|
+
// that transition's settle (the write-time stamp covers unobserved
|
|
7963
|
+
// keys; observed keys also hit the pending-node held check below).
|
|
7964
|
+
const fb = foldBatches.get(t);
|
|
7965
|
+
if (fb !== undefined) {
|
|
7966
|
+
if (currentTransition(fb)._done === false) {
|
|
7967
|
+
foldOlds.set(t, old);
|
|
7968
|
+
continue;
|
|
7969
|
+
}
|
|
7970
|
+
foldBatches.delete(t);
|
|
7971
|
+
}
|
|
7001
7972
|
// Setter path: nodes were setSignal'd at setter exit (write-time
|
|
7002
7973
|
// notification — transitions/holds ride core machinery). Commit the
|
|
7003
7974
|
// backing only for keys whose nodes have committed; a still-pending
|
|
@@ -7009,9 +7980,14 @@ function drainFolds() {
|
|
|
7009
7980
|
// Only written keys can hold (their nodes took the setSignal); the
|
|
7010
7981
|
// wk bound keeps this O(written) — see notifyWrites. Same fallback
|
|
7011
7982
|
// rules as the notify (WK_ALL / accessors / non-plain prototypes).
|
|
7012
|
-
const wkh = t.wk;
|
|
7983
|
+
const wkh = t.pc !== null ? t.pc.wk : null;
|
|
7013
7984
|
const keys =
|
|
7014
|
-
wkh === null ||
|
|
7985
|
+
wkh === null ||
|
|
7986
|
+
wkh === WK_ALL ||
|
|
7987
|
+
t.a === true ||
|
|
7988
|
+
// Overlay pbs chain to the COMMITTED object (#3044) — plainness is
|
|
7989
|
+
// the committed container's prototype, not the overlay's.
|
|
7990
|
+
!plainProto(t.ovl ? t.v : pb)
|
|
7015
7991
|
? Reflect.ownKeys(nodes)
|
|
7016
7992
|
: wkh;
|
|
7017
7993
|
for (const key of keys) {
|
|
@@ -7049,15 +8025,70 @@ function drainFolds() {
|
|
|
7049
8025
|
(t.fam?.map ?? storeNextLookup).delete(pb);
|
|
7050
8026
|
t.pb = null;
|
|
7051
8027
|
t.ovl = false;
|
|
7052
|
-
t.wk = null; // written-keys window closes with the fold commit
|
|
8028
|
+
if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
|
|
7053
8029
|
} else {
|
|
8030
|
+
// Setter-channel structural ops: a fold that changes an array's shape
|
|
8031
|
+
// (push/splice/permutation through the setter — the reconcile walk
|
|
8032
|
+
// never queues here) is a structural visibility transition for any
|
|
8033
|
+
// registered list driver. Identity-keyed; aligned folds emit nothing.
|
|
8034
|
+
// Family targets defer to their own adoption emission (fam reconcile).
|
|
8035
|
+
// Arrays always fold on this clone branch (overlay is non-array only).
|
|
8036
|
+
// Family setter drafts (writable projection push/splice through the
|
|
8037
|
+
// masked setter) fold on this branch too and the fold IS their
|
|
8038
|
+
// visibility moment — emit unless the structure already rode another
|
|
8039
|
+
// channel: adoption folds (reconcile walk emitted ops) and
|
|
8040
|
+
// optimistic families (lane-timed override channel). Re-audit
|
|
8041
|
+
// blocker 4.
|
|
8042
|
+
if (
|
|
8043
|
+
t.pc !== null &&
|
|
8044
|
+
t.pc.ro !== null &&
|
|
8045
|
+
!t.adopted &&
|
|
8046
|
+
t.fam?.opt !== true &&
|
|
8047
|
+
Array.isArray(pb) &&
|
|
8048
|
+
Array.isArray(t.v)
|
|
8049
|
+
)
|
|
8050
|
+
rowHooks.emitSetterRowOps(t, t.v, pb);
|
|
7054
8051
|
t.v = pb;
|
|
7055
8052
|
t.ch = false; // pb is always a plain clone
|
|
7056
8053
|
t.pb = null;
|
|
7057
|
-
t.wk = null; // written-keys window closes with the fold commit
|
|
8054
|
+
if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
if (t.v === old) {
|
|
8058
|
+
// A no-op adoption (A -> B -> A before flush) still consumed its walk:
|
|
8059
|
+
// clear the flag or every later setter row-op gate (!t.adopted) stays
|
|
8060
|
+
// failed and a driven family list freezes (re-audit 5, P1-1).
|
|
8061
|
+
t.adopted = false;
|
|
8062
|
+
continue;
|
|
8063
|
+
}
|
|
8064
|
+
// Patch channel (fold-commit site): family targets emit HERE — the fold
|
|
8065
|
+
// IS their visibility moment (held folds re-queued above emit when they
|
|
8066
|
+
// actually commit) — and so do PLAIN fold-adopted targets (setter-
|
|
8067
|
+
// returned root replacements, chained-store swaps: adoptions WITHOUT a
|
|
8068
|
+
// reconcile walk, so no walk-site emission ever happened — re-audit 2,
|
|
8069
|
+
// P1-2). Plain eager targets emitted at their walk/setter sites already.
|
|
8070
|
+
if (t.pc !== null && (t.fam !== null || t.adopted)) {
|
|
8071
|
+
// Structural ops for folds whose structure rode no other channel:
|
|
8072
|
+
// eager-folded family SETTER drafts (write-override swaps pb -> v at
|
|
8073
|
+
// notifyWrites' tail — the clone branch never sees them; adoption
|
|
8074
|
+
// folds re-emitting would double the walk's ops) and PLAIN fold
|
|
8075
|
+
// adoptions (no walk at all). Optimistic families ride the override
|
|
8076
|
+
// channel (lane-timed ops + revert RESYNC) — never re-emit here.
|
|
8077
|
+
if (
|
|
8078
|
+
t.pc.ro !== null &&
|
|
8079
|
+
t.fam?.opt !== true &&
|
|
8080
|
+
(t.fam !== null ? foldedEager && !t.adopted : t.adopted) &&
|
|
8081
|
+
Array.isArray(t.v) &&
|
|
8082
|
+
Array.isArray(old)
|
|
8083
|
+
)
|
|
8084
|
+
rowHooks.emitSetterRowOps(t, old, t.v);
|
|
8085
|
+
if (t.pc.p !== null) {
|
|
8086
|
+
// Accessor demotion at the fold-commit seam is DEV-ONLY (see the
|
|
8087
|
+
// reconcile seam note: prod never pays per-adoption scans).
|
|
8088
|
+
if (!targetIsPlain(t)) patchHooks.demoteToEffects(t);
|
|
8089
|
+
else patchHooks.emitPatchLocal(t, t.v, old);
|
|
7058
8090
|
}
|
|
7059
8091
|
}
|
|
7060
|
-
if (t.v === old) continue; // adopted then re-adopted back, or no-op
|
|
7061
8092
|
// Path copying (CAS: see the eager-fold twin above).
|
|
7062
8093
|
if (t.u && t.u.v[t.pk] === old) {
|
|
7063
8094
|
privatizeCommitted(t.u);
|
|
@@ -7078,17 +8109,6 @@ function drainFolds() {
|
|
|
7078
8109
|
* "pending home = the node when a node exists"). Unobserved keys stay in the
|
|
7079
8110
|
* pending backing and fold directly at commit.
|
|
7080
8111
|
*/
|
|
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
|
-
};
|
|
7092
8112
|
function notifyWrites(t) {
|
|
7093
8113
|
let pb = t.pb;
|
|
7094
8114
|
if (pb === null) return;
|
|
@@ -7143,8 +8163,15 @@ function notifyWrites(t) {
|
|
|
7143
8163
|
// not a full scan). Falls back to the full node scan when the bound can't
|
|
7144
8164
|
// hold: no trap granularity (wk null), an array length write (WK_ALL —
|
|
7145
8165
|
// 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
|
-
|
|
8166
|
+
// value can change when ANY key is written), or a non-plain prototype
|
|
8167
|
+
// (class instances: prototype getters derive from arbitrary fields).
|
|
8168
|
+
const wk0 = t.pc !== null ? t.pc.wk : null;
|
|
8169
|
+
// Overlay pbs chain to the COMMITTED object (#3044): a prototype-overlay
|
|
8170
|
+
// draft is plain data on its own layer, but its getPrototypeOf is the
|
|
8171
|
+
// committed container — judge plainness by the COMMITTED prototype or the
|
|
8172
|
+
// bound never engages for overlay writes (every plain-object setter batch
|
|
8173
|
+
// would full-scan: the exact selection-map workload wk exists for; jf
|
|
8174
|
+
// `select` regressed 2x on this).
|
|
7148
8175
|
const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
|
|
7149
8176
|
if (nodes !== null) {
|
|
7150
8177
|
const keys = writtenKeys ?? Reflect.ownKeys(nodes);
|
|
@@ -7177,15 +8204,18 @@ function notifyWrites(t) {
|
|
|
7177
8204
|
}
|
|
7178
8205
|
const has = t.h;
|
|
7179
8206
|
if (has !== null) {
|
|
7180
|
-
|
|
7181
|
-
|
|
8207
|
+
const keys = writtenKeys ?? Reflect.ownKeys(has);
|
|
8208
|
+
for (const key of keys) {
|
|
8209
|
+
const node = has[key];
|
|
8210
|
+
if (node !== undefined) setSignal(node, key in pb && !(t.del !== null && t.del.has(key)));
|
|
8211
|
+
}
|
|
7182
8212
|
}
|
|
7183
8213
|
// Deep-witness (dk): setter writes must notify a deep() subscriber even on
|
|
7184
|
-
// keys with no node. O(pb keys) equality only when a witness exists.
|
|
8214
|
+
// keys with no node. O(written/pb keys) equality only when a witness exists.
|
|
7185
8215
|
if (t.dk !== null) {
|
|
7186
8216
|
if (t.del !== null && t.del.size !== 0) bumpDeep(t);
|
|
7187
8217
|
else
|
|
7188
|
-
for (const key of Reflect.ownKeys(pb)) {
|
|
8218
|
+
for (const key of writtenKeys ?? Reflect.ownKeys(pb)) {
|
|
7189
8219
|
const nv = pb[key];
|
|
7190
8220
|
const ov = old[key];
|
|
7191
8221
|
if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
|
|
@@ -7215,6 +8245,13 @@ function notifyWrites(t) {
|
|
|
7215
8245
|
}
|
|
7216
8246
|
if (changed) setSignal(t.k, v => v + 1);
|
|
7217
8247
|
}
|
|
8248
|
+
// Patch channel (setter site): a committed write transitions this record —
|
|
8249
|
+
// queue its patches and bubble to ancestors (targeted nested writes must
|
|
8250
|
+
// reach the row patch, §4b). One number compare when no patches exist.
|
|
8251
|
+
// Family targets skip this site: their visibility moment is the FOLD
|
|
8252
|
+
// commit (drainFolds emits), not the recompute/draft write.
|
|
8253
|
+
if (t.fam === null && patchHooks !== null && patchHooks.hasPatches())
|
|
8254
|
+
patchHooks.emitPatch(t, pb, old);
|
|
7218
8255
|
// Projection backing folds split by channel (two pinned contracts):
|
|
7219
8256
|
// - sync-derive drafts (recompute body): NEVER eager — a downstream async
|
|
7220
8257
|
// hold can form LATER in the same flush and the leaf must stay at stale
|
|
@@ -7225,10 +8262,18 @@ function notifyWrites(t) {
|
|
|
7225
8262
|
// IMMEDIATE — landed truth shows to untracked readers even while a
|
|
7226
8263
|
// downstream consumer's own async still holds the effect-level reveal
|
|
7227
8264
|
// (spec-async "verdicts never inherit consumers' in-flight state").
|
|
7228
|
-
|
|
8265
|
+
// EXCEPT under an active transaction (#3164 fold): a landing riding a
|
|
8266
|
+
// retaining transaction (the optimistic module's aroundWrite binds it)
|
|
8267
|
+
// stages instead — ensurePB stamped foldBatches, so the backing commits
|
|
8268
|
+
// with the transaction and the reveal is atomic at settle. The pinned
|
|
8269
|
+
// immediate-commit contract is stated over the no-transaction microtask
|
|
8270
|
+
// posture, which `activeTransition === null` is exactly.
|
|
8271
|
+
if (t.fam !== null && t.pb !== null && getWriteOverride() && activeTransition === null) {
|
|
8272
|
+
// Landed truth (post-await write-override): immediately visible to every
|
|
8273
|
+
// reader — any staged held view is superseded.
|
|
8274
|
+
if (t.ht !== null) t.ht = t.hv = null;
|
|
7229
8275
|
const oldBacking = t.v;
|
|
7230
8276
|
t.pb = null;
|
|
7231
|
-
t.wk = null; // written-keys window closes with the eager fold
|
|
7232
8277
|
t.v = pb;
|
|
7233
8278
|
t.ch = false;
|
|
7234
8279
|
if (t.u && t.u.v[t.pk] === oldBacking) {
|
|
@@ -7433,6 +8478,17 @@ function inOwnerContext() {
|
|
|
7433
8478
|
const eff = c._root ? c._parentComputed : c;
|
|
7434
8479
|
return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
7435
8480
|
}
|
|
8481
|
+
/** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
|
|
8482
|
+
* callbacks). Distinct from context-free: these scopes get committed
|
|
8483
|
+
* visibility even against a projection's authoritative-elect pending
|
|
8484
|
+
* backing (#3082) — parity with signals, where core read() serves
|
|
8485
|
+
* committed to them regardless of staged writes. */
|
|
8486
|
+
function inForbiddenScope() {
|
|
8487
|
+
const c = getOwner();
|
|
8488
|
+
if (c === null) return false;
|
|
8489
|
+
const eff = c._root ? c._parentComputed : c;
|
|
8490
|
+
return eff != null && !!(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
8491
|
+
}
|
|
7436
8492
|
/** A pending fold is transition-held when any written node's parked value is
|
|
7437
8493
|
* stamped by a live transition (a plain batch parking — the lazy-recompute
|
|
7438
8494
|
* read case — has no transition stamp and serves fresh). */
|
|
@@ -7451,6 +8507,20 @@ function foldHeld(target) {
|
|
|
7451
8507
|
return false;
|
|
7452
8508
|
}
|
|
7453
8509
|
function readSource(target) {
|
|
8510
|
+
// Held view first (#3074): an adoption staged under a live hold serves the
|
|
8511
|
+
// pre-hold committed backing to committed-visibility readers. Speculative
|
|
8512
|
+
// readers — drafts, write-override, owner-context computeds recomputing
|
|
8513
|
+
// inside the transaction, and latest() reads — see the adopted backing.
|
|
8514
|
+
if (
|
|
8515
|
+
target.ht !== null &&
|
|
8516
|
+
!latestReadActive &&
|
|
8517
|
+
!inDraft(target) &&
|
|
8518
|
+
!getWriteOverride() &&
|
|
8519
|
+
!inOwnerContext()
|
|
8520
|
+
) {
|
|
8521
|
+
const hv = heldMaskView(target);
|
|
8522
|
+
if (hv !== null) return hv;
|
|
8523
|
+
}
|
|
7454
8524
|
// Signal-parity visibility (core read(): owner-context reads serve
|
|
7455
8525
|
// _pendingValue, context-free reads serve committed — effects recompute
|
|
7456
8526
|
// BEFORE commitPendingNodes in the flush, so the pending view must be
|
|
@@ -7461,15 +8531,37 @@ function readSource(target) {
|
|
|
7461
8531
|
target.pb !== null &&
|
|
7462
8532
|
(inDraft(target) ||
|
|
7463
8533
|
getWriteOverride() ||
|
|
7464
|
-
|
|
8534
|
+
// Owner-context readers see the pending backing — EXCEPT held truth
|
|
8535
|
+
// on an optimistic family (#3164 fold): a live pb on an opt family
|
|
8536
|
+
// outside the draft/write-override windows is a staged landing
|
|
8537
|
+
// (tentative drafts never outlive their setter), and only the
|
|
8538
|
+
// authoritative postures and latest() see it (the backing-level twin
|
|
8539
|
+
// of core read()'s A17-for-held-truth arm; ordinary readers keep
|
|
8540
|
+
// committed until the transaction's reveal).
|
|
8541
|
+
(inOwnerContext() && !heldTruthMasked(target)) ||
|
|
7465
8542
|
// A projection's pending backing is authoritative-elect: serve it to
|
|
7466
8543
|
// context-free readers too UNLESS a transition is holding the node
|
|
7467
|
-
// commits (downstream async hold — stale committed is the contract)
|
|
7468
|
-
|
|
8544
|
+
// commits (downstream async hold — stale committed is the contract)
|
|
8545
|
+
// or the reader is a CHILDREN_FORBIDDEN scope, which never observes
|
|
8546
|
+
// its own unsettled write (#3082, signal parity per #3006).
|
|
8547
|
+
(target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
|
|
7469
8548
|
)
|
|
7470
8549
|
return target.pb;
|
|
7471
8550
|
return target.v;
|
|
7472
8551
|
}
|
|
8552
|
+
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
8553
|
+
* stamped by a live transition that retains optimism — is masked from
|
|
8554
|
+
* ordinary readers (they keep committed until the transaction's reveal);
|
|
8555
|
+
* the authoritative postures and latest() tunnel through. Un-stamped
|
|
8556
|
+
* backings and optimism-free transitions keep ordinary mid-batch/
|
|
8557
|
+
* speculation visibility. */
|
|
8558
|
+
function heldTruthMasked(target) {
|
|
8559
|
+
if (target.fam?.opt !== true || latestReadActive || authoritativeServe()) return false;
|
|
8560
|
+
const fb = foldBatches.get(target);
|
|
8561
|
+
// opt families are only created by createOptimisticStore, whose module
|
|
8562
|
+
// install populates optHooks — the assertion holds by construction.
|
|
8563
|
+
return fb !== undefined && optHooks.retainsOptimism(fb);
|
|
8564
|
+
}
|
|
7473
8565
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
7474
8566
|
// Allocation-free own-accessor probe (replaces eager descriptor scans — the
|
|
7475
8567
|
// single biggest creation cost in the uibench profile): Annex-B lookups
|
|
@@ -7501,6 +8593,26 @@ function runAuthoritative(fn) {
|
|
|
7501
8593
|
function hasActiveOverride(node) {
|
|
7502
8594
|
return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
|
|
7503
8595
|
}
|
|
8596
|
+
/** The reading computation is until()'s authoritative-view predicate — same
|
|
8597
|
+
* source of truth as core read()'s A17 carve-out (`context`, which persists
|
|
8598
|
+
* under untrack). optimisticView()'s composition gate consults exactly this:
|
|
8599
|
+
* write-side machinery (patch emission, tentative re-application) must keep
|
|
8600
|
+
* composing even when it runs inside an authoritative-write bracket. */
|
|
8601
|
+
function authoritativeRead() {
|
|
8602
|
+
const c = context;
|
|
8603
|
+
return c !== null && (c._config & CONFIG_AUTHORITATIVE_READ) !== 0;
|
|
8604
|
+
}
|
|
8605
|
+
/** Serve-side authoritative gate: until()'s predicate PLUS truth authors —
|
|
8606
|
+
* the projection derive's draft (wrapDraft trap brackets, runAuthoritative;
|
|
8607
|
+
* the same posture pair ensurePB classifies drafts by). A source computing
|
|
8608
|
+
* the next truth must never read its callers' tentative overlays: a derive
|
|
8609
|
+
* continuation's `store.push` computing its index from an action's
|
|
8610
|
+
* optimistic row landed truth in the wrong slot and corrupted committed
|
|
8611
|
+
* state (#3108). Trap-level overlay serves gate on this so values, length,
|
|
8612
|
+
* membership, and keys leave the authoritative view together. */
|
|
8613
|
+
function authoritativeServe() {
|
|
8614
|
+
return projectionWriteActive || getWriteOverride() || authoritativeRead();
|
|
8615
|
+
}
|
|
7504
8616
|
/** Context-aware node view for reads outside tracking: active override >
|
|
7505
8617
|
* held pending (owner context) > the BACKING value. Committed truth lives in
|
|
7506
8618
|
* the backing (single-home rule, O6) — node `_value` is never served here,
|
|
@@ -7509,11 +8621,26 @@ function hasActiveOverride(node) {
|
|
|
7509
8621
|
* FORCE sentinels never surface (they only bump subscribers of accessor
|
|
7510
8622
|
* keys, which are served by the trap, not the node). */
|
|
7511
8623
|
function nodeValue(node, backing) {
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
8624
|
+
// latest() sees the in-flight parked value like an owner-context reader
|
|
8625
|
+
// does (#3075) — signal/memo parity for store-node-backed keys.
|
|
8626
|
+
// Authoritative-view reads (until()'s predicate) skip the override arm
|
|
8627
|
+
// only: staged pending values are authoritative, overrides are the
|
|
8628
|
+
// caller's optimism.
|
|
8629
|
+
const v =
|
|
8630
|
+
!authoritativeServe() && hasActiveOverride(node)
|
|
8631
|
+
? unwrapOverride(node._x?._overrideValue)
|
|
8632
|
+
: node._pendingValue !== NOT_PENDING &&
|
|
8633
|
+
(latestReadActive ||
|
|
8634
|
+
// Owner-context pending visibility — except HELD truth (#3164,
|
|
8635
|
+
// see CONFIG_HELD_TRUTH: fold-staged or entangle-stolen
|
|
8636
|
+
// confirming truth), which only authoritative/latest readers
|
|
8637
|
+
// see (core read()'s A17-for-held-truth twin; ordinary readers
|
|
8638
|
+
// keep committed until the transaction's reveal — latest() is
|
|
8639
|
+
// exempted by the leading arm above).
|
|
8640
|
+
((inOwnerContext() || authoritativeServe()) &&
|
|
8641
|
+
!(node._config & CONFIG_HELD_TRUTH && !authoritativeServe())))
|
|
8642
|
+
? node._pendingValue
|
|
8643
|
+
: backing;
|
|
7517
8644
|
return v === FORCE ? backing : v;
|
|
7518
8645
|
}
|
|
7519
8646
|
/** Serve an own data key: node-first when a node exists (pending visibility,
|
|
@@ -7538,13 +8665,18 @@ function serveDataKey(target, key, backingValue, src, node) {
|
|
|
7538
8665
|
read(getNode(target, key, backingValue));
|
|
7539
8666
|
}
|
|
7540
8667
|
}
|
|
7541
|
-
|
|
8668
|
+
// Truth authors read the backing's own length — an optimistic row from
|
|
8669
|
+
// the caller's transaction must not shift where the author's next write
|
|
8670
|
+
// lands (#3108).
|
|
8671
|
+
return (authoritativeServe() ? src : optHooks.optimisticView(target, src)).length;
|
|
7542
8672
|
}
|
|
7543
8673
|
if (inDraft(target)) {
|
|
7544
8674
|
// Optimistic drafts before their first write have no pending backing yet;
|
|
7545
8675
|
// reads must still see the live optimistic view (compose, not clobber —
|
|
7546
8676
|
// #2951). Once ensurePB runs, the seeded clone carries the view.
|
|
7547
|
-
|
|
8677
|
+
// AUTHORITATIVE drafts (projection derive) never overlay — ensurePB's
|
|
8678
|
+
// seeding rule, applied to the read side (#3108).
|
|
8679
|
+
if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
|
|
7548
8680
|
const node = target.n?.[key];
|
|
7549
8681
|
if (node !== undefined && hasActiveOverride(node))
|
|
7550
8682
|
v = unwrapOverride(node._x?._overrideValue);
|
|
@@ -7602,6 +8734,27 @@ function firewallGate(target) {
|
|
|
7602
8734
|
const fw = target.fam?.node;
|
|
7603
8735
|
if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
|
|
7604
8736
|
}
|
|
8737
|
+
/** latest() pull (#3075): bring the projection computed up to date so the
|
|
8738
|
+
* read serves the IN-FLIGHT derivation — signal/memo parity, where core
|
|
8739
|
+
* read() routes latest() through a companion that recomputes speculatively.
|
|
8740
|
+
* The latest flag is suspended for the recompute (the derive's own reads
|
|
8741
|
+
* are normal reads), and latestPullActive marks any adoption it commits as
|
|
8742
|
+
* staged (see adoptPB) — the speculative swap must not leak to
|
|
8743
|
+
* committed-visibility readers before the flush. */
|
|
8744
|
+
function pullProjectionForLatest(target) {
|
|
8745
|
+
const fw = target.fam.node;
|
|
8746
|
+
if (fw == null) return;
|
|
8747
|
+
const prevLatest = latestReadActive;
|
|
8748
|
+
setLatestReadActive(false);
|
|
8749
|
+
const prevPull = latestPullActive;
|
|
8750
|
+
latestPullActive = true;
|
|
8751
|
+
try {
|
|
8752
|
+
prepareComputed(fw, true);
|
|
8753
|
+
} finally {
|
|
8754
|
+
latestPullActive = prevPull;
|
|
8755
|
+
setLatestReadActive(prevLatest);
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
7605
8758
|
const traps = {
|
|
7606
8759
|
get(target, key, receiver) {
|
|
7607
8760
|
// One typeof gates every brand-symbol compare off the hot string path
|
|
@@ -7629,6 +8782,11 @@ const traps = {
|
|
|
7629
8782
|
}
|
|
7630
8783
|
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
7631
8784
|
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
8785
|
+
// latest() pull (#3075): store traps never reach core read() without an
|
|
8786
|
+
// observer, so bring the projection computed up to date here — signal/
|
|
8787
|
+
// memo parity for latest() reads through a projection.
|
|
8788
|
+
if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
|
|
8789
|
+
pullProjectionForLatest(target);
|
|
7632
8790
|
const src = readSource(target);
|
|
7633
8791
|
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
7634
8792
|
// deleted keys are tracked aside and read as absent in the pending view.
|
|
@@ -7727,7 +8885,15 @@ const traps = {
|
|
|
7727
8885
|
if (target.s) return serveShallow(target, key, nv);
|
|
7728
8886
|
return isWrappable(nv) ? draftServe(target, wrapNext(nv, target, key)) : nv;
|
|
7729
8887
|
}
|
|
7730
|
-
} else if (
|
|
8888
|
+
} else if (
|
|
8889
|
+
v === undefined &&
|
|
8890
|
+
inDraft(target) &&
|
|
8891
|
+
target.fam?.opt &&
|
|
8892
|
+
target.pb === null &&
|
|
8893
|
+
// AUTHORITATIVE drafts (landing folds) never seed from overrides —
|
|
8894
|
+
// the caller's optimism is not truth (has-trap twin below).
|
|
8895
|
+
!authoritativeServe()
|
|
8896
|
+
) {
|
|
7731
8897
|
const node = target.n?.[key];
|
|
7732
8898
|
if (node !== undefined && hasActiveOverride(node))
|
|
7733
8899
|
v = unwrapOverride(node._x?._overrideValue);
|
|
@@ -7754,14 +8920,16 @@ const traps = {
|
|
|
7754
8920
|
if (!inDraft(target)) {
|
|
7755
8921
|
if (getObserver() !== null) {
|
|
7756
8922
|
const node = getHasNode(target, key, present);
|
|
8923
|
+
// Authoritative-view readers get the right answer for free: core read()
|
|
8924
|
+
// skips the override arm for them, so nv is authoritative presence.
|
|
7757
8925
|
const nv = read(node);
|
|
7758
8926
|
if (hasActiveOverride(node)) present = !!nv;
|
|
7759
|
-
} else {
|
|
8927
|
+
} else if (!authoritativeServe()) {
|
|
7760
8928
|
const node = target.h?.[key];
|
|
7761
8929
|
if (node !== undefined && hasActiveOverride(node))
|
|
7762
8930
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
7763
8931
|
}
|
|
7764
|
-
} else if (target.fam?.opt && target.pb === null) {
|
|
8932
|
+
} else if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
|
|
7765
8933
|
const node = target.h?.[key];
|
|
7766
8934
|
if (node !== undefined && hasActiveOverride(node))
|
|
7767
8935
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
@@ -7787,8 +8955,14 @@ const traps = {
|
|
|
7787
8955
|
// Optimistic membership overlay: presence-node overrides add/remove keys
|
|
7788
8956
|
// (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
|
|
7789
8957
|
// Draft reads before the first write overlay too (pb, once created, is
|
|
7790
|
-
// seeded with the view).
|
|
7791
|
-
|
|
8958
|
+
// seeded with the view). Authoritative-view reads (until()'s predicate,
|
|
8959
|
+
// truth-author drafts) skip the overlay.
|
|
8960
|
+
if (
|
|
8961
|
+
!authoritativeServe() &&
|
|
8962
|
+
target.fam?.opt &&
|
|
8963
|
+
target.h !== null &&
|
|
8964
|
+
(!inDraft(target) || target.pb === null)
|
|
8965
|
+
) {
|
|
7792
8966
|
let set = null;
|
|
7793
8967
|
for (const key of Reflect.ownKeys(target.h)) {
|
|
7794
8968
|
const node = target.h[key];
|
|
@@ -7810,7 +8984,7 @@ const traps = {
|
|
|
7810
8984
|
if (target.del !== null && target.del.has(key)) return undefined;
|
|
7811
8985
|
if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
|
|
7812
8986
|
}
|
|
7813
|
-
if (target.fam?.opt && !inDraft(target)) {
|
|
8987
|
+
if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
|
|
7814
8988
|
const node = target.h?.[key];
|
|
7815
8989
|
if (node !== undefined && hasActiveOverride(node)) {
|
|
7816
8990
|
if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
|
|
@@ -7852,14 +9026,15 @@ const traps = {
|
|
|
7852
9026
|
// Array length writes implicitly delete indices — the written-keys bound
|
|
7853
9027
|
// can't see them, so poison to the full scan for this batch. Index
|
|
7854
9028
|
// writes implicitly GROW length, so arrays always record it alongside.
|
|
9029
|
+
const pcs = pcOf(target);
|
|
7855
9030
|
if (Array.isArray(pb)) {
|
|
7856
|
-
if (key === "length")
|
|
7857
|
-
else if (
|
|
7858
|
-
const wk = (
|
|
9031
|
+
if (key === "length") pcs.wk = WK_ALL;
|
|
9032
|
+
else if (pcs.wk !== WK_ALL) {
|
|
9033
|
+
const wk = (pcs.wk ??= new Set());
|
|
7859
9034
|
wk.add(key);
|
|
7860
9035
|
wk.add("length");
|
|
7861
9036
|
}
|
|
7862
|
-
} else if (
|
|
9037
|
+
} else if (pcs.wk !== WK_ALL) (pcs.wk ??= new Set()).add(key);
|
|
7863
9038
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
7864
9039
|
// defineProperty sidesteps a proto-chain setter named the same.
|
|
7865
9040
|
if (UNSAFE_KEYS.has(key)) {
|
|
@@ -7897,12 +9072,20 @@ const traps = {
|
|
|
7897
9072
|
const override = !draft && getWriteOverride();
|
|
7898
9073
|
if (!draft && !override) return true;
|
|
7899
9074
|
if (key === "__proto__") return true;
|
|
7900
|
-
if (desc.get || desc.set)
|
|
9075
|
+
if (desc.get || desc.set) {
|
|
9076
|
+
target.a = true;
|
|
9077
|
+
// Accessor demotion (re-audit blocker 3): a record that acquires an
|
|
9078
|
+
// accessor after patch registration stops being patchable — pull its
|
|
9079
|
+
// patches and re-drive them as tracked effect fallbacks. Hooks are
|
|
9080
|
+
// installed whenever pc.p exists (registration installs them).
|
|
9081
|
+
if (target.pc !== null && target.pc.p !== null) patchHooks.demoteToEffects(target);
|
|
9082
|
+
}
|
|
7901
9083
|
// Unwrap before ensurePB (see the set trap: self-reference materializes).
|
|
7902
9084
|
if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
|
|
7903
9085
|
const pb = ensurePB(target);
|
|
7904
9086
|
pendingNotify.add(target);
|
|
7905
|
-
|
|
9087
|
+
const pcd = pcOf(target);
|
|
9088
|
+
if (pcd.wk !== WK_ALL) (pcd.wk ??= new Set()).add(key);
|
|
7906
9089
|
Object.defineProperty(pb, key, desc);
|
|
7907
9090
|
if (target.del !== null) target.del.delete(key);
|
|
7908
9091
|
if (override) notifyWrites(target);
|
|
@@ -7914,7 +9097,8 @@ const traps = {
|
|
|
7914
9097
|
if (!draft && !override) return true;
|
|
7915
9098
|
const pb = ensurePB(target);
|
|
7916
9099
|
pendingNotify.add(target);
|
|
7917
|
-
|
|
9100
|
+
const pcx = pcOf(target);
|
|
9101
|
+
if (pcx.wk !== WK_ALL) (pcx.wk ??= new Set()).add(key);
|
|
7918
9102
|
delete pb[key];
|
|
7919
9103
|
// A prototype overlay cannot shadow a delete of a committed key —
|
|
7920
9104
|
// record it aside (#3044); reads/has/ownKeys/commit consult the set.
|
|
@@ -7987,8 +9171,39 @@ function createStoreNext(init, shallow = false) {
|
|
|
7987
9171
|
const setter = fn => storeSetterNext(proxy, fn);
|
|
7988
9172
|
return [proxy, setter];
|
|
7989
9173
|
}
|
|
9174
|
+
/** True when `proxy` is a SHALLOW store (children served verbatim, slots
|
|
9175
|
+
* replaced by reference — #2932). The list driver uses this to choose the
|
|
9176
|
+
* slot-patch channel (collected row bodies) over per-record registration. */
|
|
9177
|
+
function storeIsShallow(proxy) {
|
|
9178
|
+
const t = proxy?.[$TARGET];
|
|
9179
|
+
return t !== undefined && t.s === true;
|
|
9180
|
+
}
|
|
9181
|
+
/** True when `proxy` belongs to a projection/optimistic FAMILY. The list
|
|
9182
|
+
* driver must DECLINE family arrays (external audit finding): family
|
|
9183
|
+
* structural changes never emit row/slot ops (the setter channel is
|
|
9184
|
+
* fam-gated; optimistic writes ride node overrides), and the proxy identity
|
|
9185
|
+
* is stable so the each-watch cannot catch the change either — an engaged
|
|
9186
|
+
* list would freeze on optimistic/projection structural updates. Record-
|
|
9187
|
+
* level family patches are unaffected (they have their own emission). */
|
|
9188
|
+
function storeHasFamily(proxy) {
|
|
9189
|
+
const t = proxy?.[$TARGET];
|
|
9190
|
+
return t !== undefined && t.fam !== null;
|
|
9191
|
+
}
|
|
9192
|
+
/** True when `proxy` belongs to an OPTIMISTIC family specifically. The list
|
|
9193
|
+
* driver declines these (audit finding, narrowed): optimistic user writes
|
|
9194
|
+
* ride node-level overrides — they never enter the reconcile walk, so no
|
|
9195
|
+
* row/slot ops are emitted and an engaged list would freeze on optimistic
|
|
9196
|
+
* structural changes. PROJECTION (non-optimistic) families are drivable:
|
|
9197
|
+
* their recomputes go through the reconcile walk, whose emissions are
|
|
9198
|
+
* transition-stamped in the apply queue like any other (equivalence-matrix
|
|
9199
|
+
* gated). Re-admitting optimistic families requires a lane-timed structural
|
|
9200
|
+
* emission mirroring emitPatchOptimistic, plus revert resync. */
|
|
9201
|
+
function storeHasOptimisticFamily(proxy) {
|
|
9202
|
+
const t = proxy?.[$TARGET];
|
|
9203
|
+
return t !== undefined && t.fam?.opt === true;
|
|
9204
|
+
}
|
|
7990
9205
|
/** Tracking deep snapshot (`deep()` for next targets): subscribes to the
|
|
7991
|
-
* key-set and
|
|
9206
|
+
* key-set and deep-witness node at every reachable level, then returns the
|
|
7992
9207
|
* plain view. Shared references and cycles handled via the visited set. */
|
|
7993
9208
|
function deepNext(value) {
|
|
7994
9209
|
const t0 = value?.[$TARGET];
|
|
@@ -8188,7 +9403,7 @@ function reconcileNextState(value, state, key, replace = false) {
|
|
|
8188
9403
|
// positional so old-entity subtrees never merge into the new entity's).
|
|
8189
9404
|
const prev = t.pb ?? t.v;
|
|
8190
9405
|
const eq = keyFn(prev);
|
|
8191
|
-
if (eq !== undefined && keyFn(incoming)
|
|
9406
|
+
if (eq !== undefined && !sameKey(keyFn(incoming), eq)) {
|
|
8192
9407
|
if (!replace) throw new Error("Cannot reconcile states with different identity");
|
|
8193
9408
|
// Entity change: wholesale swap. The root proxy is stable for life
|
|
8194
9409
|
// (proj R5) but NOTHING below survives — children are never matched
|
|
@@ -8229,6 +9444,33 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8229
9444
|
const shallow = t.s === true;
|
|
8230
9445
|
const old = t.v;
|
|
8231
9446
|
adoptPB(t, incoming, eager);
|
|
9447
|
+
// Patch channel (adoption site): this record transitioned — queue its
|
|
9448
|
+
// patches with the pre-adopt prev. No bubbling walk: the adoption walk
|
|
9449
|
+
// visits parents before children, so ancestors emitted already. EAGER
|
|
9450
|
+
// only — family targets' visibility moment is their fold commit
|
|
9451
|
+
// (drainFolds emits there; emitting here too would double-fire).
|
|
9452
|
+
if (patchHooks !== null && eager && t.pc !== null && t.pc.p !== null) {
|
|
9453
|
+
// Accessor demotion at the ADOPTION seam is DEV-ONLY (prod principle:
|
|
9454
|
+
// explicitly-odd input must not cost correct-input prod — the
|
|
9455
|
+
// per-adoption scan was ~12% of dbmon's tick since adoptPB resets the
|
|
9456
|
+
// verdict every adoption). Dev demotes AND warns; prod emits directly,
|
|
9457
|
+
// so a getter adoptee's OUTSIDE deps (signals) won't re-apply in prod —
|
|
9458
|
+
// caught loudly during development instead. Registration-time admission
|
|
9459
|
+
// (patchableRaw) keeps its full one-time scan in both modes.
|
|
9460
|
+
if (!targetIsPlain(t)) {
|
|
9461
|
+
console.warn(
|
|
9462
|
+
"A reconcile adopted an object with own getters into a record that " +
|
|
9463
|
+
"carries compiled patches. Patches read raw values and will not " +
|
|
9464
|
+
"track the getters' reactive dependencies — this record's patches " +
|
|
9465
|
+
"are demoted to effects in development, but production will NOT " +
|
|
9466
|
+
"demote. Avoid getters on patched records, or key them out of " +
|
|
9467
|
+
"patch-eligible templates."
|
|
9468
|
+
);
|
|
9469
|
+
patchHooks.demoteToEffects(t);
|
|
9470
|
+
} else {
|
|
9471
|
+
patchHooks.emitPatchLocal(t, incoming, old);
|
|
9472
|
+
}
|
|
9473
|
+
}
|
|
8232
9474
|
// Shallow adoption: records are slot values — sticky raw-mark the incoming
|
|
8233
9475
|
// set (R41) and never descend; slot notification is the positional diff.
|
|
8234
9476
|
if (shallow) markRawIngest(incoming);
|
|
@@ -8268,7 +9510,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8268
9510
|
typeof pvRaw === "object" &&
|
|
8269
9511
|
nv !== null &&
|
|
8270
9512
|
typeof nv === "object" &&
|
|
8271
|
-
keyFn(pvRaw)
|
|
9513
|
+
sameKey(keyFn(pvRaw), keyFn(nv))
|
|
8272
9514
|
)
|
|
8273
9515
|
)
|
|
8274
9516
|
break; // misaligned: fall to the keyed remainder below
|
|
@@ -8296,6 +9538,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8296
9538
|
}
|
|
8297
9539
|
}
|
|
8298
9540
|
if (t.dk !== null && !dkBumpedA && i < nextRows.length) bumpDeep(t);
|
|
9541
|
+
const structStart = i; // misalignment point (== nlen on aligned ticks)
|
|
8299
9542
|
let prevByKey = null;
|
|
8300
9543
|
for (; i < nextRows.length; i++) {
|
|
8301
9544
|
const nv = nextRows[i];
|
|
@@ -8305,16 +9548,39 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8305
9548
|
let pv;
|
|
8306
9549
|
if (nk !== undefined) {
|
|
8307
9550
|
if (prevByKey === null) {
|
|
9551
|
+
// Occurrence-aware (re-audit 2, P1-5): duplicate keys queue
|
|
9552
|
+
// their prev INDICES (rows can themselves be arrays, so index
|
|
9553
|
+
// queues are the unambiguous encoding — same as buildRowOps)
|
|
9554
|
+
// and each is consumed ONCE. First-wins would adopt two next
|
|
9555
|
+
// rows into the SAME prev target while row ops retain two
|
|
9556
|
+
// separate DOM rows (the second one stale).
|
|
8308
9557
|
prevByKey = new Map();
|
|
8309
|
-
|
|
9558
|
+
// From structStart, not 0 (re-audit 3, P1-2): prefix-aligned
|
|
9559
|
+
// rows already adopted their incoming counterparts — re-offering
|
|
9560
|
+
// them here let a duplicate key adopt a prefix row AGAIN while
|
|
9561
|
+
// row ops (which correctly window from structStart) retained
|
|
9562
|
+
// the later occurrence's DOM row against a never-adopted target.
|
|
9563
|
+
for (let j = structStart; j < prevRows.length; j++) {
|
|
8310
9564
|
const p = unwrapValue(prevRows[j]);
|
|
8311
9565
|
if (p !== null && typeof p === "object") {
|
|
8312
9566
|
const pk = keyFn(p);
|
|
8313
|
-
if (pk
|
|
9567
|
+
if (pk === undefined) continue;
|
|
9568
|
+
const existing = prevByKey.get(pk);
|
|
9569
|
+
if (existing === undefined) prevByKey.set(pk, j);
|
|
9570
|
+
else if (Array.isArray(existing)) existing.push(j);
|
|
9571
|
+
else prevByKey.set(pk, [existing, j]);
|
|
8314
9572
|
}
|
|
8315
9573
|
}
|
|
8316
9574
|
}
|
|
8317
|
-
|
|
9575
|
+
const m = prevByKey.get(nk);
|
|
9576
|
+
if (m === undefined) pv = undefined;
|
|
9577
|
+
else if (Array.isArray(m)) {
|
|
9578
|
+
pv = unwrapValue(prevRows[m.shift()]);
|
|
9579
|
+
if (m.length === 1) prevByKey.set(nk, m[0]);
|
|
9580
|
+
} else {
|
|
9581
|
+
pv = unwrapValue(prevRows[m]);
|
|
9582
|
+
prevByKey.delete(nk);
|
|
9583
|
+
}
|
|
8318
9584
|
} else {
|
|
8319
9585
|
pv = unwrapValue(prevRows[i]); // keyless item: positional fallback
|
|
8320
9586
|
}
|
|
@@ -8328,12 +9594,62 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8328
9594
|
}
|
|
8329
9595
|
}
|
|
8330
9596
|
}
|
|
9597
|
+
// Row ops (PR-B): emit structural ops ONLY when structure changed —
|
|
9598
|
+
// aligned value ticks pay nothing. Built after the walk so retained
|
|
9599
|
+
// rows' value patches queue first (adds bind at op-apply).
|
|
9600
|
+
if (
|
|
9601
|
+
rowHooks !== null &&
|
|
9602
|
+
t.pc !== null &&
|
|
9603
|
+
t.pc.ro !== null &&
|
|
9604
|
+
(structStart < nlen || plen !== nlen)
|
|
9605
|
+
)
|
|
9606
|
+
buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn);
|
|
8331
9607
|
} else {
|
|
8332
9608
|
const dlen = Math.min(prevRows.length, nextRows.length);
|
|
8333
9609
|
const nlen = nextRows.length;
|
|
8334
9610
|
let dkBumpedP = false;
|
|
9611
|
+
const sp = rowHooks !== null && t.pc !== null ? t.pc.sp : null;
|
|
9612
|
+
// Row ops for shallow/positional lists: track the key-aligned prefix
|
|
9613
|
+
// (keyed) so aligned value ticks emit nothing; keyless lists emit only
|
|
9614
|
+
// on length change (append/truncate). Slot-patch consumers need the
|
|
9615
|
+
// alignment tracking too (aligned = value tick, misaligned = ops).
|
|
9616
|
+
const ro = rowHooks !== null && t.pc !== null ? t.pc.ro : null;
|
|
9617
|
+
let keyAligned = keyFn !== null && (ro !== null || sp !== null);
|
|
9618
|
+
let keyPrefix = 0;
|
|
8335
9619
|
for (let i = 0; i < nlen; i++) {
|
|
8336
9620
|
const nvP = nextRows[i];
|
|
9621
|
+
if (keyAligned && i < dlen) {
|
|
9622
|
+
const pvK = prevRows[i];
|
|
9623
|
+
if (
|
|
9624
|
+
pvK !== null &&
|
|
9625
|
+
typeof pvK === "object" &&
|
|
9626
|
+
nvP !== null &&
|
|
9627
|
+
typeof nvP === "object" &&
|
|
9628
|
+
// SameValueZero (self-sweep): strict === here broke slot
|
|
9629
|
+
// alignment on NaN keys while buildRowOps retained the row —
|
|
9630
|
+
// retained DOM with suppressed value ticks (the round-1 NaN
|
|
9631
|
+
// staleness, in the shallow branch).
|
|
9632
|
+
sameKey(keyFn(pvK), keyFn(nvP))
|
|
9633
|
+
)
|
|
9634
|
+
keyPrefix++;
|
|
9635
|
+
else keyAligned = false;
|
|
9636
|
+
}
|
|
9637
|
+
// Slot-patch dispatch (shallow): a KEY-ALIGNED slot whose value was
|
|
9638
|
+
// replaced by reference is a value tick — emit through the queue.
|
|
9639
|
+
// Misaligned/appended slots are STRUCTURE (row ops rebuild or move
|
|
9640
|
+
// them; new rows initial-apply at bind), so they emit nothing here.
|
|
9641
|
+
// Keyless positional lists treat same-index replacement as the value
|
|
9642
|
+
// tick for indices below the common length.
|
|
9643
|
+
// `i < dlen` is load-bearing for BOTH modes: an appended position
|
|
9644
|
+
// past a fully-aligned prefix (vacuously aligned when prev is empty)
|
|
9645
|
+
// has no previous slot — emitting a slot tick for it races the row
|
|
9646
|
+
// ops that CREATE the row (the slot queue applies first, indexing a
|
|
9647
|
+
// row that does not exist yet). Equivalence-matrix finding:
|
|
9648
|
+
// clear-then-refill and pure appends crashed the driver.
|
|
9649
|
+
if (sp !== null && i < dlen && (keyFn === null || keyAligned)) {
|
|
9650
|
+
const pvS = prevRows[i];
|
|
9651
|
+
if (pvS !== nvP) rowHooks.emitSlotPatch(t, i, nvP, pvS);
|
|
9652
|
+
}
|
|
8337
9653
|
if (!shallow && i < dlen && nvP !== null && typeof nvP === "object")
|
|
8338
9654
|
descend(unwrapValue(prevRows[i]), nvP, keyFn, fam, proj);
|
|
8339
9655
|
if (
|
|
@@ -8354,6 +9670,15 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8354
9670
|
}
|
|
8355
9671
|
}
|
|
8356
9672
|
}
|
|
9673
|
+
if (ro !== null) {
|
|
9674
|
+
const plen = prevRows.length;
|
|
9675
|
+
if (keyFn !== null) {
|
|
9676
|
+
if (keyPrefix < nlen || plen !== nlen)
|
|
9677
|
+
buildAndEmitRowOps(t, prevRows, nextRows, keyPrefix, keyFn);
|
|
9678
|
+
} else if (plen !== nlen) {
|
|
9679
|
+
buildAndEmitRowOps(t, prevRows, nextRows, dlen, null);
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
8357
9682
|
}
|
|
8358
9683
|
if (eager) {
|
|
8359
9684
|
if (nodes !== null && nodesHit < t.nc) {
|
|
@@ -8374,6 +9699,22 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8374
9699
|
// slots must not notify, R9). This replaces the notifyFold re-walk that
|
|
8375
9700
|
// doubled dbmon's diff cost. for-in covers own enumerable string keys
|
|
8376
9701
|
// with no key-array allocation; symbols get a pass only when present.
|
|
9702
|
+
// PROTOTYPE compiled-patch fast path: a pure-patch record (no nodes,
|
|
9703
|
+
// no presence/key-set/deep subscribers, no family) adopts and hands the
|
|
9704
|
+
// (next, prev) pair to its compiled patch — no per-key walk at all.
|
|
9705
|
+
if (
|
|
9706
|
+
t.pc !== null &&
|
|
9707
|
+
t.pc.p !== null &&
|
|
9708
|
+
eager &&
|
|
9709
|
+
t.n === null &&
|
|
9710
|
+
t.h === null &&
|
|
9711
|
+
t.k === null &&
|
|
9712
|
+
t.dk === null &&
|
|
9713
|
+
fam === null
|
|
9714
|
+
) {
|
|
9715
|
+
// Adoption already ran at applyAdopt entry; emission was queued there.
|
|
9716
|
+
return;
|
|
9717
|
+
}
|
|
8377
9718
|
const nodes = eager ? t.n : null;
|
|
8378
9719
|
let nodesHit = 0;
|
|
8379
9720
|
let dkBumped = false;
|
|
@@ -8439,6 +9780,93 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8439
9780
|
}
|
|
8440
9781
|
}
|
|
8441
9782
|
const hasOwnP = Object.prototype.hasOwnProperty;
|
|
9783
|
+
/** Setter-channel row ops (the fold site calls this for array targets with
|
|
9784
|
+
* ops consumers): structural mutation through the setter — push/splice/index
|
|
9785
|
+
* assignment/permutation — is a visibility transition for the list container
|
|
9786
|
+
* just like a reconcile walk, and drivers consuming registerRowOps must see
|
|
9787
|
+
* it. Setter mutations move the SAME row objects around, so RAW IDENTITY is
|
|
9788
|
+
* the key. Aligned arrays (value-only folds) emit nothing. */
|
|
9789
|
+
const identityKey = r => unwrapValue(r);
|
|
9790
|
+
/** Key equality for EVERY key comparison in this module (re-audit 2, P1-5):
|
|
9791
|
+
* SameValueZero, matching the Map-based matchers (buildRowOps, the adoption
|
|
9792
|
+
* window) — NaN keys are equal to themselves, so aligned NaN rows stay
|
|
9793
|
+
* aligned in the prefix walk instead of forever misaligning. Adoption and
|
|
9794
|
+
* row ops MUST agree on key equality or retained DOM rows go stale. */
|
|
9795
|
+
function sameKey(a, b) {
|
|
9796
|
+
return a === b || (a !== a && b !== b);
|
|
9797
|
+
}
|
|
9798
|
+
function emitSetterRowOps(t, prevRows, nextRows) {
|
|
9799
|
+
const ops = buildIdentityRowOps(prevRows, nextRows);
|
|
9800
|
+
if (ops !== null) rowHooks.emitRowOps(t, nextRows, ops);
|
|
9801
|
+
}
|
|
9802
|
+
/** Identity-keyed structural diff, returned rather than emitted: shared by
|
|
9803
|
+
* the setter channel (regular queue) and the OPTIMISTIC write channel (lane
|
|
9804
|
+
* queue) — same retention semantics, different dispatch timing. Returns
|
|
9805
|
+
* null when the lists are identity-aligned (no structure changed). */
|
|
9806
|
+
function buildIdentityRowOps(prevRows, nextRows) {
|
|
9807
|
+
let p = 0;
|
|
9808
|
+
const min = prevRows.length < nextRows.length ? prevRows.length : nextRows.length;
|
|
9809
|
+
while (p < min && unwrapValue(prevRows[p]) === unwrapValue(nextRows[p])) p++;
|
|
9810
|
+
if (p === prevRows.length && p === nextRows.length) return null;
|
|
9811
|
+
return buildRowOps(prevRows, nextRows, p, identityKey);
|
|
9812
|
+
}
|
|
9813
|
+
/** Shared row-ops builder (keyed deep branch + shallow/positional branch):
|
|
9814
|
+
* key-matches the misaligned window into { prefix, sources, removed }.
|
|
9815
|
+
* `keyFn === null` degrades to positional ops (append/truncate only). */
|
|
9816
|
+
function buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn) {
|
|
9817
|
+
rowHooks.emitRowOps(t, nextRows, buildRowOps(prevRows, nextRows, structStart, keyFn));
|
|
9818
|
+
}
|
|
9819
|
+
function buildRowOps(prevRows, nextRows, structStart, keyFn) {
|
|
9820
|
+
const plen = prevRows.length;
|
|
9821
|
+
const nlen = nextRows.length;
|
|
9822
|
+
const sources = new Array(nlen - structStart);
|
|
9823
|
+
// Occurrence-aware matching (re-audit): duplicate keys queue their old
|
|
9824
|
+
// indices and each is consumed ONCE — first-wins reuse would hand the same
|
|
9825
|
+
// source (and its one DOM row) to multiple next positions. The no-dup fast
|
|
9826
|
+
// shape stays a bare number; collisions upgrade to a queue.
|
|
9827
|
+
let oldIndexByKey = null;
|
|
9828
|
+
if (keyFn !== null && structStart < plen) {
|
|
9829
|
+
oldIndexByKey = new Map();
|
|
9830
|
+
for (let j = structStart; j < plen; j++) {
|
|
9831
|
+
const p = unwrapValue(prevRows[j]);
|
|
9832
|
+
if (p !== null && typeof p === "object") {
|
|
9833
|
+
const pk = keyFn(p);
|
|
9834
|
+
if (pk === undefined) continue;
|
|
9835
|
+
const existing = oldIndexByKey.get(pk);
|
|
9836
|
+
if (existing === undefined) oldIndexByKey.set(pk, j);
|
|
9837
|
+
else if (Array.isArray(existing)) existing.push(j);
|
|
9838
|
+
else oldIndexByKey.set(pk, [existing, j]);
|
|
9839
|
+
}
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
const consumed = oldIndexByKey !== null ? new Set() : null;
|
|
9843
|
+
for (let k = structStart; k < nlen; k++) {
|
|
9844
|
+
const nv = nextRows[k];
|
|
9845
|
+
let oldIdx = -1;
|
|
9846
|
+
if (nv !== null && typeof nv === "object" && oldIndexByKey !== null) {
|
|
9847
|
+
const nk = keyFn(nv);
|
|
9848
|
+
if (nk !== undefined) {
|
|
9849
|
+
const m = oldIndexByKey.get(nk);
|
|
9850
|
+
if (m !== undefined) {
|
|
9851
|
+
if (Array.isArray(m)) {
|
|
9852
|
+
oldIdx = m.shift();
|
|
9853
|
+
if (m.length === 1) oldIndexByKey.set(nk, m[0]);
|
|
9854
|
+
} else {
|
|
9855
|
+
oldIdx = m;
|
|
9856
|
+
oldIndexByKey.delete(nk);
|
|
9857
|
+
}
|
|
9858
|
+
consumed.add(oldIdx);
|
|
9859
|
+
}
|
|
9860
|
+
}
|
|
9861
|
+
}
|
|
9862
|
+
sources[k - structStart] = oldIdx;
|
|
9863
|
+
}
|
|
9864
|
+
const removed = [];
|
|
9865
|
+
for (let j = structStart; j < plen; j++) {
|
|
9866
|
+
if (consumed === null || !consumed.has(j)) removed.push(unwrapValue(prevRows[j]));
|
|
9867
|
+
}
|
|
9868
|
+
return { prefix: structStart, sources, removed };
|
|
9869
|
+
}
|
|
8442
9870
|
function descend(pv, nv, keyFn, fam, proj = false) {
|
|
8443
9871
|
if (pv === null || typeof pv !== "object" || nv === null || typeof nv !== "object") return;
|
|
8444
9872
|
// Lookup FIRST: a hit implies pv was wrappable and never raw-marked (only
|
|
@@ -8462,7 +9890,10 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
8462
9890
|
const nk = keyFn(nv);
|
|
8463
9891
|
// Key mismatch detaches: the slot takes the new entity; the old proxy
|
|
8464
9892
|
// keeps its (old) backing and a fresh proxy wraps the new value on read.
|
|
8465
|
-
|
|
9893
|
+
// SameValueZero (re-audit 2, P1-5): NaN keys are self-equal — strict
|
|
9894
|
+
// inequality detached every NaN-keyed slot on every tick while the
|
|
9895
|
+
// Map-based row-ops matcher retained its DOM row (stale forever).
|
|
9896
|
+
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return;
|
|
8466
9897
|
}
|
|
8467
9898
|
// Reachability pruning (§6d) is MODE-dependent, both pinned:
|
|
8468
9899
|
// - keyed matching descends only where subscriptions exist at/below (`d`) —
|
|
@@ -8516,7 +9947,8 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
8516
9947
|
* driven from inside an enclosing authoritative-write scope (next-store
|
|
8517
9948
|
* optimistic derives), and a hard `false` would clobber it mid-derive.
|
|
8518
9949
|
*/
|
|
8519
|
-
function wrapDraft(inner, isActive,
|
|
9950
|
+
function wrapDraft(inner, isActive, aroundWrite) {
|
|
9951
|
+
const write = op => (aroundWrite ? aroundWrite(op) : op());
|
|
8520
9952
|
const traps = {
|
|
8521
9953
|
get(_, prop) {
|
|
8522
9954
|
let value;
|
|
@@ -8531,7 +9963,7 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
8531
9963
|
}
|
|
8532
9964
|
if (prop === $TARGET) return value;
|
|
8533
9965
|
return typeof value === "object" && value !== null
|
|
8534
|
-
? wrapDraft(value, isActive,
|
|
9966
|
+
? wrapDraft(value, isActive, aroundWrite)
|
|
8535
9967
|
: value;
|
|
8536
9968
|
},
|
|
8537
9969
|
has(_, prop) {
|
|
@@ -8553,8 +9985,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
8553
9985
|
setWriteOverride(true);
|
|
8554
9986
|
setProjectionWriteActive(true);
|
|
8555
9987
|
try {
|
|
8556
|
-
|
|
8557
|
-
|
|
9988
|
+
write(() => {
|
|
9989
|
+
inner[prop] = value;
|
|
9990
|
+
});
|
|
8558
9991
|
} finally {
|
|
8559
9992
|
setWriteOverride(false);
|
|
8560
9993
|
setProjectionWriteActive(was);
|
|
@@ -8567,8 +10000,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
8567
10000
|
setWriteOverride(true);
|
|
8568
10001
|
setProjectionWriteActive(true);
|
|
8569
10002
|
try {
|
|
8570
|
-
|
|
8571
|
-
|
|
10003
|
+
write(() => {
|
|
10004
|
+
delete inner[prop];
|
|
10005
|
+
});
|
|
8572
10006
|
} finally {
|
|
8573
10007
|
setWriteOverride(false);
|
|
8574
10008
|
setProjectionWriteActive(was);
|
|
@@ -8609,8 +10043,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
8609
10043
|
setWriteOverride(true);
|
|
8610
10044
|
setProjectionWriteActive(true);
|
|
8611
10045
|
try {
|
|
8612
|
-
|
|
8613
|
-
|
|
10046
|
+
write(() => {
|
|
10047
|
+
Reflect.defineProperty(inner, prop, desc);
|
|
10048
|
+
});
|
|
8614
10049
|
} finally {
|
|
8615
10050
|
setWriteOverride(false);
|
|
8616
10051
|
setProjectionWriteActive(was);
|
|
@@ -8662,7 +10097,7 @@ function createStoreDerivedNext(fn, seed, options) {
|
|
|
8662
10097
|
}
|
|
8663
10098
|
];
|
|
8664
10099
|
}
|
|
8665
|
-
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit,
|
|
10100
|
+
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
|
|
8666
10101
|
const owner = getOwner();
|
|
8667
10102
|
let settled = false;
|
|
8668
10103
|
let result;
|
|
@@ -8676,7 +10111,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8676
10111
|
const draft = wrapDraft(
|
|
8677
10112
|
wrappedStore,
|
|
8678
10113
|
() => !settled || owner._x?._inFlight === result,
|
|
8679
|
-
|
|
10114
|
+
aroundDraftWrite
|
|
8680
10115
|
);
|
|
8681
10116
|
storeSetterNext(
|
|
8682
10117
|
draft,
|
|
@@ -8691,7 +10126,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8691
10126
|
if (v === s || v === undefined) return;
|
|
8692
10127
|
const write = () =>
|
|
8693
10128
|
storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
|
|
8694
|
-
wrapCommit ? wrapCommit(write) : write();
|
|
10129
|
+
wrapCommit ? wrapCommit(write, v) : write();
|
|
8695
10130
|
};
|
|
8696
10131
|
const sync = handleAsync(owner, result, commit);
|
|
8697
10132
|
if (!owner._loading) commit(sync);
|
|
@@ -8701,6 +10136,558 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8701
10136
|
return owner;
|
|
8702
10137
|
}
|
|
8703
10138
|
|
|
10139
|
+
/**
|
|
10140
|
+
* PR-A: the patch channel (DESIGN-PATCH-CHANNEL.md).
|
|
10141
|
+
*
|
|
10142
|
+
* Compiled patch functions — per-record compare-and-write consumers —
|
|
10143
|
+
* dispatched by the store's visibility transitions instead of render
|
|
10144
|
+
* effects. This module owns registration, the per-flush apply queue
|
|
10145
|
+
* (effect-phase timing, §2b), the owned-prev rule (§2c), and dispatch
|
|
10146
|
+
* bubbling (§4b). Emission calls live at the four visibility-transition
|
|
10147
|
+
* sites (adoption walk, setter notify, fold commit, override lifecycle)
|
|
10148
|
+
* and are gated on registration, so unpatched stores pay a null check.
|
|
10149
|
+
*
|
|
10150
|
+
* Bubbling contract: a targeted nested write reaches ancestor patches as a
|
|
10151
|
+
* FORCED re-apply — the third `force` argument makes every compiled compare
|
|
10152
|
+
* pass, so the ancestor rewrites its bound fields from its current backing
|
|
10153
|
+
* (idempotent, and prev-free: an ancestor's pre-state is not reconstructible
|
|
10154
|
+
* after in-place folds). Compiled bodies therefore have the signature
|
|
10155
|
+
* `(next, prev, force?)`.
|
|
10156
|
+
*
|
|
10157
|
+
* Tree-shaking: core never imports this module; stores without patches
|
|
10158
|
+
* never schedule the queue.
|
|
10159
|
+
*/
|
|
10160
|
+
let queue = null;
|
|
10161
|
+
let scheduled = false;
|
|
10162
|
+
function drainApplyQueue() {
|
|
10163
|
+
// Settle-time fallback for optimistic emissions (a reverting flush may
|
|
10164
|
+
// have no active lanes left to run the lane-slot drain).
|
|
10165
|
+
drainOptimistic();
|
|
10166
|
+
const q = queue;
|
|
10167
|
+
queue = null;
|
|
10168
|
+
scheduled = false;
|
|
10169
|
+
if (q === null) return;
|
|
10170
|
+
// Per-entry isolation: one throwing patch must not abort its siblings
|
|
10171
|
+
// (effect parity — each effect isolates its failure). A throwing patch
|
|
10172
|
+
// routes through its REGISTERING OWNER's queue chain exactly like a
|
|
10173
|
+
// render-effect error (§2b): an Errored boundary above the row collects
|
|
10174
|
+
// it (source = the owner, error read via owner._x?._error). Unhandled errors
|
|
10175
|
+
// rethrow after the drain so they still surface.
|
|
10176
|
+
let firstError = UNSET;
|
|
10177
|
+
for (let i = 0; i < q.length; i++) {
|
|
10178
|
+
clearStamp(q[i]);
|
|
10179
|
+
const { list, prev, force, t } = q[i];
|
|
10180
|
+
const next = t !== null ? (t.pb ?? t.v) : q[i].next;
|
|
10181
|
+
firstError = applyEntries(list, next, prev, force, firstError);
|
|
10182
|
+
}
|
|
10183
|
+
if (firstError !== UNSET) {
|
|
10184
|
+
// Unhandled patch errors HALT like unhandled effect errors (re-audit 2,
|
|
10185
|
+
// P1-4): app state is undefined past an unboundaried throw.
|
|
10186
|
+
haltReactivity(firstError);
|
|
10187
|
+
throw firstError;
|
|
10188
|
+
}
|
|
10189
|
+
}
|
|
10190
|
+
const UNSET = Symbol();
|
|
10191
|
+
/** ONE callback/error primitive for every drain (normal, transition-held,
|
|
10192
|
+
* optimistic): per-entry isolation — a throwing patch must not abort its
|
|
10193
|
+
* siblings (effect parity) — and failures route through the REGISTERING
|
|
10194
|
+
* OWNER's queue chain exactly like a render-effect error (§2b): an Errored
|
|
10195
|
+
* boundary above the row collects it. Unhandled errors are aggregated by the
|
|
10196
|
+
* caller (first one rethrows after its drain completes). */
|
|
10197
|
+
function applyEntries(list, next, prev, force, firstError) {
|
|
10198
|
+
// SNAPSHOT multi-consumer lists (re-audit 5, P1-3): a callback can dispose
|
|
10199
|
+
// a sibling's owner, whose unbind SPLICES this same array mid-iteration —
|
|
10200
|
+
// index-walking the live array skips the shifted consumer. The dominant
|
|
10201
|
+
// single-consumer case pays nothing; unbound entries are marked so a
|
|
10202
|
+
// snapshot never applies a consumer severed by an earlier callback.
|
|
10203
|
+
const snap = list.length > 1 ? list.slice() : list;
|
|
10204
|
+
for (let j = 0; j < snap.length; j++) {
|
|
10205
|
+
const entry = snap[j];
|
|
10206
|
+
if (entry.u === true) continue;
|
|
10207
|
+
// Disposed owners drop their patches (the row unmounted mid-flush).
|
|
10208
|
+
if (entry.owner !== null && isDisposed(entry.owner)) continue;
|
|
10209
|
+
try {
|
|
10210
|
+
entry.fn(next, prev, force);
|
|
10211
|
+
} catch (err) {
|
|
10212
|
+
let handled = false;
|
|
10213
|
+
const owner = entry.owner;
|
|
10214
|
+
if (owner !== null) {
|
|
10215
|
+
// Route through the nearest COMPUTED ancestor (re-audit 2, P1-4):
|
|
10216
|
+
// <Errored>.reset() recomputes its sources, and a plain owner (the
|
|
10217
|
+
// list driver's listOwner) is not recomputable — the component/memo
|
|
10218
|
+
// scope above it is, and recomputing it rebuilds the rows, exactly
|
|
10219
|
+
// what reset means for a throwing render effect.
|
|
10220
|
+
let source = owner;
|
|
10221
|
+
while (source !== null && source._fn === undefined) source = source._parent;
|
|
10222
|
+
source ??= owner;
|
|
10223
|
+
const statusErr = new StatusError(source, err);
|
|
10224
|
+
ext(source)._error = statusErr;
|
|
10225
|
+
source._statusFlags = (source._statusFlags ?? 0) | STATUS_ERROR;
|
|
10226
|
+
handled = owner._queue.notify(source, STATUS_ERROR, STATUS_ERROR, statusErr);
|
|
10227
|
+
}
|
|
10228
|
+
if (!handled && firstError === UNSET) firstError = err;
|
|
10229
|
+
}
|
|
10230
|
+
}
|
|
10231
|
+
return firstError;
|
|
10232
|
+
}
|
|
10233
|
+
// Transition-stamped emissions (§2b, "the walk is not the visibility moment
|
|
10234
|
+
// inside a transition"): entries stash DIRECTLY on their transition
|
|
10235
|
+
// (`_heldPatches`) and release into the live queue when THAT batch commits
|
|
10236
|
+
// (patchCommitHook). Reverted transitions never commit — their stash drops
|
|
10237
|
+
// with the transition object, no revert bookkeeping. The field (rather than
|
|
10238
|
+
// a WeakMap) keeps the every-flush commit-hook check to one property read;
|
|
10239
|
+
// the ambient batch never stashes.
|
|
10240
|
+
let commitHookInstalled = false;
|
|
10241
|
+
function releaseBatch(batch) {
|
|
10242
|
+
const held = batch._heldPatches;
|
|
10243
|
+
if (held === undefined) return;
|
|
10244
|
+
batch._heldPatches = undefined;
|
|
10245
|
+
for (let i = 0; i < held.length; i++) pushLive(held[i]);
|
|
10246
|
+
}
|
|
10247
|
+
function pushLive(item) {
|
|
10248
|
+
if (queue === null) queue = [];
|
|
10249
|
+
queue.push(item);
|
|
10250
|
+
if (!scheduled) {
|
|
10251
|
+
scheduled = true;
|
|
10252
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
10253
|
+
}
|
|
10254
|
+
}
|
|
10255
|
+
function push(item) {
|
|
10256
|
+
const tx = activeTransition;
|
|
10257
|
+
if (tx !== null) {
|
|
10258
|
+
let held = tx._heldPatches;
|
|
10259
|
+
if (held === undefined) tx._heldPatches = held = [];
|
|
10260
|
+
held.push(item);
|
|
10261
|
+
return;
|
|
10262
|
+
}
|
|
10263
|
+
pushLive(item);
|
|
10264
|
+
}
|
|
10265
|
+
/** Self-entry push with SAME-BATCH COALESCING (re-audit 2/3): a record's
|
|
10266
|
+
* later non-forced emission into the same container UPDATES the queued
|
|
10267
|
+
* entry in place — `next` takes the newest capture (adoption swaps the
|
|
10268
|
+
* backing object per emission; dropping the later one applied STALE state),
|
|
10269
|
+
* `prev` keeps the batch's earliest (effect semantics: one application per
|
|
10270
|
+
* batch spanning the whole window). The entry's consumer list is the live
|
|
10271
|
+
* pc.p array, so mid-batch registrants ride the single application. Forced
|
|
10272
|
+
* entries and row/slot ops never coalesce; the drain clears the stamps so a
|
|
10273
|
+
* quiet record retains nothing from its last batch. */
|
|
10274
|
+
function pushSelf(pc, item) {
|
|
10275
|
+
const tx = activeTransition;
|
|
10276
|
+
let arr;
|
|
10277
|
+
if (tx !== null) {
|
|
10278
|
+
let held = tx._heldPatches;
|
|
10279
|
+
if (held === undefined) tx._heldPatches = held = [];
|
|
10280
|
+
arr = held;
|
|
10281
|
+
} else {
|
|
10282
|
+
if (queue === null) queue = [];
|
|
10283
|
+
arr = queue;
|
|
10284
|
+
}
|
|
10285
|
+
if (pc.qa === arr && pc.qe !== null) {
|
|
10286
|
+
const qe = pc.qe;
|
|
10287
|
+
qe.next = item.next;
|
|
10288
|
+
qe.list = item.list; // pc.p can be re-created if emptied mid-batch
|
|
10289
|
+
return;
|
|
10290
|
+
}
|
|
10291
|
+
pc.qa = arr;
|
|
10292
|
+
pc.qe = item;
|
|
10293
|
+
item.pc = pc;
|
|
10294
|
+
arr.push(item);
|
|
10295
|
+
if (arr === queue && !scheduled) {
|
|
10296
|
+
scheduled = true;
|
|
10297
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
10298
|
+
}
|
|
10299
|
+
}
|
|
10300
|
+
/** Drain-side stamp clear (re-audit 3, P2-6): without it a quiet long-lived
|
|
10301
|
+
* record's channel retains its last batch's container array, entry, and both
|
|
10302
|
+
* captured backings for the record's lifetime. */
|
|
10303
|
+
function clearStamp(item) {
|
|
10304
|
+
const pc = item.pc;
|
|
10305
|
+
if (pc !== undefined && pc.qe === item) {
|
|
10306
|
+
pc.qa = null;
|
|
10307
|
+
pc.qe = null;
|
|
10308
|
+
}
|
|
10309
|
+
}
|
|
10310
|
+
/** Shallow clone for the owned-prev rule (§2c): owned backings fold values
|
|
10311
|
+
* INTO the same raw at commit, so a queued prev must be snapshotted. */
|
|
10312
|
+
function clonePrev(prev) {
|
|
10313
|
+
return Array.isArray(prev) ? prev.slice() : { ...prev };
|
|
10314
|
+
}
|
|
10315
|
+
/**
|
|
10316
|
+
* Emit a record's visibility transition. Callers gate on `hasPatches()` and
|
|
10317
|
+
* `t.d` cheaply; this function re-checks and walks ancestors (§4b).
|
|
10318
|
+
*/
|
|
10319
|
+
function emitPatch(t, next, prev) {
|
|
10320
|
+
const p = t.pc !== null ? t.pc.p : null;
|
|
10321
|
+
if (p !== null)
|
|
10322
|
+
pushSelf(t.pc, {
|
|
10323
|
+
list: p,
|
|
10324
|
+
next,
|
|
10325
|
+
prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
|
|
10326
|
+
force: false,
|
|
10327
|
+
t: null
|
|
10328
|
+
});
|
|
10329
|
+
// Bubbling: ancestors force-re-apply from their LIVE backing, resolved at
|
|
10330
|
+
// drain (privatization may clone it between now and then).
|
|
10331
|
+
let u = t.u;
|
|
10332
|
+
while (u !== null) {
|
|
10333
|
+
const up = u.pc !== null ? u.pc.p : null;
|
|
10334
|
+
if (up !== null) push({ list: up, next: null, prev: null, force: true, t: u });
|
|
10335
|
+
u = u.u;
|
|
10336
|
+
}
|
|
10337
|
+
}
|
|
10338
|
+
/** Emission for sites that already stand at the record with both sides in
|
|
10339
|
+
* hand and have already handled ancestors (the adoption walk descends —
|
|
10340
|
+
* parents were visited first), so no bubbling walk. */
|
|
10341
|
+
function emitPatchLocal(t, next, prev) {
|
|
10342
|
+
const p = t.pc !== null ? t.pc.p : null;
|
|
10343
|
+
if (p !== null)
|
|
10344
|
+
pushSelf(t.pc, {
|
|
10345
|
+
list: p,
|
|
10346
|
+
next,
|
|
10347
|
+
prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
|
|
10348
|
+
force: false,
|
|
10349
|
+
t: null
|
|
10350
|
+
});
|
|
10351
|
+
}
|
|
10352
|
+
/** Optimistic-channel emission: overrides are visible THIS flush while the
|
|
10353
|
+
* transaction is in flight — that is what optimism means. These ride a
|
|
10354
|
+
* dedicated queue drained at LANE-EFFECT timing (the regular effect queues
|
|
10355
|
+
* are stashed by an in-flight action), with the regular drain as the
|
|
10356
|
+
* settle-time fallback. `next === null` = forced re-apply from the live
|
|
10357
|
+
* target (the revert shape: committed truth back onto the DOM). */
|
|
10358
|
+
let optQueue = null;
|
|
10359
|
+
function drainOptimistic() {
|
|
10360
|
+
const q = optQueue;
|
|
10361
|
+
optQueue = null;
|
|
10362
|
+
if (q === null) return;
|
|
10363
|
+
// Same isolation/routing primitive as the normal drain (re-audit blocker
|
|
10364
|
+
// 5): one throwing optimistic patch must not abort its siblings, and it
|
|
10365
|
+
// must reach the registering owner's Errored boundary.
|
|
10366
|
+
let firstError = UNSET;
|
|
10367
|
+
for (let i = 0; i < q.length; i++) {
|
|
10368
|
+
clearStamp(q[i]);
|
|
10369
|
+
const { list, prev, force, t } = q[i];
|
|
10370
|
+
const next = t !== null ? (t.pb ?? t.v) : q[i].next;
|
|
10371
|
+
firstError = applyEntries(list, next, prev, force, firstError);
|
|
10372
|
+
}
|
|
10373
|
+
if (firstError !== UNSET) {
|
|
10374
|
+
haltReactivity(firstError);
|
|
10375
|
+
throw firstError;
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
function emitPatchOptimistic(t, next, prev) {
|
|
10379
|
+
const p = t.pc !== null ? t.pc.p : null;
|
|
10380
|
+
if (p === null) return;
|
|
10381
|
+
if (optQueue === null) optQueue = [];
|
|
10382
|
+
if (next === null) optQueue.push({ list: p, next: null, prev: null, force: true, t });
|
|
10383
|
+
else {
|
|
10384
|
+
// Same-batch coalescing, optimistic container (re-audit 3): later
|
|
10385
|
+
// non-forced emission updates the queued entry's next in place.
|
|
10386
|
+
const pc = t.pc;
|
|
10387
|
+
if (pc.qa === optQueue && pc.qe !== null) {
|
|
10388
|
+
const qe = pc.qe;
|
|
10389
|
+
qe.next = next;
|
|
10390
|
+
qe.list = p;
|
|
10391
|
+
} else {
|
|
10392
|
+
const item = { list: p, next, prev, force: false, t: null };
|
|
10393
|
+
pc.qa = optQueue;
|
|
10394
|
+
pc.qe = item;
|
|
10395
|
+
item.pc = pc;
|
|
10396
|
+
optQueue.push(item);
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
// Backup scheduling: the lane-slot drain covers in-flight application; a
|
|
10400
|
+
// stashed regular drain guarantees settle-time application when no lane
|
|
10401
|
+
// survives to the final flush (pure reverts).
|
|
10402
|
+
if (!scheduled) {
|
|
10403
|
+
scheduled = true;
|
|
10404
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
/** Row-ops emission at OPTIMISTIC (lane) timing: user drafts on an
|
|
10408
|
+
* optimistic family must show structure IN FLIGHT — bypassing the
|
|
10409
|
+
* transition stash exactly like emitPatchOptimistic. Two forms:
|
|
10410
|
+
* - `ops` given (write site): `nextRows` is the draft's intended visible
|
|
10411
|
+
* list, ops the identity diff against the pre-write optimistic view.
|
|
10412
|
+
* - `ops === null` (revert site): RESYNC — the consumer rebuilds retention
|
|
10413
|
+
* by row identity against the live post-revert view, resolved from the
|
|
10414
|
+
* target at drain time (overrides are gone by then, so `pb ?? v` IS the
|
|
10415
|
+
* committed truth). */
|
|
10416
|
+
function emitRowOpsOptimistic(t, nextRows, ops) {
|
|
10417
|
+
const list = t.pc !== null ? t.pc.ro : null;
|
|
10418
|
+
if (list === null) return;
|
|
10419
|
+
if (optQueue === null) optQueue = [];
|
|
10420
|
+
optQueue.push({
|
|
10421
|
+
list: list.map(e => ({
|
|
10422
|
+
owner: e.owner,
|
|
10423
|
+
fn: (n, _p) => e.fn(n, ops)
|
|
10424
|
+
})),
|
|
10425
|
+
next: nextRows,
|
|
10426
|
+
prev: null,
|
|
10427
|
+
force: false,
|
|
10428
|
+
t: nextRows === null ? t : null
|
|
10429
|
+
});
|
|
10430
|
+
if (!scheduled) {
|
|
10431
|
+
scheduled = true;
|
|
10432
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
/**
|
|
10436
|
+
* Register a compiled patch on a store record. Multi-consumer (two lists
|
|
10437
|
+
* can render one record); owner-scoped for disposal. Returns unbind.
|
|
10438
|
+
*/
|
|
10439
|
+
// Global registration count: the cheap gate emission sites check before any
|
|
10440
|
+
// per-record work (unpatched apps pay one number compare per transition).
|
|
10441
|
+
let patchCount = 0;
|
|
10442
|
+
function hasPatches() {
|
|
10443
|
+
return patchCount > 0;
|
|
10444
|
+
}
|
|
10445
|
+
function registerPatch(record, fn) {
|
|
10446
|
+
let t = record?.[$TARGET];
|
|
10447
|
+
if (t === undefined) throw new Error("registerPatch: not a store record");
|
|
10448
|
+
// Chained backings (§7b): register on the ULTIMATE owner — that is where
|
|
10449
|
+
// value transitions fold and dispatch; the wrapper's identity is stable
|
|
10450
|
+
// and would never fire (see ultimateTarget).
|
|
10451
|
+
t = ultimateTarget(t) ?? t;
|
|
10452
|
+
if (!commitHookInstalled) {
|
|
10453
|
+
commitHookInstalled = true;
|
|
10454
|
+
armPatchHooks();
|
|
10455
|
+
setPatchCommitHook(releaseBatch);
|
|
10456
|
+
GlobalQueue._drainPatchOptimistic = drainOptimistic;
|
|
10457
|
+
}
|
|
10458
|
+
const entry = { fn, owner: getOwner() };
|
|
10459
|
+
const pc = pcOf(t);
|
|
10460
|
+
const list = (pc.p ??= []);
|
|
10461
|
+
list.push(entry);
|
|
10462
|
+
patchCount++;
|
|
10463
|
+
// Bindings are subscriptions for reachability (§6d pruning must descend
|
|
10464
|
+
// into bound records).
|
|
10465
|
+
markDescendants(t);
|
|
10466
|
+
let unbound = false;
|
|
10467
|
+
return () => {
|
|
10468
|
+
if (unbound) return;
|
|
10469
|
+
unbound = true;
|
|
10470
|
+
entry.u = true; // dispatch snapshots skip severed consumers
|
|
10471
|
+
// Decrement ONLY on actual removal: a demotion (demoteToEffects) may
|
|
10472
|
+
// have already pulled this entry and repaired the count — the splice
|
|
10473
|
+
// miss is how this closure learns that.
|
|
10474
|
+
const idx = list.indexOf(entry);
|
|
10475
|
+
if (idx >= 0) {
|
|
10476
|
+
list.splice(idx, 1);
|
|
10477
|
+
patchCount--;
|
|
10478
|
+
}
|
|
10479
|
+
if (list.length === 0 && pc.p === list) pc.p = null;
|
|
10480
|
+
};
|
|
10481
|
+
}
|
|
10482
|
+
/** Resolve a target through CHAINED backings (§7b) to the ultimate owner.
|
|
10483
|
+
* A projection family wrapper's backing IS another store's proxy: value
|
|
10484
|
+
* transitions fold on the ULTIMATE target (the wrapper's identity never
|
|
10485
|
+
* changes), so patch registration and raw resolution must land there or
|
|
10486
|
+
* registered patches never fire (equivalence-matrix finding: projection
|
|
10487
|
+
* value ticks froze driver rows while classic effects tracked through). */
|
|
10488
|
+
function ultimateTarget(t) {
|
|
10489
|
+
while (t.ch) {
|
|
10490
|
+
const u = (t.pb ?? t.v)?.[$TARGET];
|
|
10491
|
+
if (u === undefined) return undefined;
|
|
10492
|
+
t = u;
|
|
10493
|
+
}
|
|
10494
|
+
return t;
|
|
10495
|
+
}
|
|
10496
|
+
/** Dual-driver bind probe (compiler runtime contract): when `record` is a
|
|
10497
|
+
* patchable store record, returns its CURRENT raw backing (the driver's
|
|
10498
|
+
* initial force-apply reads it directly — no proxy traffic, no tracking);
|
|
10499
|
+
* returns undefined otherwise (driver falls back to the effect path).
|
|
10500
|
+
* Not patchable: non-records, non-proxies, accessor-bearing records
|
|
10501
|
+
* (patches read raw — getters need tracked evaluation), broken chains. */
|
|
10502
|
+
function patchableRaw(record) {
|
|
10503
|
+
let t = record?.[$TARGET];
|
|
10504
|
+
if (t === undefined || t.px !== record || t.a === true) return undefined;
|
|
10505
|
+
t = ultimateTarget(t);
|
|
10506
|
+
// SCAN before trusting (re-audit blocker 3): `a` starts false and is only
|
|
10507
|
+
// discovered lazily (first draft, deep walks) — admission must run the
|
|
10508
|
+
// one-time own-accessor scan itself, or a getter-bearing record takes the
|
|
10509
|
+
// patch path and its getter's OUTSIDE dependencies (signals, other
|
|
10510
|
+
// records) never re-apply. Sticky `sc` makes this one probe pass per
|
|
10511
|
+
// record lifetime.
|
|
10512
|
+
if (t === undefined || !targetIsPlain(t)) return undefined;
|
|
10513
|
+
return t.pb ?? t.v;
|
|
10514
|
+
}
|
|
10515
|
+
/** Accessor demotion (design §5): a record that acquires an accessor after
|
|
10516
|
+
* registration stops being patchable — reads must go through tracked
|
|
10517
|
+
* evaluation. Clears patches and repairs the global count; callers re-drive
|
|
10518
|
+
* the pulled bodies (demoteToEffects). */
|
|
10519
|
+
function demotePatches(t) {
|
|
10520
|
+
if (t.pc === null) return null;
|
|
10521
|
+
const p = t.pc.p;
|
|
10522
|
+
t.pc.p = null;
|
|
10523
|
+
if (p === null) return null;
|
|
10524
|
+
patchCount -= p.length;
|
|
10525
|
+
// Drain IN PLACE: unbind closures captured this array — a late unbind must
|
|
10526
|
+
// miss its indexOf and not double-decrement the repaired count.
|
|
10527
|
+
return p.splice(0, p.length);
|
|
10528
|
+
}
|
|
10529
|
+
/** The demotion re-drive (re-audit blocker 3): each pulled body becomes the
|
|
10530
|
+
* SAME dual-driver effect fallback the web runtime would have chosen had the
|
|
10531
|
+
* record carried the accessor at bind — a tracked compute pass (next === prev
|
|
10532
|
+
* short-circuits every compare into a pure read THROUGH THE PROXY, so getter
|
|
10533
|
+
* dependencies track) plus an untracked force-apply at effect timing.
|
|
10534
|
+
*
|
|
10535
|
+
* Creation is DEFERRED to the effect phase: the trap that discovers the
|
|
10536
|
+
* accessor runs mid-draft, and an effect's initial pass must not read
|
|
10537
|
+
* through the proxy inside the write window. The record's own transition
|
|
10538
|
+
* for that draft is covered by the new effect's initial force-apply.
|
|
10539
|
+
*
|
|
10540
|
+
* Known edge (documented): a demoted LIST-ROW body re-drives under its
|
|
10541
|
+
* registering owner (the list owner), so per-row severing on removal is
|
|
10542
|
+
* lost for demoted rows — the effect lives until the LIST disposes. Rows
|
|
10543
|
+
* only demote when user code defines an accessor on a row record at
|
|
10544
|
+
* runtime. */
|
|
10545
|
+
function demoteToEffects(t) {
|
|
10546
|
+
const entries = demotePatches(t);
|
|
10547
|
+
if (entries === null || entries.length === 0) return;
|
|
10548
|
+
const proxy = t.px;
|
|
10549
|
+
globalQueue.enqueue(EFFECT_RENDER, () => {
|
|
10550
|
+
for (let i = 0; i < entries.length; i++) {
|
|
10551
|
+
const entry = entries[i];
|
|
10552
|
+
if (entry.owner !== null && isDisposed(entry.owner)) continue;
|
|
10553
|
+
const fn = entry.fn;
|
|
10554
|
+
runWithOwner(entry.owner, () =>
|
|
10555
|
+
createRenderEffect(
|
|
10556
|
+
() => {
|
|
10557
|
+
fn(proxy, proxy, false);
|
|
10558
|
+
},
|
|
10559
|
+
() => {
|
|
10560
|
+
// Block body: a compiled patch body's return value must not be
|
|
10561
|
+
// mistaken for an effect cleanup.
|
|
10562
|
+
untrack(() => fn(proxy, undefined, true));
|
|
10563
|
+
}
|
|
10564
|
+
)
|
|
10565
|
+
);
|
|
10566
|
+
}
|
|
10567
|
+
});
|
|
10568
|
+
}
|
|
10569
|
+
/** Register a structural-ops consumer on a keyed store array (the list
|
|
10570
|
+
* container's channel — what `For` consumes through the seam). */
|
|
10571
|
+
function registerRowOps(array, fn) {
|
|
10572
|
+
let t = array?.[$TARGET];
|
|
10573
|
+
if (t === undefined) throw new Error("registerRowOps: not a store array");
|
|
10574
|
+
// Chained backings resolve to the ULTIMATE owner, same as registerPatch
|
|
10575
|
+
// (§7b) — the walk/fold emits there (re-audit blocker 4).
|
|
10576
|
+
t = ultimateTarget(t) ?? t;
|
|
10577
|
+
armRowHooks();
|
|
10578
|
+
if (!commitHookInstalled) {
|
|
10579
|
+
commitHookInstalled = true;
|
|
10580
|
+
armPatchHooks();
|
|
10581
|
+
setPatchCommitHook(releaseBatch);
|
|
10582
|
+
GlobalQueue._drainPatchOptimistic = drainOptimistic;
|
|
10583
|
+
}
|
|
10584
|
+
const entry = { fn, owner: getOwner() };
|
|
10585
|
+
const pc = pcOf(t);
|
|
10586
|
+
const list = (pc.ro ??= []);
|
|
10587
|
+
list.push(entry);
|
|
10588
|
+
patchCount++;
|
|
10589
|
+
markDescendants(t);
|
|
10590
|
+
let unbound = false;
|
|
10591
|
+
return () => {
|
|
10592
|
+
if (unbound) return;
|
|
10593
|
+
unbound = true;
|
|
10594
|
+
patchCount--;
|
|
10595
|
+
const idx = list.indexOf(entry);
|
|
10596
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
10597
|
+
if (list.length === 0 && pc.ro === list) pc.ro = null;
|
|
10598
|
+
};
|
|
10599
|
+
}
|
|
10600
|
+
/** Slot patches (shallow arrays) ride the same apply queue: the walk emits
|
|
10601
|
+
* per aligned value-replaced slot; application happens at effect phase under
|
|
10602
|
+
* the registration owner's lifetime. */
|
|
10603
|
+
function emitSlotPatch(t, index, next, prev) {
|
|
10604
|
+
const sp = t.pc !== null ? t.pc.sp : null;
|
|
10605
|
+
if (sp === null) return;
|
|
10606
|
+
push({
|
|
10607
|
+
list: sp.map(e => ({ owner: e.owner, fn: () => e.fn(index, next, prev) })),
|
|
10608
|
+
next,
|
|
10609
|
+
prev,
|
|
10610
|
+
force: false,
|
|
10611
|
+
t: null
|
|
10612
|
+
});
|
|
10613
|
+
}
|
|
10614
|
+
/** Slot patch for shallow arrays: the reconcile walk emits (index, next,
|
|
10615
|
+
* prev) for KEY-ALIGNED value-replaced slots (structure rides row ops), and
|
|
10616
|
+
* the emission queues through the patch apply queue — effect-phase timing,
|
|
10617
|
+
* transition stamping, disposed-owner drop — like every other channel. */
|
|
10618
|
+
function registerSlotPatchNext(arr, fn) {
|
|
10619
|
+
let t = arr?.[$TARGET];
|
|
10620
|
+
if (t === undefined) throw new Error("registerSlotPatchNext: not a store array");
|
|
10621
|
+
// Chained backings resolve to the ULTIMATE owner, same as registerPatch
|
|
10622
|
+
// (§7b) — the walk emits slot ticks there (re-audit blocker 4).
|
|
10623
|
+
t = ultimateTarget(t) ?? t;
|
|
10624
|
+
armRowHooks();
|
|
10625
|
+
if (!commitHookInstalled) {
|
|
10626
|
+
commitHookInstalled = true;
|
|
10627
|
+
armPatchHooks();
|
|
10628
|
+
setPatchCommitHook(releaseBatch);
|
|
10629
|
+
GlobalQueue._drainPatchOptimistic = drainOptimistic;
|
|
10630
|
+
}
|
|
10631
|
+
// Multi-consumer (external audit): one shallow array can drive several
|
|
10632
|
+
// lists — registrations are a list, unbinds splice their own entry.
|
|
10633
|
+
const pc = pcOf(t);
|
|
10634
|
+
const entry = { fn, owner: getOwner() };
|
|
10635
|
+
(pc.sp ??= []).push(entry);
|
|
10636
|
+
markDescendants(t);
|
|
10637
|
+
let unbound = false;
|
|
10638
|
+
return () => {
|
|
10639
|
+
if (unbound || pc.sp === null) return;
|
|
10640
|
+
unbound = true;
|
|
10641
|
+
const idx = pc.sp.indexOf(entry);
|
|
10642
|
+
if (idx >= 0) pc.sp.splice(idx, 1);
|
|
10643
|
+
if (pc.sp.length === 0) pc.sp = null;
|
|
10644
|
+
};
|
|
10645
|
+
}
|
|
10646
|
+
/** Row-ops ride the SAME apply queue/timing as record patches: transition-
|
|
10647
|
+
* stamped, applied at effect phase, in emission order (structure before the
|
|
10648
|
+
* new rows' own patches can exist; retained rows' value patches commute). */
|
|
10649
|
+
function emitRowOps(t, next, ops) {
|
|
10650
|
+
const list = t.pc !== null ? t.pc.ro : null;
|
|
10651
|
+
if (list === null) return;
|
|
10652
|
+
push({
|
|
10653
|
+
list: list.map(e => ({
|
|
10654
|
+
owner: e.owner,
|
|
10655
|
+
fn: (n, _p) => e.fn(n, ops)
|
|
10656
|
+
})),
|
|
10657
|
+
next,
|
|
10658
|
+
prev: null,
|
|
10659
|
+
force: false,
|
|
10660
|
+
t: null
|
|
10661
|
+
});
|
|
10662
|
+
}
|
|
10663
|
+
// Pay-for-use seams: the write paths (store/reconcile/optimistic) emit
|
|
10664
|
+
// through installed hooks instead of importing this module. Installation is
|
|
10665
|
+
// LAZY (first registration) rather than a module-scope call — the dist is a
|
|
10666
|
+
// flat bundle, and a top-level side effect would retain the whole channel in
|
|
10667
|
+
// every consumer. TWO TIERS so a value-only registration (registerPatch —
|
|
10668
|
+
// present in ~every bundle under patch-mode default) does not retain the
|
|
10669
|
+
// list machinery (row-ops emitters + reconcile's diff builders): row hooks
|
|
10670
|
+
// arm only from the list driver's registrations. Sound because every
|
|
10671
|
+
// emission site is guarded by the matching pc channel, which only the
|
|
10672
|
+
// corresponding registration creates. See patch-hooks.ts.
|
|
10673
|
+
function armPatchHooks() {
|
|
10674
|
+
installPatchHooks({
|
|
10675
|
+
emitPatch,
|
|
10676
|
+
emitPatchLocal,
|
|
10677
|
+
emitPatchOptimistic,
|
|
10678
|
+
hasPatches,
|
|
10679
|
+
demoteToEffects
|
|
10680
|
+
});
|
|
10681
|
+
}
|
|
10682
|
+
function armRowHooks() {
|
|
10683
|
+
installRowHooks({
|
|
10684
|
+
emitRowOps,
|
|
10685
|
+
emitSlotPatch,
|
|
10686
|
+
emitSetterRowOps,
|
|
10687
|
+
emitRowOpsOptimistic
|
|
10688
|
+
});
|
|
10689
|
+
}
|
|
10690
|
+
|
|
8704
10691
|
/**
|
|
8705
10692
|
* Store rewrite — optimistic stores (§3/§7, RUL-3): no store-side layer, no
|
|
8706
10693
|
* backup snapshots. Nodes in an optimistic family are ARMED core signals
|
|
@@ -8710,12 +10697,31 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8710
10697
|
* armed presence nodes (the §6 overlay), so structural optimism reverts with
|
|
8711
10698
|
* the same per-transaction granularity (FINDING-2's fix by construction).
|
|
8712
10699
|
*
|
|
8713
|
-
* Derived form = an optimistic projection
|
|
8714
|
-
*
|
|
8715
|
-
*
|
|
8716
|
-
*
|
|
10700
|
+
* Derived form = an optimistic projection. Landings follow the fold rule
|
|
10701
|
+
* (#3164, RUL-2 as re-ruled): while a transaction retains optimistic edits
|
|
10702
|
+
* on the family, truth that lands STAGES into that transaction — a keyed
|
|
10703
|
+
* identity-preserving walk written through the ordinary staged setter
|
|
10704
|
+
* channel — and reveals atomically at settle, exactly like a signal landing
|
|
10705
|
+
* under an active override (asyncWrite's held branch). Optimistic edits are
|
|
10706
|
+
* never consumed by landings; they live exactly as long as their transaction
|
|
10707
|
+
* and die by engine-native revert. With no retainer, landings commit
|
|
10708
|
+
* immediately under projectionWriteActive (authoritative, silently beneath
|
|
10709
|
+
* any bare-write overrides — those ride the flight's own transition, #2951).
|
|
10710
|
+
* Authoritative readers (until()'s predicate) tunnel into staged truth via
|
|
10711
|
+
* the node read path's pending-value arm. The transitionBlocked store-half
|
|
10712
|
+
* (#2951) is installed here for next-shaped targets, chaining the
|
|
8717
10713
|
* legacy/engine checks.
|
|
8718
10714
|
*/
|
|
10715
|
+
/** #3164 fold: a stamped truth is HELD (masked from ordinary readers until
|
|
10716
|
+
* the reveal) only while its transition is live AND retaining optimism —
|
|
10717
|
+
* overrides are what make partial-coverage composition a tear. A plain
|
|
10718
|
+
* async transition carries no overrides, so downstream computes must see
|
|
10719
|
+
* staged values to converge (normal speculation). Resolves merges first:
|
|
10720
|
+
* merge unions optimistic nodes/stores into the target. */
|
|
10721
|
+
function transitionHoldsOptimism(transition) {
|
|
10722
|
+
const t = currentTransition(transition);
|
|
10723
|
+
return t._done !== true && (t._optimisticNodes.length !== 0 || t._optimisticStores.size !== 0);
|
|
10724
|
+
}
|
|
8719
10725
|
let blockedInstalled = false;
|
|
8720
10726
|
function installNextBlockedHalf() {
|
|
8721
10727
|
if (blockedInstalled) return;
|
|
@@ -8723,13 +10729,42 @@ function installNextBlockedHalf() {
|
|
|
8723
10729
|
// Late-bind the optimistic machinery into the plain store/reconcile paths
|
|
8724
10730
|
// (all call sites are fam?.opt-gated, so this always runs first) and the
|
|
8725
10731
|
// affects witness's view resolver.
|
|
8726
|
-
setOptHooks({
|
|
10732
|
+
setOptHooks({
|
|
10733
|
+
notifyOptimisticWrites,
|
|
10734
|
+
optimisticView,
|
|
10735
|
+
applyTentative,
|
|
10736
|
+
retainsOptimism: transitionHoldsOptimism
|
|
10737
|
+
});
|
|
8727
10738
|
setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw));
|
|
8728
10739
|
// Scheduler flush tails call _clearOptimisticStores whenever tracked
|
|
8729
10740
|
// stores exist; next has no layer to clear — reverts are engine-native —
|
|
8730
10741
|
// so the hook only empties the batch set.
|
|
8731
10742
|
if (!GlobalQueue._clearOptimisticStores) {
|
|
8732
10743
|
GlobalQueue._clearOptimisticStores = stores => {
|
|
10744
|
+
// Patch channel (revert site): engine-native reverts flip node values
|
|
10745
|
+
// back to committed; patched records need a forced DOM re-apply from
|
|
10746
|
+
// the post-revert view. Emission only — next keeps no layer to clear.
|
|
10747
|
+
for (const px of stores) {
|
|
10748
|
+
const t = px?.[$TARGET];
|
|
10749
|
+
const overlaid = t?.fam?.overlaid;
|
|
10750
|
+
if (overlaid !== undefined) {
|
|
10751
|
+
for (const ot of overlaid) {
|
|
10752
|
+
if (ot.pc !== null && ot.pc.p !== null) patchHooks.emitPatchOptimistic(ot, null, null);
|
|
10753
|
+
// Row-ops resync (family increment 2): reverts flip node values
|
|
10754
|
+
// back engine-natively; a driven list must rebuild retention by
|
|
10755
|
+
// row identity against the post-revert view (resolved from the
|
|
10756
|
+
// target at drain — overrides are gone by then).
|
|
10757
|
+
if (ot.pc !== null && ot.pc.ro !== null) rowHooks.emitRowOpsOptimistic(ot, null, null);
|
|
10758
|
+
// Keyset resync (classic channel twin): the keyset node's own
|
|
10759
|
+
// revert can compare EQUAL (a landing's bump matched the
|
|
10760
|
+
// tentative bump) while the arrangement underneath changed —
|
|
10761
|
+
// mapArray/ownKeys subscribers must re-read the post-revert
|
|
10762
|
+
// view. Authoritative bump: never re-arm the node we are
|
|
10763
|
+
// clearing.
|
|
10764
|
+
if (ot.k !== null) runAuthoritative(() => setSignal(ot.k, v => v + 1));
|
|
10765
|
+
}
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
8733
10768
|
stores.clear();
|
|
8734
10769
|
};
|
|
8735
10770
|
}
|
|
@@ -8787,20 +10822,41 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
8787
10822
|
};
|
|
8788
10823
|
const store = wrapNext(initialValue, null, null, fam);
|
|
8789
10824
|
fam.px = store;
|
|
10825
|
+
// Same key resolution the projection channels use ("id" default) — replay's
|
|
10826
|
+
// satisfaction rule reads it off the family.
|
|
10827
|
+
const keyOption = options?.key === undefined ? "id" : options.key;
|
|
10828
|
+
fam.key =
|
|
10829
|
+
typeof keyOption === "function"
|
|
10830
|
+
? keyOption
|
|
10831
|
+
: keyOption === null
|
|
10832
|
+
? null
|
|
10833
|
+
: row => (isWrappable(row) ? row[keyOption] : undefined);
|
|
8790
10834
|
if (fam.shallow) {
|
|
8791
10835
|
store[$TARGET].s = true;
|
|
8792
10836
|
markRawIngest(initialValue);
|
|
8793
10837
|
}
|
|
8794
10838
|
if (derived) {
|
|
8795
10839
|
const fn = first;
|
|
8796
|
-
//
|
|
8797
|
-
//
|
|
8798
|
-
//
|
|
8799
|
-
//
|
|
8800
|
-
|
|
8801
|
-
const wrapCommit = write => {
|
|
8802
|
-
|
|
8803
|
-
|
|
10840
|
+
// Landing router (#3164 fold ruling): while a transaction retains
|
|
10841
|
+
// optimistic edits on this family, truth landings stage INTO it and
|
|
10842
|
+
// reveal atomically at settle; with no retainer they commit immediately
|
|
10843
|
+
// under the authoritative posture (async commits land outside the
|
|
10844
|
+
// computed's sync body, so the posture is re-applied here).
|
|
10845
|
+
const wrapCommit = (write, value) => {
|
|
10846
|
+
const txn = retainingTransition(fam);
|
|
10847
|
+
if (txn === null) runAuthoritative(write);
|
|
10848
|
+
else stageLanding(fam, txn, value);
|
|
10849
|
+
};
|
|
10850
|
+
// Draft writes (the derive mutating its draft, sync body and post-await
|
|
10851
|
+
// continuations alike) are the same truth channel per-operation: bind
|
|
10852
|
+
// each op to the retaining transaction so its node writes stage and its
|
|
10853
|
+
// backing fold defers (ensurePB stamps foldBatches with the swapped-in
|
|
10854
|
+
// batch; the write-override eager-commit branch in notifyWrites yields
|
|
10855
|
+
// to any active transaction).
|
|
10856
|
+
const aroundDraftWrite = op => {
|
|
10857
|
+
const txn = retainingTransition(fam);
|
|
10858
|
+
if (txn === null) op();
|
|
10859
|
+
else runFolded(txn, op);
|
|
8804
10860
|
};
|
|
8805
10861
|
let nodeOptions;
|
|
8806
10862
|
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
@@ -8812,14 +10868,201 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
8812
10868
|
fn,
|
|
8813
10869
|
options?.key === undefined ? "id" : options.key,
|
|
8814
10870
|
wrapCommit,
|
|
8815
|
-
|
|
10871
|
+
aroundDraftWrite
|
|
8816
10872
|
)
|
|
8817
10873
|
);
|
|
8818
10874
|
}, nodeOptions);
|
|
8819
10875
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
8820
10876
|
fam.node = node;
|
|
8821
10877
|
}
|
|
8822
|
-
return [
|
|
10878
|
+
return [
|
|
10879
|
+
store,
|
|
10880
|
+
fn => {
|
|
10881
|
+
// Retention ledger (#3164): record the owning transaction so landings
|
|
10882
|
+
// know to fold. Captured at entry — the action machinery has the
|
|
10883
|
+
// transaction ambient while user code runs; a bare write with no
|
|
10884
|
+
// transaction retains nothing (it rides the flight's own transition
|
|
10885
|
+
// per #2951 and dies with it).
|
|
10886
|
+
const txn = activeTransition;
|
|
10887
|
+
storeSetterNext(store, fn);
|
|
10888
|
+
if (txn !== null) (fam.rt ??= new Set()).add(txn);
|
|
10889
|
+
}
|
|
10890
|
+
];
|
|
10891
|
+
}
|
|
10892
|
+
/** Resolve a retained transition through its merge chain (`_done` holds the
|
|
10893
|
+
* merge target while merged, `true` once settled). Null = dead. */
|
|
10894
|
+
function liveTransition(txn) {
|
|
10895
|
+
while (typeof txn._done === "object") txn = txn._done;
|
|
10896
|
+
return txn._done === true ? null : txn;
|
|
10897
|
+
}
|
|
10898
|
+
/** The transaction truth landings fold into: the first live member of the
|
|
10899
|
+
* family's retention ledger (dead members prune here). Multiple live
|
|
10900
|
+
* retainers entangle through their shared family writes and settle
|
|
10901
|
+
* together, so folding into the first reaches all of them. */
|
|
10902
|
+
function retainingTransition(fam) {
|
|
10903
|
+
const rt = fam.rt;
|
|
10904
|
+
if (rt === undefined || rt.size === 0) return null;
|
|
10905
|
+
let live = null;
|
|
10906
|
+
for (const txn of rt) {
|
|
10907
|
+
const resolved = liveTransition(txn);
|
|
10908
|
+
if (resolved === null) rt.delete(txn);
|
|
10909
|
+
else live ??= resolved;
|
|
10910
|
+
}
|
|
10911
|
+
return live;
|
|
10912
|
+
}
|
|
10913
|
+
/** Fold a landing into the retaining transaction (#3164): the landed value
|
|
10914
|
+
* is written through the ORDINARY staged setter channel — node writes park
|
|
10915
|
+
* as `_pendingValue` registered with the transaction's batch (speculation
|
|
10916
|
+
* and until()'s authoritative tunnel see them; live view stays coherent),
|
|
10917
|
+
* and the backing fold defers via the foldBatches stamp — under the
|
|
10918
|
+
* authoritative posture, so armed nodes take the engine bypass and no
|
|
10919
|
+
* override is created. The engine's own commit machinery reveals everything
|
|
10920
|
+
* atomically when the transaction settles (transitions never abort: failed
|
|
10921
|
+
* actions still commit — only optimistic overrides revert). */
|
|
10922
|
+
function stageLanding(fam, txn, incoming) {
|
|
10923
|
+
runFolded(txn, () =>
|
|
10924
|
+
runAuthoritative(() =>
|
|
10925
|
+
storeSetterNext(
|
|
10926
|
+
fam.px,
|
|
10927
|
+
draft => {
|
|
10928
|
+
stagedApply(draft, unwrapValue(incoming), fam.key ?? null);
|
|
10929
|
+
},
|
|
10930
|
+
false
|
|
10931
|
+
)
|
|
10932
|
+
)
|
|
10933
|
+
);
|
|
10934
|
+
}
|
|
10935
|
+
/** Run a fold write inside the retaining transaction's batch, then
|
|
10936
|
+
* transition-stamp its staged nodes NOW (parity with the parked-transition
|
|
10937
|
+
* flush path's reassignPendingTransition): the stamp is what routes stale
|
|
10938
|
+
* (render) readers to the committed value — core read's cross-transaction
|
|
10939
|
+
* guard — and what makes foldHeld defer the backing for context-free
|
|
10940
|
+
* readers. A microtask staging never crosses that flush path, so without
|
|
10941
|
+
* the stamp a render effect's speculative recompute would compose staged
|
|
10942
|
+
* truth with live overrides — the #3164 tear, one window later. Armed
|
|
10943
|
+
* nodes additionally raise CONFIG_HELD_TRUTH: their staged value is
|
|
10944
|
+
* confirming truth masked from ordinary readers until the reveal (plain
|
|
10945
|
+
* staged nodes stay visible — normal speculation; override-covered nodes
|
|
10946
|
+
* stay unarmed — the override is their display and its revert their
|
|
10947
|
+
* notification, A17). */
|
|
10948
|
+
function runFolded(txn, op) {
|
|
10949
|
+
runAsTransitionBatch(txn, op);
|
|
10950
|
+
const pending = txn._pendingNodes;
|
|
10951
|
+
for (let i = 0; i < pending.length; i++) {
|
|
10952
|
+
const node = pending[i];
|
|
10953
|
+
node._transition = txn;
|
|
10954
|
+
if (node._config & CONFIG_OPTIMISTIC && !hasActiveOverride(node))
|
|
10955
|
+
node._config |= CONFIG_HELD_TRUTH;
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
/** Keyed identity-preserving deep merge through live draft proxies — the
|
|
10959
|
+
* staged twin of the adoption walk. Reads see the pending backing (staged
|
|
10960
|
+
* view), so consecutive landings during one hold compose; key-matched rows
|
|
10961
|
+
* keep their raw (and so their proxy) in the slot with only changed leaves
|
|
10962
|
+
* written; unmatched rows land wholesale. Runs inside stageLanding's
|
|
10963
|
+
* authoritative bracket: drafts seed from committed truth, never overlays. */
|
|
10964
|
+
function stagedApply(cur, incoming, keyFn) {
|
|
10965
|
+
const curArr = Array.isArray(cur);
|
|
10966
|
+
if (curArr && Array.isArray(incoming)) {
|
|
10967
|
+
const len = incoming.length;
|
|
10968
|
+
if (keyFn !== null) {
|
|
10969
|
+
// Occurrence-aware key queues (parity with the adoption window):
|
|
10970
|
+
// duplicate keys match per occurrence, each current row consumed once.
|
|
10971
|
+
let byKey = null;
|
|
10972
|
+
const curLen = cur.length;
|
|
10973
|
+
for (let j = 0; j < curLen; j++) {
|
|
10974
|
+
const raw = unwrapValue(cur[j]);
|
|
10975
|
+
if (!isWrappable(raw)) continue;
|
|
10976
|
+
const k = keyFn(raw);
|
|
10977
|
+
if (k === undefined) continue;
|
|
10978
|
+
const q = (byKey ??= new Map()).get(k);
|
|
10979
|
+
if (q === undefined) byKey.set(k, [raw]);
|
|
10980
|
+
else q.push(raw);
|
|
10981
|
+
}
|
|
10982
|
+
// Echo adoption: an optimistic structural add whose key the landing
|
|
10983
|
+
// confirms must keep its raw (and so its proxy — list drivers keep the
|
|
10984
|
+
// DOM row). Tentative rows never reach committed truth (they live in
|
|
10985
|
+
// node overrides), so key-match the draft target's active override
|
|
10986
|
+
// rows as a secondary pool. Committed rows queued first own their
|
|
10987
|
+
// keys; overlay rows only extend coverage. Adopted raws enter staged
|
|
10988
|
+
// truth; at settle the override reverts and the reveal re-seats the
|
|
10989
|
+
// same raw.
|
|
10990
|
+
const overlayNodes = cur[$TARGET]?.n;
|
|
10991
|
+
if (overlayNodes != null) {
|
|
10992
|
+
for (const ok of Reflect.ownKeys(overlayNodes)) {
|
|
10993
|
+
const node = overlayNodes[ok];
|
|
10994
|
+
if (!hasActiveOverride(node)) continue;
|
|
10995
|
+
const raw = unwrapValue(unwrapOverride(node._x._overrideValue));
|
|
10996
|
+
if (!isWrappable(raw)) continue;
|
|
10997
|
+
const k = keyFn(raw);
|
|
10998
|
+
if (k === undefined) continue;
|
|
10999
|
+
const q = (byKey ??= new Map()).get(k);
|
|
11000
|
+
if (q === undefined) byKey.set(k, [raw]);
|
|
11001
|
+
else q.push(raw);
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
for (let i = 0; i < len; i++) {
|
|
11005
|
+
const nv = incoming[i];
|
|
11006
|
+
let matched;
|
|
11007
|
+
if (isWrappable(nv) && byKey !== null) {
|
|
11008
|
+
const nk = keyFn(nv);
|
|
11009
|
+
if (nk !== undefined) {
|
|
11010
|
+
for (const [k, q] of byKey) {
|
|
11011
|
+
if (!sameKey(k, nk)) continue;
|
|
11012
|
+
matched = q.shift();
|
|
11013
|
+
if (q.length === 0) byKey.delete(k);
|
|
11014
|
+
break;
|
|
11015
|
+
}
|
|
11016
|
+
}
|
|
11017
|
+
}
|
|
11018
|
+
if (matched !== undefined) {
|
|
11019
|
+
if (unwrapValue(cur[i]) !== matched) cur[i] = matched;
|
|
11020
|
+
stagedApply(cur[i], nv, keyFn);
|
|
11021
|
+
} else {
|
|
11022
|
+
const pv = unwrapValue(cur[i]);
|
|
11023
|
+
if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) cur[i] = nv;
|
|
11024
|
+
}
|
|
11025
|
+
}
|
|
11026
|
+
} else {
|
|
11027
|
+
for (let i = 0; i < len; i++) {
|
|
11028
|
+
const nv = incoming[i];
|
|
11029
|
+
const pv = unwrapValue(cur[i]);
|
|
11030
|
+
if (pv === nv) continue;
|
|
11031
|
+
if (isWrappable(nv) && isWrappable(pv) && Array.isArray(nv) === Array.isArray(pv))
|
|
11032
|
+
stagedApply(cur[i], nv, keyFn);
|
|
11033
|
+
else if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) cur[i] = nv;
|
|
11034
|
+
}
|
|
11035
|
+
}
|
|
11036
|
+
if (cur.length !== len) cur.length = len;
|
|
11037
|
+
return;
|
|
11038
|
+
}
|
|
11039
|
+
// Object merge; also the degenerate root-kind-change shape (arrays accept
|
|
11040
|
+
// keyed writes/deletes, so a wholesale restatement still lands staged).
|
|
11041
|
+
for (const k of Reflect.ownKeys(incoming)) {
|
|
11042
|
+
if (curArr && k === "length") continue;
|
|
11043
|
+
const nv = incoming[k];
|
|
11044
|
+
const pv = unwrapValue(cur[k]);
|
|
11045
|
+
if (pv === nv) continue;
|
|
11046
|
+
if (isWrappable(nv) && isWrappable(pv) && Array.isArray(nv) === Array.isArray(pv)) {
|
|
11047
|
+
// Different-keyed entities never merge (tentative-channel parity):
|
|
11048
|
+
// the incoming object replaces the slot wholesale.
|
|
11049
|
+
if (keyFn !== null) {
|
|
11050
|
+
const pk = keyFn(pv);
|
|
11051
|
+
const nk = keyFn(nv);
|
|
11052
|
+
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) {
|
|
11053
|
+
cur[k] = nv;
|
|
11054
|
+
continue;
|
|
11055
|
+
}
|
|
11056
|
+
}
|
|
11057
|
+
stagedApply(cur[k], nv, keyFn);
|
|
11058
|
+
} else if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) {
|
|
11059
|
+
cur[k] = nv;
|
|
11060
|
+
}
|
|
11061
|
+
}
|
|
11062
|
+
for (const k of Reflect.ownKeys(cur)) {
|
|
11063
|
+
if ((curArr && k === "length") || k in incoming) continue;
|
|
11064
|
+
delete cur[k];
|
|
11065
|
+
}
|
|
8823
11066
|
}
|
|
8824
11067
|
// ---- optimistic-only store machinery (moved from next/store.ts /
|
|
8825
11068
|
// next/reconcile.ts so plain-store bundles tree-shake it) ----
|
|
@@ -8836,6 +11079,22 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
8836
11079
|
const fw = t.fam?.node;
|
|
8837
11080
|
if (fw?._transition) globalQueue.initTransition(fw._transition);
|
|
8838
11081
|
const old = t.v;
|
|
11082
|
+
// Patch channel (override-application site): the draft IS the intended
|
|
11083
|
+
// visible state; prev is the view before these overrides apply. Bypasses
|
|
11084
|
+
// the transition stash — optimism is visible in flight.
|
|
11085
|
+
if (t.pc !== null && t.pc.p !== null)
|
|
11086
|
+
patchHooks.emitPatchOptimistic(t, pb, optimisticView(t, old));
|
|
11087
|
+
// Row-ops channel (family increment 2): optimistic STRUCTURE on an array
|
|
11088
|
+
// rides node overrides — it never enters the reconcile walk — so a driven
|
|
11089
|
+
// list must get its structural ops here, lane-timed. Identity diff of the
|
|
11090
|
+
// pre-write optimistic view against the draft; aligned writes emit nothing.
|
|
11091
|
+
if (t.pc !== null && t.pc.ro !== null && Array.isArray(pb)) {
|
|
11092
|
+
const prevView = optimisticView(t, old);
|
|
11093
|
+
if (Array.isArray(prevView)) {
|
|
11094
|
+
const ops = buildIdentityRowOps(prevView, pb);
|
|
11095
|
+
if (ops !== null) rowHooks.emitRowOpsOptimistic(t, pb, ops);
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
8839
11098
|
const visible = (key, fallback) => {
|
|
8840
11099
|
const node = t.n?.[key];
|
|
8841
11100
|
return node !== undefined && hasActiveOverride(node)
|
|
@@ -8886,92 +11145,22 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
8886
11145
|
// (structural ones already ride the key-set bump above).
|
|
8887
11146
|
bumpDeep(t);
|
|
8888
11147
|
// Discard the draft — committed raw is untouched (revert target by
|
|
8889
|
-
// construction)
|
|
8890
|
-
//
|
|
8891
|
-
|
|
11148
|
+
// construction) — restoring any truth-staged backing this draft displaced
|
|
11149
|
+
// (#3164 fold: ensurePB parked it so tentative writes could not pollute
|
|
11150
|
+
// staged truth). Register the root store for the scheduler's settle hooks.
|
|
11151
|
+
t.pb = stagedTruthPB.get(t) ?? null;
|
|
11152
|
+
if (t.pb !== null) stagedTruthPB.delete(t);
|
|
8892
11153
|
(t.fam.overlaid ??= new Set()).add(t);
|
|
8893
11154
|
GlobalQueue._trackOptimisticStore?.(t.fam.px ?? t.px);
|
|
8894
11155
|
}
|
|
8895
|
-
/**
|
|
8896
|
-
* Landing consumption (RUL-2): fresh authoritative data supersedes every
|
|
8897
|
-
* tentative override in the family. Mirrors legacy clearProjectionOverride —
|
|
8898
|
-
* drop the override, clear lane/ownership, notify subscribers whose visible
|
|
8899
|
-
* value changes (reversion effects go to regular queues via the projection
|
|
8900
|
-
* write posture the caller holds).
|
|
8901
|
-
*/
|
|
8902
|
-
function consumeOverridesNext(fam) {
|
|
8903
|
-
const overlaid = fam.overlaid;
|
|
8904
|
-
if (overlaid === undefined || overlaid.size === 0) return;
|
|
8905
|
-
runAuthoritative(() => {
|
|
8906
|
-
for (const t of overlaid) {
|
|
8907
|
-
const drop = (node, committed) => {
|
|
8908
|
-
if (!hasActiveOverride(node)) return;
|
|
8909
|
-
const prev = unwrapOverride(node._x?._overrideValue);
|
|
8910
|
-
// Full legacy reset (clearOptimisticOverride parity): the landing is
|
|
8911
|
-
// authoritative NOW — fold committed into the node directly instead
|
|
8912
|
-
// of riding a transaction's commit (whose queues may be stashed with
|
|
8913
|
-
// the transaction parked; the wake would strand until it settles).
|
|
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
|
-
}
|
|
8921
|
-
node._pendingValue = NOT_PENDING;
|
|
8922
|
-
node._value = committed;
|
|
8923
|
-
if (!node._equals || !node._equals(prev, committed)) {
|
|
8924
|
-
insertSubs(node, true);
|
|
8925
|
-
schedule();
|
|
8926
|
-
}
|
|
8927
|
-
};
|
|
8928
|
-
// Landing consumes STRUCTURAL optimism only (legacy layer parity):
|
|
8929
|
-
// membership edits, array length, and the value overrides written WITH
|
|
8930
|
-
// them (a key carrying an active presence override is an add/delete —
|
|
8931
|
-
// classified BEFORE the adoption may have made the key exist in landed
|
|
8932
|
-
// data). A pure value override on a key the landing carries stays with
|
|
8933
|
-
// its owning transaction (rapid-toggle contract: a live action's edit
|
|
8934
|
-
// of an existing entity rides on top of landed truth).
|
|
8935
|
-
const isArr = Array.isArray(t.v);
|
|
8936
|
-
const has = t.h;
|
|
8937
|
-
let structuralKeys = null;
|
|
8938
|
-
if (has !== null) {
|
|
8939
|
-
for (const key of Reflect.ownKeys(has)) {
|
|
8940
|
-
if (hasActiveOverride(has[key])) (structuralKeys ??= new Set()).add(key);
|
|
8941
|
-
}
|
|
8942
|
-
}
|
|
8943
|
-
const nodes = t.n;
|
|
8944
|
-
if (nodes !== null) {
|
|
8945
|
-
for (const key of Reflect.ownKeys(nodes)) {
|
|
8946
|
-
const structural =
|
|
8947
|
-
structuralKeys?.has(key) || !(key in t.v) || (isArr && key === "length");
|
|
8948
|
-
if (!structural) continue;
|
|
8949
|
-
drop(nodes[key], isArr && key === "length" ? t.v.length : t.v[key]);
|
|
8950
|
-
}
|
|
8951
|
-
}
|
|
8952
|
-
if (has !== null) {
|
|
8953
|
-
for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
|
|
8954
|
-
}
|
|
8955
|
-
if (t.k !== null && hasActiveOverride(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
|
-
}
|
|
8963
|
-
insertSubs(t.k, true);
|
|
8964
|
-
schedule();
|
|
8965
|
-
}
|
|
8966
|
-
}
|
|
8967
|
-
overlaid.clear();
|
|
8968
|
-
});
|
|
8969
|
-
}
|
|
8970
11156
|
/** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
|
|
8971
11157
|
* view, lane values included; a fresh copy per call during pending windows —
|
|
8972
|
-
* RUL-12). Returns `src` untouched when no override is active on `t`.
|
|
11158
|
+
* RUL-12). Returns `src` untouched when no override is active on `t`.
|
|
11159
|
+
* Authoritative-view reads (until()'s predicate) skip composition entirely:
|
|
11160
|
+
* the predicate observes authoritative truth, never the caller's tentative
|
|
11161
|
+
* overlay. (Write-side emission callers never run under such a compute.) */
|
|
8973
11162
|
function optimisticView(t, src) {
|
|
8974
|
-
if (t.fam?.opt !== true) return src;
|
|
11163
|
+
if (t.fam?.opt !== true || authoritativeRead()) return src;
|
|
8975
11164
|
let out = null;
|
|
8976
11165
|
const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
|
|
8977
11166
|
const nodes = t.n;
|
|
@@ -9011,7 +11200,9 @@ function applyTentative(t, incoming, keyFn) {
|
|
|
9011
11200
|
if (keyFn) {
|
|
9012
11201
|
const pk = keyFn(pv);
|
|
9013
11202
|
const nk = keyFn(nv);
|
|
9014
|
-
|
|
11203
|
+
// SameValueZero (re-audit 3, P1-3): parity with the plain reconcile
|
|
11204
|
+
// channel — NaN keys are self-equal.
|
|
11205
|
+
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return null;
|
|
9015
11206
|
}
|
|
9016
11207
|
return map.get(unwrapValue(pv)) ?? null;
|
|
9017
11208
|
};
|
|
@@ -9026,16 +11217,31 @@ function applyTentative(t, incoming, keyFn) {
|
|
|
9026
11217
|
const nk = keyFn(nv);
|
|
9027
11218
|
if (nk !== undefined) {
|
|
9028
11219
|
if (viewByKey === null) {
|
|
11220
|
+
// Occurrence-aware index queues (re-audit 3, P1-3): parity with
|
|
11221
|
+
// the plain adoption window — duplicate keys match per
|
|
11222
|
+
// occurrence, each view row consumed once.
|
|
9029
11223
|
viewByKey = new Map();
|
|
9030
11224
|
for (let j = 0; j < viewRows.length; j++) {
|
|
9031
11225
|
const p = unwrapValue(viewRows[j]);
|
|
9032
11226
|
if (isWrappable(p)) {
|
|
9033
11227
|
const pk = keyFn(p);
|
|
9034
|
-
if (pk
|
|
11228
|
+
if (pk === undefined) continue;
|
|
11229
|
+
const existing = viewByKey.get(pk);
|
|
11230
|
+
if (existing === undefined) viewByKey.set(pk, j);
|
|
11231
|
+
else if (Array.isArray(existing)) existing.push(j);
|
|
11232
|
+
else viewByKey.set(pk, [existing, j]);
|
|
9035
11233
|
}
|
|
9036
11234
|
}
|
|
9037
11235
|
}
|
|
9038
|
-
|
|
11236
|
+
const m = viewByKey.get(nk);
|
|
11237
|
+
if (m === undefined) pv = undefined;
|
|
11238
|
+
else if (Array.isArray(m)) {
|
|
11239
|
+
pv = unwrapValue(viewRows[m.shift()]);
|
|
11240
|
+
if (m.length === 1) viewByKey.set(nk, m[0]);
|
|
11241
|
+
} else {
|
|
11242
|
+
pv = unwrapValue(viewRows[m]);
|
|
11243
|
+
viewByKey.delete(nk);
|
|
11244
|
+
}
|
|
9039
11245
|
} else pv = unwrapValue(viewRows[i]);
|
|
9040
11246
|
} else pv = unwrapValue(viewRows[i]);
|
|
9041
11247
|
const ct = match(pv, nv);
|
|
@@ -10139,7 +12345,12 @@ function createLoadingBoundary(fn, fallback, options) {
|
|
|
10139
12345
|
function createErrorBoundary(fn, fallback) {
|
|
10140
12346
|
return createCollectionBoundary(STATUS_ERROR, fn, queue => {
|
|
10141
12347
|
return fallback(accessor(queue._error), () => {
|
|
10142
|
-
for (const source of queue._sources)
|
|
12348
|
+
for (const source of queue._sources) {
|
|
12349
|
+
// Non-computed sources (patch-channel registrations under plain
|
|
12350
|
+
// owners) are not recomputable — their reset is the record's next
|
|
12351
|
+
// transition re-applying the patch (re-audit 2, P1-4).
|
|
12352
|
+
if (source._fn !== undefined) recompute(source);
|
|
12353
|
+
}
|
|
10143
12354
|
schedule();
|
|
10144
12355
|
});
|
|
10145
12356
|
});
|
|
@@ -10273,7 +12484,10 @@ function flattenArray(children, results = [], options) {
|
|
|
10273
12484
|
} while (typeof child === "function" && !child.length);
|
|
10274
12485
|
}
|
|
10275
12486
|
if (Array.isArray(child)) {
|
|
10276
|
-
|
|
12487
|
+
// OR, don't overwrite: an accessor already pushed under doNotUnwrap
|
|
12488
|
+
// still needs the resolving wrapper even when a later sibling
|
|
12489
|
+
// fragment contains no functions (#3133).
|
|
12490
|
+
needsUnwrap = flattenArray(child, results, options) || needsUnwrap;
|
|
10277
12491
|
} else if (
|
|
10278
12492
|
options?.skipNonRendered &&
|
|
10279
12493
|
(child == null || child === true || child === false || child === "")
|
|
@@ -10301,6 +12515,7 @@ export {
|
|
|
10301
12515
|
NoOwnerError,
|
|
10302
12516
|
NotReadyError,
|
|
10303
12517
|
SUPPORTS_PROXY,
|
|
12518
|
+
TimeoutError,
|
|
10304
12519
|
action,
|
|
10305
12520
|
affects,
|
|
10306
12521
|
clearSnapshots,
|
|
@@ -10340,9 +12555,13 @@ export {
|
|
|
10340
12555
|
omit,
|
|
10341
12556
|
onCleanup,
|
|
10342
12557
|
onSettled,
|
|
12558
|
+
patchableRaw,
|
|
10343
12559
|
peekNextChildId,
|
|
10344
12560
|
reconcile,
|
|
10345
12561
|
refresh,
|
|
12562
|
+
registerPatch,
|
|
12563
|
+
registerRowOps,
|
|
12564
|
+
registerSlotPatchNext as registerSlotPatch,
|
|
10346
12565
|
releaseSnapshotScope,
|
|
10347
12566
|
repeat,
|
|
10348
12567
|
resetErrorHalt,
|
|
@@ -10351,6 +12570,10 @@ export {
|
|
|
10351
12570
|
setContext,
|
|
10352
12571
|
setSnapshotCapture,
|
|
10353
12572
|
snapshot,
|
|
12573
|
+
storeHasFamily,
|
|
12574
|
+
storeHasOptimisticFamily,
|
|
12575
|
+
storeIsShallow,
|
|
10354
12576
|
storePath,
|
|
12577
|
+
until,
|
|
10355
12578
|
untrack
|
|
10356
12579
|
};
|