@solidjs/signals 2.0.0-rc.4 → 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 +1169 -186
- package/dist/node.cjs +2041 -1167
- package/dist/prod/boundaries.js +4 -1
- package/dist/prod/core/async.js +124 -95
- package/dist/prod/core/constants.js +55 -1
- package/dist/prod/core/core.js +297 -222
- package/dist/prod/core/effect.js +28 -28
- package/dist/prod/core/error.js +13 -1
- package/dist/prod/core/external.js +2 -2
- package/dist/prod/core/graph.js +27 -27
- package/dist/prod/core/heap.js +30 -30
- package/dist/prod/core/lanes.js +32 -32
- package/dist/prod/core/optimistic.js +54 -54
- package/dist/prod/core/owner.js +34 -34
- package/dist/prod/core/scheduler.js +318 -137
- package/dist/prod/core/verdict.js +112 -59
- package/dist/prod/index.js +3 -3
- package/dist/prod/map.js +106 -106
- package/dist/prod/signals.js +253 -25
- package/dist/prod/store/next/optimistic.js +262 -123
- package/dist/prod/store/next/patch.js +6 -6
- package/dist/prod/store/next/projection.js +23 -19
- package/dist/prod/store/next/store.js +129 -35
- package/dist/prod/store/store.js +2 -2
- 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 +19 -20
- package/dist/types/core/error.d.ts +9 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/scheduler.d.ts +34 -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/next/optimistic.d.ts +13 -10
- package/dist/types/store/next/projection.d.ts +1 -1
- package/dist/types/store/next/store.d.ts +22 -0
- package/dist/types/store/next/target.d.ts +15 -0
- 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 +19 -20
- package/dist/types-cjs/core/error.d.cts +9 -0
- package/dist/types-cjs/core/index.d.cts +2 -2
- package/dist/types-cjs/core/scheduler.d.cts +34 -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/next/optimistic.d.cts +13 -10
- package/dist/types-cjs/store/next/projection.d.cts +1 -1
- package/dist/types-cjs/store/next/store.d.cts +22 -0
- package/dist/types-cjs/store/next/target.d.cts +15 -0
- package/package.json +1 -1
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
|
|
@@ -1248,6 +1326,107 @@ function mergeTransitionState(target, outgoing) {
|
|
|
1248
1326
|
}
|
|
1249
1327
|
for (const sub of outgoing._gatedSubs) target._gatedSubs.add(sub);
|
|
1250
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
|
+
}
|
|
1251
1430
|
function schedule() {
|
|
1252
1431
|
if (halted) {
|
|
1253
1432
|
notifyHalted();
|
|
@@ -1446,6 +1625,10 @@ class GlobalQueue extends Queue {
|
|
|
1446
1625
|
static _laneReadsCommitted = null;
|
|
1447
1626
|
static _recomputeLane = null;
|
|
1448
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;
|
|
1449
1632
|
static _laneAsyncSettled = null;
|
|
1450
1633
|
static _trackOptimisticStore = null;
|
|
1451
1634
|
flush() {
|
|
@@ -1589,8 +1772,17 @@ class GlobalQueue extends Queue {
|
|
|
1589
1772
|
return false;
|
|
1590
1773
|
}
|
|
1591
1774
|
initTransition(transition) {
|
|
1592
|
-
if (transition)
|
|
1593
|
-
|
|
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
|
+
}
|
|
1594
1786
|
if (!transition && activeTransition && activeTransition._time === clock) return;
|
|
1595
1787
|
if (!activeTransition) {
|
|
1596
1788
|
activeTransition = transition ?? createBatch();
|
|
@@ -1634,11 +1826,25 @@ class GlobalQueue extends Queue {
|
|
|
1634
1826
|
for (const lane of activeLanes) {
|
|
1635
1827
|
if (!lane._transition) lane._transition = activeTransition;
|
|
1636
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();
|
|
1637
1838
|
}
|
|
1638
1839
|
}
|
|
1639
1840
|
function queuePendingNode(node) {
|
|
1841
|
+
lastStagedNodeName = node._name ?? null;
|
|
1640
1842
|
currentBatch._pendingNodes.push(node);
|
|
1641
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;
|
|
1642
1848
|
// Sticky: flips true on the first refresh() ever (the only setter of
|
|
1643
1849
|
// REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
|
|
1644
1850
|
// clear entirely in apps that never refresh.
|
|
@@ -1744,10 +1950,34 @@ let patchCommitHook = null;
|
|
|
1744
1950
|
function setPatchCommitHook(fn) {
|
|
1745
1951
|
patchCommitHook = fn;
|
|
1746
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 = [];
|
|
1747
1960
|
function commitPendingNodes() {
|
|
1748
1961
|
const pendingNodes = currentBatch._pendingNodes;
|
|
1749
1962
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
1750
|
-
|
|
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
|
+
}
|
|
1751
1981
|
}
|
|
1752
1982
|
pendingNodes.length = 0;
|
|
1753
1983
|
storeCommitHook?.();
|
|
@@ -1799,6 +2029,22 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
1799
2029
|
// completing transition scopes the clear to its own layer keys (#2899).
|
|
1800
2030
|
if (batch._optimisticStores.size)
|
|
1801
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
|
+
}
|
|
1802
2048
|
sweepTransientStoreNodes();
|
|
1803
2049
|
// Lanes only enter activeLanes through the engine's getOrCreateLane.
|
|
1804
2050
|
if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
|
|
@@ -1879,7 +2125,20 @@ function flush(fn) {
|
|
|
1879
2125
|
// `flush()` is an explicit drain point, so it must also process an active
|
|
1880
2126
|
// transition even if no microtask was scheduled for it yet.
|
|
1881
2127
|
while (scheduled$1 || activeTransition) {
|
|
1882
|
-
if (++count === 1e5)
|
|
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
|
+
}
|
|
1883
2142
|
globalQueue.flush();
|
|
1884
2143
|
}
|
|
1885
2144
|
}
|
|
@@ -1941,6 +2200,29 @@ function runInTransition(transition, fn) {
|
|
|
1941
2200
|
activeTransition = prevTransition;
|
|
1942
2201
|
}
|
|
1943
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
|
+
}
|
|
1944
2226
|
|
|
1945
2227
|
/** The queue a node belongs to, picked from its own zombie flag. */
|
|
1946
2228
|
function queueFor(n) {
|
|
@@ -2709,11 +2991,11 @@ function settlePendingSource(el) {
|
|
|
2709
2991
|
const errored = node._statusFlags & STATUS_ERROR;
|
|
2710
2992
|
if (remaining) {
|
|
2711
2993
|
if (!errored) setPendingError(node, remaining);
|
|
2712
|
-
updateCompanions
|
|
2994
|
+
updateCompanions?.(node);
|
|
2713
2995
|
} else {
|
|
2714
2996
|
node._statusFlags &= ~STATUS_PENDING;
|
|
2715
2997
|
if (!errored) setPendingError(node);
|
|
2716
|
-
updateCompanions
|
|
2998
|
+
updateCompanions?.(node);
|
|
2717
2999
|
if (node._x?._blocked) {
|
|
2718
3000
|
enqueueSub(node);
|
|
2719
3001
|
scheduled = true;
|
|
@@ -2736,6 +3018,14 @@ function settlePendingSource(el) {
|
|
|
2736
3018
|
function isThenable(value) {
|
|
2737
3019
|
return value != null && typeof value === "object" && typeof value.then === "function";
|
|
2738
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
|
+
}
|
|
2739
3029
|
function handleAsync(el, result, setter) {
|
|
2740
3030
|
let iterator = false;
|
|
2741
3031
|
let thenable = false;
|
|
@@ -2773,6 +3063,11 @@ function handleAsync(el, result, setter) {
|
|
|
2773
3063
|
});
|
|
2774
3064
|
throw new Error(message);
|
|
2775
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.
|
|
2776
3071
|
ext(el)._inFlight = result;
|
|
2777
3072
|
let syncValue;
|
|
2778
3073
|
// Settle-time transition re-entry. The loading rail is invisible to
|
|
@@ -2876,10 +3171,21 @@ function handleAsync(el, result, setter) {
|
|
|
2876
3171
|
// only notified when the hold is visible to them: under an active
|
|
2877
3172
|
// override every reader sees the override (A17), so waking subs would
|
|
2878
3173
|
// re-show an unchanged view — the revert is the notification point.
|
|
2879
|
-
GlobalQueue._syncCompanions
|
|
3174
|
+
GlobalQueue._syncCompanions?.(el, value);
|
|
2880
3175
|
if (!hasActiveOverride$1(el)) {
|
|
2881
3176
|
if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
|
|
2882
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);
|
|
2883
3189
|
}
|
|
2884
3190
|
el._time = clock;
|
|
2885
3191
|
} else if (lane) {
|
|
@@ -2894,7 +3200,7 @@ function handleAsync(el, result, setter) {
|
|
|
2894
3200
|
// The latest() shadow write gives latest() effects independent lanes; the
|
|
2895
3201
|
// _pendingSignal update is a no-op repeat of the clearStatus() call above
|
|
2896
3202
|
// (computePendingState doesn't read _value).
|
|
2897
|
-
GlobalQueue._syncCompanions
|
|
3203
|
+
GlobalQueue._syncCompanions?.(el, value);
|
|
2898
3204
|
insertSubs(el, true);
|
|
2899
3205
|
}
|
|
2900
3206
|
} catch (e) {
|
|
@@ -2976,6 +3282,11 @@ function handleAsync(el, result, setter) {
|
|
|
2976
3282
|
} catch {}
|
|
2977
3283
|
};
|
|
2978
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;
|
|
2979
3290
|
// Release check before each next pull: an unobserved lazy node must tear
|
|
2980
3291
|
// down (its close above runs via disposal, closing the iterator) instead
|
|
2981
3292
|
// of pumping the stream forever with zero subscribers (#2935).
|
|
@@ -3207,7 +3518,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3207
3518
|
status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
|
|
3208
3519
|
ext(el)._error = error;
|
|
3209
3520
|
}
|
|
3210
|
-
GlobalQueue._updatePendingSignal
|
|
3521
|
+
GlobalQueue._updatePendingSignal?.(el);
|
|
3211
3522
|
if (
|
|
3212
3523
|
el._x?._child &&
|
|
3213
3524
|
el._config & CONFIG_CHILD_COMPANIONS &&
|
|
@@ -3352,7 +3663,15 @@ function recompute(el, create = false) {
|
|
|
3352
3663
|
if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
|
|
3353
3664
|
globalQueue.initTransition(el._transition);
|
|
3354
3665
|
deleteFromHeap(el, queueFor(el));
|
|
3355
|
-
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
|
+
}
|
|
3356
3675
|
// Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
|
|
3357
3676
|
if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
|
|
3358
3677
|
else if (el._firstChild !== null || el._disposal !== null) {
|
|
@@ -3376,6 +3695,14 @@ function recompute(el, create = false) {
|
|
|
3376
3695
|
// recovers to an unchanged value, dependents still holding this object must
|
|
3377
3696
|
// be swept (settleErroredDependents, #2949).
|
|
3378
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;
|
|
3379
3706
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
3380
3707
|
// the recompute wipes _flags below.
|
|
3381
3708
|
const hadReask = (el._flags & REACTIVE_REASK) !== 0;
|
|
@@ -3550,7 +3877,14 @@ function recompute(el, create = false) {
|
|
|
3550
3877
|
// values directly — the pending round-trip (queuePendingNode +
|
|
3551
3878
|
// commitPendingNodes) exists to sequence transition reveals, and
|
|
3552
3879
|
// paying it per effect on the plain path is pure overhead.
|
|
3553
|
-
|
|
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)) ||
|
|
3554
3888
|
isOptimisticDirty
|
|
3555
3889
|
// NOTE (stage-3, 2026-08-21): a quiet-world MEMO direct-commit was
|
|
3556
3890
|
// attempted here and REVERTED — memo staging is load-bearing beyond
|
|
@@ -3596,6 +3930,12 @@ function recompute(el, create = false) {
|
|
|
3596
3930
|
if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
|
|
3597
3931
|
el._pendingValue = value;
|
|
3598
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);
|
|
3599
3939
|
} else if (el._height != oldHeight) {
|
|
3600
3940
|
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
3601
3941
|
insertIntoHeapHeight(s._sub, queueFor(s._sub));
|
|
@@ -3608,6 +3948,17 @@ function recompute(el, create = false) {
|
|
|
3608
3948
|
// (el._x?._error re-set), so this only runs on a genuinely clean recovery.
|
|
3609
3949
|
if (outgoingError !== undefined && !valueChanged && !el._x?._error)
|
|
3610
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);
|
|
3611
3962
|
}
|
|
3612
3963
|
// Attribution hook: fired before the lane restore so `currentOptimisticLane`
|
|
3613
3964
|
// still reflects THIS run's posture. The facts distinguish an overlay
|
|
@@ -3656,7 +4007,9 @@ function updateIfNecessary(el) {
|
|
|
3656
4007
|
// (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
|
|
3657
4008
|
// A mid-pass mark stays latched for recompute's own tail to reschedule
|
|
3658
4009
|
// (#3037); readers meanwhile serve the values the pass has so far.
|
|
3659
|
-
|
|
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;
|
|
3660
4013
|
if (el._flags & REACTIVE_CHECK) {
|
|
3661
4014
|
for (let d = el._deps; d; d = d._nextDep) {
|
|
3662
4015
|
const dep1 = d._dep;
|
|
@@ -3692,7 +4045,7 @@ function computed(fn, options) {
|
|
|
3692
4045
|
(options?.sync ? CONFIG_SYNC : 0) |
|
|
3693
4046
|
(options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0) |
|
|
3694
4047
|
(snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
3695
|
-
_equals: options?.equals
|
|
4048
|
+
_equals: options?.equals ?? isEqual,
|
|
3696
4049
|
_disposal: null,
|
|
3697
4050
|
_queue: context?._queue ?? globalQueue,
|
|
3698
4051
|
_context: context?._context ?? defaultContext,
|
|
@@ -3744,6 +4097,7 @@ function ext(el) {
|
|
|
3744
4097
|
_parentSource: undefined,
|
|
3745
4098
|
_affectsCount: 0,
|
|
3746
4099
|
_inFlight: null,
|
|
4100
|
+
_flightTeardown: null,
|
|
3747
4101
|
_error: undefined,
|
|
3748
4102
|
_blocked: undefined,
|
|
3749
4103
|
_pendingSources: undefined,
|
|
@@ -3771,6 +4125,7 @@ function createEffectNode(fn, effectFn, errorFn, type, options) {
|
|
|
3771
4125
|
(transparent ? CONFIG_TRANSPARENT : 0) |
|
|
3772
4126
|
(options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
|
|
3773
4127
|
(options?.sync ? CONFIG_SYNC : 0) |
|
|
4128
|
+
(options?._extraConfig ?? 0) |
|
|
3774
4129
|
(snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
3775
4130
|
_equals: false,
|
|
3776
4131
|
_disposal: null,
|
|
@@ -3832,8 +4187,7 @@ function setEffectStatusNotify(fn) {
|
|
|
3832
4187
|
* "display consumer" membership test in the status walks, exactly as the
|
|
3833
4188
|
* per-node field did when every effect carried one. */
|
|
3834
4189
|
function statusNotifierOf(el) {
|
|
3835
|
-
const
|
|
3836
|
-
const own = x !== null && x !== undefined ? x._notifyStatus : undefined;
|
|
4190
|
+
const own = el._x?._notifyStatus;
|
|
3837
4191
|
if (own !== undefined) return own;
|
|
3838
4192
|
return el._type ? (effectStatusNotify ?? undefined) : undefined;
|
|
3839
4193
|
}
|
|
@@ -3876,7 +4230,7 @@ function setupComputedNode(self, options) {
|
|
|
3876
4230
|
}
|
|
3877
4231
|
function signal(v, options, firewall = null) {
|
|
3878
4232
|
const s = {
|
|
3879
|
-
_equals: options?.equals
|
|
4233
|
+
_equals: options?.equals ?? isEqual,
|
|
3880
4234
|
_config:
|
|
3881
4235
|
(options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
|
|
3882
4236
|
(options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0),
|
|
@@ -4013,6 +4367,35 @@ const READ_SLOW = Symbol("read-slow");
|
|
|
4013
4367
|
* snapshot / transition / lane / dev-strictRead state all take the full
|
|
4014
4368
|
* resolution. Anything slow returns READ_SLOW; the caller then calls read().
|
|
4015
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
|
+
}
|
|
4016
4399
|
function readNodeFast(el) {
|
|
4017
4400
|
if (
|
|
4018
4401
|
latestReadActive ||
|
|
@@ -4092,6 +4475,13 @@ function read(el) {
|
|
|
4092
4475
|
markHeap(elQueue);
|
|
4093
4476
|
updateIfNecessary(owner);
|
|
4094
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);
|
|
4095
4485
|
const height = owner._height;
|
|
4096
4486
|
// parent check is shallow, might need to be recursive
|
|
4097
4487
|
if (height >= c._height && el._parent !== c) {
|
|
@@ -4170,8 +4560,18 @@ function read(el) {
|
|
|
4170
4560
|
nodeName: owner?._name
|
|
4171
4561
|
});
|
|
4172
4562
|
if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
|
|
4173
|
-
// A17: the override IS the value for every reader
|
|
4174
|
-
|
|
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;
|
|
4175
4575
|
}
|
|
4176
4576
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
4177
4577
|
// a pending mid-transition write sees the committed value. Projection-store
|
|
@@ -4199,7 +4599,18 @@ function read(el) {
|
|
|
4199
4599
|
(currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
|
|
4200
4600
|
el._pendingValue === NOT_PENDING ||
|
|
4201
4601
|
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
4202
|
-
(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))
|
|
4203
4614
|
? el._value
|
|
4204
4615
|
: el._pendingValue;
|
|
4205
4616
|
// Record that this isPending() probe observed the fresh pending value, so
|
|
@@ -4244,9 +4655,17 @@ function devGuardStoreSetterWrite() {
|
|
|
4244
4655
|
ownerName: context._name,
|
|
4245
4656
|
data: { operation: "setStore" }
|
|
4246
4657
|
});
|
|
4247
|
-
|
|
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));
|
|
4248
4661
|
}
|
|
4249
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
|
+
}
|
|
4250
4669
|
function setSignal(el, v) {
|
|
4251
4670
|
if (
|
|
4252
4671
|
!(el._config & CONFIG_OWNED_WRITE) &&
|
|
@@ -4264,7 +4683,7 @@ function setSignal(el, v) {
|
|
|
4264
4683
|
nodeName: el._name,
|
|
4265
4684
|
data: { operation: "setSignal" }
|
|
4266
4685
|
});
|
|
4267
|
-
throw new Error(
|
|
4686
|
+
throw new Error(ownedScopeWriteMessage(context));
|
|
4268
4687
|
}
|
|
4269
4688
|
if (el._transition && activeTransition !== el._transition)
|
|
4270
4689
|
globalQueue.initTransition(el._transition);
|
|
@@ -4390,41 +4809,13 @@ function staleValues(fn, set = true) {
|
|
|
4390
4809
|
}
|
|
4391
4810
|
}
|
|
4392
4811
|
/**
|
|
4393
|
-
*
|
|
4394
|
-
*
|
|
4395
|
-
*
|
|
4396
|
-
*
|
|
4397
|
-
*
|
|
4398
|
-
* write-like invalidation operation: it does not read the target's value, and
|
|
4399
|
-
* refreshing a plain signal accessor is a no-op.
|
|
4400
|
-
*
|
|
4401
|
-
* Use it to invalidate cached async values (e.g. force a re-fetch) without
|
|
4402
|
-
* tearing the consumer down.
|
|
4403
|
-
*
|
|
4404
|
-
* @example
|
|
4405
|
-
* ```ts
|
|
4406
|
-
* const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
|
|
4407
|
-
*
|
|
4408
|
-
* // Re-fetch on demand
|
|
4409
|
-
* <button onClick={() => refresh(user)}>Reload</button>
|
|
4410
|
-
* ```
|
|
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.
|
|
4411
4817
|
*/
|
|
4412
|
-
function
|
|
4413
|
-
const node = target?.[$REFRESH];
|
|
4414
|
-
if (!node) {
|
|
4415
|
-
{
|
|
4416
|
-
const message =
|
|
4417
|
-
"[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
|
|
4418
|
-
"Pass the original source target, not a wrapper function or derived property read.";
|
|
4419
|
-
emitDiagnostic({
|
|
4420
|
-
code: "INVALID_REFRESH_TARGET",
|
|
4421
|
-
kind: "write",
|
|
4422
|
-
severity: "error",
|
|
4423
|
-
message
|
|
4424
|
-
});
|
|
4425
|
-
throw new Error(message);
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
4818
|
+
function markRefresh(node) {
|
|
4428
4819
|
if (
|
|
4429
4820
|
context &&
|
|
4430
4821
|
!((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
|
|
@@ -5236,7 +5627,22 @@ function latestRead(el) {
|
|
|
5236
5627
|
!(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
5237
5628
|
) {
|
|
5238
5629
|
markHeap(queue);
|
|
5239
|
-
|
|
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
|
+
}
|
|
5240
5646
|
}
|
|
5241
5647
|
value = read(pendingComputed);
|
|
5242
5648
|
} catch (e) {
|
|
@@ -5266,12 +5672,42 @@ function latestRead(el) {
|
|
|
5266
5672
|
return pendingComputed._pendingValue;
|
|
5267
5673
|
return value;
|
|
5268
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
|
+
}
|
|
5269
5691
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
5270
5692
|
function pendingCheckRead(el, c, owner, firewall) {
|
|
5271
5693
|
setPendingCheckActive(false);
|
|
5272
5694
|
if (typeof el._fn === "function") prepareComputed(el, true);
|
|
5273
5695
|
const ownerStatus = owner._statusFlags;
|
|
5274
|
-
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
|
+
) {
|
|
5275
5711
|
if (tracking && el !== c) link(el, c);
|
|
5276
5712
|
setPendingCheckActive(true);
|
|
5277
5713
|
throw owner._x?._error;
|
|
@@ -5352,6 +5788,17 @@ function isPending(fn) {
|
|
|
5352
5788
|
});
|
|
5353
5789
|
const collectPending = () => {
|
|
5354
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);
|
|
5355
5802
|
const prevStrictRead = strictRead;
|
|
5356
5803
|
setStrictRead(false);
|
|
5357
5804
|
try {
|
|
@@ -5363,6 +5810,7 @@ function isPending(fn) {
|
|
|
5363
5810
|
});
|
|
5364
5811
|
} finally {
|
|
5365
5812
|
setStrictRead(prevStrictRead);
|
|
5813
|
+
setLatestReadActive(prevLatest);
|
|
5366
5814
|
setPendingCheckActive(true);
|
|
5367
5815
|
}
|
|
5368
5816
|
// A "not pending" verdict that exists only because this reader saw the
|
|
@@ -6086,11 +6534,259 @@ function resolve(fn) {
|
|
|
6086
6534
|
rej(err);
|
|
6087
6535
|
dispose();
|
|
6088
6536
|
},
|
|
6089
|
-
|
|
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 }
|
|
6090
6542
|
);
|
|
6091
6543
|
});
|
|
6092
6544
|
});
|
|
6093
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 }
|
|
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
|
+
}
|
|
6787
|
+
});
|
|
6788
|
+
});
|
|
6789
|
+
}
|
|
6094
6790
|
function createOptimistic(first, second) {
|
|
6095
6791
|
// Install before the node exists: only engine-installed programs can carry
|
|
6096
6792
|
// an _overrideValue slot (same runtime-install pattern as
|
|
@@ -7060,8 +7756,29 @@ function materializePB(target) {
|
|
|
7060
7756
|
target.ovl = false;
|
|
7061
7757
|
}
|
|
7062
7758
|
function ensurePB(target) {
|
|
7063
|
-
if (activeTransition !== null) foldBatches.set(target, activeTransition);
|
|
7064
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);
|
|
7065
7782
|
if (pb === null) {
|
|
7066
7783
|
// Prototype-chain overlay (#3044): plain-data non-array containers
|
|
7067
7784
|
// outside projection/optimistic families open drafts in O(1) — own keys
|
|
@@ -7083,6 +7800,7 @@ function ensurePB(target) {
|
|
|
7083
7800
|
// seed from committed truth — seeding overrides there would fold a lane
|
|
7084
7801
|
// value into the committed home ("authority wins at reveal" would break).
|
|
7085
7802
|
if (target.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
|
|
7803
|
+
tentativePBs.add(pb);
|
|
7086
7804
|
const nodes = target.n;
|
|
7087
7805
|
if (nodes !== null) {
|
|
7088
7806
|
for (const key of Reflect.ownKeys(nodes)) {
|
|
@@ -7200,6 +7918,17 @@ function queueFold(target) {
|
|
|
7200
7918
|
* Refreshed on every write; resolved through currentTransition at drain
|
|
7201
7919
|
* (transitions merge — same rule as heldMaskView). */
|
|
7202
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();
|
|
7203
7932
|
/** Committed-time privatization for parent-chain slot updates (path copying). */
|
|
7204
7933
|
function privatizeCommitted(target) {
|
|
7205
7934
|
if (ownedRaw.has(target.v)) return;
|
|
@@ -7533,7 +8262,13 @@ function notifyWrites(t) {
|
|
|
7533
8262
|
// IMMEDIATE — landed truth shows to untracked readers even while a
|
|
7534
8263
|
// downstream consumer's own async still holds the effect-level reveal
|
|
7535
8264
|
// (spec-async "verdicts never inherit consumers' in-flight state").
|
|
7536
|
-
|
|
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) {
|
|
7537
8272
|
// Landed truth (post-await write-override): immediately visible to every
|
|
7538
8273
|
// reader — any staged held view is superseded.
|
|
7539
8274
|
if (t.ht !== null) t.ht = t.hv = null;
|
|
@@ -7796,17 +8531,37 @@ function readSource(target) {
|
|
|
7796
8531
|
target.pb !== null &&
|
|
7797
8532
|
(inDraft(target) ||
|
|
7798
8533
|
getWriteOverride() ||
|
|
7799
|
-
|
|
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)) ||
|
|
7800
8542
|
// A projection's pending backing is authoritative-elect: serve it to
|
|
7801
8543
|
// context-free readers too UNLESS a transition is holding the node
|
|
7802
8544
|
// commits (downstream async hold — stale committed is the contract)
|
|
7803
8545
|
// or the reader is a CHILDREN_FORBIDDEN scope, which never observes
|
|
7804
8546
|
// its own unsettled write (#3082, signal parity per #3006).
|
|
7805
|
-
(target.fam !== null && !foldHeld(target) && !inForbiddenScope()))
|
|
8547
|
+
(target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
|
|
7806
8548
|
)
|
|
7807
8549
|
return target.pb;
|
|
7808
8550
|
return target.v;
|
|
7809
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
|
+
}
|
|
7810
8565
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
7811
8566
|
// Allocation-free own-accessor probe (replaces eager descriptor scans — the
|
|
7812
8567
|
// single biggest creation cost in the uibench profile): Annex-B lookups
|
|
@@ -7838,6 +8593,26 @@ function runAuthoritative(fn) {
|
|
|
7838
8593
|
function hasActiveOverride(node) {
|
|
7839
8594
|
return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
|
|
7840
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
|
+
}
|
|
7841
8616
|
/** Context-aware node view for reads outside tracking: active override >
|
|
7842
8617
|
* held pending (owner context) > the BACKING value. Committed truth lives in
|
|
7843
8618
|
* the backing (single-home rule, O6) — node `_value` is never served here,
|
|
@@ -7848,11 +8623,24 @@ function hasActiveOverride(node) {
|
|
|
7848
8623
|
function nodeValue(node, backing) {
|
|
7849
8624
|
// latest() sees the in-flight parked value like an owner-context reader
|
|
7850
8625
|
// does (#3075) — signal/memo parity for store-node-backed keys.
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
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;
|
|
7856
8644
|
return v === FORCE ? backing : v;
|
|
7857
8645
|
}
|
|
7858
8646
|
/** Serve an own data key: node-first when a node exists (pending visibility,
|
|
@@ -7877,13 +8665,18 @@ function serveDataKey(target, key, backingValue, src, node) {
|
|
|
7877
8665
|
read(getNode(target, key, backingValue));
|
|
7878
8666
|
}
|
|
7879
8667
|
}
|
|
7880
|
-
|
|
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;
|
|
7881
8672
|
}
|
|
7882
8673
|
if (inDraft(target)) {
|
|
7883
8674
|
// Optimistic drafts before their first write have no pending backing yet;
|
|
7884
8675
|
// reads must still see the live optimistic view (compose, not clobber —
|
|
7885
8676
|
// #2951). Once ensurePB runs, the seeded clone carries the view.
|
|
7886
|
-
|
|
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()) {
|
|
7887
8680
|
const node = target.n?.[key];
|
|
7888
8681
|
if (node !== undefined && hasActiveOverride(node))
|
|
7889
8682
|
v = unwrapOverride(node._x?._overrideValue);
|
|
@@ -8092,7 +8885,15 @@ const traps = {
|
|
|
8092
8885
|
if (target.s) return serveShallow(target, key, nv);
|
|
8093
8886
|
return isWrappable(nv) ? draftServe(target, wrapNext(nv, target, key)) : nv;
|
|
8094
8887
|
}
|
|
8095
|
-
} 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
|
+
) {
|
|
8096
8897
|
const node = target.n?.[key];
|
|
8097
8898
|
if (node !== undefined && hasActiveOverride(node))
|
|
8098
8899
|
v = unwrapOverride(node._x?._overrideValue);
|
|
@@ -8119,14 +8920,16 @@ const traps = {
|
|
|
8119
8920
|
if (!inDraft(target)) {
|
|
8120
8921
|
if (getObserver() !== null) {
|
|
8121
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.
|
|
8122
8925
|
const nv = read(node);
|
|
8123
8926
|
if (hasActiveOverride(node)) present = !!nv;
|
|
8124
|
-
} else {
|
|
8927
|
+
} else if (!authoritativeServe()) {
|
|
8125
8928
|
const node = target.h?.[key];
|
|
8126
8929
|
if (node !== undefined && hasActiveOverride(node))
|
|
8127
8930
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
8128
8931
|
}
|
|
8129
|
-
} else if (target.fam?.opt && target.pb === null) {
|
|
8932
|
+
} else if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
|
|
8130
8933
|
const node = target.h?.[key];
|
|
8131
8934
|
if (node !== undefined && hasActiveOverride(node))
|
|
8132
8935
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
@@ -8152,8 +8955,14 @@ const traps = {
|
|
|
8152
8955
|
// Optimistic membership overlay: presence-node overrides add/remove keys
|
|
8153
8956
|
// (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
|
|
8154
8957
|
// Draft reads before the first write overlay too (pb, once created, is
|
|
8155
|
-
// seeded with the view).
|
|
8156
|
-
|
|
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
|
+
) {
|
|
8157
8966
|
let set = null;
|
|
8158
8967
|
for (const key of Reflect.ownKeys(target.h)) {
|
|
8159
8968
|
const node = target.h[key];
|
|
@@ -8175,7 +8984,7 @@ const traps = {
|
|
|
8175
8984
|
if (target.del !== null && target.del.has(key)) return undefined;
|
|
8176
8985
|
if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
|
|
8177
8986
|
}
|
|
8178
|
-
if (target.fam?.opt && !inDraft(target)) {
|
|
8987
|
+
if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
|
|
8179
8988
|
const node = target.h?.[key];
|
|
8180
8989
|
if (node !== undefined && hasActiveOverride(node)) {
|
|
8181
8990
|
if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
|
|
@@ -9138,7 +9947,8 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
9138
9947
|
* driven from inside an enclosing authoritative-write scope (next-store
|
|
9139
9948
|
* optimistic derives), and a hard `false` would clobber it mid-derive.
|
|
9140
9949
|
*/
|
|
9141
|
-
function wrapDraft(inner, isActive,
|
|
9950
|
+
function wrapDraft(inner, isActive, aroundWrite) {
|
|
9951
|
+
const write = op => (aroundWrite ? aroundWrite(op) : op());
|
|
9142
9952
|
const traps = {
|
|
9143
9953
|
get(_, prop) {
|
|
9144
9954
|
let value;
|
|
@@ -9153,7 +9963,7 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9153
9963
|
}
|
|
9154
9964
|
if (prop === $TARGET) return value;
|
|
9155
9965
|
return typeof value === "object" && value !== null
|
|
9156
|
-
? wrapDraft(value, isActive,
|
|
9966
|
+
? wrapDraft(value, isActive, aroundWrite)
|
|
9157
9967
|
: value;
|
|
9158
9968
|
},
|
|
9159
9969
|
has(_, prop) {
|
|
@@ -9175,8 +9985,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9175
9985
|
setWriteOverride(true);
|
|
9176
9986
|
setProjectionWriteActive(true);
|
|
9177
9987
|
try {
|
|
9178
|
-
|
|
9179
|
-
|
|
9988
|
+
write(() => {
|
|
9989
|
+
inner[prop] = value;
|
|
9990
|
+
});
|
|
9180
9991
|
} finally {
|
|
9181
9992
|
setWriteOverride(false);
|
|
9182
9993
|
setProjectionWriteActive(was);
|
|
@@ -9189,8 +10000,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9189
10000
|
setWriteOverride(true);
|
|
9190
10001
|
setProjectionWriteActive(true);
|
|
9191
10002
|
try {
|
|
9192
|
-
|
|
9193
|
-
|
|
10003
|
+
write(() => {
|
|
10004
|
+
delete inner[prop];
|
|
10005
|
+
});
|
|
9194
10006
|
} finally {
|
|
9195
10007
|
setWriteOverride(false);
|
|
9196
10008
|
setProjectionWriteActive(was);
|
|
@@ -9231,8 +10043,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
|
|
|
9231
10043
|
setWriteOverride(true);
|
|
9232
10044
|
setProjectionWriteActive(true);
|
|
9233
10045
|
try {
|
|
9234
|
-
|
|
9235
|
-
|
|
10046
|
+
write(() => {
|
|
10047
|
+
Reflect.defineProperty(inner, prop, desc);
|
|
10048
|
+
});
|
|
9236
10049
|
} finally {
|
|
9237
10050
|
setWriteOverride(false);
|
|
9238
10051
|
setProjectionWriteActive(was);
|
|
@@ -9284,7 +10097,7 @@ function createStoreDerivedNext(fn, seed, options) {
|
|
|
9284
10097
|
}
|
|
9285
10098
|
];
|
|
9286
10099
|
}
|
|
9287
|
-
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit,
|
|
10100
|
+
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
|
|
9288
10101
|
const owner = getOwner();
|
|
9289
10102
|
let settled = false;
|
|
9290
10103
|
let result;
|
|
@@ -9298,7 +10111,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
9298
10111
|
const draft = wrapDraft(
|
|
9299
10112
|
wrappedStore,
|
|
9300
10113
|
() => !settled || owner._x?._inFlight === result,
|
|
9301
|
-
|
|
10114
|
+
aroundDraftWrite
|
|
9302
10115
|
);
|
|
9303
10116
|
storeSetterNext(
|
|
9304
10117
|
draft,
|
|
@@ -9313,7 +10126,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
9313
10126
|
if (v === s || v === undefined) return;
|
|
9314
10127
|
const write = () =>
|
|
9315
10128
|
storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
|
|
9316
|
-
wrapCommit ? wrapCommit(write) : write();
|
|
10129
|
+
wrapCommit ? wrapCommit(write, v) : write();
|
|
9317
10130
|
};
|
|
9318
10131
|
const sync = handleAsync(owner, result, commit);
|
|
9319
10132
|
if (!owner._loading) commit(sync);
|
|
@@ -9884,12 +10697,31 @@ function armRowHooks() {
|
|
|
9884
10697
|
* armed presence nodes (the §6 overlay), so structural optimism reverts with
|
|
9885
10698
|
* the same per-transaction granularity (FINDING-2's fix by construction).
|
|
9886
10699
|
*
|
|
9887
|
-
* Derived form = an optimistic projection
|
|
9888
|
-
*
|
|
9889
|
-
*
|
|
9890
|
-
*
|
|
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
|
|
9891
10713
|
* legacy/engine checks.
|
|
9892
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
|
+
}
|
|
9893
10725
|
let blockedInstalled = false;
|
|
9894
10726
|
function installNextBlockedHalf() {
|
|
9895
10727
|
if (blockedInstalled) return;
|
|
@@ -9897,7 +10729,12 @@ function installNextBlockedHalf() {
|
|
|
9897
10729
|
// Late-bind the optimistic machinery into the plain store/reconcile paths
|
|
9898
10730
|
// (all call sites are fam?.opt-gated, so this always runs first) and the
|
|
9899
10731
|
// affects witness's view resolver.
|
|
9900
|
-
setOptHooks({
|
|
10732
|
+
setOptHooks({
|
|
10733
|
+
notifyOptimisticWrites,
|
|
10734
|
+
optimisticView,
|
|
10735
|
+
applyTentative,
|
|
10736
|
+
retainsOptimism: transitionHoldsOptimism
|
|
10737
|
+
});
|
|
9901
10738
|
setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw));
|
|
9902
10739
|
// Scheduler flush tails call _clearOptimisticStores whenever tracked
|
|
9903
10740
|
// stores exist; next has no layer to clear — reverts are engine-native —
|
|
@@ -9918,6 +10755,13 @@ function installNextBlockedHalf() {
|
|
|
9918
10755
|
// row identity against the post-revert view (resolved from the
|
|
9919
10756
|
// target at drain — overrides are gone by then).
|
|
9920
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));
|
|
9921
10765
|
}
|
|
9922
10766
|
}
|
|
9923
10767
|
}
|
|
@@ -9978,20 +10822,41 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
9978
10822
|
};
|
|
9979
10823
|
const store = wrapNext(initialValue, null, null, fam);
|
|
9980
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);
|
|
9981
10834
|
if (fam.shallow) {
|
|
9982
10835
|
store[$TARGET].s = true;
|
|
9983
10836
|
markRawIngest(initialValue);
|
|
9984
10837
|
}
|
|
9985
10838
|
if (derived) {
|
|
9986
10839
|
const fn = first;
|
|
9987
|
-
//
|
|
9988
|
-
//
|
|
9989
|
-
//
|
|
9990
|
-
//
|
|
9991
|
-
|
|
9992
|
-
const wrapCommit = write => {
|
|
9993
|
-
|
|
9994
|
-
|
|
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);
|
|
9995
10860
|
};
|
|
9996
10861
|
let nodeOptions;
|
|
9997
10862
|
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
@@ -10003,14 +10868,201 @@ function createOptimisticStoreNext(first, second, options) {
|
|
|
10003
10868
|
fn,
|
|
10004
10869
|
options?.key === undefined ? "id" : options.key,
|
|
10005
10870
|
wrapCommit,
|
|
10006
|
-
|
|
10871
|
+
aroundDraftWrite
|
|
10007
10872
|
)
|
|
10008
10873
|
);
|
|
10009
10874
|
}, nodeOptions);
|
|
10010
10875
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
10011
10876
|
fam.node = node;
|
|
10012
10877
|
}
|
|
10013
|
-
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
|
+
}
|
|
10014
11066
|
}
|
|
10015
11067
|
// ---- optimistic-only store machinery (moved from next/store.ts /
|
|
10016
11068
|
// next/reconcile.ts so plain-store bundles tree-shake it) ----
|
|
@@ -10093,96 +11145,22 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
10093
11145
|
// (structural ones already ride the key-set bump above).
|
|
10094
11146
|
bumpDeep(t);
|
|
10095
11147
|
// Discard the draft — committed raw is untouched (revert target by
|
|
10096
|
-
// construction)
|
|
10097
|
-
//
|
|
10098
|
-
|
|
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);
|
|
10099
11153
|
(t.fam.overlaid ??= new Set()).add(t);
|
|
10100
11154
|
GlobalQueue._trackOptimisticStore?.(t.fam.px ?? t.px);
|
|
10101
11155
|
}
|
|
10102
|
-
/**
|
|
10103
|
-
* Landing consumption (RUL-2): fresh authoritative data supersedes every
|
|
10104
|
-
* tentative override in the family. Mirrors legacy clearProjectionOverride —
|
|
10105
|
-
* drop the override, clear lane/ownership, notify subscribers whose visible
|
|
10106
|
-
* value changes (reversion effects go to regular queues via the projection
|
|
10107
|
-
* write posture the caller holds).
|
|
10108
|
-
*/
|
|
10109
|
-
function consumeOverridesNext(fam) {
|
|
10110
|
-
const overlaid = fam.overlaid;
|
|
10111
|
-
if (overlaid === undefined || overlaid.size === 0) return;
|
|
10112
|
-
runAuthoritative(() => {
|
|
10113
|
-
for (const t of overlaid) {
|
|
10114
|
-
const drop = (node, committed) => {
|
|
10115
|
-
if (!hasActiveOverride(node)) return;
|
|
10116
|
-
const prev = unwrapOverride(node._x?._overrideValue);
|
|
10117
|
-
// Full legacy reset (clearOptimisticOverride parity): the landing is
|
|
10118
|
-
// authoritative NOW — fold committed into the node directly instead
|
|
10119
|
-
// of riding a transaction's commit (whose queues may be stashed with
|
|
10120
|
-
// the transaction parked; the wake would strand until it settles).
|
|
10121
|
-
ext(node)._overrideValue = NOT_PENDING;
|
|
10122
|
-
node._config |= CONFIG_OPTIMISTIC;
|
|
10123
|
-
const nx = node._x;
|
|
10124
|
-
if (nx) {
|
|
10125
|
-
nx._overrideOwner = null;
|
|
10126
|
-
nx._optimisticLane = undefined;
|
|
10127
|
-
}
|
|
10128
|
-
node._pendingValue = NOT_PENDING;
|
|
10129
|
-
node._value = committed;
|
|
10130
|
-
if (!node._equals || !node._equals(prev, committed)) {
|
|
10131
|
-
insertSubs(node, true);
|
|
10132
|
-
schedule();
|
|
10133
|
-
}
|
|
10134
|
-
};
|
|
10135
|
-
// Landing consumes STRUCTURAL optimism only (legacy layer parity):
|
|
10136
|
-
// membership edits, array length, and the value overrides written WITH
|
|
10137
|
-
// them (a key carrying an active presence override is an add/delete —
|
|
10138
|
-
// classified BEFORE the adoption may have made the key exist in landed
|
|
10139
|
-
// data). A pure value override on a key the landing carries stays with
|
|
10140
|
-
// its owning transaction (rapid-toggle contract: a live action's edit
|
|
10141
|
-
// of an existing entity rides on top of landed truth).
|
|
10142
|
-
const isArr = Array.isArray(t.v);
|
|
10143
|
-
const has = t.h;
|
|
10144
|
-
let structuralKeys = null;
|
|
10145
|
-
if (has !== null) {
|
|
10146
|
-
for (const key of Reflect.ownKeys(has)) {
|
|
10147
|
-
if (hasActiveOverride(has[key])) (structuralKeys ??= new Set()).add(key);
|
|
10148
|
-
}
|
|
10149
|
-
}
|
|
10150
|
-
const nodes = t.n;
|
|
10151
|
-
if (nodes !== null) {
|
|
10152
|
-
for (const key of Reflect.ownKeys(nodes)) {
|
|
10153
|
-
const structural =
|
|
10154
|
-
structuralKeys?.has(key) || !(key in t.v) || (isArr && key === "length");
|
|
10155
|
-
if (!structural) continue;
|
|
10156
|
-
drop(nodes[key], isArr && key === "length" ? t.v.length : t.v[key]);
|
|
10157
|
-
}
|
|
10158
|
-
}
|
|
10159
|
-
if (has !== null) {
|
|
10160
|
-
for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
|
|
10161
|
-
}
|
|
10162
|
-
if (t.k !== null && hasActiveOverride(t.k)) {
|
|
10163
|
-
ext(t.k)._overrideValue = NOT_PENDING;
|
|
10164
|
-
t.k._config |= CONFIG_OPTIMISTIC;
|
|
10165
|
-
const kx = t.k._x;
|
|
10166
|
-
if (kx) {
|
|
10167
|
-
kx._overrideOwner = null;
|
|
10168
|
-
kx._optimisticLane = undefined;
|
|
10169
|
-
}
|
|
10170
|
-
insertSubs(t.k, true);
|
|
10171
|
-
schedule();
|
|
10172
|
-
}
|
|
10173
|
-
// Patch channel (override-consumption site): visible truth flipped to
|
|
10174
|
-
// committed for the consumed keys — force a re-apply from the live
|
|
10175
|
-
// view so the DOM leaves the override state.
|
|
10176
|
-
if (t.pc !== null && t.pc.p !== null) patchHooks.emitPatchOptimistic(t, null, null);
|
|
10177
|
-
}
|
|
10178
|
-
overlaid.clear();
|
|
10179
|
-
});
|
|
10180
|
-
}
|
|
10181
11156
|
/** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
|
|
10182
11157
|
* view, lane values included; a fresh copy per call during pending windows —
|
|
10183
|
-
* 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.) */
|
|
10184
11162
|
function optimisticView(t, src) {
|
|
10185
|
-
if (t.fam?.opt !== true) return src;
|
|
11163
|
+
if (t.fam?.opt !== true || authoritativeRead()) return src;
|
|
10186
11164
|
let out = null;
|
|
10187
11165
|
const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
|
|
10188
11166
|
const nodes = t.n;
|
|
@@ -11506,7 +12484,10 @@ function flattenArray(children, results = [], options) {
|
|
|
11506
12484
|
} while (typeof child === "function" && !child.length);
|
|
11507
12485
|
}
|
|
11508
12486
|
if (Array.isArray(child)) {
|
|
11509
|
-
|
|
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;
|
|
11510
12491
|
} else if (
|
|
11511
12492
|
options?.skipNonRendered &&
|
|
11512
12493
|
(child == null || child === true || child === false || child === "")
|
|
@@ -11534,6 +12515,7 @@ export {
|
|
|
11534
12515
|
NoOwnerError,
|
|
11535
12516
|
NotReadyError,
|
|
11536
12517
|
SUPPORTS_PROXY,
|
|
12518
|
+
TimeoutError,
|
|
11537
12519
|
action,
|
|
11538
12520
|
affects,
|
|
11539
12521
|
clearSnapshots,
|
|
@@ -11592,5 +12574,6 @@ export {
|
|
|
11592
12574
|
storeHasOptimisticFamily,
|
|
11593
12575
|
storeIsShallow,
|
|
11594
12576
|
storePath,
|
|
12577
|
+
until,
|
|
11595
12578
|
untrack
|
|
11596
12579
|
};
|