@solidjs/signals 2.0.0-rc.3 → 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 +1334 -94
- package/dist/node.cjs +2589 -1367
- 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 +36 -33
- package/dist/prod/core/owner.js +13 -13
- package/dist/prod/core/scheduler.js +131 -85
- package/dist/prod/core/verdict.js +29 -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/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/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/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 +2 -2
package/dist/dev.js
CHANGED
|
@@ -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
|
}
|
|
@@ -4617,6 +4743,9 @@ function runLaneEffects(type) {
|
|
|
4617
4743
|
runQueue(effects, type);
|
|
4618
4744
|
}
|
|
4619
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?.();
|
|
4620
4749
|
}
|
|
4621
4750
|
function cleanupCompletedLanes(completingTransition) {
|
|
4622
4751
|
for (const lane of activeLanes) {
|
|
@@ -5161,8 +5290,22 @@ function pendingCheckRead(el, c, owner, firewall) {
|
|
|
5161
5290
|
*/
|
|
5162
5291
|
function heldAwaitingAsync(el) {
|
|
5163
5292
|
const et = el._transition;
|
|
5164
|
-
const t = et ? currentTransition(et) :
|
|
5293
|
+
const t = et ? currentTransition(et) : activeTransition;
|
|
5165
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;
|
|
5166
5309
|
for (const [source, reporters] of t._asyncReporters) {
|
|
5167
5310
|
if (
|
|
5168
5311
|
reporters.size &&
|
|
@@ -5279,7 +5422,6 @@ function effect(compute, effect, error, options) {
|
|
|
5279
5422
|
effect,
|
|
5280
5423
|
error,
|
|
5281
5424
|
isUser ? EFFECT_USER : EFFECT_RENDER,
|
|
5282
|
-
notifyEffectStatus,
|
|
5283
5425
|
options
|
|
5284
5426
|
);
|
|
5285
5427
|
recompute(node, true);
|
|
@@ -5452,17 +5594,9 @@ function trackedEffect(fn, options) {
|
|
|
5452
5594
|
node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
|
|
5453
5595
|
node._modified = true;
|
|
5454
5596
|
node._type = EFFECT_TRACKED;
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
node._queue.notify(node, STATUS_PENDING, 0);
|
|
5459
|
-
const err = error !== undefined ? error : node._x?._error;
|
|
5460
|
-
if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
|
|
5461
|
-
haltReactivity(unwrapStatusError(err));
|
|
5462
|
-
throw err;
|
|
5463
|
-
}
|
|
5464
|
-
}
|
|
5465
|
-
};
|
|
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.
|
|
5466
5600
|
node._run = run;
|
|
5467
5601
|
node._queue.enqueue(EFFECT_USER, run);
|
|
5468
5602
|
if (!node._parent) {
|
|
@@ -5480,6 +5614,10 @@ function trackedEffect(fn, options) {
|
|
|
5480
5614
|
console.warn(message);
|
|
5481
5615
|
}
|
|
5482
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);
|
|
5483
5621
|
|
|
5484
5622
|
const ACTION_CALLED_IN_OWNED_SCOPE_MESSAGE =
|
|
5485
5623
|
"[ACTION_CALLED_IN_OWNED_SCOPE] Calling an action inside an owned scope (component, computation) is not allowed. " +
|
|
@@ -6100,6 +6238,15 @@ let optHooks = null;
|
|
|
6100
6238
|
function setOptHooks(h) {
|
|
6101
6239
|
optHooks = h;
|
|
6102
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
|
+
}
|
|
6103
6250
|
|
|
6104
6251
|
/**
|
|
6105
6252
|
* Brand symbols used internally by the store proxy / projection plumbing.
|
|
@@ -6448,8 +6595,9 @@ function notifyMarkBoundaries(node) {
|
|
|
6448
6595
|
visited.add(sub);
|
|
6449
6596
|
// Display consumers (render effects, boundary computeds) act on the
|
|
6450
6597
|
// notification; descent stops there, exactly like the status rails.
|
|
6451
|
-
|
|
6452
|
-
|
|
6598
|
+
const notify = statusNotifierOf(sub);
|
|
6599
|
+
if (notify) {
|
|
6600
|
+
notify.call(sub, STATUS_PENDING, error);
|
|
6453
6601
|
return;
|
|
6454
6602
|
}
|
|
6455
6603
|
forEachDependent(sub, visit);
|
|
@@ -6557,6 +6705,15 @@ function affects(target, key) {
|
|
|
6557
6705
|
}
|
|
6558
6706
|
}
|
|
6559
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
|
+
|
|
6560
6717
|
/**
|
|
6561
6718
|
* Store rewrite — increment 2: plain deep stores with pending-backing writes.
|
|
6562
6719
|
* Contract: INTERNALS-STORE-STATE.md.
|
|
@@ -6585,8 +6742,13 @@ function affects(target, key) {
|
|
|
6585
6742
|
* headroom for future fields. The prototype is reset to `Object.prototype`
|
|
6586
6743
|
* so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
|
|
6587
6744
|
* plain object's. Array targets keep the bare-`[]` path — they must carry
|
|
6588
|
-
* the array exotic class for `Array.isArray(proxy)
|
|
6589
|
-
*
|
|
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. */
|
|
6590
6752
|
function TargetShape() {
|
|
6591
6753
|
this.v = undefined;
|
|
6592
6754
|
this.ch = undefined;
|
|
@@ -6607,9 +6769,15 @@ function TargetShape() {
|
|
|
6607
6769
|
this.s = undefined;
|
|
6608
6770
|
this.ovl = undefined;
|
|
6609
6771
|
this.del = undefined;
|
|
6610
|
-
this.
|
|
6772
|
+
this.pc = undefined;
|
|
6773
|
+
this.hv = undefined;
|
|
6774
|
+
this.ht = undefined;
|
|
6611
6775
|
}
|
|
6612
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
|
+
}
|
|
6613
6781
|
function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
6614
6782
|
// The proxy target carries the array exotic class when the value is an
|
|
6615
6783
|
// array, so Array.isArray(proxy) is true; the fields live on it directly.
|
|
@@ -6626,6 +6794,7 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
6626
6794
|
t.h = null;
|
|
6627
6795
|
t.k = null;
|
|
6628
6796
|
t.dk = null;
|
|
6797
|
+
t.pc = null;
|
|
6629
6798
|
t.u = parent;
|
|
6630
6799
|
t.pk = parentKey;
|
|
6631
6800
|
t.px = null;
|
|
@@ -6638,7 +6807,8 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
|
|
|
6638
6807
|
t.s = false;
|
|
6639
6808
|
t.ovl = false;
|
|
6640
6809
|
t.del = null;
|
|
6641
|
-
t.
|
|
6810
|
+
t.hv = null;
|
|
6811
|
+
t.ht = null;
|
|
6642
6812
|
t.px = new Proxy(t, traps);
|
|
6643
6813
|
// Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
|
|
6644
6814
|
// proxy off looked-up targets as a field.
|
|
@@ -6819,13 +6989,6 @@ function getDeepNode(target) {
|
|
|
6819
6989
|
function bumpDeep(t) {
|
|
6820
6990
|
if (t.dk !== null) setSignal(t.dk, 1);
|
|
6821
6991
|
}
|
|
6822
|
-
function markDescendants(target) {
|
|
6823
|
-
let t = target;
|
|
6824
|
-
while (t && !t.d) {
|
|
6825
|
-
t.d = true;
|
|
6826
|
-
t = t.u;
|
|
6827
|
-
}
|
|
6828
|
-
}
|
|
6829
6992
|
// ---------------------------------------------------------------------------
|
|
6830
6993
|
// pending backing + fold (the single mutation point)
|
|
6831
6994
|
/** target → committed backing at batch start (the fold diff's old side). */
|
|
@@ -6850,6 +7013,12 @@ function cloneRaw(source, t) {
|
|
|
6850
7013
|
? Object.defineProperties([], descs)
|
|
6851
7014
|
: Object.create(Object.getPrototypeOf(source), descs);
|
|
6852
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
|
+
}
|
|
6853
7022
|
/** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
|
|
6854
7023
|
* returns true when the container is plain data (overlay-safe). */
|
|
6855
7024
|
function scanAccessorsOnce(target) {
|
|
@@ -6891,6 +7060,7 @@ function materializePB(target) {
|
|
|
6891
7060
|
target.ovl = false;
|
|
6892
7061
|
}
|
|
6893
7062
|
function ensurePB(target) {
|
|
7063
|
+
if (activeTransition !== null) foldBatches.set(target, activeTransition);
|
|
6894
7064
|
let pb = target.pb;
|
|
6895
7065
|
if (pb === null) {
|
|
6896
7066
|
// Prototype-chain overlay (#3044): plain-data non-array containers
|
|
@@ -6934,6 +7104,25 @@ function ensurePB(target) {
|
|
|
6934
7104
|
}
|
|
6935
7105
|
return pb;
|
|
6936
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
|
+
}
|
|
6937
7126
|
/**
|
|
6938
7127
|
* Adoption (2026-08-16c): the incoming object becomes the committed backing
|
|
6939
7128
|
* IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
|
|
@@ -6949,6 +7138,20 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
6949
7138
|
if (!eager) {
|
|
6950
7139
|
queueFold(target); // records the pre-batch old before we swap
|
|
6951
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
|
+
}
|
|
6952
7155
|
}
|
|
6953
7156
|
target.pb = null;
|
|
6954
7157
|
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
@@ -6960,24 +7163,43 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
6960
7163
|
// draft rescans once (#3044 audit follow-up).
|
|
6961
7164
|
target.ovl = false;
|
|
6962
7165
|
target.del = null;
|
|
6963
|
-
target.wk = null; // adoption supersedes any staged trap writes
|
|
6964
7166
|
target.sc = false;
|
|
6965
7167
|
target.a = false;
|
|
7168
|
+
if (target.pc !== null) target.pc.wk = null; // adoption supersedes staged trap writes
|
|
6966
7169
|
target.v = incoming;
|
|
6967
7170
|
target.ch = incoming[$TARGET] !== undefined;
|
|
6968
7171
|
(target.fam?.map ?? storeNextLookup).set(incoming, target);
|
|
6969
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
|
+
};
|
|
6970
7180
|
function queueFold(target) {
|
|
6971
7181
|
if (foldOlds.has(target)) return;
|
|
6972
|
-
if (
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
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();
|
|
6979
7193
|
foldOlds.set(target, target.v);
|
|
6980
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();
|
|
6981
7203
|
/** Committed-time privatization for parent-chain slot updates (path copying). */
|
|
6982
7204
|
function privatizeCommitted(target) {
|
|
6983
7205
|
if (ownedRaw.has(target.v)) return;
|
|
@@ -6997,7 +7219,27 @@ function drainFolds() {
|
|
|
6997
7219
|
const entries = [...foldOlds];
|
|
6998
7220
|
foldOlds.clear();
|
|
6999
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;
|
|
7000
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
|
+
}
|
|
7001
7243
|
// Setter path: nodes were setSignal'd at setter exit (write-time
|
|
7002
7244
|
// notification — transitions/holds ride core machinery). Commit the
|
|
7003
7245
|
// backing only for keys whose nodes have committed; a still-pending
|
|
@@ -7009,9 +7251,14 @@ function drainFolds() {
|
|
|
7009
7251
|
// Only written keys can hold (their nodes took the setSignal); the
|
|
7010
7252
|
// wk bound keeps this O(written) — see notifyWrites. Same fallback
|
|
7011
7253
|
// rules as the notify (WK_ALL / accessors / non-plain prototypes).
|
|
7012
|
-
const wkh = t.wk;
|
|
7254
|
+
const wkh = t.pc !== null ? t.pc.wk : null;
|
|
7013
7255
|
const keys =
|
|
7014
|
-
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)
|
|
7015
7262
|
? Reflect.ownKeys(nodes)
|
|
7016
7263
|
: wkh;
|
|
7017
7264
|
for (const key of keys) {
|
|
@@ -7049,15 +7296,70 @@ function drainFolds() {
|
|
|
7049
7296
|
(t.fam?.map ?? storeNextLookup).delete(pb);
|
|
7050
7297
|
t.pb = null;
|
|
7051
7298
|
t.ovl = false;
|
|
7052
|
-
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
|
|
7053
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);
|
|
7054
7322
|
t.v = pb;
|
|
7055
7323
|
t.ch = false; // pb is always a plain clone
|
|
7056
7324
|
t.pb = null;
|
|
7057
|
-
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);
|
|
7058
7361
|
}
|
|
7059
7362
|
}
|
|
7060
|
-
if (t.v === old) continue; // adopted then re-adopted back, or no-op
|
|
7061
7363
|
// Path copying (CAS: see the eager-fold twin above).
|
|
7062
7364
|
if (t.u && t.u.v[t.pk] === old) {
|
|
7063
7365
|
privatizeCommitted(t.u);
|
|
@@ -7078,17 +7380,6 @@ function drainFolds() {
|
|
|
7078
7380
|
* "pending home = the node when a node exists"). Unobserved keys stay in the
|
|
7079
7381
|
* pending backing and fold directly at commit.
|
|
7080
7382
|
*/
|
|
7081
|
-
/** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
|
|
7082
|
-
* array length write implicitly deleted indices) — consumers full-scan. */
|
|
7083
|
-
const WK_ALL = new Set();
|
|
7084
|
-
/** Plain-prototype check for the written-keys bound: prototype getters on
|
|
7085
|
-
* class instances can derive from ANY field, so only plain-data containers
|
|
7086
|
-
* may bound the notify to written keys. Overlay pbs chain to the COMMITTED
|
|
7087
|
-
* object (#3044), so overlay plainness is judged on the committed proto. */
|
|
7088
|
-
const plainProto = o => {
|
|
7089
|
-
const p = Object.getPrototypeOf(o);
|
|
7090
|
-
return p === Object.prototype || p === Array.prototype || p === null;
|
|
7091
|
-
};
|
|
7092
7383
|
function notifyWrites(t) {
|
|
7093
7384
|
let pb = t.pb;
|
|
7094
7385
|
if (pb === null) return;
|
|
@@ -7143,8 +7434,15 @@ function notifyWrites(t) {
|
|
|
7143
7434
|
// not a full scan). Falls back to the full node scan when the bound can't
|
|
7144
7435
|
// hold: no trap granularity (wk null), an array length write (WK_ALL —
|
|
7145
7436
|
// implicit index deletes), accessors on the record (t.a — a getter node's
|
|
7146
|
-
// value can change when ANY key is written), or a non-plain prototype
|
|
7147
|
-
|
|
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).
|
|
7148
7446
|
const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
|
|
7149
7447
|
if (nodes !== null) {
|
|
7150
7448
|
const keys = writtenKeys ?? Reflect.ownKeys(nodes);
|
|
@@ -7177,15 +7475,18 @@ function notifyWrites(t) {
|
|
|
7177
7475
|
}
|
|
7178
7476
|
const has = t.h;
|
|
7179
7477
|
if (has !== null) {
|
|
7180
|
-
|
|
7181
|
-
|
|
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
|
+
}
|
|
7182
7483
|
}
|
|
7183
7484
|
// Deep-witness (dk): setter writes must notify a deep() subscriber even on
|
|
7184
|
-
// keys with no node. O(pb keys) equality only when a witness exists.
|
|
7485
|
+
// keys with no node. O(written/pb keys) equality only when a witness exists.
|
|
7185
7486
|
if (t.dk !== null) {
|
|
7186
7487
|
if (t.del !== null && t.del.size !== 0) bumpDeep(t);
|
|
7187
7488
|
else
|
|
7188
|
-
for (const key of Reflect.ownKeys(pb)) {
|
|
7489
|
+
for (const key of writtenKeys ?? Reflect.ownKeys(pb)) {
|
|
7189
7490
|
const nv = pb[key];
|
|
7190
7491
|
const ov = old[key];
|
|
7191
7492
|
if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
|
|
@@ -7215,6 +7516,13 @@ function notifyWrites(t) {
|
|
|
7215
7516
|
}
|
|
7216
7517
|
if (changed) setSignal(t.k, v => v + 1);
|
|
7217
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);
|
|
7218
7526
|
// Projection backing folds split by channel (two pinned contracts):
|
|
7219
7527
|
// - sync-derive drafts (recompute body): NEVER eager — a downstream async
|
|
7220
7528
|
// hold can form LATER in the same flush and the leaf must stay at stale
|
|
@@ -7226,9 +7534,11 @@ function notifyWrites(t) {
|
|
|
7226
7534
|
// downstream consumer's own async still holds the effect-level reveal
|
|
7227
7535
|
// (spec-async "verdicts never inherit consumers' in-flight state").
|
|
7228
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;
|
|
7229
7540
|
const oldBacking = t.v;
|
|
7230
7541
|
t.pb = null;
|
|
7231
|
-
t.wk = null; // written-keys window closes with the eager fold
|
|
7232
7542
|
t.v = pb;
|
|
7233
7543
|
t.ch = false;
|
|
7234
7544
|
if (t.u && t.u.v[t.pk] === oldBacking) {
|
|
@@ -7433,6 +7743,17 @@ function inOwnerContext() {
|
|
|
7433
7743
|
const eff = c._root ? c._parentComputed : c;
|
|
7434
7744
|
return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
7435
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
|
+
}
|
|
7436
7757
|
/** A pending fold is transition-held when any written node's parked value is
|
|
7437
7758
|
* stamped by a live transition (a plain batch parking — the lazy-recompute
|
|
7438
7759
|
* read case — has no transition stamp and serves fresh). */
|
|
@@ -7451,6 +7772,20 @@ function foldHeld(target) {
|
|
|
7451
7772
|
return false;
|
|
7452
7773
|
}
|
|
7453
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
|
+
}
|
|
7454
7789
|
// Signal-parity visibility (core read(): owner-context reads serve
|
|
7455
7790
|
// _pendingValue, context-free reads serve committed — effects recompute
|
|
7456
7791
|
// BEFORE commitPendingNodes in the flush, so the pending view must be
|
|
@@ -7464,8 +7799,10 @@ function readSource(target) {
|
|
|
7464
7799
|
inOwnerContext() ||
|
|
7465
7800
|
// A projection's pending backing is authoritative-elect: serve it to
|
|
7466
7801
|
// context-free readers too UNLESS a transition is holding the node
|
|
7467
|
-
// commits (downstream async hold — stale committed is the contract)
|
|
7468
|
-
|
|
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()))
|
|
7469
7806
|
)
|
|
7470
7807
|
return target.pb;
|
|
7471
7808
|
return target.v;
|
|
@@ -7509,9 +7846,11 @@ function hasActiveOverride(node) {
|
|
|
7509
7846
|
* FORCE sentinels never surface (they only bump subscribers of accessor
|
|
7510
7847
|
* keys, which are served by the trap, not the node). */
|
|
7511
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.
|
|
7512
7851
|
const v = hasActiveOverride(node)
|
|
7513
7852
|
? unwrapOverride(node._x?._overrideValue)
|
|
7514
|
-
: node._pendingValue !== NOT_PENDING && inOwnerContext()
|
|
7853
|
+
: node._pendingValue !== NOT_PENDING && (latestReadActive || inOwnerContext())
|
|
7515
7854
|
? node._pendingValue
|
|
7516
7855
|
: backing;
|
|
7517
7856
|
return v === FORCE ? backing : v;
|
|
@@ -7602,6 +7941,27 @@ function firewallGate(target) {
|
|
|
7602
7941
|
const fw = target.fam?.node;
|
|
7603
7942
|
if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
|
|
7604
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
|
+
}
|
|
7605
7965
|
const traps = {
|
|
7606
7966
|
get(target, key, receiver) {
|
|
7607
7967
|
// One typeof gates every brand-symbol compare off the hot string path
|
|
@@ -7629,6 +7989,11 @@ const traps = {
|
|
|
7629
7989
|
}
|
|
7630
7990
|
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
7631
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);
|
|
7632
7997
|
const src = readSource(target);
|
|
7633
7998
|
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
7634
7999
|
// deleted keys are tracked aside and read as absent in the pending view.
|
|
@@ -7852,14 +8217,15 @@ const traps = {
|
|
|
7852
8217
|
// Array length writes implicitly delete indices — the written-keys bound
|
|
7853
8218
|
// can't see them, so poison to the full scan for this batch. Index
|
|
7854
8219
|
// writes implicitly GROW length, so arrays always record it alongside.
|
|
8220
|
+
const pcs = pcOf(target);
|
|
7855
8221
|
if (Array.isArray(pb)) {
|
|
7856
|
-
if (key === "length")
|
|
7857
|
-
else if (
|
|
7858
|
-
const wk = (
|
|
8222
|
+
if (key === "length") pcs.wk = WK_ALL;
|
|
8223
|
+
else if (pcs.wk !== WK_ALL) {
|
|
8224
|
+
const wk = (pcs.wk ??= new Set());
|
|
7859
8225
|
wk.add(key);
|
|
7860
8226
|
wk.add("length");
|
|
7861
8227
|
}
|
|
7862
|
-
} else if (
|
|
8228
|
+
} else if (pcs.wk !== WK_ALL) (pcs.wk ??= new Set()).add(key);
|
|
7863
8229
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
7864
8230
|
// defineProperty sidesteps a proto-chain setter named the same.
|
|
7865
8231
|
if (UNSAFE_KEYS.has(key)) {
|
|
@@ -7897,12 +8263,20 @@ const traps = {
|
|
|
7897
8263
|
const override = !draft && getWriteOverride();
|
|
7898
8264
|
if (!draft && !override) return true;
|
|
7899
8265
|
if (key === "__proto__") return true;
|
|
7900
|
-
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
|
+
}
|
|
7901
8274
|
// Unwrap before ensurePB (see the set trap: self-reference materializes).
|
|
7902
8275
|
if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
|
|
7903
8276
|
const pb = ensurePB(target);
|
|
7904
8277
|
pendingNotify.add(target);
|
|
7905
|
-
|
|
8278
|
+
const pcd = pcOf(target);
|
|
8279
|
+
if (pcd.wk !== WK_ALL) (pcd.wk ??= new Set()).add(key);
|
|
7906
8280
|
Object.defineProperty(pb, key, desc);
|
|
7907
8281
|
if (target.del !== null) target.del.delete(key);
|
|
7908
8282
|
if (override) notifyWrites(target);
|
|
@@ -7914,7 +8288,8 @@ const traps = {
|
|
|
7914
8288
|
if (!draft && !override) return true;
|
|
7915
8289
|
const pb = ensurePB(target);
|
|
7916
8290
|
pendingNotify.add(target);
|
|
7917
|
-
|
|
8291
|
+
const pcx = pcOf(target);
|
|
8292
|
+
if (pcx.wk !== WK_ALL) (pcx.wk ??= new Set()).add(key);
|
|
7918
8293
|
delete pb[key];
|
|
7919
8294
|
// A prototype overlay cannot shadow a delete of a committed key —
|
|
7920
8295
|
// record it aside (#3044); reads/has/ownKeys/commit consult the set.
|
|
@@ -7987,8 +8362,39 @@ function createStoreNext(init, shallow = false) {
|
|
|
7987
8362
|
const setter = fn => storeSetterNext(proxy, fn);
|
|
7988
8363
|
return [proxy, setter];
|
|
7989
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
|
+
}
|
|
7990
8396
|
/** Tracking deep snapshot (`deep()` for next targets): subscribes to the
|
|
7991
|
-
* key-set and
|
|
8397
|
+
* key-set and deep-witness node at every reachable level, then returns the
|
|
7992
8398
|
* plain view. Shared references and cycles handled via the visited set. */
|
|
7993
8399
|
function deepNext(value) {
|
|
7994
8400
|
const t0 = value?.[$TARGET];
|
|
@@ -8188,7 +8594,7 @@ function reconcileNextState(value, state, key, replace = false) {
|
|
|
8188
8594
|
// positional so old-entity subtrees never merge into the new entity's).
|
|
8189
8595
|
const prev = t.pb ?? t.v;
|
|
8190
8596
|
const eq = keyFn(prev);
|
|
8191
|
-
if (eq !== undefined && keyFn(incoming)
|
|
8597
|
+
if (eq !== undefined && !sameKey(keyFn(incoming), eq)) {
|
|
8192
8598
|
if (!replace) throw new Error("Cannot reconcile states with different identity");
|
|
8193
8599
|
// Entity change: wholesale swap. The root proxy is stable for life
|
|
8194
8600
|
// (proj R5) but NOTHING below survives — children are never matched
|
|
@@ -8229,6 +8635,33 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8229
8635
|
const shallow = t.s === true;
|
|
8230
8636
|
const old = t.v;
|
|
8231
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
|
+
}
|
|
8232
8665
|
// Shallow adoption: records are slot values — sticky raw-mark the incoming
|
|
8233
8666
|
// set (R41) and never descend; slot notification is the positional diff.
|
|
8234
8667
|
if (shallow) markRawIngest(incoming);
|
|
@@ -8268,7 +8701,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8268
8701
|
typeof pvRaw === "object" &&
|
|
8269
8702
|
nv !== null &&
|
|
8270
8703
|
typeof nv === "object" &&
|
|
8271
|
-
keyFn(pvRaw)
|
|
8704
|
+
sameKey(keyFn(pvRaw), keyFn(nv))
|
|
8272
8705
|
)
|
|
8273
8706
|
)
|
|
8274
8707
|
break; // misaligned: fall to the keyed remainder below
|
|
@@ -8296,6 +8729,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8296
8729
|
}
|
|
8297
8730
|
}
|
|
8298
8731
|
if (t.dk !== null && !dkBumpedA && i < nextRows.length) bumpDeep(t);
|
|
8732
|
+
const structStart = i; // misalignment point (== nlen on aligned ticks)
|
|
8299
8733
|
let prevByKey = null;
|
|
8300
8734
|
for (; i < nextRows.length; i++) {
|
|
8301
8735
|
const nv = nextRows[i];
|
|
@@ -8305,16 +8739,39 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8305
8739
|
let pv;
|
|
8306
8740
|
if (nk !== undefined) {
|
|
8307
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).
|
|
8308
8748
|
prevByKey = new Map();
|
|
8309
|
-
|
|
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++) {
|
|
8310
8755
|
const p = unwrapValue(prevRows[j]);
|
|
8311
8756
|
if (p !== null && typeof p === "object") {
|
|
8312
8757
|
const pk = keyFn(p);
|
|
8313
|
-
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]);
|
|
8314
8763
|
}
|
|
8315
8764
|
}
|
|
8316
8765
|
}
|
|
8317
|
-
|
|
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
|
+
}
|
|
8318
8775
|
} else {
|
|
8319
8776
|
pv = unwrapValue(prevRows[i]); // keyless item: positional fallback
|
|
8320
8777
|
}
|
|
@@ -8328,12 +8785,62 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8328
8785
|
}
|
|
8329
8786
|
}
|
|
8330
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);
|
|
8331
8798
|
} else {
|
|
8332
8799
|
const dlen = Math.min(prevRows.length, nextRows.length);
|
|
8333
8800
|
const nlen = nextRows.length;
|
|
8334
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;
|
|
8335
8810
|
for (let i = 0; i < nlen; i++) {
|
|
8336
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
|
+
}
|
|
8337
8844
|
if (!shallow && i < dlen && nvP !== null && typeof nvP === "object")
|
|
8338
8845
|
descend(unwrapValue(prevRows[i]), nvP, keyFn, fam, proj);
|
|
8339
8846
|
if (
|
|
@@ -8354,6 +8861,15 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8354
8861
|
}
|
|
8355
8862
|
}
|
|
8356
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
|
+
}
|
|
8357
8873
|
}
|
|
8358
8874
|
if (eager) {
|
|
8359
8875
|
if (nodes !== null && nodesHit < t.nc) {
|
|
@@ -8374,6 +8890,22 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8374
8890
|
// slots must not notify, R9). This replaces the notifyFold re-walk that
|
|
8375
8891
|
// doubled dbmon's diff cost. for-in covers own enumerable string keys
|
|
8376
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
|
+
}
|
|
8377
8909
|
const nodes = eager ? t.n : null;
|
|
8378
8910
|
let nodesHit = 0;
|
|
8379
8911
|
let dkBumped = false;
|
|
@@ -8439,6 +8971,93 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
8439
8971
|
}
|
|
8440
8972
|
}
|
|
8441
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
|
+
}
|
|
8442
9061
|
function descend(pv, nv, keyFn, fam, proj = false) {
|
|
8443
9062
|
if (pv === null || typeof pv !== "object" || nv === null || typeof nv !== "object") return;
|
|
8444
9063
|
// Lookup FIRST: a hit implies pv was wrappable and never raw-marked (only
|
|
@@ -8462,7 +9081,10 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
8462
9081
|
const nk = keyFn(nv);
|
|
8463
9082
|
// Key mismatch detaches: the slot takes the new entity; the old proxy
|
|
8464
9083
|
// keeps its (old) backing and a fresh proxy wraps the new value on read.
|
|
8465
|
-
|
|
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;
|
|
8466
9088
|
}
|
|
8467
9089
|
// Reachability pruning (§6d) is MODE-dependent, both pinned:
|
|
8468
9090
|
// - keyed matching descends only where subscriptions exist at/below (`d`) —
|
|
@@ -8701,6 +9323,558 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
|
|
|
8701
9323
|
return owner;
|
|
8702
9324
|
}
|
|
8703
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
|
+
|
|
8704
9878
|
/**
|
|
8705
9879
|
* Store rewrite — optimistic stores (§3/§7, RUL-3): no store-side layer, no
|
|
8706
9880
|
* backup snapshots. Nodes in an optimistic family are ARMED core signals
|
|
@@ -8730,6 +9904,23 @@ function installNextBlockedHalf() {
|
|
|
8730
9904
|
// so the hook only empties the batch set.
|
|
8731
9905
|
if (!GlobalQueue._clearOptimisticStores) {
|
|
8732
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
|
+
}
|
|
8733
9924
|
stores.clear();
|
|
8734
9925
|
};
|
|
8735
9926
|
}
|
|
@@ -8836,6 +10027,22 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
8836
10027
|
const fw = t.fam?.node;
|
|
8837
10028
|
if (fw?._transition) globalQueue.initTransition(fw._transition);
|
|
8838
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
|
+
}
|
|
8839
10046
|
const visible = (key, fallback) => {
|
|
8840
10047
|
const node = t.n?.[key];
|
|
8841
10048
|
return node !== undefined && hasActiveOverride(node)
|
|
@@ -8963,6 +10170,10 @@ function consumeOverridesNext(fam) {
|
|
|
8963
10170
|
insertSubs(t.k, true);
|
|
8964
10171
|
schedule();
|
|
8965
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);
|
|
8966
10177
|
}
|
|
8967
10178
|
overlaid.clear();
|
|
8968
10179
|
});
|
|
@@ -9011,7 +10222,9 @@ function applyTentative(t, incoming, keyFn) {
|
|
|
9011
10222
|
if (keyFn) {
|
|
9012
10223
|
const pk = keyFn(pv);
|
|
9013
10224
|
const nk = keyFn(nv);
|
|
9014
|
-
|
|
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;
|
|
9015
10228
|
}
|
|
9016
10229
|
return map.get(unwrapValue(pv)) ?? null;
|
|
9017
10230
|
};
|
|
@@ -9026,16 +10239,31 @@ function applyTentative(t, incoming, keyFn) {
|
|
|
9026
10239
|
const nk = keyFn(nv);
|
|
9027
10240
|
if (nk !== undefined) {
|
|
9028
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.
|
|
9029
10245
|
viewByKey = new Map();
|
|
9030
10246
|
for (let j = 0; j < viewRows.length; j++) {
|
|
9031
10247
|
const p = unwrapValue(viewRows[j]);
|
|
9032
10248
|
if (isWrappable(p)) {
|
|
9033
10249
|
const pk = keyFn(p);
|
|
9034
|
-
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]);
|
|
9035
10255
|
}
|
|
9036
10256
|
}
|
|
9037
10257
|
}
|
|
9038
|
-
|
|
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
|
+
}
|
|
9039
10267
|
} else pv = unwrapValue(viewRows[i]);
|
|
9040
10268
|
} else pv = unwrapValue(viewRows[i]);
|
|
9041
10269
|
const ct = match(pv, nv);
|
|
@@ -10139,7 +11367,12 @@ function createLoadingBoundary(fn, fallback, options) {
|
|
|
10139
11367
|
function createErrorBoundary(fn, fallback) {
|
|
10140
11368
|
return createCollectionBoundary(STATUS_ERROR, fn, queue => {
|
|
10141
11369
|
return fallback(accessor(queue._error), () => {
|
|
10142
|
-
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
|
+
}
|
|
10143
11376
|
schedule();
|
|
10144
11377
|
});
|
|
10145
11378
|
});
|
|
@@ -10340,9 +11573,13 @@ export {
|
|
|
10340
11573
|
omit,
|
|
10341
11574
|
onCleanup,
|
|
10342
11575
|
onSettled,
|
|
11576
|
+
patchableRaw,
|
|
10343
11577
|
peekNextChildId,
|
|
10344
11578
|
reconcile,
|
|
10345
11579
|
refresh,
|
|
11580
|
+
registerPatch,
|
|
11581
|
+
registerRowOps,
|
|
11582
|
+
registerSlotPatchNext as registerSlotPatch,
|
|
10346
11583
|
releaseSnapshotScope,
|
|
10347
11584
|
repeat,
|
|
10348
11585
|
resetErrorHalt,
|
|
@@ -10351,6 +11588,9 @@ export {
|
|
|
10351
11588
|
setContext,
|
|
10352
11589
|
setSnapshotCapture,
|
|
10353
11590
|
snapshot,
|
|
11591
|
+
storeHasFamily,
|
|
11592
|
+
storeHasOptimisticFamily,
|
|
11593
|
+
storeIsShallow,
|
|
10354
11594
|
storePath,
|
|
10355
11595
|
untrack
|
|
10356
11596
|
};
|