@solidjs/signals 2.0.0-rc.2 → 2.0.0-rc.4
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 +1454 -118
- package/dist/node.cjs +2709 -1396
- package/dist/prod/affects.js +13 -12
- package/dist/prod/boundaries.js +39 -34
- package/dist/prod/core/action.js +3 -3
- package/dist/prod/core/async.js +48 -46
- package/dist/prod/core/core.js +99 -67
- package/dist/prod/core/effect.js +25 -28
- package/dist/prod/core/external.js +2 -2
- package/dist/prod/core/graph.js +85 -49
- package/dist/prod/core/heap.js +10 -10
- package/dist/prod/core/lanes.js +19 -19
- package/dist/prod/core/optimistic.js +66 -41
- package/dist/prod/core/owner.js +13 -13
- package/dist/prod/core/scheduler.js +131 -85
- package/dist/prod/core/verdict.js +36 -15
- package/dist/prod/index.js +4 -0
- package/dist/prod/map.js +101 -101
- package/dist/prod/signals.js +1 -1
- package/dist/prod/store/index.js +2 -0
- package/dist/prod/store/next/optimistic.js +65 -11
- package/dist/prod/store/next/patch-hooks.js +13 -0
- package/dist/prod/store/next/patch.js +614 -0
- package/dist/prod/store/next/projection.js +107 -45
- package/dist/prod/store/next/reconcile.js +307 -120
- package/dist/prod/store/next/store.js +321 -92
- package/dist/prod/store/next/target.js +13 -4
- package/dist/prod/store/store.js +5 -5
- package/dist/types/core/core.d.ts +15 -1
- package/dist/types/core/dev.d.ts +8 -0
- package/dist/types/core/graph.d.ts +22 -0
- package/dist/types/core/invariants.d.ts +1 -1
- package/dist/types/core/scheduler.d.ts +12 -0
- package/dist/types/store/index.d.ts +2 -0
- package/dist/types/store/next/patch-hooks.d.ts +41 -0
- package/dist/types/store/next/patch.d.ts +91 -0
- package/dist/types/store/next/reconcile.d.ts +14 -0
- package/dist/types/store/next/store.d.ts +30 -2
- package/dist/types/store/next/target.d.ts +58 -8
- package/dist/types-cjs/core/core.d.cts +15 -1
- package/dist/types-cjs/core/dev.d.cts +8 -0
- package/dist/types-cjs/core/graph.d.cts +22 -0
- package/dist/types-cjs/core/invariants.d.cts +1 -1
- package/dist/types-cjs/core/scheduler.d.cts +12 -0
- package/dist/types-cjs/store/index.d.cts +2 -0
- package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
- package/dist/types-cjs/store/next/patch.d.cts +91 -0
- package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
- package/dist/types-cjs/store/next/store.d.cts +30 -2
- package/dist/types-cjs/store/next/target.d.cts +58 -8
- package/package.json +14 -14
- package/dist/types/store/optimistic.d.ts +0 -45
- package/dist/types/store/projection.d.ts +0 -70
- package/dist/types/store/reconcile.d.ts +0 -46
- package/dist/types-cjs/store/optimistic.d.cts +0 -45
- package/dist/types-cjs/store/projection.d.cts +0 -70
- package/dist/types-cjs/store/reconcile.d.cts +0 -46
package/dist/dev.js
CHANGED
|
@@ -236,7 +236,7 @@ function captureStack() {
|
|
|
236
236
|
// remaining frames are the user code that performed the write.
|
|
237
237
|
return raw
|
|
238
238
|
.slice(1)
|
|
239
|
-
.filter(line => !/
|
|
239
|
+
.filter(line => !/(?:^|[/\\])(?:packages[/\\])?signals[/\\](src|dist)[/\\]/.test(line))
|
|
240
240
|
.slice(0, 3)
|
|
241
241
|
.map(line => line.trim());
|
|
242
242
|
}
|
|
@@ -708,11 +708,17 @@ const hooks = {};
|
|
|
708
708
|
const diagnosticListeners = new Set();
|
|
709
709
|
const diagnosticCaptures = new Set();
|
|
710
710
|
let diagnosticSequence = 0;
|
|
711
|
+
let consoleFooter;
|
|
712
|
+
const footeredCodes = new Set();
|
|
711
713
|
const diagnostics = {
|
|
712
714
|
subscribe(listener) {
|
|
713
715
|
diagnosticListeners.add(listener);
|
|
714
716
|
return () => diagnosticListeners.delete(listener);
|
|
715
717
|
},
|
|
718
|
+
setConsoleFooter(footer) {
|
|
719
|
+
consoleFooter = footer;
|
|
720
|
+
footeredCodes.clear();
|
|
721
|
+
},
|
|
716
722
|
capture() {
|
|
717
723
|
const events = [];
|
|
718
724
|
diagnosticCaptures.add(events);
|
|
@@ -752,6 +758,13 @@ function emitDiagnostic(event) {
|
|
|
752
758
|
};
|
|
753
759
|
for (const listener of diagnosticListeners) listener(entry);
|
|
754
760
|
for (const capture of diagnosticCaptures) capture.push(entry);
|
|
761
|
+
if (consoleFooter && !footeredCodes.has(entry.code)) {
|
|
762
|
+
footeredCodes.add(entry.code);
|
|
763
|
+
const footer = consoleFooter(entry);
|
|
764
|
+
// Call sites console.warn/error their message after emitDiagnostic
|
|
765
|
+
// returns; a microtask lands the footer right below that report.
|
|
766
|
+
if (footer) queueMicrotask(() => console.warn(footer));
|
|
767
|
+
}
|
|
755
768
|
return entry;
|
|
756
769
|
}
|
|
757
770
|
/**
|
|
@@ -1097,7 +1110,7 @@ function cancelZombieRecompute(el) {
|
|
|
1097
1110
|
}
|
|
1098
1111
|
let clock = 0;
|
|
1099
1112
|
let activeTransition = null;
|
|
1100
|
-
let scheduled = false;
|
|
1113
|
+
let scheduled$1 = false;
|
|
1101
1114
|
let halted = false;
|
|
1102
1115
|
let haltNotified = false;
|
|
1103
1116
|
let syncDepth = 0;
|
|
@@ -1207,6 +1220,27 @@ function mergeTransitionState(target, outgoing) {
|
|
|
1207
1220
|
outgoing._affectsNodes.length = 0;
|
|
1208
1221
|
}
|
|
1209
1222
|
for (const store of outgoing._optimisticStores) target._optimisticStores.add(store);
|
|
1223
|
+
// Patch-channel stash (store/next/patch.ts): entries held for the outgoing
|
|
1224
|
+
// transition must ride the merge like every other per-transition
|
|
1225
|
+
// collection — releaseBatch only reads the COMMITTING transition's stash,
|
|
1226
|
+
// so a stranded sidecar would silently drop its patches. Move (don't
|
|
1227
|
+
// copy), same aliasing rule as the collections above. The field is an
|
|
1228
|
+
// expando so this module stays free of patch imports (pay-for-use).
|
|
1229
|
+
const heldPatches = outgoing._heldPatches;
|
|
1230
|
+
if (heldPatches !== undefined) {
|
|
1231
|
+
outgoing._heldPatches = undefined;
|
|
1232
|
+
let dest = target._heldPatches;
|
|
1233
|
+
if (dest !== undefined) dest.push(...heldPatches);
|
|
1234
|
+
else dest = target._heldPatches = heldPatches;
|
|
1235
|
+
// Retarget the entries' coalescing stamps to the surviving stash
|
|
1236
|
+
// (opaque backref contract with store/next/patch.ts): without this a
|
|
1237
|
+
// post-merge emission misses the stamp and pushes a SECOND entry —
|
|
1238
|
+
// the record's patch applies twice at commit (re-audit 5, P1-2).
|
|
1239
|
+
for (let i = 0; i < heldPatches.length; i++) {
|
|
1240
|
+
const pc = heldPatches[i].pc;
|
|
1241
|
+
if (pc !== undefined && pc.qe === heldPatches[i]) pc.qa = dest;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1210
1244
|
for (const [source, reporters] of outgoing._asyncReporters) {
|
|
1211
1245
|
let targetReporters = target._asyncReporters.get(source);
|
|
1212
1246
|
if (!targetReporters) target._asyncReporters.set(source, (targetReporters = new Set()));
|
|
@@ -1219,8 +1253,8 @@ function schedule() {
|
|
|
1219
1253
|
notifyHalted();
|
|
1220
1254
|
return;
|
|
1221
1255
|
}
|
|
1222
|
-
if (scheduled) return;
|
|
1223
|
-
scheduled = true;
|
|
1256
|
+
if (scheduled$1) return;
|
|
1257
|
+
scheduled$1 = true;
|
|
1224
1258
|
if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);
|
|
1225
1259
|
}
|
|
1226
1260
|
/**
|
|
@@ -1403,6 +1437,10 @@ class GlobalQueue extends Queue {
|
|
|
1403
1437
|
static _transitionBlocked = null;
|
|
1404
1438
|
static _cleanupLanes = null;
|
|
1405
1439
|
static _runLaneEffects = null;
|
|
1440
|
+
/** Patch-channel optimistic drain (next/patch.ts): optimistic emissions
|
|
1441
|
+
* apply at lane-effect timing — visible in flight, unlike the regular
|
|
1442
|
+
* effect queues an action stashes. Injected; null when unused. */
|
|
1443
|
+
static _drainPatchOptimistic = null;
|
|
1406
1444
|
static _gatedRead = null;
|
|
1407
1445
|
static _laneSuspends = null;
|
|
1408
1446
|
static _laneReadsCommitted = null;
|
|
@@ -1415,6 +1453,10 @@ class GlobalQueue extends Queue {
|
|
|
1415
1453
|
this._running = true;
|
|
1416
1454
|
try {
|
|
1417
1455
|
if (true) devCheckFlushStart();
|
|
1456
|
+
// Before runHeap for the same reason as the fast drain above; late
|
|
1457
|
+
// subscribers (an effect reading a swept memo this flush) revive it,
|
|
1458
|
+
// which is the pay-for-use contract.
|
|
1459
|
+
sweepDormant();
|
|
1418
1460
|
runHeap(dirtyQueue, GlobalQueue._update);
|
|
1419
1461
|
if (activeTransition) {
|
|
1420
1462
|
const isComplete = transitionComplete(activeTransition);
|
|
@@ -1449,7 +1491,7 @@ class GlobalQueue extends Queue {
|
|
|
1449
1491
|
// A kept ambient batch may hold pending nodes (#2916): stay
|
|
1450
1492
|
// scheduled so the outer drain loop commits them via the plain
|
|
1451
1493
|
// flush path instead of leaving them until the next natural flush.
|
|
1452
|
-
scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
1494
|
+
scheduled$1 = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
1453
1495
|
reassignPendingTransition(stashedTransition._pendingNodes);
|
|
1454
1496
|
activeTransition = null;
|
|
1455
1497
|
finalizePureQueue(null, true);
|
|
@@ -1489,7 +1531,7 @@ class GlobalQueue extends Queue {
|
|
|
1489
1531
|
}
|
|
1490
1532
|
clock++;
|
|
1491
1533
|
// Check if finalization added items to the heap (from optimistic reversion)
|
|
1492
|
-
scheduled = dirtyQueue._max >= dirtyQueue._min;
|
|
1534
|
+
scheduled$1 = dirtyQueue._max >= dirtyQueue._min;
|
|
1493
1535
|
// Run lane effects first (for ready lanes), then regular effects
|
|
1494
1536
|
activeLanes.size && GlobalQueue._runLaneEffects(EFFECT_RENDER);
|
|
1495
1537
|
this.run(EFFECT_RENDER);
|
|
@@ -1506,7 +1548,7 @@ class GlobalQueue extends Queue {
|
|
|
1506
1548
|
}
|
|
1507
1549
|
if (
|
|
1508
1550
|
true &&
|
|
1509
|
-
!scheduled &&
|
|
1551
|
+
!scheduled$1 &&
|
|
1510
1552
|
!activeTransition &&
|
|
1511
1553
|
transitions.size === 0 &&
|
|
1512
1554
|
activeLanes.size === 0
|
|
@@ -1692,6 +1734,16 @@ let storeCommitHook = null;
|
|
|
1692
1734
|
function setStoreCommitHook(fn) {
|
|
1693
1735
|
storeCommitHook = fn;
|
|
1694
1736
|
}
|
|
1737
|
+
/** Patch-channel release hook (next/patch.ts): transition-stamped patch
|
|
1738
|
+
* emissions are released when THEIR batch commits. Transitions never
|
|
1739
|
+
* abort: failed actions still commit (only optimistic overrides revert),
|
|
1740
|
+
* and merged-away transitions hand their stash to the survivor
|
|
1741
|
+
* (mergeTransitionState) — every stash drains exactly once. Injected like
|
|
1742
|
+
* storeCommitHook to stay tree-shakeable. */
|
|
1743
|
+
let patchCommitHook = null;
|
|
1744
|
+
function setPatchCommitHook(fn) {
|
|
1745
|
+
patchCommitHook = fn;
|
|
1746
|
+
}
|
|
1695
1747
|
function commitPendingNodes() {
|
|
1696
1748
|
const pendingNodes = currentBatch._pendingNodes;
|
|
1697
1749
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
@@ -1699,6 +1751,7 @@ function commitPendingNodes() {
|
|
|
1699
1751
|
}
|
|
1700
1752
|
pendingNodes.length = 0;
|
|
1701
1753
|
storeCommitHook?.();
|
|
1754
|
+
patchCommitHook?.(currentBatch);
|
|
1702
1755
|
}
|
|
1703
1756
|
function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
1704
1757
|
// For incomplete transitions, skip pending resolution and optimistic reversion
|
|
@@ -1825,7 +1878,7 @@ function flush(fn) {
|
|
|
1825
1878
|
let count = 0;
|
|
1826
1879
|
// `flush()` is an explicit drain point, so it must also process an active
|
|
1827
1880
|
// transition even if no microtask was scheduled for it yet.
|
|
1828
|
-
while (scheduled || activeTransition) {
|
|
1881
|
+
while (scheduled$1 || activeTransition) {
|
|
1829
1882
|
if (++count === 1e5) throw new Error("Potential Infinite Loop Detected.");
|
|
1830
1883
|
globalQueue.flush();
|
|
1831
1884
|
}
|
|
@@ -2363,7 +2416,7 @@ function unlinkSubs(link) {
|
|
|
2363
2416
|
// transition holding it) is an observer — tearing down would orphan
|
|
2364
2417
|
// the work and re-execute it on the next read. The settle path runs
|
|
2365
2418
|
// this same last-one-out check when that observer releases (the
|
|
2366
|
-
// untracked-read
|
|
2419
|
+
// untracked-read dormancy sweep guards on pending identically).
|
|
2367
2420
|
const c = dep;
|
|
2368
2421
|
c._fn &&
|
|
2369
2422
|
c._config & CONFIG_AUTO_DISPOSE &&
|
|
@@ -2403,6 +2456,46 @@ function unobserved(el) {
|
|
|
2403
2456
|
clearDeps(el);
|
|
2404
2457
|
disposeChildren(el, true);
|
|
2405
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Deferred dormancy for never-observed auto-dispose computeds (#3078).
|
|
2461
|
+
*
|
|
2462
|
+
* An untracked top-level read of a subscriber-less observation-lifecycle memo
|
|
2463
|
+
* used to call unobserved() inline at the end of read(). That kept the leak
|
|
2464
|
+
* closed (the compute links the memo into its deps' sub lists — without a
|
|
2465
|
+
* teardown point a never-observed memo is retained by its sources forever;
|
|
2466
|
+
* upstream alien-signals has exactly this retention), but it made reads
|
|
2467
|
+
* destructive: each read disposed the node, the next read revived it with a
|
|
2468
|
+
* full recompute in whatever ambient transition/lane context happened to be
|
|
2469
|
+
* current, so consecutive reads could return different answers with no write
|
|
2470
|
+
* in between.
|
|
2471
|
+
*
|
|
2472
|
+
* Instead, reads queue the node here and the scheduler sweeps at the top of
|
|
2473
|
+
* the next flush (before runHeap, so a same-tick dirtying is reclaimed
|
|
2474
|
+
* instead of recomputed). Reads become idempotent within a tick (the node
|
|
2475
|
+
* stays alive and serves its cache, uniform with observed memos) while
|
|
2476
|
+
* reclamation still happens within one microtask — the enqueue site arms
|
|
2477
|
+
* schedule(), so a flush is guaranteed even when no other work is queued.
|
|
2478
|
+
*/
|
|
2479
|
+
const dormantNodes = new Set();
|
|
2480
|
+
function sweepDormant() {
|
|
2481
|
+
if (dormantNodes.size === 0) return;
|
|
2482
|
+
for (const el of dormantNodes) {
|
|
2483
|
+
// Re-validate at sweep time: the node may have gained a subscriber (its
|
|
2484
|
+
// lifecycle is the unlinkSubs cascade now), gone pending (in-flight async
|
|
2485
|
+
// is an observer; the settle path re-runs last-one-out), lost its
|
|
2486
|
+
// AUTO_DISPOSE bit (owner teardown strips it, #3024), or already been
|
|
2487
|
+
// torn down.
|
|
2488
|
+
if (
|
|
2489
|
+
!el._subs &&
|
|
2490
|
+
el._config & CONFIG_AUTO_DISPOSE &&
|
|
2491
|
+
!(el._statusFlags & STATUS_PENDING) &&
|
|
2492
|
+
!(el._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
2493
|
+
) {
|
|
2494
|
+
unobserved(el);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
dormantNodes.clear();
|
|
2498
|
+
}
|
|
2406
2499
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
2407
2500
|
function link(dep, sub, pendingObserver = false) {
|
|
2408
2501
|
// Repeat touches within one pass AND-combine `_pendingObserver`: a probe
|
|
@@ -3084,7 +3177,8 @@ function clearStatus(el, clearUninitialized = false) {
|
|
|
3084
3177
|
GlobalQueue._updateChildCompanions !== null
|
|
3085
3178
|
)
|
|
3086
3179
|
GlobalQueue._updateChildCompanions(el);
|
|
3087
|
-
|
|
3180
|
+
const notify = statusNotifierOf(el);
|
|
3181
|
+
if (notify) notify.call(el);
|
|
3088
3182
|
}
|
|
3089
3183
|
function notifyStatus(el, status, error, blockStatus, lane) {
|
|
3090
3184
|
// Wrap regular errors to track source node
|
|
@@ -3126,14 +3220,15 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
3126
3220
|
}
|
|
3127
3221
|
const downstreamBlockStatus = blockStatus || startsBlocking;
|
|
3128
3222
|
const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
|
|
3129
|
-
|
|
3223
|
+
const elNotify = statusNotifierOf(el);
|
|
3224
|
+
if (elNotify) {
|
|
3130
3225
|
if (blockStatus && status === STATUS_PENDING) {
|
|
3131
3226
|
return;
|
|
3132
3227
|
}
|
|
3133
3228
|
if (downstreamBlockStatus) {
|
|
3134
|
-
|
|
3229
|
+
elNotify.call(el, status, error);
|
|
3135
3230
|
} else {
|
|
3136
|
-
|
|
3231
|
+
elNotify.call(el);
|
|
3137
3232
|
}
|
|
3138
3233
|
return;
|
|
3139
3234
|
}
|
|
@@ -3668,7 +3763,7 @@ function ext(el) {
|
|
|
3668
3763
|
* mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip
|
|
3669
3764
|
* the auto-dispose CONFIG bit (effect() previously cleared it post-construction).
|
|
3670
3765
|
*/
|
|
3671
|
-
function createEffectNode(fn, effectFn, errorFn, type,
|
|
3766
|
+
function createEffectNode(fn, effectFn, errorFn, type, options) {
|
|
3672
3767
|
const transparent = options?.transparent ?? false;
|
|
3673
3768
|
const self = {
|
|
3674
3769
|
id: inheritId(options, transparent, context),
|
|
@@ -3712,12 +3807,36 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
|
|
|
3712
3807
|
_x: null
|
|
3713
3808
|
};
|
|
3714
3809
|
self._name = options?.name ?? "effect";
|
|
3715
|
-
//
|
|
3716
|
-
|
|
3810
|
+
// Effects dispatch status through the SHARED notifier (statusNotifierOf,
|
|
3811
|
+
// keyed off _type) — storing it per node forced a full NodeExtension
|
|
3812
|
+
// allocation on EVERY effect at creation (an alloc + 19 field stores,
|
|
3813
|
+
// +23% effect creation, caught by the creation benches). Only genuinely
|
|
3814
|
+
// per-node channels (boundaries) live on _x.
|
|
3717
3815
|
if (options?.unobserved) ext(self)._unobserved = options.unobserved;
|
|
3718
3816
|
setupComputedNode(self, lazyOptions);
|
|
3719
3817
|
return self;
|
|
3720
3818
|
}
|
|
3819
|
+
/**
|
|
3820
|
+
* The shared status notifier for effect nodes, installed once by effect.ts
|
|
3821
|
+
* at module evaluation (`this`-dispatched — one function serves every
|
|
3822
|
+
* effect, so nodes never store it). Boundary computeds keep their own
|
|
3823
|
+
* per-node channel on `_x._notifyStatus`, which takes precedence.
|
|
3824
|
+
*/
|
|
3825
|
+
let effectStatusNotify = null;
|
|
3826
|
+
function setEffectStatusNotify(fn) {
|
|
3827
|
+
effectStatusNotify = fn;
|
|
3828
|
+
}
|
|
3829
|
+
/** Resolve a node's status notifier: an own `_x` channel (boundaries) wins;
|
|
3830
|
+
* effect nodes (`_type` — EFFECT_PURE is 0, and only effect literals carry
|
|
3831
|
+
* the field) fall back to the shared notifier. Presence doubles as the
|
|
3832
|
+
* "display consumer" membership test in the status walks, exactly as the
|
|
3833
|
+
* per-node field did when every effect carried one. */
|
|
3834
|
+
function statusNotifierOf(el) {
|
|
3835
|
+
const x = el._x;
|
|
3836
|
+
const own = x !== null && x !== undefined ? x._notifyStatus : undefined;
|
|
3837
|
+
if (own !== undefined) return own;
|
|
3838
|
+
return el._type ? (effectStatusNotify ?? undefined) : undefined;
|
|
3839
|
+
}
|
|
3721
3840
|
const lazyOptions = { lazy: true };
|
|
3722
3841
|
function setupComputedNode(self, options) {
|
|
3723
3842
|
self._prevHeap = self;
|
|
@@ -4094,7 +4213,14 @@ function read(el) {
|
|
|
4094
4213
|
!(owner._statusFlags & STATUS_PENDING) &&
|
|
4095
4214
|
!el._subs
|
|
4096
4215
|
) {
|
|
4097
|
-
unobserved(
|
|
4216
|
+
// Deferred, not inline (#3078): an inline unobserved() here made untracked
|
|
4217
|
+
// reads destructive — dispose on this read, full revival recompute on the
|
|
4218
|
+
// next — so consecutive reads could answer differently with no write in
|
|
4219
|
+
// between (the revival samples the ambient transition/lane context).
|
|
4220
|
+
// The sweep at flush finalization re-validates and reclaims; schedule()
|
|
4221
|
+
// guarantees that flush happens even if nothing else is queued.
|
|
4222
|
+
dormantNodes.add(el);
|
|
4223
|
+
schedule();
|
|
4098
4224
|
}
|
|
4099
4225
|
return value;
|
|
4100
4226
|
}
|
|
@@ -4507,7 +4633,16 @@ function optimisticWrite(el, v) {
|
|
|
4507
4633
|
const currentValue = hasOverride ? unwrapOverride(el._x?._overrideValue) : el._value;
|
|
4508
4634
|
if (typeof v === "function") v = v(currentValue);
|
|
4509
4635
|
const valueChanged =
|
|
4510
|
-
!!(el._statusFlags & STATUS_UNINITIALIZED) ||
|
|
4636
|
+
!!(el._statusFlags & STATUS_UNINITIALIZED) ||
|
|
4637
|
+
// A dirty node's _value is stale (its queued recompute hasn't run — e.g.
|
|
4638
|
+
// a latest() shadow marked by the previous landing's companion snap), so
|
|
4639
|
+
// equality against it must not swallow the write. Without this, a sync
|
|
4640
|
+
// push returning the shadow to that stale value was dropped, the snap
|
|
4641
|
+
// recompute then committed the parent's old value, and the banner showed
|
|
4642
|
+
// the previous transition's target (#3041 follow-up).
|
|
4643
|
+
!!((el._flags ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) ||
|
|
4644
|
+
!el._equals ||
|
|
4645
|
+
!el._equals(currentValue, v);
|
|
4511
4646
|
if (!valueChanged) {
|
|
4512
4647
|
// Same-value write with an active override still entangles the current
|
|
4513
4648
|
// action's transition — the hold must outlast all overlapping actions.
|
|
@@ -4608,6 +4743,9 @@ function runLaneEffects(type) {
|
|
|
4608
4743
|
runQueue(effects, type);
|
|
4609
4744
|
}
|
|
4610
4745
|
}
|
|
4746
|
+
// Optimistic patch applications ride the same visibility slot as lane
|
|
4747
|
+
// effects (in-flight DOM updates); no-op unless patches registered.
|
|
4748
|
+
if (type === EFFECT_RENDER) GlobalQueue._drainPatchOptimistic?.();
|
|
4611
4749
|
}
|
|
4612
4750
|
function cleanupCompletedLanes(completingTransition) {
|
|
4613
4751
|
for (const lane of activeLanes) {
|
|
@@ -4663,18 +4801,33 @@ function laneReadsCommitted(el, owner, c) {
|
|
|
4663
4801
|
el._x?._overrideValue !== undefined ||
|
|
4664
4802
|
!!el._x?._optimisticLane ||
|
|
4665
4803
|
!!(owner._statusFlags & STATUS_PENDING)
|
|
4666
|
-
)
|
|
4804
|
+
) {
|
|
4805
|
+
// The committed view hides a staged in-flight value that will promote
|
|
4806
|
+
// silently (commitPendingNode never re-notifies). gatedRead records plain
|
|
4807
|
+
// signals for replay at commit; async memos are excluded from it by the
|
|
4808
|
+
// `_fn` check and reach here instead — a lane-assigned source whose async
|
|
4809
|
+
// already settled (laneAsyncSettled keeps _optimisticLane) served its
|
|
4810
|
+
// committed value to a reader that never re-ran after the landing, so a
|
|
4811
|
+
// pending-gated branch stayed one value behind permanently (#3041
|
|
4812
|
+
// follow-up). Record the reader under the same replay contract.
|
|
4813
|
+
if (el._pendingValue !== NOT_PENDING)
|
|
4814
|
+
(activeTransition ?? globalQueue._batch)._gatedSubs.add(c);
|
|
4667
4815
|
return true;
|
|
4816
|
+
}
|
|
4668
4817
|
if (owner === el && stale && c._x?._parentSource !== el) {
|
|
4669
|
-
// The committed view can hide a
|
|
4670
|
-
//
|
|
4671
|
-
//
|
|
4672
|
-
//
|
|
4673
|
-
//
|
|
4674
|
-
//
|
|
4675
|
-
//
|
|
4676
|
-
|
|
4677
|
-
|
|
4818
|
+
// The committed view can hide a staged write (a lane member — even just
|
|
4819
|
+
// an isPending companion flip — puts the reader "under a lane"). The
|
|
4820
|
+
// staged value commits with no re-delivery (commitPendingNode never
|
|
4821
|
+
// re-notifies), so record the reader for replay at commit — the same
|
|
4822
|
+
// contract gatedRead provides (#2963). gatedRead itself only covers
|
|
4823
|
+
// signal reads where the reading computed differs from the source; the
|
|
4824
|
+
// owner === el memo/self read lands here instead. With a transaction
|
|
4825
|
+
// active the staged value promotes silently at ITS landing, so record
|
|
4826
|
+
// into the transaction (#3041 follow-up: a pending-gated branch that
|
|
4827
|
+
// first read its async source during the landing flush stayed one value
|
|
4828
|
+
// behind permanently); with none, into the ambient batch.
|
|
4829
|
+
if (el._pendingValue !== NOT_PENDING)
|
|
4830
|
+
(activeTransition ?? globalQueue._batch)._gatedSubs.add(c);
|
|
4678
4831
|
return true;
|
|
4679
4832
|
}
|
|
4680
4833
|
return false;
|
|
@@ -5030,6 +5183,13 @@ function snapCompanionsToState(owner) {
|
|
|
5030
5183
|
}
|
|
5031
5184
|
function getLatestValueComputed(el) {
|
|
5032
5185
|
let lvc = el._x?._latestValueComputed;
|
|
5186
|
+
// A shadow disposed while unobserved (its gated reader unmounted at a
|
|
5187
|
+
// landing) is a corpse: sync writes into it equality-swallow against its
|
|
5188
|
+
// frozen _value, and a later read revives it via recompute — clearing
|
|
5189
|
+
// DISPOSED and re-deriving from the committed view, so the banner showed
|
|
5190
|
+
// the previous transition's target (#3041 follow-up). Treat it as absent;
|
|
5191
|
+
// recreation backfills from the in-flight write below.
|
|
5192
|
+
if (lvc && lvc._flags & REACTIVE_DISPOSED) lvc = undefined;
|
|
5033
5193
|
if (!lvc) {
|
|
5034
5194
|
const prevPending = latestReadActive;
|
|
5035
5195
|
setLatestReadActive(false);
|
|
@@ -5130,8 +5290,22 @@ function pendingCheckRead(el, c, owner, firewall) {
|
|
|
5130
5290
|
*/
|
|
5131
5291
|
function heldAwaitingAsync(el) {
|
|
5132
5292
|
const et = el._transition;
|
|
5133
|
-
const t = et ? currentTransition(et) :
|
|
5293
|
+
const t = et ? currentTransition(et) : activeTransition;
|
|
5134
5294
|
if (!t || t._done) return false;
|
|
5295
|
+
// A plain staged write (a signal/store leaf — no _fn) held while an action
|
|
5296
|
+
// is still running is an INPUT to a computation still in flight (#3078):
|
|
5297
|
+
// the pairing rule must not suppress the verdict, or a memo recomputing
|
|
5298
|
+
// mid-action reads the staged value, gets told "not pending", and
|
|
5299
|
+
// disagrees with a direct isPending() probe for the whole action window.
|
|
5300
|
+
// A computed's staged value is the opposite case — a LANDED answer
|
|
5301
|
+
// awaiting reveal — where the pairing rule stands even inside an open
|
|
5302
|
+
// action (#2831: a reader that saw the new value must not also see
|
|
5303
|
+
// pending); still-computing answers are covered by the reporter scan.
|
|
5304
|
+
if (t._actions.length && !el._fn) return true;
|
|
5305
|
+
// A node not yet stamped with a transition only qualifies through the
|
|
5306
|
+
// action check above; the reporter scan below is for transition-held
|
|
5307
|
+
// writes whose source async is still computing.
|
|
5308
|
+
if (!et) return false;
|
|
5135
5309
|
for (const [source, reporters] of t._asyncReporters) {
|
|
5136
5310
|
if (
|
|
5137
5311
|
reporters.size &&
|
|
@@ -5248,7 +5422,6 @@ function effect(compute, effect, error, options) {
|
|
|
5248
5422
|
effect,
|
|
5249
5423
|
error,
|
|
5250
5424
|
isUser ? EFFECT_USER : EFFECT_RENDER,
|
|
5251
|
-
notifyEffectStatus,
|
|
5252
5425
|
options
|
|
5253
5426
|
);
|
|
5254
5427
|
recompute(node, true);
|
|
@@ -5421,17 +5594,9 @@ function trackedEffect(fn, options) {
|
|
|
5421
5594
|
node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
|
|
5422
5595
|
node._modified = true;
|
|
5423
5596
|
node._type = EFFECT_TRACKED;
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
node._queue.notify(node, STATUS_PENDING, 0);
|
|
5428
|
-
const err = error !== undefined ? error : node._x?._error;
|
|
5429
|
-
if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
|
|
5430
|
-
haltReactivity(unwrapStatusError(err));
|
|
5431
|
-
throw err;
|
|
5432
|
-
}
|
|
5433
|
-
}
|
|
5434
|
-
};
|
|
5597
|
+
// Status dispatch rides the SHARED notifier (statusNotifierOf keys off
|
|
5598
|
+
// _type): its error arm is behavior-identical to the closure that used to
|
|
5599
|
+
// live here, without the per-node NodeExtension allocation.
|
|
5435
5600
|
node._run = run;
|
|
5436
5601
|
node._queue.enqueue(EFFECT_USER, run);
|
|
5437
5602
|
if (!node._parent) {
|
|
@@ -5449,6 +5614,10 @@ function trackedEffect(fn, options) {
|
|
|
5449
5614
|
console.warn(message);
|
|
5450
5615
|
}
|
|
5451
5616
|
}
|
|
5617
|
+
// Install the shared effect status notifier (statusNotifierOf serves it to
|
|
5618
|
+
// every effect node) — module-scope: any bundle that creates effects
|
|
5619
|
+
// evaluates this module.
|
|
5620
|
+
setEffectStatusNotify(notifyEffectStatus);
|
|
5452
5621
|
|
|
5453
5622
|
const ACTION_CALLED_IN_OWNED_SCOPE_MESSAGE =
|
|
5454
5623
|
"[ACTION_CALLED_IN_OWNED_SCOPE] Calling an action inside an owned scope (component, computation) is not allowed. " +
|
|
@@ -6069,6 +6238,15 @@ let optHooks = null;
|
|
|
6069
6238
|
function setOptHooks(h) {
|
|
6070
6239
|
optHooks = h;
|
|
6071
6240
|
}
|
|
6241
|
+
/** Sticky descendants flag walk (§6d): reconcile's keyed pruning descends
|
|
6242
|
+
* only where subscriptions exist at/below. Nodes AND patches count. */
|
|
6243
|
+
function markDescendants(target) {
|
|
6244
|
+
let t = target;
|
|
6245
|
+
while (t && !t.d) {
|
|
6246
|
+
t.d = true;
|
|
6247
|
+
t = t.u;
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6072
6250
|
|
|
6073
6251
|
/**
|
|
6074
6252
|
* Brand symbols used internally by the store proxy / projection plumbing.
|
|
@@ -6417,8 +6595,9 @@ function notifyMarkBoundaries(node) {
|
|
|
6417
6595
|
visited.add(sub);
|
|
6418
6596
|
// Display consumers (render effects, boundary computeds) act on the
|
|
6419
6597
|
// notification; descent stops there, exactly like the status rails.
|
|
6420
|
-
|
|
6421
|
-
|
|
6598
|
+
const notify = statusNotifierOf(sub);
|
|
6599
|
+
if (notify) {
|
|
6600
|
+
notify.call(sub, STATUS_PENDING, error);
|
|
6422
6601
|
return;
|
|
6423
6602
|
}
|
|
6424
6603
|
forEachDependent(sub, visit);
|
|
@@ -6526,6 +6705,15 @@ function affects(target, key) {
|
|
|
6526
6705
|
}
|
|
6527
6706
|
}
|
|
6528
6707
|
|
|
6708
|
+
let patchHooks = null;
|
|
6709
|
+
let rowHooks = null;
|
|
6710
|
+
function installPatchHooks(hooks) {
|
|
6711
|
+
patchHooks = hooks;
|
|
6712
|
+
}
|
|
6713
|
+
function installRowHooks(hooks) {
|
|
6714
|
+
rowHooks = hooks;
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6529
6717
|
/**
|
|
6530
6718
|
* Store rewrite — increment 2: plain deep stores with pending-backing writes.
|
|
6531
6719
|
* Contract: INTERNALS-STORE-STATE.md.
|
|
@@ -6554,8 +6742,13 @@ function affects(target, key) {
|
|
|
6554
6742
|
* headroom for future fields. The prototype is reset to `Object.prototype`
|
|
6555
6743
|
* so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
|
|
6556
6744
|
* plain object's. Array targets keep the bare-`[]` path — they must carry
|
|
6557
|
-
* the array exotic class for `Array.isArray(proxy)
|
|
6558
|
-
*
|
|
6745
|
+
* the array exotic class for `Array.isArray(proxy)`.
|
|
6746
|
+
*
|
|
6747
|
+
* ARRAY SHAPE RULE: arrays normalize their named properties to dictionary
|
|
6748
|
+
* mode as the count grows (V8 13.x: counts ≡ 0 mod 3 from 18 up), so the
|
|
6749
|
+
* target's named field count is capped at 20 — write-side patch-channel
|
|
6750
|
+
* state lives inside the single `pc` extension (see target.ts), never as
|
|
6751
|
+
* new named fields here. */
|
|
6559
6752
|
function TargetShape() {
|
|
6560
6753
|
this.v = undefined;
|
|
6561
6754
|
this.ch = undefined;
|
|
@@ -6576,9 +6769,15 @@ function TargetShape() {
|
|
|
6576
6769
|
this.s = undefined;
|
|
6577
6770
|
this.ovl = undefined;
|
|
6578
6771
|
this.del = undefined;
|
|
6579
|
-
this.
|
|
6772
|
+
this.pc = undefined;
|
|
6773
|
+
this.hv = undefined;
|
|
6774
|
+
this.ht = undefined;
|
|
6580
6775
|
}
|
|
6581
6776
|
TargetShape.prototype = Object.prototype;
|
|
6777
|
+
/** Lazily allocate the patch-channel extension (one literal shape). */
|
|
6778
|
+
function pcOf(t) {
|
|
6779
|
+
return t.pc ?? (t.pc = { sp: null, p: null, ro: null, wk: null, qa: null, qe: null });
|
|
6780
|
+
}
|
|
6582
6781
|
function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
6583
6782
|
// The proxy target carries the array exotic class when the value is an
|
|
6584
6783
|
// array, so Array.isArray(proxy) is true; the fields live on it directly.
|
|
@@ -6595,6 +6794,7 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
6595
6794
|
t.h = null;
|
|
6596
6795
|
t.k = null;
|
|
6597
6796
|
t.dk = null;
|
|
6797
|
+
t.pc = null;
|
|
6598
6798
|
t.u = parent;
|
|
6599
6799
|
t.pk = parentKey;
|
|
6600
6800
|
t.px = null;
|
|
@@ -6607,7 +6807,8 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
6607
6807
|
t.s = false;
|
|
6608
6808
|
t.ovl = false;
|
|
6609
6809
|
t.del = null;
|
|
6610
|
-
t.
|
|
6810
|
+
t.hv = null;
|
|
6811
|
+
t.ht = null;
|
|
6611
6812
|
t.px = new Proxy(t, traps);
|
|
6612
6813
|
// Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
|
|
6613
6814
|
// proxy off looked-up targets as a field.
|
|
@@ -6788,13 +6989,6 @@ function getDeepNode(target) {
|
|
|
6788
6989
|
function bumpDeep(t) {
|
|
6789
6990
|
if (t.dk !== null) setSignal(t.dk, 1);
|
|
6790
6991
|
}
|
|
6791
|
-
function markDescendants(target) {
|
|
6792
|
-
let t = target;
|
|
6793
|
-
while (t && !t.d) {
|
|
6794
|
-
t.d = true;
|
|
6795
|
-
t = t.u;
|
|
6796
|
-
}
|
|
6797
|
-
}
|
|
6798
6992
|
// ---------------------------------------------------------------------------
|
|
6799
6993
|
// pending backing + fold (the single mutation point)
|
|
6800
6994
|
/** target → committed backing at batch start (the fold diff's old side). */
|
|
@@ -6819,6 +7013,12 @@ function cloneRaw(source, t) {
|
|
|
6819
7013
|
? Object.defineProperties([], descs)
|
|
6820
7014
|
: Object.create(Object.getPrototypeOf(source), descs);
|
|
6821
7015
|
}
|
|
7016
|
+
/** Scanned plainness for patch admission (patchableRaw): runs the one-time
|
|
7017
|
+
* accessor scan if it hasn't happened yet — the sticky `a` flag alone is not
|
|
7018
|
+
* trustworthy before a scan (it starts false and is discovered lazily). */
|
|
7019
|
+
function targetIsPlain(target) {
|
|
7020
|
+
return target.sc ? !target.a : scanAccessorsOnce(target);
|
|
7021
|
+
}
|
|
6822
7022
|
/** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
|
|
6823
7023
|
* returns true when the container is plain data (overlay-safe). */
|
|
6824
7024
|
function scanAccessorsOnce(target) {
|
|
@@ -6860,6 +7060,7 @@ function materializePB(target) {
|
|
|
6860
7060
|
target.ovl = false;
|
|
6861
7061
|
}
|
|
6862
7062
|
function ensurePB(target) {
|
|
7063
|
+
if (activeTransition !== null) foldBatches.set(target, activeTransition);
|
|
6863
7064
|
let pb = target.pb;
|
|
6864
7065
|
if (pb === null) {
|
|
6865
7066
|
// Prototype-chain overlay (#3044): plain-data non-array containers
|
|
@@ -6903,6 +7104,25 @@ function ensurePB(target) {
|
|
|
6903
7104
|
}
|
|
6904
7105
|
return pb;
|
|
6905
7106
|
}
|
|
7107
|
+
/** Sentinel holder for `t.ht`: a latest()-pull staged this adoption outside
|
|
7108
|
+
* any transition — the hold lasts until the fold commit (drainFolds). */
|
|
7109
|
+
const PLAIN_HOLD = Symbol("plainHold");
|
|
7110
|
+
/** True while a latest() read is pulling the projection computed up to date
|
|
7111
|
+
* (see the get trap): adoptions landing during the pull are speculative
|
|
7112
|
+
* against the un-flushed batch and stage a held view. (Not injectable — the
|
|
7113
|
+
* derived createStore overload retains projection machinery in every store
|
|
7114
|
+
* bundle, see treeshake.test.ts.) */
|
|
7115
|
+
let latestPullActive = false;
|
|
7116
|
+
/** Resolve the held committed view (#3074): answers the masked old backing
|
|
7117
|
+
* while the hold is live, and lazily clears a hold whose transition has
|
|
7118
|
+
* committed (transitions merge — resolve through currentTransition, same as
|
|
7119
|
+
* foldHeld's node stamps). */
|
|
7120
|
+
function heldMaskView(t) {
|
|
7121
|
+
const ht = t.ht;
|
|
7122
|
+
if (ht === null) return null;
|
|
7123
|
+
if (ht !== PLAIN_HOLD && currentTransition(ht)?._done === true) return (t.ht = t.hv = null);
|
|
7124
|
+
return t.hv;
|
|
7125
|
+
}
|
|
6906
7126
|
/**
|
|
6907
7127
|
* Adoption (2026-08-16c): the incoming object becomes the committed backing
|
|
6908
7128
|
* IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
|
|
@@ -6918,6 +7138,20 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
6918
7138
|
if (!eager) {
|
|
6919
7139
|
queueFold(target); // records the pre-batch old before we swap
|
|
6920
7140
|
target.adopted = true;
|
|
7141
|
+
// #3074/#3075: a projection recompute deriving from uncommitted inputs
|
|
7142
|
+
// swaps the backing SPECULATIVELY — committed-visibility readers must
|
|
7143
|
+
// keep the pre-hold view until the hold resolves (a source held by a
|
|
7144
|
+
// live transition, or a latest()-pull ahead of the flush). Post-await
|
|
7145
|
+
// landings (write-override) stay immediately visible — landed truth —
|
|
7146
|
+
// and clear any hold; optimistic families ride the lane machinery.
|
|
7147
|
+
if (target.fam?.opt !== true) {
|
|
7148
|
+
if (getWriteOverride()) {
|
|
7149
|
+
target.ht = target.hv = null;
|
|
7150
|
+
} else if (activeTransition !== null || latestPullActive) {
|
|
7151
|
+
if (heldMaskView(target) === null) target.hv = target.v;
|
|
7152
|
+
target.ht = activeTransition ?? PLAIN_HOLD;
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
6921
7155
|
}
|
|
6922
7156
|
target.pb = null;
|
|
6923
7157
|
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
@@ -6929,24 +7163,43 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
6929
7163
|
// draft rescans once (#3044 audit follow-up).
|
|
6930
7164
|
target.ovl = false;
|
|
6931
7165
|
target.del = null;
|
|
6932
|
-
target.wk = null; // adoption supersedes any staged trap writes
|
|
6933
7166
|
target.sc = false;
|
|
6934
7167
|
target.a = false;
|
|
7168
|
+
if (target.pc !== null) target.pc.wk = null; // adoption supersedes staged trap writes
|
|
6935
7169
|
target.v = incoming;
|
|
6936
7170
|
target.ch = incoming[$TARGET] !== undefined;
|
|
6937
7171
|
(target.fam?.map ?? storeNextLookup).set(incoming, target);
|
|
6938
7172
|
}
|
|
7173
|
+
/** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
|
|
7174
|
+
* array length write implicitly deleted indices) — consumers full-scan. */
|
|
7175
|
+
const WK_ALL = new Set();
|
|
7176
|
+
const plainProto = o => {
|
|
7177
|
+
const p = Object.getPrototypeOf(o);
|
|
7178
|
+
return p === Object.prototype || p === Array.prototype || p === null;
|
|
7179
|
+
};
|
|
6939
7180
|
function queueFold(target) {
|
|
6940
7181
|
if (foldOlds.has(target)) return;
|
|
6941
|
-
if (
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
7182
|
+
if (!hookInstalled) {
|
|
7183
|
+
hookInstalled = true;
|
|
7184
|
+
setStoreCommitHook(drainFolds);
|
|
7185
|
+
}
|
|
7186
|
+
// Always arm — "map non-empty ⇒ drain scheduled" is NOT an invariant: a
|
|
7187
|
+
// held re-queue, or an incomplete-transition flush (which skips
|
|
7188
|
+
// commitPendingNodes entirely), leaves entries behind after `scheduled`
|
|
7189
|
+
// was consumed. A size-gated arm then strands every LATER fold — queued
|
|
7190
|
+
// silently, never drained, committed base frozen at stale state while its
|
|
7191
|
+
// nodes commit (#3089). schedule() early-returns when already armed.
|
|
7192
|
+
schedule();
|
|
6948
7193
|
foldOlds.set(target, target.v);
|
|
6949
7194
|
}
|
|
7195
|
+
/** Fold write-attribution (#3089): a draft written while a transition is
|
|
7196
|
+
* active belongs to that transition — its fold must not commit before the
|
|
7197
|
+
* transition settles. Observed keys already defer through the held check in
|
|
7198
|
+
* drainFolds (their nodes carry _pendingValue); this write-time stamp is the
|
|
7199
|
+
* equivalent hold for UNOBSERVED keys, which have no node to consult.
|
|
7200
|
+
* Refreshed on every write; resolved through currentTransition at drain
|
|
7201
|
+
* (transitions merge — same rule as heldMaskView). */
|
|
7202
|
+
const foldBatches = new WeakMap();
|
|
6950
7203
|
/** Committed-time privatization for parent-chain slot updates (path copying). */
|
|
6951
7204
|
function privatizeCommitted(target) {
|
|
6952
7205
|
if (ownedRaw.has(target.v)) return;
|
|
@@ -6966,7 +7219,27 @@ function drainFolds() {
|
|
|
6966
7219
|
const entries = [...foldOlds];
|
|
6967
7220
|
foldOlds.clear();
|
|
6968
7221
|
for (const [t, old] of entries) {
|
|
7222
|
+
// A latest()-pull staging holds only until the fold commit: this flush
|
|
7223
|
+
// is committing the batch the pull ran ahead of. Transition holds stay —
|
|
7224
|
+
// they clear when their transition is done (heldMaskView).
|
|
7225
|
+
if (t.ht === PLAIN_HOLD) t.ht = t.hv = null;
|
|
7226
|
+
// Eager (write-override) family folds swap pb -> v at notifyWrites'
|
|
7227
|
+
// tail: by the time this drain runs they carry no pb, and their
|
|
7228
|
+
// structural ops must emit at the fold-commit site below (the clone
|
|
7229
|
+
// branch never sees them). Re-audit blocker 4.
|
|
7230
|
+
const foldedEager = t.pb === null;
|
|
6969
7231
|
if (t.pb !== null) {
|
|
7232
|
+
// #3089: a fold written under a still-running transition defers to
|
|
7233
|
+
// that transition's settle (the write-time stamp covers unobserved
|
|
7234
|
+
// keys; observed keys also hit the pending-node held check below).
|
|
7235
|
+
const fb = foldBatches.get(t);
|
|
7236
|
+
if (fb !== undefined) {
|
|
7237
|
+
if (currentTransition(fb)._done === false) {
|
|
7238
|
+
foldOlds.set(t, old);
|
|
7239
|
+
continue;
|
|
7240
|
+
}
|
|
7241
|
+
foldBatches.delete(t);
|
|
7242
|
+
}
|
|
6970
7243
|
// Setter path: nodes were setSignal'd at setter exit (write-time
|
|
6971
7244
|
// notification — transitions/holds ride core machinery). Commit the
|
|
6972
7245
|
// backing only for keys whose nodes have committed; a still-pending
|
|
@@ -6978,9 +7251,14 @@ function drainFolds() {
|
|
|
6978
7251
|
// Only written keys can hold (their nodes took the setSignal); the
|
|
6979
7252
|
// wk bound keeps this O(written) — see notifyWrites. Same fallback
|
|
6980
7253
|
// rules as the notify (WK_ALL / accessors / non-plain prototypes).
|
|
6981
|
-
const wkh = t.wk;
|
|
7254
|
+
const wkh = t.pc !== null ? t.pc.wk : null;
|
|
6982
7255
|
const keys =
|
|
6983
|
-
wkh === null ||
|
|
7256
|
+
wkh === null ||
|
|
7257
|
+
wkh === WK_ALL ||
|
|
7258
|
+
t.a === true ||
|
|
7259
|
+
// Overlay pbs chain to the COMMITTED object (#3044) — plainness is
|
|
7260
|
+
// the committed container's prototype, not the overlay's.
|
|
7261
|
+
!plainProto(t.ovl ? t.v : pb)
|
|
6984
7262
|
? Reflect.ownKeys(nodes)
|
|
6985
7263
|
: wkh;
|
|
6986
7264
|
for (const key of keys) {
|
|
@@ -7018,15 +7296,70 @@ function drainFolds() {
|
|
|
7018
7296
|
(t.fam?.map ?? storeNextLookup).delete(pb);
|
|
7019
7297
|
t.pb = null;
|
|
7020
7298
|
t.ovl = false;
|
|
7021
|
-
t.wk = null; // written-keys window closes with the fold commit
|
|
7299
|
+
if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
|
|
7022
7300
|
} else {
|
|
7301
|
+
// Setter-channel structural ops: a fold that changes an array's shape
|
|
7302
|
+
// (push/splice/permutation through the setter — the reconcile walk
|
|
7303
|
+
// never queues here) is a structural visibility transition for any
|
|
7304
|
+
// registered list driver. Identity-keyed; aligned folds emit nothing.
|
|
7305
|
+
// Family targets defer to their own adoption emission (fam reconcile).
|
|
7306
|
+
// Arrays always fold on this clone branch (overlay is non-array only).
|
|
7307
|
+
// Family setter drafts (writable projection push/splice through the
|
|
7308
|
+
// masked setter) fold on this branch too and the fold IS their
|
|
7309
|
+
// visibility moment — emit unless the structure already rode another
|
|
7310
|
+
// channel: adoption folds (reconcile walk emitted ops) and
|
|
7311
|
+
// optimistic families (lane-timed override channel). Re-audit
|
|
7312
|
+
// blocker 4.
|
|
7313
|
+
if (
|
|
7314
|
+
t.pc !== null &&
|
|
7315
|
+
t.pc.ro !== null &&
|
|
7316
|
+
!t.adopted &&
|
|
7317
|
+
t.fam?.opt !== true &&
|
|
7318
|
+
Array.isArray(pb) &&
|
|
7319
|
+
Array.isArray(t.v)
|
|
7320
|
+
)
|
|
7321
|
+
rowHooks.emitSetterRowOps(t, t.v, pb);
|
|
7023
7322
|
t.v = pb;
|
|
7024
7323
|
t.ch = false; // pb is always a plain clone
|
|
7025
7324
|
t.pb = null;
|
|
7026
|
-
t.wk = null; // written-keys window closes with the fold commit
|
|
7325
|
+
if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
if (t.v === old) {
|
|
7329
|
+
// A no-op adoption (A -> B -> A before flush) still consumed its walk:
|
|
7330
|
+
// clear the flag or every later setter row-op gate (!t.adopted) stays
|
|
7331
|
+
// failed and a driven family list freezes (re-audit 5, P1-1).
|
|
7332
|
+
t.adopted = false;
|
|
7333
|
+
continue;
|
|
7334
|
+
}
|
|
7335
|
+
// Patch channel (fold-commit site): family targets emit HERE — the fold
|
|
7336
|
+
// IS their visibility moment (held folds re-queued above emit when they
|
|
7337
|
+
// actually commit) — and so do PLAIN fold-adopted targets (setter-
|
|
7338
|
+
// returned root replacements, chained-store swaps: adoptions WITHOUT a
|
|
7339
|
+
// reconcile walk, so no walk-site emission ever happened — re-audit 2,
|
|
7340
|
+
// P1-2). Plain eager targets emitted at their walk/setter sites already.
|
|
7341
|
+
if (t.pc !== null && (t.fam !== null || t.adopted)) {
|
|
7342
|
+
// Structural ops for folds whose structure rode no other channel:
|
|
7343
|
+
// eager-folded family SETTER drafts (write-override swaps pb -> v at
|
|
7344
|
+
// notifyWrites' tail — the clone branch never sees them; adoption
|
|
7345
|
+
// folds re-emitting would double the walk's ops) and PLAIN fold
|
|
7346
|
+
// adoptions (no walk at all). Optimistic families ride the override
|
|
7347
|
+
// channel (lane-timed ops + revert RESYNC) — never re-emit here.
|
|
7348
|
+
if (
|
|
7349
|
+
t.pc.ro !== null &&
|
|
7350
|
+
t.fam?.opt !== true &&
|
|
7351
|
+
(t.fam !== null ? foldedEager && !t.adopted : t.adopted) &&
|
|
7352
|
+
Array.isArray(t.v) &&
|
|
7353
|
+
Array.isArray(old)
|
|
7354
|
+
)
|
|
7355
|
+
rowHooks.emitSetterRowOps(t, old, t.v);
|
|
7356
|
+
if (t.pc.p !== null) {
|
|
7357
|
+
// Accessor demotion at the fold-commit seam is DEV-ONLY (see the
|
|
7358
|
+
// reconcile seam note: prod never pays per-adoption scans).
|
|
7359
|
+
if (!targetIsPlain(t)) patchHooks.demoteToEffects(t);
|
|
7360
|
+
else patchHooks.emitPatchLocal(t, t.v, old);
|
|
7027
7361
|
}
|
|
7028
7362
|
}
|
|
7029
|
-
if (t.v === old) continue; // adopted then re-adopted back, or no-op
|
|
7030
7363
|
// Path copying (CAS: see the eager-fold twin above).
|
|
7031
7364
|
if (t.u && t.u.v[t.pk] === old) {
|
|
7032
7365
|
privatizeCommitted(t.u);
|
|
@@ -7047,17 +7380,6 @@ function drainFolds() {
|
|
|
7047
7380
|
* "pending home = the node when a node exists"). Unobserved keys stay in the
|
|
7048
7381
|
* pending backing and fold directly at commit.
|
|
7049
7382
|
*/
|
|
7050
|
-
/** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
|
|
7051
|
-
* array length write implicitly deleted indices) — consumers full-scan. */
|
|
7052
|
-
const WK_ALL = new Set();
|
|
7053
|
-
/** Plain-prototype check for the written-keys bound: prototype getters on
|
|
7054
|
-
* class instances can derive from ANY field, so only plain-data containers
|
|
7055
|
-
* may bound the notify to written keys. Overlay pbs chain to the COMMITTED
|
|
7056
|
-
* object (#3044), so overlay plainness is judged on the committed proto. */
|
|
7057
|
-
const plainProto = o => {
|
|
7058
|
-
const p = Object.getPrototypeOf(o);
|
|
7059
|
-
return p === Object.prototype || p === Array.prototype || p === null;
|
|
7060
|
-
};
|
|
7061
7383
|
function notifyWrites(t) {
|
|
7062
7384
|
let pb = t.pb;
|
|
7063
7385
|
if (pb === null) return;
|
|
@@ -7112,8 +7434,15 @@ function notifyWrites(t) {
|
|
|
7112
7434
|
// not a full scan). Falls back to the full node scan when the bound can't
|
|
7113
7435
|
// hold: no trap granularity (wk null), an array length write (WK_ALL —
|
|
7114
7436
|
// implicit index deletes), accessors on the record (t.a — a getter node's
|
|
7115
|
-
// value can change when ANY key is written), or a non-plain prototype
|
|
7116
|
-
|
|
7437
|
+
// value can change when ANY key is written), or a non-plain prototype
|
|
7438
|
+
// (class instances: prototype getters derive from arbitrary fields).
|
|
7439
|
+
const wk0 = t.pc !== null ? t.pc.wk : null;
|
|
7440
|
+
// Overlay pbs chain to the COMMITTED object (#3044): a prototype-overlay
|
|
7441
|
+
// draft is plain data on its own layer, but its getPrototypeOf is the
|
|
7442
|
+
// committed container — judge plainness by the COMMITTED prototype or the
|
|
7443
|
+
// bound never engages for overlay writes (every plain-object setter batch
|
|
7444
|
+
// would full-scan: the exact selection-map workload wk exists for; jf
|
|
7445
|
+
// `select` regressed 2x on this).
|
|
7117
7446
|
const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
|
|
7118
7447
|
if (nodes !== null) {
|
|
7119
7448
|
const keys = writtenKeys ?? Reflect.ownKeys(nodes);
|
|
@@ -7146,15 +7475,18 @@ function notifyWrites(t) {
|
|
|
7146
7475
|
}
|
|
7147
7476
|
const has = t.h;
|
|
7148
7477
|
if (has !== null) {
|
|
7149
|
-
|
|
7150
|
-
|
|
7478
|
+
const keys = writtenKeys ?? Reflect.ownKeys(has);
|
|
7479
|
+
for (const key of keys) {
|
|
7480
|
+
const node = has[key];
|
|
7481
|
+
if (node !== undefined) setSignal(node, key in pb && !(t.del !== null && t.del.has(key)));
|
|
7482
|
+
}
|
|
7151
7483
|
}
|
|
7152
7484
|
// Deep-witness (dk): setter writes must notify a deep() subscriber even on
|
|
7153
|
-
// keys with no node. O(pb keys) equality only when a witness exists.
|
|
7485
|
+
// keys with no node. O(written/pb keys) equality only when a witness exists.
|
|
7154
7486
|
if (t.dk !== null) {
|
|
7155
7487
|
if (t.del !== null && t.del.size !== 0) bumpDeep(t);
|
|
7156
7488
|
else
|
|
7157
|
-
for (const key of Reflect.ownKeys(pb)) {
|
|
7489
|
+
for (const key of writtenKeys ?? Reflect.ownKeys(pb)) {
|
|
7158
7490
|
const nv = pb[key];
|
|
7159
7491
|
const ov = old[key];
|
|
7160
7492
|
if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
|
|
@@ -7184,6 +7516,13 @@ function notifyWrites(t) {
|
|
|
7184
7516
|
}
|
|
7185
7517
|
if (changed) setSignal(t.k, v => v + 1);
|
|
7186
7518
|
}
|
|
7519
|
+
// Patch channel (setter site): a committed write transitions this record —
|
|
7520
|
+
// queue its patches and bubble to ancestors (targeted nested writes must
|
|
7521
|
+
// reach the row patch, §4b). One number compare when no patches exist.
|
|
7522
|
+
// Family targets skip this site: their visibility moment is the FOLD
|
|
7523
|
+
// commit (drainFolds emits), not the recompute/draft write.
|
|
7524
|
+
if (t.fam === null && patchHooks !== null && patchHooks.hasPatches())
|
|
7525
|
+
patchHooks.emitPatch(t, pb, old);
|
|
7187
7526
|
// Projection backing folds split by channel (two pinned contracts):
|
|
7188
7527
|
// - sync-derive drafts (recompute body): NEVER eager — a downstream async
|
|
7189
7528
|
// hold can form LATER in the same flush and the leaf must stay at stale
|
|
@@ -7195,9 +7534,11 @@ function notifyWrites(t) {
|
|
|
7195
7534
|
// downstream consumer's own async still holds the effect-level reveal
|
|
7196
7535
|
// (spec-async "verdicts never inherit consumers' in-flight state").
|
|
7197
7536
|
if (t.fam !== null && t.pb !== null && getWriteOverride()) {
|
|
7537
|
+
// Landed truth (post-await write-override): immediately visible to every
|
|
7538
|
+
// reader — any staged held view is superseded.
|
|
7539
|
+
if (t.ht !== null) t.ht = t.hv = null;
|
|
7198
7540
|
const oldBacking = t.v;
|
|
7199
7541
|
t.pb = null;
|
|
7200
|
-
t.wk = null; // written-keys window closes with the eager fold
|
|
7201
7542
|
t.v = pb;
|
|
7202
7543
|
t.ch = false;
|
|
7203
7544
|
if (t.u && t.u.v[t.pk] === oldBacking) {
|
|
@@ -7402,6 +7743,17 @@ function inOwnerContext() {
|
|
|
7402
7743
|
const eff = c._root ? c._parentComputed : c;
|
|
7403
7744
|
return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
7404
7745
|
}
|
|
7746
|
+
/** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
|
|
7747
|
+
* callbacks). Distinct from context-free: these scopes get committed
|
|
7748
|
+
* visibility even against a projection's authoritative-elect pending
|
|
7749
|
+
* backing (#3082) — parity with signals, where core read() serves
|
|
7750
|
+
* committed to them regardless of staged writes. */
|
|
7751
|
+
function inForbiddenScope() {
|
|
7752
|
+
const c = getOwner();
|
|
7753
|
+
if (c === null) return false;
|
|
7754
|
+
const eff = c._root ? c._parentComputed : c;
|
|
7755
|
+
return eff != null && !!(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
7756
|
+
}
|
|
7405
7757
|
/** A pending fold is transition-held when any written node's parked value is
|
|
7406
7758
|
* stamped by a live transition (a plain batch parking — the lazy-recompute
|
|
7407
7759
|
* read case — has no transition stamp and serves fresh). */
|
|
@@ -7420,6 +7772,20 @@ function foldHeld(target) {
|
|
|
7420
7772
|
return false;
|
|
7421
7773
|
}
|
|
7422
7774
|
function readSource(target) {
|
|
7775
|
+
// Held view first (#3074): an adoption staged under a live hold serves the
|
|
7776
|
+
// pre-hold committed backing to committed-visibility readers. Speculative
|
|
7777
|
+
// readers — drafts, write-override, owner-context computeds recomputing
|
|
7778
|
+
// inside the transaction, and latest() reads — see the adopted backing.
|
|
7779
|
+
if (
|
|
7780
|
+
target.ht !== null &&
|
|
7781
|
+
!latestReadActive &&
|
|
7782
|
+
!inDraft(target) &&
|
|
7783
|
+
!getWriteOverride() &&
|
|
7784
|
+
!inOwnerContext()
|
|
7785
|
+
) {
|
|
7786
|
+
const hv = heldMaskView(target);
|
|
7787
|
+
if (hv !== null) return hv;
|
|
7788
|
+
}
|
|
7423
7789
|
// Signal-parity visibility (core read(): owner-context reads serve
|
|
7424
7790
|
// _pendingValue, context-free reads serve committed — effects recompute
|
|
7425
7791
|
// BEFORE commitPendingNodes in the flush, so the pending view must be
|
|
@@ -7433,8 +7799,10 @@ function readSource(target) {
|
|
|
7433
7799
|
inOwnerContext() ||
|
|
7434
7800
|
// A projection's pending backing is authoritative-elect: serve it to
|
|
7435
7801
|
// context-free readers too UNLESS a transition is holding the node
|
|
7436
|
-
// commits (downstream async hold — stale committed is the contract)
|
|
7437
|
-
|
|
7802
|
+
// commits (downstream async hold — stale committed is the contract)
|
|
7803
|
+
// or the reader is a CHILDREN_FORBIDDEN scope, which never observes
|
|
7804
|
+
// its own unsettled write (#3082, signal parity per #3006).
|
|
7805
|
+
(target.fam !== null && !foldHeld(target) && !inForbiddenScope()))
|
|
7438
7806
|
)
|
|
7439
7807
|
return target.pb;
|
|
7440
7808
|
return target.v;
|
|
@@ -7478,9 +7846,11 @@ function hasActiveOverride(node) {
|
|
|
7478
7846
|
* FORCE sentinels never surface (they only bump subscribers of accessor
|
|
7479
7847
|
* keys, which are served by the trap, not the node). */
|
|
7480
7848
|
function nodeValue(node, backing) {
|
|
7849
|
+
// latest() sees the in-flight parked value like an owner-context reader
|
|
7850
|
+
// does (#3075) — signal/memo parity for store-node-backed keys.
|
|
7481
7851
|
const v = hasActiveOverride(node)
|
|
7482
7852
|
? unwrapOverride(node._x?._overrideValue)
|
|
7483
|
-
: node._pendingValue !== NOT_PENDING && inOwnerContext()
|
|
7853
|
+
: node._pendingValue !== NOT_PENDING && (latestReadActive || inOwnerContext())
|
|
7484
7854
|
? node._pendingValue
|
|
7485
7855
|
: backing;
|
|
7486
7856
|
return v === FORCE ? backing : v;
|
|
@@ -7571,6 +7941,27 @@ function firewallGate(target) {
|
|
|
7571
7941
|
const fw = target.fam?.node;
|
|
7572
7942
|
if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
|
|
7573
7943
|
}
|
|
7944
|
+
/** latest() pull (#3075): bring the projection computed up to date so the
|
|
7945
|
+
* read serves the IN-FLIGHT derivation — signal/memo parity, where core
|
|
7946
|
+
* read() routes latest() through a companion that recomputes speculatively.
|
|
7947
|
+
* The latest flag is suspended for the recompute (the derive's own reads
|
|
7948
|
+
* are normal reads), and latestPullActive marks any adoption it commits as
|
|
7949
|
+
* staged (see adoptPB) — the speculative swap must not leak to
|
|
7950
|
+
* committed-visibility readers before the flush. */
|
|
7951
|
+
function pullProjectionForLatest(target) {
|
|
7952
|
+
const fw = target.fam.node;
|
|
7953
|
+
if (fw == null) return;
|
|
7954
|
+
const prevLatest = latestReadActive;
|
|
7955
|
+
setLatestReadActive(false);
|
|
7956
|
+
const prevPull = latestPullActive;
|
|
7957
|
+
latestPullActive = true;
|
|
7958
|
+
try {
|
|
7959
|
+
prepareComputed(fw, true);
|
|
7960
|
+
} finally {
|
|
7961
|
+
latestPullActive = prevPull;
|
|
7962
|
+
setLatestReadActive(prevLatest);
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7574
7965
|
const traps = {
|
|
7575
7966
|
get(target, key, receiver) {
|
|
7576
7967
|
// One typeof gates every brand-symbol compare off the hot string path
|
|
@@ -7598,6 +7989,11 @@ const traps = {
|
|
|
7598
7989
|
}
|
|
7599
7990
|
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
7600
7991
|
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
7992
|
+
// latest() pull (#3075): store traps never reach core read() without an
|
|
7993
|
+
// observer, so bring the projection computed up to date here — signal/
|
|
7994
|
+
// memo parity for latest() reads through a projection.
|
|
7995
|
+
if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
|
|
7996
|
+
pullProjectionForLatest(target);
|
|
7601
7997
|
const src = readSource(target);
|
|
7602
7998
|
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
7603
7999
|
// deleted keys are tracked aside and read as absent in the pending view.
|
|
@@ -7821,14 +8217,15 @@ const traps = {
|
|
|
7821
8217
|
// Array length writes implicitly delete indices — the written-keys bound
|
|
7822
8218
|
// can't see them, so poison to the full scan for this batch. Index
|
|
7823
8219
|
// writes implicitly GROW length, so arrays always record it alongside.
|
|
8220
|
+
const pcs = pcOf(target);
|
|
7824
8221
|
if (Array.isArray(pb)) {
|
|
7825
|
-
if (key === "length")
|
|
7826
|
-
else if (
|
|
7827
|
-
const wk = (
|
|
8222
|
+
if (key === "length") pcs.wk = WK_ALL;
|
|
8223
|
+
else if (pcs.wk !== WK_ALL) {
|
|
8224
|
+
const wk = (pcs.wk ??= new Set());
|
|
7828
8225
|
wk.add(key);
|
|
7829
8226
|
wk.add("length");
|
|
7830
8227
|
}
|
|
7831
|
-
} else if (
|
|
8228
|
+
} else if (pcs.wk !== WK_ALL) (pcs.wk ??= new Set()).add(key);
|
|
7832
8229
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
7833
8230
|
// defineProperty sidesteps a proto-chain setter named the same.
|
|
7834
8231
|
if (UNSAFE_KEYS.has(key)) {
|
|
@@ -7866,12 +8263,20 @@ const traps = {
|
|
|
7866
8263
|
const override = !draft && getWriteOverride();
|
|
7867
8264
|
if (!draft && !override) return true;
|
|
7868
8265
|
if (key === "__proto__") return true;
|
|
7869
|
-
if (desc.get || desc.set)
|
|
8266
|
+
if (desc.get || desc.set) {
|
|
8267
|
+
target.a = true;
|
|
8268
|
+
// Accessor demotion (re-audit blocker 3): a record that acquires an
|
|
8269
|
+
// accessor after patch registration stops being patchable — pull its
|
|
8270
|
+
// patches and re-drive them as tracked effect fallbacks. Hooks are
|
|
8271
|
+
// installed whenever pc.p exists (registration installs them).
|
|
8272
|
+
if (target.pc !== null && target.pc.p !== null) patchHooks.demoteToEffects(target);
|
|
8273
|
+
}
|
|
7870
8274
|
// Unwrap before ensurePB (see the set trap: self-reference materializes).
|
|
7871
8275
|
if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
|
|
7872
8276
|
const pb = ensurePB(target);
|
|
7873
8277
|
pendingNotify.add(target);
|
|
7874
|
-
|
|
8278
|
+
const pcd = pcOf(target);
|
|
8279
|
+
if (pcd.wk !== WK_ALL) (pcd.wk ??= new Set()).add(key);
|
|
7875
8280
|
Object.defineProperty(pb, key, desc);
|
|
7876
8281
|
if (target.del !== null) target.del.delete(key);
|
|
7877
8282
|
if (override) notifyWrites(target);
|
|
@@ -7883,7 +8288,8 @@ const traps = {
|
|
|
7883
8288
|
if (!draft && !override) return true;
|
|
7884
8289
|
const pb = ensurePB(target);
|
|
7885
8290
|
pendingNotify.add(target);
|
|
7886
|
-
|
|
8291
|
+
const pcx = pcOf(target);
|
|
8292
|
+
if (pcx.wk !== WK_ALL) (pcx.wk ??= new Set()).add(key);
|
|
7887
8293
|
delete pb[key];
|
|
7888
8294
|
// A prototype overlay cannot shadow a delete of a committed key —
|
|
7889
8295
|
// record it aside (#3044); reads/has/ownKeys/commit consult the set.
|
|
@@ -7956,8 +8362,39 @@ function createStoreNext(init, shallow = false) {
|
|
|
7956
8362
|
const setter = fn => storeSetterNext(proxy, fn);
|
|
7957
8363
|
return [proxy, setter];
|
|
7958
8364
|
}
|
|
8365
|
+
/** True when `proxy` is a SHALLOW store (children served verbatim, slots
|
|
8366
|
+
* replaced by reference — #2932). The list driver uses this to choose the
|
|
8367
|
+
* slot-patch channel (collected row bodies) over per-record registration. */
|
|
8368
|
+
function storeIsShallow(proxy) {
|
|
8369
|
+
const t = proxy?.[$TARGET];
|
|
8370
|
+
return t !== undefined && t.s === true;
|
|
8371
|
+
}
|
|
8372
|
+
/** True when `proxy` belongs to a projection/optimistic FAMILY. The list
|
|
8373
|
+
* driver must DECLINE family arrays (external audit finding): family
|
|
8374
|
+
* structural changes never emit row/slot ops (the setter channel is
|
|
8375
|
+
* fam-gated; optimistic writes ride node overrides), and the proxy identity
|
|
8376
|
+
* is stable so the each-watch cannot catch the change either — an engaged
|
|
8377
|
+
* list would freeze on optimistic/projection structural updates. Record-
|
|
8378
|
+
* level family patches are unaffected (they have their own emission). */
|
|
8379
|
+
function storeHasFamily(proxy) {
|
|
8380
|
+
const t = proxy?.[$TARGET];
|
|
8381
|
+
return t !== undefined && t.fam !== null;
|
|
8382
|
+
}
|
|
8383
|
+
/** True when `proxy` belongs to an OPTIMISTIC family specifically. The list
|
|
8384
|
+
* driver declines these (audit finding, narrowed): optimistic user writes
|
|
8385
|
+
* ride node-level overrides — they never enter the reconcile walk, so no
|
|
8386
|
+
* row/slot ops are emitted and an engaged list would freeze on optimistic
|
|
8387
|
+
* structural changes. PROJECTION (non-optimistic) families are drivable:
|
|
8388
|
+
* their recomputes go through the reconcile walk, whose emissions are
|
|
8389
|
+
* transition-stamped in the apply queue like any other (equivalence-matrix
|
|
8390
|
+
* gated). Re-admitting optimistic families requires a lane-timed structural
|
|
8391
|
+
* emission mirroring emitPatchOptimistic, plus revert resync. */
|
|
8392
|
+
function storeHasOptimisticFamily(proxy) {
|
|
8393
|
+
const t = proxy?.[$TARGET];
|
|
8394
|
+
return t !== undefined && t.fam?.opt === true;
|
|
8395
|
+
}
|
|
7959
8396
|
/** Tracking deep snapshot (`deep()` for next targets): subscribes to the
|
|
7960
|
-
* key-set and
|
|
8397
|
+
* key-set and deep-witness node at every reachable level, then returns the
|
|
7961
8398
|
* plain view. Shared references and cycles handled via the visited set. */
|
|
7962
8399
|
function deepNext(value) {
|
|
7963
8400
|
const t0 = value?.[$TARGET];
|
|
@@ -8157,7 +8594,7 @@ function reconcileNextState(value, state, key, replace = false) {
|
|
|
8157
8594
|
// positional so old-entity subtrees never merge into the new entity's).
|
|
8158
8595
|
const prev = t.pb ?? t.v;
|
|
8159
8596
|
const eq = keyFn(prev);
|
|
8160
|
-
if (eq !== undefined && keyFn(incoming)
|
|
8597
|
+
if (eq !== undefined && !sameKey(keyFn(incoming), eq)) {
|
|
8161
8598
|
if (!replace) throw new Error("Cannot reconcile states with different identity");
|
|
8162
8599
|
// Entity change: wholesale swap. The root proxy is stable for life
|
|
8163
8600
|
// (proj R5) but NOTHING below survives — children are never matched
|
|
@@ -8198,6 +8635,33 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8198
8635
|
const shallow = t.s === true;
|
|
8199
8636
|
const old = t.v;
|
|
8200
8637
|
adoptPB(t, incoming, eager);
|
|
8638
|
+
// Patch channel (adoption site): this record transitioned — queue its
|
|
8639
|
+
// patches with the pre-adopt prev. No bubbling walk: the adoption walk
|
|
8640
|
+
// visits parents before children, so ancestors emitted already. EAGER
|
|
8641
|
+
// only — family targets' visibility moment is their fold commit
|
|
8642
|
+
// (drainFolds emits there; emitting here too would double-fire).
|
|
8643
|
+
if (patchHooks !== null && eager && t.pc !== null && t.pc.p !== null) {
|
|
8644
|
+
// Accessor demotion at the ADOPTION seam is DEV-ONLY (prod principle:
|
|
8645
|
+
// explicitly-odd input must not cost correct-input prod — the
|
|
8646
|
+
// per-adoption scan was ~12% of dbmon's tick since adoptPB resets the
|
|
8647
|
+
// verdict every adoption). Dev demotes AND warns; prod emits directly,
|
|
8648
|
+
// so a getter adoptee's OUTSIDE deps (signals) won't re-apply in prod —
|
|
8649
|
+
// caught loudly during development instead. Registration-time admission
|
|
8650
|
+
// (patchableRaw) keeps its full one-time scan in both modes.
|
|
8651
|
+
if (!targetIsPlain(t)) {
|
|
8652
|
+
console.warn(
|
|
8653
|
+
"A reconcile adopted an object with own getters into a record that " +
|
|
8654
|
+
"carries compiled patches. Patches read raw values and will not " +
|
|
8655
|
+
"track the getters' reactive dependencies — this record's patches " +
|
|
8656
|
+
"are demoted to effects in development, but production will NOT " +
|
|
8657
|
+
"demote. Avoid getters on patched records, or key them out of " +
|
|
8658
|
+
"patch-eligible templates."
|
|
8659
|
+
);
|
|
8660
|
+
patchHooks.demoteToEffects(t);
|
|
8661
|
+
} else {
|
|
8662
|
+
patchHooks.emitPatchLocal(t, incoming, old);
|
|
8663
|
+
}
|
|
8664
|
+
}
|
|
8201
8665
|
// Shallow adoption: records are slot values — sticky raw-mark the incoming
|
|
8202
8666
|
// set (R41) and never descend; slot notification is the positional diff.
|
|
8203
8667
|
if (shallow) markRawIngest(incoming);
|
|
@@ -8237,7 +8701,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8237
8701
|
typeof pvRaw === "object" &&
|
|
8238
8702
|
nv !== null &&
|
|
8239
8703
|
typeof nv === "object" &&
|
|
8240
|
-
keyFn(pvRaw)
|
|
8704
|
+
sameKey(keyFn(pvRaw), keyFn(nv))
|
|
8241
8705
|
)
|
|
8242
8706
|
)
|
|
8243
8707
|
break; // misaligned: fall to the keyed remainder below
|
|
@@ -8265,6 +8729,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8265
8729
|
}
|
|
8266
8730
|
}
|
|
8267
8731
|
if (t.dk !== null && !dkBumpedA && i < nextRows.length) bumpDeep(t);
|
|
8732
|
+
const structStart = i; // misalignment point (== nlen on aligned ticks)
|
|
8268
8733
|
let prevByKey = null;
|
|
8269
8734
|
for (; i < nextRows.length; i++) {
|
|
8270
8735
|
const nv = nextRows[i];
|
|
@@ -8274,16 +8739,39 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8274
8739
|
let pv;
|
|
8275
8740
|
if (nk !== undefined) {
|
|
8276
8741
|
if (prevByKey === null) {
|
|
8742
|
+
// Occurrence-aware (re-audit 2, P1-5): duplicate keys queue
|
|
8743
|
+
// their prev INDICES (rows can themselves be arrays, so index
|
|
8744
|
+
// queues are the unambiguous encoding — same as buildRowOps)
|
|
8745
|
+
// and each is consumed ONCE. First-wins would adopt two next
|
|
8746
|
+
// rows into the SAME prev target while row ops retain two
|
|
8747
|
+
// separate DOM rows (the second one stale).
|
|
8277
8748
|
prevByKey = new Map();
|
|
8278
|
-
|
|
8749
|
+
// From structStart, not 0 (re-audit 3, P1-2): prefix-aligned
|
|
8750
|
+
// rows already adopted their incoming counterparts — re-offering
|
|
8751
|
+
// them here let a duplicate key adopt a prefix row AGAIN while
|
|
8752
|
+
// row ops (which correctly window from structStart) retained
|
|
8753
|
+
// the later occurrence's DOM row against a never-adopted target.
|
|
8754
|
+
for (let j = structStart; j < prevRows.length; j++) {
|
|
8279
8755
|
const p = unwrapValue(prevRows[j]);
|
|
8280
8756
|
if (p !== null && typeof p === "object") {
|
|
8281
8757
|
const pk = keyFn(p);
|
|
8282
|
-
if (pk
|
|
8758
|
+
if (pk === undefined) continue;
|
|
8759
|
+
const existing = prevByKey.get(pk);
|
|
8760
|
+
if (existing === undefined) prevByKey.set(pk, j);
|
|
8761
|
+
else if (Array.isArray(existing)) existing.push(j);
|
|
8762
|
+
else prevByKey.set(pk, [existing, j]);
|
|
8283
8763
|
}
|
|
8284
8764
|
}
|
|
8285
8765
|
}
|
|
8286
|
-
|
|
8766
|
+
const m = prevByKey.get(nk);
|
|
8767
|
+
if (m === undefined) pv = undefined;
|
|
8768
|
+
else if (Array.isArray(m)) {
|
|
8769
|
+
pv = unwrapValue(prevRows[m.shift()]);
|
|
8770
|
+
if (m.length === 1) prevByKey.set(nk, m[0]);
|
|
8771
|
+
} else {
|
|
8772
|
+
pv = unwrapValue(prevRows[m]);
|
|
8773
|
+
prevByKey.delete(nk);
|
|
8774
|
+
}
|
|
8287
8775
|
} else {
|
|
8288
8776
|
pv = unwrapValue(prevRows[i]); // keyless item: positional fallback
|
|
8289
8777
|
}
|
|
@@ -8297,12 +8785,62 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8297
8785
|
}
|
|
8298
8786
|
}
|
|
8299
8787
|
}
|
|
8788
|
+
// Row ops (PR-B): emit structural ops ONLY when structure changed —
|
|
8789
|
+
// aligned value ticks pay nothing. Built after the walk so retained
|
|
8790
|
+
// rows' value patches queue first (adds bind at op-apply).
|
|
8791
|
+
if (
|
|
8792
|
+
rowHooks !== null &&
|
|
8793
|
+
t.pc !== null &&
|
|
8794
|
+
t.pc.ro !== null &&
|
|
8795
|
+
(structStart < nlen || plen !== nlen)
|
|
8796
|
+
)
|
|
8797
|
+
buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn);
|
|
8300
8798
|
} else {
|
|
8301
8799
|
const dlen = Math.min(prevRows.length, nextRows.length);
|
|
8302
8800
|
const nlen = nextRows.length;
|
|
8303
8801
|
let dkBumpedP = false;
|
|
8802
|
+
const sp = rowHooks !== null && t.pc !== null ? t.pc.sp : null;
|
|
8803
|
+
// Row ops for shallow/positional lists: track the key-aligned prefix
|
|
8804
|
+
// (keyed) so aligned value ticks emit nothing; keyless lists emit only
|
|
8805
|
+
// on length change (append/truncate). Slot-patch consumers need the
|
|
8806
|
+
// alignment tracking too (aligned = value tick, misaligned = ops).
|
|
8807
|
+
const ro = rowHooks !== null && t.pc !== null ? t.pc.ro : null;
|
|
8808
|
+
let keyAligned = keyFn !== null && (ro !== null || sp !== null);
|
|
8809
|
+
let keyPrefix = 0;
|
|
8304
8810
|
for (let i = 0; i < nlen; i++) {
|
|
8305
8811
|
const nvP = nextRows[i];
|
|
8812
|
+
if (keyAligned && i < dlen) {
|
|
8813
|
+
const pvK = prevRows[i];
|
|
8814
|
+
if (
|
|
8815
|
+
pvK !== null &&
|
|
8816
|
+
typeof pvK === "object" &&
|
|
8817
|
+
nvP !== null &&
|
|
8818
|
+
typeof nvP === "object" &&
|
|
8819
|
+
// SameValueZero (self-sweep): strict === here broke slot
|
|
8820
|
+
// alignment on NaN keys while buildRowOps retained the row —
|
|
8821
|
+
// retained DOM with suppressed value ticks (the round-1 NaN
|
|
8822
|
+
// staleness, in the shallow branch).
|
|
8823
|
+
sameKey(keyFn(pvK), keyFn(nvP))
|
|
8824
|
+
)
|
|
8825
|
+
keyPrefix++;
|
|
8826
|
+
else keyAligned = false;
|
|
8827
|
+
}
|
|
8828
|
+
// Slot-patch dispatch (shallow): a KEY-ALIGNED slot whose value was
|
|
8829
|
+
// replaced by reference is a value tick — emit through the queue.
|
|
8830
|
+
// Misaligned/appended slots are STRUCTURE (row ops rebuild or move
|
|
8831
|
+
// them; new rows initial-apply at bind), so they emit nothing here.
|
|
8832
|
+
// Keyless positional lists treat same-index replacement as the value
|
|
8833
|
+
// tick for indices below the common length.
|
|
8834
|
+
// `i < dlen` is load-bearing for BOTH modes: an appended position
|
|
8835
|
+
// past a fully-aligned prefix (vacuously aligned when prev is empty)
|
|
8836
|
+
// has no previous slot — emitting a slot tick for it races the row
|
|
8837
|
+
// ops that CREATE the row (the slot queue applies first, indexing a
|
|
8838
|
+
// row that does not exist yet). Equivalence-matrix finding:
|
|
8839
|
+
// clear-then-refill and pure appends crashed the driver.
|
|
8840
|
+
if (sp !== null && i < dlen && (keyFn === null || keyAligned)) {
|
|
8841
|
+
const pvS = prevRows[i];
|
|
8842
|
+
if (pvS !== nvP) rowHooks.emitSlotPatch(t, i, nvP, pvS);
|
|
8843
|
+
}
|
|
8306
8844
|
if (!shallow && i < dlen && nvP !== null && typeof nvP === "object")
|
|
8307
8845
|
descend(unwrapValue(prevRows[i]), nvP, keyFn, fam, proj);
|
|
8308
8846
|
if (
|
|
@@ -8323,6 +8861,15 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8323
8861
|
}
|
|
8324
8862
|
}
|
|
8325
8863
|
}
|
|
8864
|
+
if (ro !== null) {
|
|
8865
|
+
const plen = prevRows.length;
|
|
8866
|
+
if (keyFn !== null) {
|
|
8867
|
+
if (keyPrefix < nlen || plen !== nlen)
|
|
8868
|
+
buildAndEmitRowOps(t, prevRows, nextRows, keyPrefix, keyFn);
|
|
8869
|
+
} else if (plen !== nlen) {
|
|
8870
|
+
buildAndEmitRowOps(t, prevRows, nextRows, dlen, null);
|
|
8871
|
+
}
|
|
8872
|
+
}
|
|
8326
8873
|
}
|
|
8327
8874
|
if (eager) {
|
|
8328
8875
|
if (nodes !== null && nodesHit < t.nc) {
|
|
@@ -8343,6 +8890,22 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8343
8890
|
// slots must not notify, R9). This replaces the notifyFold re-walk that
|
|
8344
8891
|
// doubled dbmon's diff cost. for-in covers own enumerable string keys
|
|
8345
8892
|
// with no key-array allocation; symbols get a pass only when present.
|
|
8893
|
+
// PROTOTYPE compiled-patch fast path: a pure-patch record (no nodes,
|
|
8894
|
+
// no presence/key-set/deep subscribers, no family) adopts and hands the
|
|
8895
|
+
// (next, prev) pair to its compiled patch — no per-key walk at all.
|
|
8896
|
+
if (
|
|
8897
|
+
t.pc !== null &&
|
|
8898
|
+
t.pc.p !== null &&
|
|
8899
|
+
eager &&
|
|
8900
|
+
t.n === null &&
|
|
8901
|
+
t.h === null &&
|
|
8902
|
+
t.k === null &&
|
|
8903
|
+
t.dk === null &&
|
|
8904
|
+
fam === null
|
|
8905
|
+
) {
|
|
8906
|
+
// Adoption already ran at applyAdopt entry; emission was queued there.
|
|
8907
|
+
return;
|
|
8908
|
+
}
|
|
8346
8909
|
const nodes = eager ? t.n : null;
|
|
8347
8910
|
let nodesHit = 0;
|
|
8348
8911
|
let dkBumped = false;
|
|
@@ -8408,6 +8971,93 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8408
8971
|
}
|
|
8409
8972
|
}
|
|
8410
8973
|
const hasOwnP = Object.prototype.hasOwnProperty;
|
|
8974
|
+
/** Setter-channel row ops (the fold site calls this for array targets with
|
|
8975
|
+
* ops consumers): structural mutation through the setter — push/splice/index
|
|
8976
|
+
* assignment/permutation — is a visibility transition for the list container
|
|
8977
|
+
* just like a reconcile walk, and drivers consuming registerRowOps must see
|
|
8978
|
+
* it. Setter mutations move the SAME row objects around, so RAW IDENTITY is
|
|
8979
|
+
* the key. Aligned arrays (value-only folds) emit nothing. */
|
|
8980
|
+
const identityKey = r => unwrapValue(r);
|
|
8981
|
+
/** Key equality for EVERY key comparison in this module (re-audit 2, P1-5):
|
|
8982
|
+
* SameValueZero, matching the Map-based matchers (buildRowOps, the adoption
|
|
8983
|
+
* window) — NaN keys are equal to themselves, so aligned NaN rows stay
|
|
8984
|
+
* aligned in the prefix walk instead of forever misaligning. Adoption and
|
|
8985
|
+
* row ops MUST agree on key equality or retained DOM rows go stale. */
|
|
8986
|
+
function sameKey(a, b) {
|
|
8987
|
+
return a === b || (a !== a && b !== b);
|
|
8988
|
+
}
|
|
8989
|
+
function emitSetterRowOps(t, prevRows, nextRows) {
|
|
8990
|
+
const ops = buildIdentityRowOps(prevRows, nextRows);
|
|
8991
|
+
if (ops !== null) rowHooks.emitRowOps(t, nextRows, ops);
|
|
8992
|
+
}
|
|
8993
|
+
/** Identity-keyed structural diff, returned rather than emitted: shared by
|
|
8994
|
+
* the setter channel (regular queue) and the OPTIMISTIC write channel (lane
|
|
8995
|
+
* queue) — same retention semantics, different dispatch timing. Returns
|
|
8996
|
+
* null when the lists are identity-aligned (no structure changed). */
|
|
8997
|
+
function buildIdentityRowOps(prevRows, nextRows) {
|
|
8998
|
+
let p = 0;
|
|
8999
|
+
const min = prevRows.length < nextRows.length ? prevRows.length : nextRows.length;
|
|
9000
|
+
while (p < min && unwrapValue(prevRows[p]) === unwrapValue(nextRows[p])) p++;
|
|
9001
|
+
if (p === prevRows.length && p === nextRows.length) return null;
|
|
9002
|
+
return buildRowOps(prevRows, nextRows, p, identityKey);
|
|
9003
|
+
}
|
|
9004
|
+
/** Shared row-ops builder (keyed deep branch + shallow/positional branch):
|
|
9005
|
+
* key-matches the misaligned window into { prefix, sources, removed }.
|
|
9006
|
+
* `keyFn === null` degrades to positional ops (append/truncate only). */
|
|
9007
|
+
function buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn) {
|
|
9008
|
+
rowHooks.emitRowOps(t, nextRows, buildRowOps(prevRows, nextRows, structStart, keyFn));
|
|
9009
|
+
}
|
|
9010
|
+
function buildRowOps(prevRows, nextRows, structStart, keyFn) {
|
|
9011
|
+
const plen = prevRows.length;
|
|
9012
|
+
const nlen = nextRows.length;
|
|
9013
|
+
const sources = new Array(nlen - structStart);
|
|
9014
|
+
// Occurrence-aware matching (re-audit): duplicate keys queue their old
|
|
9015
|
+
// indices and each is consumed ONCE — first-wins reuse would hand the same
|
|
9016
|
+
// source (and its one DOM row) to multiple next positions. The no-dup fast
|
|
9017
|
+
// shape stays a bare number; collisions upgrade to a queue.
|
|
9018
|
+
let oldIndexByKey = null;
|
|
9019
|
+
if (keyFn !== null && structStart < plen) {
|
|
9020
|
+
oldIndexByKey = new Map();
|
|
9021
|
+
for (let j = structStart; j < plen; j++) {
|
|
9022
|
+
const p = unwrapValue(prevRows[j]);
|
|
9023
|
+
if (p !== null && typeof p === "object") {
|
|
9024
|
+
const pk = keyFn(p);
|
|
9025
|
+
if (pk === undefined) continue;
|
|
9026
|
+
const existing = oldIndexByKey.get(pk);
|
|
9027
|
+
if (existing === undefined) oldIndexByKey.set(pk, j);
|
|
9028
|
+
else if (Array.isArray(existing)) existing.push(j);
|
|
9029
|
+
else oldIndexByKey.set(pk, [existing, j]);
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
}
|
|
9033
|
+
const consumed = oldIndexByKey !== null ? new Set() : null;
|
|
9034
|
+
for (let k = structStart; k < nlen; k++) {
|
|
9035
|
+
const nv = nextRows[k];
|
|
9036
|
+
let oldIdx = -1;
|
|
9037
|
+
if (nv !== null && typeof nv === "object" && oldIndexByKey !== null) {
|
|
9038
|
+
const nk = keyFn(nv);
|
|
9039
|
+
if (nk !== undefined) {
|
|
9040
|
+
const m = oldIndexByKey.get(nk);
|
|
9041
|
+
if (m !== undefined) {
|
|
9042
|
+
if (Array.isArray(m)) {
|
|
9043
|
+
oldIdx = m.shift();
|
|
9044
|
+
if (m.length === 1) oldIndexByKey.set(nk, m[0]);
|
|
9045
|
+
} else {
|
|
9046
|
+
oldIdx = m;
|
|
9047
|
+
oldIndexByKey.delete(nk);
|
|
9048
|
+
}
|
|
9049
|
+
consumed.add(oldIdx);
|
|
9050
|
+
}
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
9053
|
+
sources[k - structStart] = oldIdx;
|
|
9054
|
+
}
|
|
9055
|
+
const removed = [];
|
|
9056
|
+
for (let j = structStart; j < plen; j++) {
|
|
9057
|
+
if (consumed === null || !consumed.has(j)) removed.push(unwrapValue(prevRows[j]));
|
|
9058
|
+
}
|
|
9059
|
+
return { prefix: structStart, sources, removed };
|
|
9060
|
+
}
|
|
8411
9061
|
function descend(pv, nv, keyFn, fam, proj = false) {
|
|
8412
9062
|
if (pv === null || typeof pv !== "object" || nv === null || typeof nv !== "object") return;
|
|
8413
9063
|
// Lookup FIRST: a hit implies pv was wrappable and never raw-marked (only
|
|
@@ -8431,7 +9081,10 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
8431
9081
|
const nk = keyFn(nv);
|
|
8432
9082
|
// Key mismatch detaches: the slot takes the new entity; the old proxy
|
|
8433
9083
|
// keeps its (old) backing and a fresh proxy wraps the new value on read.
|
|
8434
|
-
|
|
9084
|
+
// SameValueZero (re-audit 2, P1-5): NaN keys are self-equal — strict
|
|
9085
|
+
// inequality detached every NaN-keyed slot on every tick while the
|
|
9086
|
+
// Map-based row-ops matcher retained its DOM row (stale forever).
|
|
9087
|
+
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return;
|
|
8435
9088
|
}
|
|
8436
9089
|
// Reachability pruning (§6d) is MODE-dependent, both pinned:
|
|
8437
9090
|
// - keyed matching descends only where subscriptions exist at/below (`d`) —
|
|
@@ -8463,10 +9116,29 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
8463
9116
|
* primitives; the generic draft write-traps are reused from the legacy
|
|
8464
9117
|
* module unchanged.
|
|
8465
9118
|
*/
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
9119
|
+
/**
|
|
9120
|
+
* Wrap a store proxy as a projection DRAFT: every operation carries the write
|
|
9121
|
+
* override (the derive is the author — its ops must not hit the §6c firewall
|
|
9122
|
+
* gate, even in a continuation after an `await`/`yield` where the sync write
|
|
9123
|
+
* scope has closed).
|
|
9124
|
+
*
|
|
9125
|
+
* FAKE TARGET, not the store proxy itself (#3060): after a proxy trap
|
|
9126
|
+
* returns, the engine runs spec invariant validation against the proxy's
|
|
9127
|
+
* TARGET — [[OwnPropertyKeys]] after ownKeys, [[GetOwnProperty]] after
|
|
9128
|
+
* set/getOwnPropertyDescriptor/defineProperty. With the store proxy as
|
|
9129
|
+
* target those checks re-enter the store's traps OUTSIDE the override
|
|
9130
|
+
* bracket (the trap's finally has already run), so `Object.keys(state)` in
|
|
9131
|
+
* a derive continuation fired the firewall gate and re-threw the
|
|
9132
|
+
* projection's own pending NotReadyError into the derive. A dummy of
|
|
9133
|
+
* matching kind (array/object, same trick as the store's own TargetShape)
|
|
9134
|
+
* keeps invariant validation away from the store entirely; the traps
|
|
9135
|
+
* forward to the closed-over inner proxy inside the bracket.
|
|
9136
|
+
*
|
|
9137
|
+
* Save/restore projectionWriteActive, never hard-reset: the draft can be
|
|
9138
|
+
* driven from inside an enclosing authoritative-write scope (next-store
|
|
9139
|
+
* optimistic derives), and a hard `false` would clobber it mid-derive.
|
|
9140
|
+
*/
|
|
9141
|
+
function wrapDraft(inner, isActive, onDraftWrite) {
|
|
8470
9142
|
const traps = {
|
|
8471
9143
|
get(_, prop) {
|
|
8472
9144
|
let value;
|
|
@@ -8474,13 +9146,15 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
8474
9146
|
setWriteOverride(true);
|
|
8475
9147
|
setProjectionWriteActive(true);
|
|
8476
9148
|
try {
|
|
8477
|
-
value =
|
|
9149
|
+
value = inner[prop];
|
|
8478
9150
|
} finally {
|
|
8479
9151
|
setWriteOverride(false);
|
|
8480
9152
|
setProjectionWriteActive(was);
|
|
8481
9153
|
}
|
|
8482
9154
|
if (prop === $TARGET) return value;
|
|
8483
|
-
return typeof value === "object" && value !== null
|
|
9155
|
+
return typeof value === "object" && value !== null
|
|
9156
|
+
? wrapDraft(value, isActive, onDraftWrite)
|
|
9157
|
+
: value;
|
|
8484
9158
|
},
|
|
8485
9159
|
has(_, prop) {
|
|
8486
9160
|
let value;
|
|
@@ -8488,7 +9162,7 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
8488
9162
|
setWriteOverride(true);
|
|
8489
9163
|
setProjectionWriteActive(true);
|
|
8490
9164
|
try {
|
|
8491
|
-
value = prop in
|
|
9165
|
+
value = prop in inner;
|
|
8492
9166
|
} finally {
|
|
8493
9167
|
setWriteOverride(false);
|
|
8494
9168
|
setProjectionWriteActive(was);
|
|
@@ -8501,7 +9175,7 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
8501
9175
|
setWriteOverride(true);
|
|
8502
9176
|
setProjectionWriteActive(true);
|
|
8503
9177
|
try {
|
|
8504
|
-
|
|
9178
|
+
inner[prop] = value;
|
|
8505
9179
|
onDraftWrite?.();
|
|
8506
9180
|
} finally {
|
|
8507
9181
|
setWriteOverride(false);
|
|
@@ -8515,7 +9189,49 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
8515
9189
|
setWriteOverride(true);
|
|
8516
9190
|
setProjectionWriteActive(true);
|
|
8517
9191
|
try {
|
|
8518
|
-
delete
|
|
9192
|
+
delete inner[prop];
|
|
9193
|
+
onDraftWrite?.();
|
|
9194
|
+
} finally {
|
|
9195
|
+
setWriteOverride(false);
|
|
9196
|
+
setProjectionWriteActive(was);
|
|
9197
|
+
}
|
|
9198
|
+
return true;
|
|
9199
|
+
},
|
|
9200
|
+
ownKeys() {
|
|
9201
|
+
const was = projectionWriteActive;
|
|
9202
|
+
setWriteOverride(true);
|
|
9203
|
+
setProjectionWriteActive(true);
|
|
9204
|
+
try {
|
|
9205
|
+
return Reflect.ownKeys(inner);
|
|
9206
|
+
} finally {
|
|
9207
|
+
setWriteOverride(false);
|
|
9208
|
+
setProjectionWriteActive(was);
|
|
9209
|
+
}
|
|
9210
|
+
},
|
|
9211
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
9212
|
+
let d;
|
|
9213
|
+
const was = projectionWriteActive;
|
|
9214
|
+
setWriteOverride(true);
|
|
9215
|
+
setProjectionWriteActive(true);
|
|
9216
|
+
try {
|
|
9217
|
+
d = Reflect.getOwnPropertyDescriptor(inner, prop);
|
|
9218
|
+
} finally {
|
|
9219
|
+
setWriteOverride(false);
|
|
9220
|
+
setProjectionWriteActive(was);
|
|
9221
|
+
}
|
|
9222
|
+
// The dummy target doesn't hold the key, so a non-configurable report
|
|
9223
|
+
// would violate the proxy invariant. Store descriptors are already
|
|
9224
|
+
// normalized configurable; enforce it for raw leaves too.
|
|
9225
|
+
if (d) d.configurable = true;
|
|
9226
|
+
return d;
|
|
9227
|
+
},
|
|
9228
|
+
defineProperty(_, prop, desc) {
|
|
9229
|
+
if (isActive && !isActive()) return true;
|
|
9230
|
+
const was = projectionWriteActive;
|
|
9231
|
+
setWriteOverride(true);
|
|
9232
|
+
setProjectionWriteActive(true);
|
|
9233
|
+
try {
|
|
9234
|
+
Reflect.defineProperty(inner, prop, desc);
|
|
8519
9235
|
onDraftWrite?.();
|
|
8520
9236
|
} finally {
|
|
8521
9237
|
setWriteOverride(false);
|
|
@@ -8524,7 +9240,8 @@ function createWriteTraps(isActive, onDraftWrite) {
|
|
|
8524
9240
|
return true;
|
|
8525
9241
|
}
|
|
8526
9242
|
};
|
|
8527
|
-
|
|
9243
|
+
// Matching-kind dummy so Array.isArray(draft) answers like the store.
|
|
9244
|
+
return new Proxy(Array.isArray(inner) ? [] : {}, traps);
|
|
8528
9245
|
}
|
|
8529
9246
|
function createProjectionNextInternal(fn, seed, options) {
|
|
8530
9247
|
const fam = {
|
|
@@ -8578,9 +9295,10 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8578
9295
|
const shadow = owner._loading
|
|
8579
9296
|
? JSON.parse(JSON.stringify(wrappedStore[$TARGET][STORE_VALUE]))
|
|
8580
9297
|
: null;
|
|
8581
|
-
const draft =
|
|
9298
|
+
const draft = wrapDraft(
|
|
8582
9299
|
wrappedStore,
|
|
8583
|
-
|
|
9300
|
+
() => !settled || owner._x?._inFlight === result,
|
|
9301
|
+
onDraftWrite
|
|
8584
9302
|
);
|
|
8585
9303
|
storeSetterNext(
|
|
8586
9304
|
draft,
|
|
@@ -8605,6 +9323,558 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8605
9323
|
return owner;
|
|
8606
9324
|
}
|
|
8607
9325
|
|
|
9326
|
+
/**
|
|
9327
|
+
* PR-A: the patch channel (DESIGN-PATCH-CHANNEL.md).
|
|
9328
|
+
*
|
|
9329
|
+
* Compiled patch functions — per-record compare-and-write consumers —
|
|
9330
|
+
* dispatched by the store's visibility transitions instead of render
|
|
9331
|
+
* effects. This module owns registration, the per-flush apply queue
|
|
9332
|
+
* (effect-phase timing, §2b), the owned-prev rule (§2c), and dispatch
|
|
9333
|
+
* bubbling (§4b). Emission calls live at the four visibility-transition
|
|
9334
|
+
* sites (adoption walk, setter notify, fold commit, override lifecycle)
|
|
9335
|
+
* and are gated on registration, so unpatched stores pay a null check.
|
|
9336
|
+
*
|
|
9337
|
+
* Bubbling contract: a targeted nested write reaches ancestor patches as a
|
|
9338
|
+
* FORCED re-apply — the third `force` argument makes every compiled compare
|
|
9339
|
+
* pass, so the ancestor rewrites its bound fields from its current backing
|
|
9340
|
+
* (idempotent, and prev-free: an ancestor's pre-state is not reconstructible
|
|
9341
|
+
* after in-place folds). Compiled bodies therefore have the signature
|
|
9342
|
+
* `(next, prev, force?)`.
|
|
9343
|
+
*
|
|
9344
|
+
* Tree-shaking: core never imports this module; stores without patches
|
|
9345
|
+
* never schedule the queue.
|
|
9346
|
+
*/
|
|
9347
|
+
let queue = null;
|
|
9348
|
+
let scheduled = false;
|
|
9349
|
+
function drainApplyQueue() {
|
|
9350
|
+
// Settle-time fallback for optimistic emissions (a reverting flush may
|
|
9351
|
+
// have no active lanes left to run the lane-slot drain).
|
|
9352
|
+
drainOptimistic();
|
|
9353
|
+
const q = queue;
|
|
9354
|
+
queue = null;
|
|
9355
|
+
scheduled = false;
|
|
9356
|
+
if (q === null) return;
|
|
9357
|
+
// Per-entry isolation: one throwing patch must not abort its siblings
|
|
9358
|
+
// (effect parity — each effect isolates its failure). A throwing patch
|
|
9359
|
+
// routes through its REGISTERING OWNER's queue chain exactly like a
|
|
9360
|
+
// render-effect error (§2b): an Errored boundary above the row collects
|
|
9361
|
+
// it (source = the owner, error read via owner._x?._error). Unhandled errors
|
|
9362
|
+
// rethrow after the drain so they still surface.
|
|
9363
|
+
let firstError = UNSET;
|
|
9364
|
+
for (let i = 0; i < q.length; i++) {
|
|
9365
|
+
clearStamp(q[i]);
|
|
9366
|
+
const { list, prev, force, t } = q[i];
|
|
9367
|
+
const next = t !== null ? (t.pb ?? t.v) : q[i].next;
|
|
9368
|
+
firstError = applyEntries(list, next, prev, force, firstError);
|
|
9369
|
+
}
|
|
9370
|
+
if (firstError !== UNSET) {
|
|
9371
|
+
// Unhandled patch errors HALT like unhandled effect errors (re-audit 2,
|
|
9372
|
+
// P1-4): app state is undefined past an unboundaried throw.
|
|
9373
|
+
haltReactivity(firstError);
|
|
9374
|
+
throw firstError;
|
|
9375
|
+
}
|
|
9376
|
+
}
|
|
9377
|
+
const UNSET = Symbol();
|
|
9378
|
+
/** ONE callback/error primitive for every drain (normal, transition-held,
|
|
9379
|
+
* optimistic): per-entry isolation — a throwing patch must not abort its
|
|
9380
|
+
* siblings (effect parity) — and failures route through the REGISTERING
|
|
9381
|
+
* OWNER's queue chain exactly like a render-effect error (§2b): an Errored
|
|
9382
|
+
* boundary above the row collects it. Unhandled errors are aggregated by the
|
|
9383
|
+
* caller (first one rethrows after its drain completes). */
|
|
9384
|
+
function applyEntries(list, next, prev, force, firstError) {
|
|
9385
|
+
// SNAPSHOT multi-consumer lists (re-audit 5, P1-3): a callback can dispose
|
|
9386
|
+
// a sibling's owner, whose unbind SPLICES this same array mid-iteration —
|
|
9387
|
+
// index-walking the live array skips the shifted consumer. The dominant
|
|
9388
|
+
// single-consumer case pays nothing; unbound entries are marked so a
|
|
9389
|
+
// snapshot never applies a consumer severed by an earlier callback.
|
|
9390
|
+
const snap = list.length > 1 ? list.slice() : list;
|
|
9391
|
+
for (let j = 0; j < snap.length; j++) {
|
|
9392
|
+
const entry = snap[j];
|
|
9393
|
+
if (entry.u === true) continue;
|
|
9394
|
+
// Disposed owners drop their patches (the row unmounted mid-flush).
|
|
9395
|
+
if (entry.owner !== null && isDisposed(entry.owner)) continue;
|
|
9396
|
+
try {
|
|
9397
|
+
entry.fn(next, prev, force);
|
|
9398
|
+
} catch (err) {
|
|
9399
|
+
let handled = false;
|
|
9400
|
+
const owner = entry.owner;
|
|
9401
|
+
if (owner !== null) {
|
|
9402
|
+
// Route through the nearest COMPUTED ancestor (re-audit 2, P1-4):
|
|
9403
|
+
// <Errored>.reset() recomputes its sources, and a plain owner (the
|
|
9404
|
+
// list driver's listOwner) is not recomputable — the component/memo
|
|
9405
|
+
// scope above it is, and recomputing it rebuilds the rows, exactly
|
|
9406
|
+
// what reset means for a throwing render effect.
|
|
9407
|
+
let source = owner;
|
|
9408
|
+
while (source !== null && source._fn === undefined) source = source._parent;
|
|
9409
|
+
source ??= owner;
|
|
9410
|
+
const statusErr = new StatusError(source, err);
|
|
9411
|
+
ext(source)._error = statusErr;
|
|
9412
|
+
source._statusFlags = (source._statusFlags ?? 0) | STATUS_ERROR;
|
|
9413
|
+
handled = owner._queue.notify(source, STATUS_ERROR, STATUS_ERROR, statusErr);
|
|
9414
|
+
}
|
|
9415
|
+
if (!handled && firstError === UNSET) firstError = err;
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
return firstError;
|
|
9419
|
+
}
|
|
9420
|
+
// Transition-stamped emissions (§2b, "the walk is not the visibility moment
|
|
9421
|
+
// inside a transition"): entries stash DIRECTLY on their transition
|
|
9422
|
+
// (`_heldPatches`) and release into the live queue when THAT batch commits
|
|
9423
|
+
// (patchCommitHook). Reverted transitions never commit — their stash drops
|
|
9424
|
+
// with the transition object, no revert bookkeeping. The field (rather than
|
|
9425
|
+
// a WeakMap) keeps the every-flush commit-hook check to one property read;
|
|
9426
|
+
// the ambient batch never stashes.
|
|
9427
|
+
let commitHookInstalled = false;
|
|
9428
|
+
function releaseBatch(batch) {
|
|
9429
|
+
const held = batch._heldPatches;
|
|
9430
|
+
if (held === undefined) return;
|
|
9431
|
+
batch._heldPatches = undefined;
|
|
9432
|
+
for (let i = 0; i < held.length; i++) pushLive(held[i]);
|
|
9433
|
+
}
|
|
9434
|
+
function pushLive(item) {
|
|
9435
|
+
if (queue === null) queue = [];
|
|
9436
|
+
queue.push(item);
|
|
9437
|
+
if (!scheduled) {
|
|
9438
|
+
scheduled = true;
|
|
9439
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9442
|
+
function push(item) {
|
|
9443
|
+
const tx = activeTransition;
|
|
9444
|
+
if (tx !== null) {
|
|
9445
|
+
let held = tx._heldPatches;
|
|
9446
|
+
if (held === undefined) tx._heldPatches = held = [];
|
|
9447
|
+
held.push(item);
|
|
9448
|
+
return;
|
|
9449
|
+
}
|
|
9450
|
+
pushLive(item);
|
|
9451
|
+
}
|
|
9452
|
+
/** Self-entry push with SAME-BATCH COALESCING (re-audit 2/3): a record's
|
|
9453
|
+
* later non-forced emission into the same container UPDATES the queued
|
|
9454
|
+
* entry in place — `next` takes the newest capture (adoption swaps the
|
|
9455
|
+
* backing object per emission; dropping the later one applied STALE state),
|
|
9456
|
+
* `prev` keeps the batch's earliest (effect semantics: one application per
|
|
9457
|
+
* batch spanning the whole window). The entry's consumer list is the live
|
|
9458
|
+
* pc.p array, so mid-batch registrants ride the single application. Forced
|
|
9459
|
+
* entries and row/slot ops never coalesce; the drain clears the stamps so a
|
|
9460
|
+
* quiet record retains nothing from its last batch. */
|
|
9461
|
+
function pushSelf(pc, item) {
|
|
9462
|
+
const tx = activeTransition;
|
|
9463
|
+
let arr;
|
|
9464
|
+
if (tx !== null) {
|
|
9465
|
+
let held = tx._heldPatches;
|
|
9466
|
+
if (held === undefined) tx._heldPatches = held = [];
|
|
9467
|
+
arr = held;
|
|
9468
|
+
} else {
|
|
9469
|
+
if (queue === null) queue = [];
|
|
9470
|
+
arr = queue;
|
|
9471
|
+
}
|
|
9472
|
+
if (pc.qa === arr && pc.qe !== null) {
|
|
9473
|
+
const qe = pc.qe;
|
|
9474
|
+
qe.next = item.next;
|
|
9475
|
+
qe.list = item.list; // pc.p can be re-created if emptied mid-batch
|
|
9476
|
+
return;
|
|
9477
|
+
}
|
|
9478
|
+
pc.qa = arr;
|
|
9479
|
+
pc.qe = item;
|
|
9480
|
+
item.pc = pc;
|
|
9481
|
+
arr.push(item);
|
|
9482
|
+
if (arr === queue && !scheduled) {
|
|
9483
|
+
scheduled = true;
|
|
9484
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
9485
|
+
}
|
|
9486
|
+
}
|
|
9487
|
+
/** Drain-side stamp clear (re-audit 3, P2-6): without it a quiet long-lived
|
|
9488
|
+
* record's channel retains its last batch's container array, entry, and both
|
|
9489
|
+
* captured backings for the record's lifetime. */
|
|
9490
|
+
function clearStamp(item) {
|
|
9491
|
+
const pc = item.pc;
|
|
9492
|
+
if (pc !== undefined && pc.qe === item) {
|
|
9493
|
+
pc.qa = null;
|
|
9494
|
+
pc.qe = null;
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
/** Shallow clone for the owned-prev rule (§2c): owned backings fold values
|
|
9498
|
+
* INTO the same raw at commit, so a queued prev must be snapshotted. */
|
|
9499
|
+
function clonePrev(prev) {
|
|
9500
|
+
return Array.isArray(prev) ? prev.slice() : { ...prev };
|
|
9501
|
+
}
|
|
9502
|
+
/**
|
|
9503
|
+
* Emit a record's visibility transition. Callers gate on `hasPatches()` and
|
|
9504
|
+
* `t.d` cheaply; this function re-checks and walks ancestors (§4b).
|
|
9505
|
+
*/
|
|
9506
|
+
function emitPatch(t, next, prev) {
|
|
9507
|
+
const p = t.pc !== null ? t.pc.p : null;
|
|
9508
|
+
if (p !== null)
|
|
9509
|
+
pushSelf(t.pc, {
|
|
9510
|
+
list: p,
|
|
9511
|
+
next,
|
|
9512
|
+
prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
|
|
9513
|
+
force: false,
|
|
9514
|
+
t: null
|
|
9515
|
+
});
|
|
9516
|
+
// Bubbling: ancestors force-re-apply from their LIVE backing, resolved at
|
|
9517
|
+
// drain (privatization may clone it between now and then).
|
|
9518
|
+
let u = t.u;
|
|
9519
|
+
while (u !== null) {
|
|
9520
|
+
const up = u.pc !== null ? u.pc.p : null;
|
|
9521
|
+
if (up !== null) push({ list: up, next: null, prev: null, force: true, t: u });
|
|
9522
|
+
u = u.u;
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
/** Emission for sites that already stand at the record with both sides in
|
|
9526
|
+
* hand and have already handled ancestors (the adoption walk descends —
|
|
9527
|
+
* parents were visited first), so no bubbling walk. */
|
|
9528
|
+
function emitPatchLocal(t, next, prev) {
|
|
9529
|
+
const p = t.pc !== null ? t.pc.p : null;
|
|
9530
|
+
if (p !== null)
|
|
9531
|
+
pushSelf(t.pc, {
|
|
9532
|
+
list: p,
|
|
9533
|
+
next,
|
|
9534
|
+
prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
|
|
9535
|
+
force: false,
|
|
9536
|
+
t: null
|
|
9537
|
+
});
|
|
9538
|
+
}
|
|
9539
|
+
/** Optimistic-channel emission: overrides are visible THIS flush while the
|
|
9540
|
+
* transaction is in flight — that is what optimism means. These ride a
|
|
9541
|
+
* dedicated queue drained at LANE-EFFECT timing (the regular effect queues
|
|
9542
|
+
* are stashed by an in-flight action), with the regular drain as the
|
|
9543
|
+
* settle-time fallback. `next === null` = forced re-apply from the live
|
|
9544
|
+
* target (the revert shape: committed truth back onto the DOM). */
|
|
9545
|
+
let optQueue = null;
|
|
9546
|
+
function drainOptimistic() {
|
|
9547
|
+
const q = optQueue;
|
|
9548
|
+
optQueue = null;
|
|
9549
|
+
if (q === null) return;
|
|
9550
|
+
// Same isolation/routing primitive as the normal drain (re-audit blocker
|
|
9551
|
+
// 5): one throwing optimistic patch must not abort its siblings, and it
|
|
9552
|
+
// must reach the registering owner's Errored boundary.
|
|
9553
|
+
let firstError = UNSET;
|
|
9554
|
+
for (let i = 0; i < q.length; i++) {
|
|
9555
|
+
clearStamp(q[i]);
|
|
9556
|
+
const { list, prev, force, t } = q[i];
|
|
9557
|
+
const next = t !== null ? (t.pb ?? t.v) : q[i].next;
|
|
9558
|
+
firstError = applyEntries(list, next, prev, force, firstError);
|
|
9559
|
+
}
|
|
9560
|
+
if (firstError !== UNSET) {
|
|
9561
|
+
haltReactivity(firstError);
|
|
9562
|
+
throw firstError;
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
function emitPatchOptimistic(t, next, prev) {
|
|
9566
|
+
const p = t.pc !== null ? t.pc.p : null;
|
|
9567
|
+
if (p === null) return;
|
|
9568
|
+
if (optQueue === null) optQueue = [];
|
|
9569
|
+
if (next === null) optQueue.push({ list: p, next: null, prev: null, force: true, t });
|
|
9570
|
+
else {
|
|
9571
|
+
// Same-batch coalescing, optimistic container (re-audit 3): later
|
|
9572
|
+
// non-forced emission updates the queued entry's next in place.
|
|
9573
|
+
const pc = t.pc;
|
|
9574
|
+
if (pc.qa === optQueue && pc.qe !== null) {
|
|
9575
|
+
const qe = pc.qe;
|
|
9576
|
+
qe.next = next;
|
|
9577
|
+
qe.list = p;
|
|
9578
|
+
} else {
|
|
9579
|
+
const item = { list: p, next, prev, force: false, t: null };
|
|
9580
|
+
pc.qa = optQueue;
|
|
9581
|
+
pc.qe = item;
|
|
9582
|
+
item.pc = pc;
|
|
9583
|
+
optQueue.push(item);
|
|
9584
|
+
}
|
|
9585
|
+
}
|
|
9586
|
+
// Backup scheduling: the lane-slot drain covers in-flight application; a
|
|
9587
|
+
// stashed regular drain guarantees settle-time application when no lane
|
|
9588
|
+
// survives to the final flush (pure reverts).
|
|
9589
|
+
if (!scheduled) {
|
|
9590
|
+
scheduled = true;
|
|
9591
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
9592
|
+
}
|
|
9593
|
+
}
|
|
9594
|
+
/** Row-ops emission at OPTIMISTIC (lane) timing: user drafts on an
|
|
9595
|
+
* optimistic family must show structure IN FLIGHT — bypassing the
|
|
9596
|
+
* transition stash exactly like emitPatchOptimistic. Two forms:
|
|
9597
|
+
* - `ops` given (write site): `nextRows` is the draft's intended visible
|
|
9598
|
+
* list, ops the identity diff against the pre-write optimistic view.
|
|
9599
|
+
* - `ops === null` (revert site): RESYNC — the consumer rebuilds retention
|
|
9600
|
+
* by row identity against the live post-revert view, resolved from the
|
|
9601
|
+
* target at drain time (overrides are gone by then, so `pb ?? v` IS the
|
|
9602
|
+
* committed truth). */
|
|
9603
|
+
function emitRowOpsOptimistic(t, nextRows, ops) {
|
|
9604
|
+
const list = t.pc !== null ? t.pc.ro : null;
|
|
9605
|
+
if (list === null) return;
|
|
9606
|
+
if (optQueue === null) optQueue = [];
|
|
9607
|
+
optQueue.push({
|
|
9608
|
+
list: list.map(e => ({
|
|
9609
|
+
owner: e.owner,
|
|
9610
|
+
fn: (n, _p) => e.fn(n, ops)
|
|
9611
|
+
})),
|
|
9612
|
+
next: nextRows,
|
|
9613
|
+
prev: null,
|
|
9614
|
+
force: false,
|
|
9615
|
+
t: nextRows === null ? t : null
|
|
9616
|
+
});
|
|
9617
|
+
if (!scheduled) {
|
|
9618
|
+
scheduled = true;
|
|
9619
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
9620
|
+
}
|
|
9621
|
+
}
|
|
9622
|
+
/**
|
|
9623
|
+
* Register a compiled patch on a store record. Multi-consumer (two lists
|
|
9624
|
+
* can render one record); owner-scoped for disposal. Returns unbind.
|
|
9625
|
+
*/
|
|
9626
|
+
// Global registration count: the cheap gate emission sites check before any
|
|
9627
|
+
// per-record work (unpatched apps pay one number compare per transition).
|
|
9628
|
+
let patchCount = 0;
|
|
9629
|
+
function hasPatches() {
|
|
9630
|
+
return patchCount > 0;
|
|
9631
|
+
}
|
|
9632
|
+
function registerPatch(record, fn) {
|
|
9633
|
+
let t = record?.[$TARGET];
|
|
9634
|
+
if (t === undefined) throw new Error("registerPatch: not a store record");
|
|
9635
|
+
// Chained backings (§7b): register on the ULTIMATE owner — that is where
|
|
9636
|
+
// value transitions fold and dispatch; the wrapper's identity is stable
|
|
9637
|
+
// and would never fire (see ultimateTarget).
|
|
9638
|
+
t = ultimateTarget(t) ?? t;
|
|
9639
|
+
if (!commitHookInstalled) {
|
|
9640
|
+
commitHookInstalled = true;
|
|
9641
|
+
armPatchHooks();
|
|
9642
|
+
setPatchCommitHook(releaseBatch);
|
|
9643
|
+
GlobalQueue._drainPatchOptimistic = drainOptimistic;
|
|
9644
|
+
}
|
|
9645
|
+
const entry = { fn, owner: getOwner() };
|
|
9646
|
+
const pc = pcOf(t);
|
|
9647
|
+
const list = (pc.p ??= []);
|
|
9648
|
+
list.push(entry);
|
|
9649
|
+
patchCount++;
|
|
9650
|
+
// Bindings are subscriptions for reachability (§6d pruning must descend
|
|
9651
|
+
// into bound records).
|
|
9652
|
+
markDescendants(t);
|
|
9653
|
+
let unbound = false;
|
|
9654
|
+
return () => {
|
|
9655
|
+
if (unbound) return;
|
|
9656
|
+
unbound = true;
|
|
9657
|
+
entry.u = true; // dispatch snapshots skip severed consumers
|
|
9658
|
+
// Decrement ONLY on actual removal: a demotion (demoteToEffects) may
|
|
9659
|
+
// have already pulled this entry and repaired the count — the splice
|
|
9660
|
+
// miss is how this closure learns that.
|
|
9661
|
+
const idx = list.indexOf(entry);
|
|
9662
|
+
if (idx >= 0) {
|
|
9663
|
+
list.splice(idx, 1);
|
|
9664
|
+
patchCount--;
|
|
9665
|
+
}
|
|
9666
|
+
if (list.length === 0 && pc.p === list) pc.p = null;
|
|
9667
|
+
};
|
|
9668
|
+
}
|
|
9669
|
+
/** Resolve a target through CHAINED backings (§7b) to the ultimate owner.
|
|
9670
|
+
* A projection family wrapper's backing IS another store's proxy: value
|
|
9671
|
+
* transitions fold on the ULTIMATE target (the wrapper's identity never
|
|
9672
|
+
* changes), so patch registration and raw resolution must land there or
|
|
9673
|
+
* registered patches never fire (equivalence-matrix finding: projection
|
|
9674
|
+
* value ticks froze driver rows while classic effects tracked through). */
|
|
9675
|
+
function ultimateTarget(t) {
|
|
9676
|
+
while (t.ch) {
|
|
9677
|
+
const u = (t.pb ?? t.v)?.[$TARGET];
|
|
9678
|
+
if (u === undefined) return undefined;
|
|
9679
|
+
t = u;
|
|
9680
|
+
}
|
|
9681
|
+
return t;
|
|
9682
|
+
}
|
|
9683
|
+
/** Dual-driver bind probe (compiler runtime contract): when `record` is a
|
|
9684
|
+
* patchable store record, returns its CURRENT raw backing (the driver's
|
|
9685
|
+
* initial force-apply reads it directly — no proxy traffic, no tracking);
|
|
9686
|
+
* returns undefined otherwise (driver falls back to the effect path).
|
|
9687
|
+
* Not patchable: non-records, non-proxies, accessor-bearing records
|
|
9688
|
+
* (patches read raw — getters need tracked evaluation), broken chains. */
|
|
9689
|
+
function patchableRaw(record) {
|
|
9690
|
+
let t = record?.[$TARGET];
|
|
9691
|
+
if (t === undefined || t.px !== record || t.a === true) return undefined;
|
|
9692
|
+
t = ultimateTarget(t);
|
|
9693
|
+
// SCAN before trusting (re-audit blocker 3): `a` starts false and is only
|
|
9694
|
+
// discovered lazily (first draft, deep walks) — admission must run the
|
|
9695
|
+
// one-time own-accessor scan itself, or a getter-bearing record takes the
|
|
9696
|
+
// patch path and its getter's OUTSIDE dependencies (signals, other
|
|
9697
|
+
// records) never re-apply. Sticky `sc` makes this one probe pass per
|
|
9698
|
+
// record lifetime.
|
|
9699
|
+
if (t === undefined || !targetIsPlain(t)) return undefined;
|
|
9700
|
+
return t.pb ?? t.v;
|
|
9701
|
+
}
|
|
9702
|
+
/** Accessor demotion (design §5): a record that acquires an accessor after
|
|
9703
|
+
* registration stops being patchable — reads must go through tracked
|
|
9704
|
+
* evaluation. Clears patches and repairs the global count; callers re-drive
|
|
9705
|
+
* the pulled bodies (demoteToEffects). */
|
|
9706
|
+
function demotePatches(t) {
|
|
9707
|
+
if (t.pc === null) return null;
|
|
9708
|
+
const p = t.pc.p;
|
|
9709
|
+
t.pc.p = null;
|
|
9710
|
+
if (p === null) return null;
|
|
9711
|
+
patchCount -= p.length;
|
|
9712
|
+
// Drain IN PLACE: unbind closures captured this array — a late unbind must
|
|
9713
|
+
// miss its indexOf and not double-decrement the repaired count.
|
|
9714
|
+
return p.splice(0, p.length);
|
|
9715
|
+
}
|
|
9716
|
+
/** The demotion re-drive (re-audit blocker 3): each pulled body becomes the
|
|
9717
|
+
* SAME dual-driver effect fallback the web runtime would have chosen had the
|
|
9718
|
+
* record carried the accessor at bind — a tracked compute pass (next === prev
|
|
9719
|
+
* short-circuits every compare into a pure read THROUGH THE PROXY, so getter
|
|
9720
|
+
* dependencies track) plus an untracked force-apply at effect timing.
|
|
9721
|
+
*
|
|
9722
|
+
* Creation is DEFERRED to the effect phase: the trap that discovers the
|
|
9723
|
+
* accessor runs mid-draft, and an effect's initial pass must not read
|
|
9724
|
+
* through the proxy inside the write window. The record's own transition
|
|
9725
|
+
* for that draft is covered by the new effect's initial force-apply.
|
|
9726
|
+
*
|
|
9727
|
+
* Known edge (documented): a demoted LIST-ROW body re-drives under its
|
|
9728
|
+
* registering owner (the list owner), so per-row severing on removal is
|
|
9729
|
+
* lost for demoted rows — the effect lives until the LIST disposes. Rows
|
|
9730
|
+
* only demote when user code defines an accessor on a row record at
|
|
9731
|
+
* runtime. */
|
|
9732
|
+
function demoteToEffects(t) {
|
|
9733
|
+
const entries = demotePatches(t);
|
|
9734
|
+
if (entries === null || entries.length === 0) return;
|
|
9735
|
+
const proxy = t.px;
|
|
9736
|
+
globalQueue.enqueue(EFFECT_RENDER, () => {
|
|
9737
|
+
for (let i = 0; i < entries.length; i++) {
|
|
9738
|
+
const entry = entries[i];
|
|
9739
|
+
if (entry.owner !== null && isDisposed(entry.owner)) continue;
|
|
9740
|
+
const fn = entry.fn;
|
|
9741
|
+
runWithOwner(entry.owner, () =>
|
|
9742
|
+
createRenderEffect(
|
|
9743
|
+
() => {
|
|
9744
|
+
fn(proxy, proxy, false);
|
|
9745
|
+
},
|
|
9746
|
+
() => {
|
|
9747
|
+
// Block body: a compiled patch body's return value must not be
|
|
9748
|
+
// mistaken for an effect cleanup.
|
|
9749
|
+
untrack(() => fn(proxy, undefined, true));
|
|
9750
|
+
}
|
|
9751
|
+
)
|
|
9752
|
+
);
|
|
9753
|
+
}
|
|
9754
|
+
});
|
|
9755
|
+
}
|
|
9756
|
+
/** Register a structural-ops consumer on a keyed store array (the list
|
|
9757
|
+
* container's channel — what `For` consumes through the seam). */
|
|
9758
|
+
function registerRowOps(array, fn) {
|
|
9759
|
+
let t = array?.[$TARGET];
|
|
9760
|
+
if (t === undefined) throw new Error("registerRowOps: not a store array");
|
|
9761
|
+
// Chained backings resolve to the ULTIMATE owner, same as registerPatch
|
|
9762
|
+
// (§7b) — the walk/fold emits there (re-audit blocker 4).
|
|
9763
|
+
t = ultimateTarget(t) ?? t;
|
|
9764
|
+
armRowHooks();
|
|
9765
|
+
if (!commitHookInstalled) {
|
|
9766
|
+
commitHookInstalled = true;
|
|
9767
|
+
armPatchHooks();
|
|
9768
|
+
setPatchCommitHook(releaseBatch);
|
|
9769
|
+
GlobalQueue._drainPatchOptimistic = drainOptimistic;
|
|
9770
|
+
}
|
|
9771
|
+
const entry = { fn, owner: getOwner() };
|
|
9772
|
+
const pc = pcOf(t);
|
|
9773
|
+
const list = (pc.ro ??= []);
|
|
9774
|
+
list.push(entry);
|
|
9775
|
+
patchCount++;
|
|
9776
|
+
markDescendants(t);
|
|
9777
|
+
let unbound = false;
|
|
9778
|
+
return () => {
|
|
9779
|
+
if (unbound) return;
|
|
9780
|
+
unbound = true;
|
|
9781
|
+
patchCount--;
|
|
9782
|
+
const idx = list.indexOf(entry);
|
|
9783
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
9784
|
+
if (list.length === 0 && pc.ro === list) pc.ro = null;
|
|
9785
|
+
};
|
|
9786
|
+
}
|
|
9787
|
+
/** Slot patches (shallow arrays) ride the same apply queue: the walk emits
|
|
9788
|
+
* per aligned value-replaced slot; application happens at effect phase under
|
|
9789
|
+
* the registration owner's lifetime. */
|
|
9790
|
+
function emitSlotPatch(t, index, next, prev) {
|
|
9791
|
+
const sp = t.pc !== null ? t.pc.sp : null;
|
|
9792
|
+
if (sp === null) return;
|
|
9793
|
+
push({
|
|
9794
|
+
list: sp.map(e => ({ owner: e.owner, fn: () => e.fn(index, next, prev) })),
|
|
9795
|
+
next,
|
|
9796
|
+
prev,
|
|
9797
|
+
force: false,
|
|
9798
|
+
t: null
|
|
9799
|
+
});
|
|
9800
|
+
}
|
|
9801
|
+
/** Slot patch for shallow arrays: the reconcile walk emits (index, next,
|
|
9802
|
+
* prev) for KEY-ALIGNED value-replaced slots (structure rides row ops), and
|
|
9803
|
+
* the emission queues through the patch apply queue — effect-phase timing,
|
|
9804
|
+
* transition stamping, disposed-owner drop — like every other channel. */
|
|
9805
|
+
function registerSlotPatchNext(arr, fn) {
|
|
9806
|
+
let t = arr?.[$TARGET];
|
|
9807
|
+
if (t === undefined) throw new Error("registerSlotPatchNext: not a store array");
|
|
9808
|
+
// Chained backings resolve to the ULTIMATE owner, same as registerPatch
|
|
9809
|
+
// (§7b) — the walk emits slot ticks there (re-audit blocker 4).
|
|
9810
|
+
t = ultimateTarget(t) ?? t;
|
|
9811
|
+
armRowHooks();
|
|
9812
|
+
if (!commitHookInstalled) {
|
|
9813
|
+
commitHookInstalled = true;
|
|
9814
|
+
armPatchHooks();
|
|
9815
|
+
setPatchCommitHook(releaseBatch);
|
|
9816
|
+
GlobalQueue._drainPatchOptimistic = drainOptimistic;
|
|
9817
|
+
}
|
|
9818
|
+
// Multi-consumer (external audit): one shallow array can drive several
|
|
9819
|
+
// lists — registrations are a list, unbinds splice their own entry.
|
|
9820
|
+
const pc = pcOf(t);
|
|
9821
|
+
const entry = { fn, owner: getOwner() };
|
|
9822
|
+
(pc.sp ??= []).push(entry);
|
|
9823
|
+
markDescendants(t);
|
|
9824
|
+
let unbound = false;
|
|
9825
|
+
return () => {
|
|
9826
|
+
if (unbound || pc.sp === null) return;
|
|
9827
|
+
unbound = true;
|
|
9828
|
+
const idx = pc.sp.indexOf(entry);
|
|
9829
|
+
if (idx >= 0) pc.sp.splice(idx, 1);
|
|
9830
|
+
if (pc.sp.length === 0) pc.sp = null;
|
|
9831
|
+
};
|
|
9832
|
+
}
|
|
9833
|
+
/** Row-ops ride the SAME apply queue/timing as record patches: transition-
|
|
9834
|
+
* stamped, applied at effect phase, in emission order (structure before the
|
|
9835
|
+
* new rows' own patches can exist; retained rows' value patches commute). */
|
|
9836
|
+
function emitRowOps(t, next, ops) {
|
|
9837
|
+
const list = t.pc !== null ? t.pc.ro : null;
|
|
9838
|
+
if (list === null) return;
|
|
9839
|
+
push({
|
|
9840
|
+
list: list.map(e => ({
|
|
9841
|
+
owner: e.owner,
|
|
9842
|
+
fn: (n, _p) => e.fn(n, ops)
|
|
9843
|
+
})),
|
|
9844
|
+
next,
|
|
9845
|
+
prev: null,
|
|
9846
|
+
force: false,
|
|
9847
|
+
t: null
|
|
9848
|
+
});
|
|
9849
|
+
}
|
|
9850
|
+
// Pay-for-use seams: the write paths (store/reconcile/optimistic) emit
|
|
9851
|
+
// through installed hooks instead of importing this module. Installation is
|
|
9852
|
+
// LAZY (first registration) rather than a module-scope call — the dist is a
|
|
9853
|
+
// flat bundle, and a top-level side effect would retain the whole channel in
|
|
9854
|
+
// every consumer. TWO TIERS so a value-only registration (registerPatch —
|
|
9855
|
+
// present in ~every bundle under patch-mode default) does not retain the
|
|
9856
|
+
// list machinery (row-ops emitters + reconcile's diff builders): row hooks
|
|
9857
|
+
// arm only from the list driver's registrations. Sound because every
|
|
9858
|
+
// emission site is guarded by the matching pc channel, which only the
|
|
9859
|
+
// corresponding registration creates. See patch-hooks.ts.
|
|
9860
|
+
function armPatchHooks() {
|
|
9861
|
+
installPatchHooks({
|
|
9862
|
+
emitPatch,
|
|
9863
|
+
emitPatchLocal,
|
|
9864
|
+
emitPatchOptimistic,
|
|
9865
|
+
hasPatches,
|
|
9866
|
+
demoteToEffects
|
|
9867
|
+
});
|
|
9868
|
+
}
|
|
9869
|
+
function armRowHooks() {
|
|
9870
|
+
installRowHooks({
|
|
9871
|
+
emitRowOps,
|
|
9872
|
+
emitSlotPatch,
|
|
9873
|
+
emitSetterRowOps,
|
|
9874
|
+
emitRowOpsOptimistic
|
|
9875
|
+
});
|
|
9876
|
+
}
|
|
9877
|
+
|
|
8608
9878
|
/**
|
|
8609
9879
|
* Store rewrite — optimistic stores (§3/§7, RUL-3): no store-side layer, no
|
|
8610
9880
|
* backup snapshots. Nodes in an optimistic family are ARMED core signals
|
|
@@ -8634,6 +9904,23 @@ function installNextBlockedHalf() {
|
|
|
8634
9904
|
// so the hook only empties the batch set.
|
|
8635
9905
|
if (!GlobalQueue._clearOptimisticStores) {
|
|
8636
9906
|
GlobalQueue._clearOptimisticStores = stores => {
|
|
9907
|
+
// Patch channel (revert site): engine-native reverts flip node values
|
|
9908
|
+
// back to committed; patched records need a forced DOM re-apply from
|
|
9909
|
+
// the post-revert view. Emission only — next keeps no layer to clear.
|
|
9910
|
+
for (const px of stores) {
|
|
9911
|
+
const t = px?.[$TARGET];
|
|
9912
|
+
const overlaid = t?.fam?.overlaid;
|
|
9913
|
+
if (overlaid !== undefined) {
|
|
9914
|
+
for (const ot of overlaid) {
|
|
9915
|
+
if (ot.pc !== null && ot.pc.p !== null) patchHooks.emitPatchOptimistic(ot, null, null);
|
|
9916
|
+
// Row-ops resync (family increment 2): reverts flip node values
|
|
9917
|
+
// back engine-natively; a driven list must rebuild retention by
|
|
9918
|
+
// row identity against the post-revert view (resolved from the
|
|
9919
|
+
// target at drain — overrides are gone by then).
|
|
9920
|
+
if (ot.pc !== null && ot.pc.ro !== null) rowHooks.emitRowOpsOptimistic(ot, null, null);
|
|
9921
|
+
}
|
|
9922
|
+
}
|
|
9923
|
+
}
|
|
8637
9924
|
stores.clear();
|
|
8638
9925
|
};
|
|
8639
9926
|
}
|
|
@@ -8740,6 +10027,22 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
8740
10027
|
const fw = t.fam?.node;
|
|
8741
10028
|
if (fw?._transition) globalQueue.initTransition(fw._transition);
|
|
8742
10029
|
const old = t.v;
|
|
10030
|
+
// Patch channel (override-application site): the draft IS the intended
|
|
10031
|
+
// visible state; prev is the view before these overrides apply. Bypasses
|
|
10032
|
+
// the transition stash — optimism is visible in flight.
|
|
10033
|
+
if (t.pc !== null && t.pc.p !== null)
|
|
10034
|
+
patchHooks.emitPatchOptimistic(t, pb, optimisticView(t, old));
|
|
10035
|
+
// Row-ops channel (family increment 2): optimistic STRUCTURE on an array
|
|
10036
|
+
// rides node overrides — it never enters the reconcile walk — so a driven
|
|
10037
|
+
// list must get its structural ops here, lane-timed. Identity diff of the
|
|
10038
|
+
// pre-write optimistic view against the draft; aligned writes emit nothing.
|
|
10039
|
+
if (t.pc !== null && t.pc.ro !== null && Array.isArray(pb)) {
|
|
10040
|
+
const prevView = optimisticView(t, old);
|
|
10041
|
+
if (Array.isArray(prevView)) {
|
|
10042
|
+
const ops = buildIdentityRowOps(prevView, pb);
|
|
10043
|
+
if (ops !== null) rowHooks.emitRowOpsOptimistic(t, pb, ops);
|
|
10044
|
+
}
|
|
10045
|
+
}
|
|
8743
10046
|
const visible = (key, fallback) => {
|
|
8744
10047
|
const node = t.n?.[key];
|
|
8745
10048
|
return node !== undefined && hasActiveOverride(node)
|
|
@@ -8867,6 +10170,10 @@ function consumeOverridesNext(fam) {
|
|
|
8867
10170
|
insertSubs(t.k, true);
|
|
8868
10171
|
schedule();
|
|
8869
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);
|
|
8870
10177
|
}
|
|
8871
10178
|
overlaid.clear();
|
|
8872
10179
|
});
|
|
@@ -8915,7 +10222,9 @@ function applyTentative(t, incoming, keyFn) {
|
|
|
8915
10222
|
if (keyFn) {
|
|
8916
10223
|
const pk = keyFn(pv);
|
|
8917
10224
|
const nk = keyFn(nv);
|
|
8918
|
-
|
|
10225
|
+
// SameValueZero (re-audit 3, P1-3): parity with the plain reconcile
|
|
10226
|
+
// channel — NaN keys are self-equal.
|
|
10227
|
+
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return null;
|
|
8919
10228
|
}
|
|
8920
10229
|
return map.get(unwrapValue(pv)) ?? null;
|
|
8921
10230
|
};
|
|
@@ -8930,16 +10239,31 @@ function applyTentative(t, incoming, keyFn) {
|
|
|
8930
10239
|
const nk = keyFn(nv);
|
|
8931
10240
|
if (nk !== undefined) {
|
|
8932
10241
|
if (viewByKey === null) {
|
|
10242
|
+
// Occurrence-aware index queues (re-audit 3, P1-3): parity with
|
|
10243
|
+
// the plain adoption window — duplicate keys match per
|
|
10244
|
+
// occurrence, each view row consumed once.
|
|
8933
10245
|
viewByKey = new Map();
|
|
8934
10246
|
for (let j = 0; j < viewRows.length; j++) {
|
|
8935
10247
|
const p = unwrapValue(viewRows[j]);
|
|
8936
10248
|
if (isWrappable(p)) {
|
|
8937
10249
|
const pk = keyFn(p);
|
|
8938
|
-
if (pk
|
|
10250
|
+
if (pk === undefined) continue;
|
|
10251
|
+
const existing = viewByKey.get(pk);
|
|
10252
|
+
if (existing === undefined) viewByKey.set(pk, j);
|
|
10253
|
+
else if (Array.isArray(existing)) existing.push(j);
|
|
10254
|
+
else viewByKey.set(pk, [existing, j]);
|
|
8939
10255
|
}
|
|
8940
10256
|
}
|
|
8941
10257
|
}
|
|
8942
|
-
|
|
10258
|
+
const m = viewByKey.get(nk);
|
|
10259
|
+
if (m === undefined) pv = undefined;
|
|
10260
|
+
else if (Array.isArray(m)) {
|
|
10261
|
+
pv = unwrapValue(viewRows[m.shift()]);
|
|
10262
|
+
if (m.length === 1) viewByKey.set(nk, m[0]);
|
|
10263
|
+
} else {
|
|
10264
|
+
pv = unwrapValue(viewRows[m]);
|
|
10265
|
+
viewByKey.delete(nk);
|
|
10266
|
+
}
|
|
8943
10267
|
} else pv = unwrapValue(viewRows[i]);
|
|
8944
10268
|
} else pv = unwrapValue(viewRows[i]);
|
|
8945
10269
|
const ct = match(pv, nv);
|
|
@@ -10043,7 +11367,12 @@ function createLoadingBoundary(fn, fallback, options) {
|
|
|
10043
11367
|
function createErrorBoundary(fn, fallback) {
|
|
10044
11368
|
return createCollectionBoundary(STATUS_ERROR, fn, queue => {
|
|
10045
11369
|
return fallback(accessor(queue._error), () => {
|
|
10046
|
-
for (const source of queue._sources)
|
|
11370
|
+
for (const source of queue._sources) {
|
|
11371
|
+
// Non-computed sources (patch-channel registrations under plain
|
|
11372
|
+
// owners) are not recomputable — their reset is the record's next
|
|
11373
|
+
// transition re-applying the patch (re-audit 2, P1-4).
|
|
11374
|
+
if (source._fn !== undefined) recompute(source);
|
|
11375
|
+
}
|
|
10047
11376
|
schedule();
|
|
10048
11377
|
});
|
|
10049
11378
|
});
|
|
@@ -10244,9 +11573,13 @@ export {
|
|
|
10244
11573
|
omit,
|
|
10245
11574
|
onCleanup,
|
|
10246
11575
|
onSettled,
|
|
11576
|
+
patchableRaw,
|
|
10247
11577
|
peekNextChildId,
|
|
10248
11578
|
reconcile,
|
|
10249
11579
|
refresh,
|
|
11580
|
+
registerPatch,
|
|
11581
|
+
registerRowOps,
|
|
11582
|
+
registerSlotPatchNext as registerSlotPatch,
|
|
10250
11583
|
releaseSnapshotScope,
|
|
10251
11584
|
repeat,
|
|
10252
11585
|
resetErrorHalt,
|
|
@@ -10255,6 +11588,9 @@ export {
|
|
|
10255
11588
|
setContext,
|
|
10256
11589
|
setSnapshotCapture,
|
|
10257
11590
|
snapshot,
|
|
11591
|
+
storeHasFamily,
|
|
11592
|
+
storeHasOptimisticFamily,
|
|
11593
|
+
storeIsShallow,
|
|
10258
11594
|
storePath,
|
|
10259
11595
|
untrack
|
|
10260
11596
|
};
|