@solidjs/signals 2.0.0-rc.10 → 2.0.0-rc.13
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-shared.js +128 -60
- package/dist/dev.attribution.js +27 -12
- package/dist/dev.js +242 -32
- package/dist/observe/core/attribution.js +36 -21
- package/dist/observe/core/constants.js +21 -12
- package/dist/observe/core/core.js +44 -34
- package/dist/observe/core/dev.js +38 -15
- package/dist/observe/core/optimistic.js +65 -23
- package/dist/observe/core/owner.js +11 -11
- package/dist/observe/core/scheduler.js +26 -12
- package/dist/observe/core/verdict.js +2 -2
- package/dist/observe/map.js +241 -218
- package/dist/observe/store/next/store.js +159 -31
- package/dist/prod/core/constants.js +21 -12
- package/dist/prod/core/core.js +27 -17
- package/dist/prod/core/optimistic.js +62 -20
- package/dist/prod/core/owner.js +2 -2
- package/dist/prod/core/scheduler.js +29 -15
- package/dist/prod/core/verdict.js +1 -1
- package/dist/prod/map.js +200 -177
- package/dist/prod/store/next/optimistic.js +2 -2
- package/dist/prod/store/next/store.js +201 -73
- package/dist/types/core/attribution-hooks.d.ts +28 -0
- package/dist/types/core/attribution.d.ts +9 -0
- package/dist/types/core/constants.d.ts +21 -12
- package/dist/types/core/dev.d.ts +59 -6
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/scheduler.d.ts +5 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/signals.d.ts +18 -1
- package/dist/types/store/next/store.d.ts +4 -2
- package/package.json +1 -1
|
@@ -208,6 +208,8 @@ r = -1) {
|
|
|
208
208
|
// committed is the held view `hv[key]`, staged is `v[key]`.
|
|
209
209
|
const l = heldFoldTransition(e);
|
|
210
210
|
let s = heldAdoptionTransition(e);
|
|
211
|
+
// A key the adoption left unchanged is not born holding (#3706).
|
|
212
|
+
if (s !== null && !heldKey(e, t)) s = null;
|
|
211
213
|
if (s !== null) n = e.hv[t]; else if ((s = l) !== null) n = e.v[t];
|
|
212
214
|
// Create-floor diet: slotSignal bakes the whole node into one literal —
|
|
213
215
|
// no options object, no equals/unobserved closures, no NodeExtension,
|
|
@@ -315,6 +317,33 @@ r = -1) {
|
|
|
315
317
|
return true;
|
|
316
318
|
}
|
|
317
319
|
|
|
320
|
+
// #3706: the keys an adoption under a live hold changed against the held
|
|
321
|
+
// view — the adoption twin of `wk` (#3688), keyed on the target. adoptPB
|
|
322
|
+
// records `[adopted object]`; the keys are diffed against IT, once, on the
|
|
323
|
+
// first held read (its children are re-pointed by then), and replace the
|
|
324
|
+
// entry. Never against the live backing — a mainline write during the hold
|
|
325
|
+
// replaces that, and its key is not the adoption's. WK_ALL holds the whole
|
|
326
|
+
// container.
|
|
327
|
+
const heldKeys = new WeakMap;
|
|
328
|
+
|
|
329
|
+
function heldKey(e, t) {
|
|
330
|
+
let n = heldKeys.get(e);
|
|
331
|
+
if (Array.isArray(n)) heldKeys.set(e, n = adoptionChangedKeys(e, n[0]));
|
|
332
|
+
return n === WK_ALL || n.has(t);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function adoptionChangedKeys(e, t) {
|
|
336
|
+
const n = e.hv;
|
|
337
|
+
// A chained backing, an optimistic family, a chained held view, a swapped
|
|
338
|
+
// or non-plain prototype (inherited accessors read through `this`): whole.
|
|
339
|
+
if (t[$TARGET] !== undefined || e.fam?.opt === true || n[$TARGET] !== undefined || Object.getPrototypeOf(n) !== Object.getPrototypeOf(t) || !plainProto(t)) return WK_ALL;
|
|
340
|
+
const r = new Set;
|
|
341
|
+
// Accessors are never invoked: they and a flipped enumerability are changes.
|
|
342
|
+
for (const i of Reflect.ownKeys(n)) if (!hasOwn.call(t, i) || isOwnAccessor(n, i) || isOwnAccessor(t, i) || propertyIsEnumerable.call(n, i) !== propertyIsEnumerable.call(t, i) || !(isEqual(n[i], t[i]) || sameLogicalSlot(e, n[i], t[i]))) r.add(i);
|
|
343
|
+
for (const e of Reflect.ownKeys(t)) if (!hasOwn.call(n, e)) r.add(e);
|
|
344
|
+
return r;
|
|
345
|
+
}
|
|
346
|
+
|
|
318
347
|
function stageHeldKey(e, t, n) {
|
|
319
348
|
if (slotNodeEquals.call(e, e.ce, t)) return;
|
|
320
349
|
e._e = t;
|
|
@@ -508,6 +537,23 @@ let hookInstalled = false;
|
|
|
508
537
|
return r;
|
|
509
538
|
}
|
|
510
539
|
|
|
540
|
+
/** Shallow copy of a WIDE plain-data (sc 2) backing for the overlay rebuild
|
|
541
|
+
* (#3689). Own keys land on a null-prototype object — V8 creates those in
|
|
542
|
+
* dictionary mode, so the copy is O(keys) hash inserts — and the prototype
|
|
543
|
+
* is attached after. A spread builds a fast-mode object one map transition
|
|
544
|
+
* per key; from a source in dictionary mode (a backing that took deletes)
|
|
545
|
+
* whose key order differs from the last copy's, every transition is a fresh
|
|
546
|
+
* map with an O(keys) descriptor copy: measured 270 µs vs 28 µs at 400 keys,
|
|
547
|
+
* 1.3 ms vs 75 µs at 1000. Narrow, stable-shaped backings keep the spread
|
|
548
|
+
* (cloneRaw): there the clone IC copies the property store at once and is
|
|
549
|
+
* 3–8× cheaper than any per-key loop. */ function wideClone(e, t) {
|
|
550
|
+
const n = Object.create(null);
|
|
551
|
+
for (const t of Reflect.ownKeys(e)) n[t] = e[t];
|
|
552
|
+
Object.setPrototypeOf(n, Object.prototype);
|
|
553
|
+
n[$OWNER] = t;
|
|
554
|
+
return n;
|
|
555
|
+
}
|
|
556
|
+
|
|
511
557
|
/** Copy own `key` from `from` onto `to`. A plain data slot (enumerable,
|
|
512
558
|
* writable, configurable, no accessor) is a bare assignment — the common case
|
|
513
559
|
* and the cheap one; anything else goes through defineProperty so accessors
|
|
@@ -544,21 +590,67 @@ let hookInstalled = false;
|
|
|
544
590
|
* backing into a prototype, and every later flatten writes into that
|
|
545
591
|
* prototype); above it the clone's O(keys) copy dominates (#3044). */ const OVERLAY_MIN_KEYS = 32;
|
|
546
592
|
|
|
593
|
+
/** Overlay commit choice (#3689): flatten in place, or rebuild the backing
|
|
594
|
+
* (materializePB, then the clone path's swap)? Measured on V8 (Node 26): a
|
|
595
|
+
* committed backing that has served as an overlay prototype is kept as a
|
|
596
|
+
* FAST-MODE prototype object up to V8's descriptor limit (1020 own
|
|
597
|
+
* properties), and there every in-place DELETE is O(keys) — normalize to
|
|
598
|
+
* dictionary plus re-optimize as a prototype, ~0.08 µs × keys — and every
|
|
599
|
+
* in-place ADD is a map transition copying the descriptors, ~0.006 µs ×
|
|
600
|
+
* keys. Detaching the overlay first does not help: prototype-ness is a
|
|
601
|
+
* property of the object's map. One rebuild (wideClone, deletes, writes) is
|
|
602
|
+
* ~0.07–0.09 µs × keys — the price of ONE in-place delete. So a fold that
|
|
603
|
+
* deleted anything, or added more than ~16 keys, commits cheaper rebuilt (a
|
|
604
|
+
* 400-key record churning 100 keys per commit, 10,000 membership readers:
|
|
605
|
+
* 4.7 ms → 0.22 ms per step); a pure rewrite of existing keys — the
|
|
606
|
+
* overlay's home case — stays in place at O(written).
|
|
607
|
+
* Past the descriptor limit V8 keeps the backing in dictionary mode where
|
|
608
|
+
* every in-place op is O(1), and a rebuild would be a pure O(keys) loss on
|
|
609
|
+
* exactly the growing-record pattern the overlay exists for (#3044) — the
|
|
610
|
+
* count gate keeps those in place. The gate reads `kc`, which the set trap
|
|
611
|
+
* counts up for keys new to the container and the delete commit counts down. */ const OVERLAY_REBUILD_MAX_KEYS = 1024;
|
|
612
|
+
|
|
613
|
+
const OVERLAY_REBUILD_MIN_ADDS = 16;
|
|
614
|
+
|
|
615
|
+
function overlayRebuilds(e, t) {
|
|
616
|
+
if (e.kc > OVERLAY_REBUILD_MAX_KEYS) return false;
|
|
617
|
+
if (e.del !== null && e.del.size !== 0) return true;
|
|
618
|
+
// Every trap write records its key, so the written-key count bounds the
|
|
619
|
+
// adds — the common few-key fold (#3044's growing record) answers here
|
|
620
|
+
// without enumerating the overlay.
|
|
621
|
+
const n = e.wk;
|
|
622
|
+
if (n !== null && n !== WK_ALL && n.size <= OVERLAY_REBUILD_MIN_ADDS) return false;
|
|
623
|
+
const r = e.v;
|
|
624
|
+
let i = 0;
|
|
625
|
+
for (const e of Reflect.ownKeys(t)) if (!hasOwn.call(r, e) && ++i > OVERLAY_REBUILD_MIN_ADDS) return true;
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
547
629
|
/** Downgrade a prototype-overlay pending backing to the clone path: builds
|
|
548
630
|
* the real container (committed + overlay writes − deletes) that fold will
|
|
549
631
|
* SWAP in as the committed backing, exactly as if the draft had started on
|
|
550
632
|
* the clone path. Consumers that need a complete container (reconcile's
|
|
551
|
-
* diff walks, drafts escaping into other storage) call this
|
|
552
|
-
|
|
633
|
+
* diff walks, drafts escaping into other storage) call this, and so does the
|
|
634
|
+
* commit itself when the fold changed the key set (overlayRebuilds). Returns
|
|
635
|
+
* the pending backing (the clone, or the pb as-is when not an overlay). */ function materializePB(e) {
|
|
553
636
|
const t = e.pb;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
637
|
+
if (!e.ovl) return t;
|
|
638
|
+
const n = e.sc === 2 ? wideClone(e.v, e) : cloneRaw(e.v, e);
|
|
639
|
+
// Deletes before writes (#3689): on a fast-mode clone (the descriptor
|
|
640
|
+
// path) the first delete normalizes it once, and the writes then land as
|
|
641
|
+
// O(1) dictionary adds instead of O(keys) map transitions each. Order is
|
|
642
|
+
// free semantically — a rewritten key left `del` at write time, so no
|
|
643
|
+
// write targets a deleted key.
|
|
644
|
+
if (e.del !== null) {
|
|
557
645
|
for (const t of e.del) delete n[t];
|
|
646
|
+
e.kc -= e.del.size;
|
|
558
647
|
e.del = null;
|
|
559
648
|
}
|
|
649
|
+
// Plain-data grade (sc 2): bare assignment, as flattenOverlay does.
|
|
650
|
+
for (const r of Reflect.ownKeys(t)) e.sc === 2 ? n[r] = t[r] : copyOwn(n, t, r);
|
|
560
651
|
e.pb = n;
|
|
561
652
|
e.ovl = false;
|
|
653
|
+
return n;
|
|
562
654
|
}
|
|
563
655
|
|
|
564
656
|
function ensurePB(e) {
|
|
@@ -698,6 +790,7 @@ function ensurePB(e) {
|
|
|
698
790
|
if (heldMaskView(e) === null) e.hv = e.v;
|
|
699
791
|
e.ht = activeTransition ?? PLAIN_HOLD;
|
|
700
792
|
if (!n && activeTransition !== null) heldAdoptions.add(e);
|
|
793
|
+
heldKeys.set(e, [ t ]);
|
|
701
794
|
}
|
|
702
795
|
e.pb = null;
|
|
703
796
|
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
@@ -829,6 +922,7 @@ function queueFold(e) {
|
|
|
829
922
|
for (const r of Reflect.ownKeys(t)) e.sc === 2 ? n[r] = t[r] : copyOwn(n, t, r);
|
|
830
923
|
if (e.del !== null) {
|
|
831
924
|
for (const t of e.del) delete n[t];
|
|
925
|
+
e.kc -= e.del.size;
|
|
832
926
|
e.del = null;
|
|
833
927
|
}
|
|
834
928
|
e.pb = null;
|
|
@@ -887,11 +981,13 @@ function drainFolds() {
|
|
|
887
981
|
// re-queue: commit happens when the hold settles
|
|
888
982
|
continue;
|
|
889
983
|
}
|
|
890
|
-
if (t.ovl) {
|
|
984
|
+
if (t.ovl && (t.v !== n || !overlayRebuilds(t, i))) {
|
|
891
985
|
// Overlay flatten (#3044): the backing keeps its identity, so the
|
|
892
986
|
// `t.v === old` gate below skips path copying (the parent slot
|
|
893
987
|
// already points here) and the adopted-notify (setter notifications
|
|
894
|
-
// happened at write time).
|
|
988
|
+
// happened at write time). A backing privatized mid-batch is a fresh
|
|
989
|
+
// clone, never a prototype — in-place writes into it are cheap, and
|
|
990
|
+
// the merge branch below is what a materialized pb would need anyway.
|
|
895
991
|
flattenOverlay(t, i);
|
|
896
992
|
} else if (t.v !== n) {
|
|
897
993
|
// Privatized mid-batch (#3271): an earlier fold in this drain
|
|
@@ -932,7 +1028,10 @@ function drainFolds() {
|
|
|
932
1028
|
t.wk = null;
|
|
933
1029
|
// written-keys window closes with the fold commit
|
|
934
1030
|
} else {
|
|
935
|
-
|
|
1031
|
+
// An overlay whose fold changed the key set rebuilds (#3689) and
|
|
1032
|
+
// takes the swap like a clone-path draft: the parent slot re-points
|
|
1033
|
+
// in the path copy below.
|
|
1034
|
+
t.v = t.ovl ? materializePB(t) : i;
|
|
936
1035
|
t.ch = false;
|
|
937
1036
|
// pb is always a plain clone
|
|
938
1037
|
t.pb = null;
|
|
@@ -1162,8 +1261,12 @@ function sameLeaf(e, t) {
|
|
|
1162
1261
|
if (e.ht !== null) e.ht = e.hv = null;
|
|
1163
1262
|
// Overlay landing (#3352): flatten in place — identity-stable, and
|
|
1164
1263
|
// privatizeCommitted re-slots the parent itself when it has to clone an
|
|
1165
|
-
// unowned seed, so no path copy is needed.
|
|
1166
|
-
|
|
1264
|
+
// unowned seed, so no path copy is needed. A landing that changed the
|
|
1265
|
+
// key set rebuilds instead (#3689) and swaps like a clone-path landing.
|
|
1266
|
+
if (e.ovl) {
|
|
1267
|
+
if (!overlayRebuilds(e, t)) return flattenOverlay(e, t);
|
|
1268
|
+
t = materializePB(e);
|
|
1269
|
+
}
|
|
1167
1270
|
const n = e.v;
|
|
1168
1271
|
e.pb = null;
|
|
1169
1272
|
e.v = t;
|
|
@@ -1405,21 +1508,25 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
|
|
|
1405
1508
|
return false;
|
|
1406
1509
|
}
|
|
1407
1510
|
|
|
1408
|
-
|
|
1511
|
+
/** The backing a reader is served. `key` (the get/has/descriptor traps)
|
|
1512
|
+
* scopes a fold hold to the keys the fold touched — see pendingBackingVisible —
|
|
1513
|
+
* and an adoption hold to the keys the adoption changed (heldKey). */ function readSource(e, t) {
|
|
1409
1514
|
// Adoption hold first (#3074): an adoption staged under a live transaction
|
|
1410
1515
|
// (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
|
|
1411
1516
|
// committed-visibility readers — context-free and children-forbidden ones,
|
|
1412
1517
|
// and stale passes off a foreign hold. Drafts, write-override and latest()
|
|
1413
1518
|
// see the adopted backing.
|
|
1414
|
-
const
|
|
1415
|
-
if (
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1519
|
+
const n = e.ht;
|
|
1520
|
+
if (n !== null && !latestReadActive && !inDraft(e) && !getWriteOverride()) {
|
|
1521
|
+
const r = heldMaskView(e);
|
|
1522
|
+
// A key the adoption left unchanged derives nothing from the hold, for
|
|
1523
|
+
// any reader (#3706): the backing serves it.
|
|
1524
|
+
if (r !== null && (t === undefined || heldKey(e, t))) {
|
|
1418
1525
|
const e = readerContext();
|
|
1419
|
-
if (e === null || e.C & CONFIG_CHILDREN_FORBIDDEN || !holdVisible(
|
|
1526
|
+
if (e === null || e.C & CONFIG_CHILDREN_FORBIDDEN || !holdVisible(n === PLAIN_HOLD ? null : currentTransition(n), e)) return r;
|
|
1420
1527
|
}
|
|
1421
1528
|
}
|
|
1422
|
-
return pendingBackingVisible(e, false) ? e.pb : e.v;
|
|
1529
|
+
return pendingBackingVisible(e, false, t) ? e.pb : e.v;
|
|
1423
1530
|
}
|
|
1424
1531
|
|
|
1425
1532
|
/** The single pb-vs-committed visibility decision (#3147), shared by per-key
|
|
@@ -1436,7 +1543,7 @@ function readSource(e) {
|
|
|
1436
1543
|
* `speculative` is deep()/snapshot's posture: an untrack/deep PEEK that sees
|
|
1437
1544
|
* ordinary pending staging regardless of owner context (the documented
|
|
1438
1545
|
* divergence from context-free per-key reads) — but never through a hold:
|
|
1439
|
-
* held truth stays masked exactly as it is for per-key readers. */ function pendingBackingVisible(e, t) {
|
|
1546
|
+
* held truth stays masked exactly as it is for per-key readers. */ function pendingBackingVisible(e, t, n) {
|
|
1440
1547
|
if (e.pb === null) return false;
|
|
1441
1548
|
// The writer's own channels compose on the pending backing regardless.
|
|
1442
1549
|
if (inDraft(e) || getWriteOverride()) return true;
|
|
@@ -1447,8 +1554,8 @@ function readSource(e) {
|
|
|
1447
1554
|
// hold arms below: committed for no pass, the staged backing for the
|
|
1448
1555
|
// holding pass, stale-of-foreign or A29 for a foreign one.
|
|
1449
1556
|
if (heldTruthMasked(e)) return false;
|
|
1450
|
-
const
|
|
1451
|
-
if (
|
|
1557
|
+
const r = readerContext();
|
|
1558
|
+
if (r === null || r.C & CONFIG_CHILDREN_FORBIDDEN) {
|
|
1452
1559
|
// No pass, or a children-forbidden one: the committed frame (A32) —
|
|
1453
1560
|
// except the speculative peek (deep()/snapshot()), which sees ordinary
|
|
1454
1561
|
// pending staging but never through a live foreign hold, and a
|
|
@@ -1456,11 +1563,30 @@ function readSource(e) {
|
|
|
1456
1563
|
// readers UNLESS a transition holds the node commits (downstream async
|
|
1457
1564
|
// hold — stale committed is the contract; the write-time stamp covers
|
|
1458
1565
|
// keys with no node, #3336) or the scope is children-forbidden (#3082).
|
|
1459
|
-
const
|
|
1460
|
-
if (t) return
|
|
1461
|
-
return e.fam !== null &&
|
|
1566
|
+
const n = liveFoldTransition(e);
|
|
1567
|
+
if (t) return n === null || ownsHold(n);
|
|
1568
|
+
return e.fam !== null && r === null && !foldHeld(e) && n === null;
|
|
1569
|
+
}
|
|
1570
|
+
const i = liveFoldTransition(e);
|
|
1571
|
+
// A hold changes what a reader of ONE key sees only through the fold's
|
|
1572
|
+
// writes to that key. A key the fold left alone reads the same from either
|
|
1573
|
+
// backing, so a pass reading it derives nothing from the hold: serve
|
|
1574
|
+
// committed, with no transaction entry and no stale replay (#3688 — a memo
|
|
1575
|
+
// reading an unchanged key beside an independent signal was held with
|
|
1576
|
+
// someone else's action). Per-node reads already have this precision (a
|
|
1577
|
+
// node with nothing staged enters nothing; stageHeldKey skips an equal
|
|
1578
|
+
// value); this is the same rule at the container gate. `wk` is the trap's
|
|
1579
|
+
// record of every write and delete this batch (deletes included, on the
|
|
1580
|
+
// overlay and clone paths alike); null (a fold with no trap writes) or
|
|
1581
|
+
// WK_ALL (an array length write) leaves the whole container held. Same
|
|
1582
|
+
// exclusions as heldFoldTransition: an optimistic family's draft is seeded
|
|
1583
|
+
// from node overrides the trap never saw, and a chained backing's
|
|
1584
|
+
// committed layer is a live proxy, not a frozen twin of the clone.
|
|
1585
|
+
if (i !== null && n !== undefined && !e.ch && e.fam?.opt !== true) {
|
|
1586
|
+
const t = e.wk;
|
|
1587
|
+
if (t != null && t !== WK_ALL && !t.has(n)) return false;
|
|
1462
1588
|
}
|
|
1463
|
-
return holdVisible(
|
|
1589
|
+
return holdVisible(i, r);
|
|
1464
1590
|
}
|
|
1465
1591
|
|
|
1466
1592
|
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
@@ -1756,7 +1882,7 @@ const traps = {
|
|
|
1756
1882
|
// observer, so bring the projection computed up to date here — signal/
|
|
1757
1883
|
// memo parity for latest() reads through a projection.
|
|
1758
1884
|
if (e.fam !== null && latestReadActive && !inDraft(e) && !getWriteOverride()) pullProjectionForLatest(e);
|
|
1759
|
-
const r = readSource(e);
|
|
1885
|
+
const r = readSource(e, t);
|
|
1760
1886
|
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
1761
1887
|
// deleted keys are tracked aside and read as absent in the pending view.
|
|
1762
1888
|
if (e.del !== null && r === e.pb && e.del.has(t)) {
|
|
@@ -1866,7 +1992,7 @@ const traps = {
|
|
|
1866
1992
|
if (t === $OWNER || t === $RECORD) return false;
|
|
1867
1993
|
if (pendingCheckActive) witnessAffectsMark(e, t);
|
|
1868
1994
|
if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
|
|
1869
|
-
const n = readSource(e);
|
|
1995
|
+
const n = readSource(e, t);
|
|
1870
1996
|
let r = t in n;
|
|
1871
1997
|
// Overlay deletes read as absent in the pending view (#3044).
|
|
1872
1998
|
if (r && e.del !== null && n === e.pb && e.del.has(t)) r = false;
|
|
@@ -1922,7 +2048,7 @@ const traps = {
|
|
|
1922
2048
|
if (pendingCheckActive) witnessAffectsMark(e, t);
|
|
1923
2049
|
const n = getObserver();
|
|
1924
2050
|
if (e.fam !== null && n === null && !inDraft(e)) firewallGate(e);
|
|
1925
|
-
const r = readSource(e);
|
|
2051
|
+
const r = readSource(e, t);
|
|
1926
2052
|
const i = visibleDescriptor(e, r, t);
|
|
1927
2053
|
if (!inDraft(e) && n !== null && !observerHoldsKeySet(e, n)) {
|
|
1928
2054
|
// The node is born from the source's presence (as `has` births it),
|
|
@@ -1967,10 +2093,12 @@ const traps = {
|
|
|
1967
2093
|
}
|
|
1968
2094
|
} else {
|
|
1969
2095
|
if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
|
|
1970
|
-
// Live own-key estimate for the overlay/clone choice (#3360)
|
|
1971
|
-
// sees through an overlay to the
|
|
1972
|
-
//
|
|
1973
|
-
//
|
|
2096
|
+
// Live own-key estimate for the overlay/clone choice (#3360) and the
|
|
2097
|
+
// overlay's commit choice (#3689): `in` sees through an overlay to the
|
|
2098
|
+
// committed keys, so this counts keys NEW to the container. Overlay
|
|
2099
|
+
// deletes are counted down when they commit (`del` is exact); clone-
|
|
2100
|
+
// path deletes are not (a stale high count on a narrow container only
|
|
2101
|
+
// picks the overlay a little early).
|
|
1974
2102
|
if (!(t in l)) e.kc++;
|
|
1975
2103
|
}
|
|
1976
2104
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
@@ -180,20 +180,29 @@ const CONFIG_HAS_LANE = 1 << 10;
|
|
|
180
180
|
* transaction. A parked node (status propagation stamps `_transition`
|
|
181
181
|
* without recomputing) recomputed when its source lands otherwise disposed
|
|
182
182
|
* its committed children mid-hold, running their cleanups before the
|
|
183
|
-
* transaction's atomic reveal. Cleared by `commitPendingNode`.
|
|
183
|
+
* transaction's atomic reveal. Cleared by `commitPendingNode`. Transaction
|
|
184
|
+
* work only (A15 lane work and transaction work, #3698): zombies are parked
|
|
185
|
+
* by a pass under a held transaction; a pass over a lane parks a LANE frame
|
|
186
|
+
* instead (`CONFIG_LANE_FRAME`), whatever the node's kind. */ const CONFIG_HELD_CHILDREN = 1 << 20;
|
|
184
187
|
|
|
185
188
|
/** The frame parked in `_pendingFirstChild` / `_pendingDisposal` is a LANE
|
|
186
|
-
* frame (#3662, A15
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
189
|
+
* frame (#3662, #3698; A15 lane work and transaction work): a lane pass —
|
|
190
|
+
* on an effect or a memo alike — publishes into the lane's frame (an
|
|
191
|
+
* effect's run; a memo's derived override, A17), so the frame it replaces
|
|
192
|
+
* leaves the screen when the lane applies (A30) — not at the action's
|
|
193
|
+
* commit like #3404's transaction zombies, and not at the pass (a held lane
|
|
194
|
+
* defers the apply with the frame still displayed). Drained by the lane's
|
|
195
|
+
* render entry the parking site pushed ahead of the new frame's effects
|
|
196
|
+
* (cleanups before side effects), by `commitPendingNode` if a hold commits
|
|
197
|
+
* the node first, or with the owner's death. While set the parked frame is
|
|
198
|
+
* not a hold (the node is not queued or stamped for it — lane work never
|
|
199
|
+
* makes its node transaction work), a superseding pass disposes the
|
|
200
|
+
* never-shown live children on the spot, and a lane-channel dirty on a
|
|
201
|
+
* member is cancelled (`laneZombie`). Ruled 2026-09-28 (#3698): a pass is
|
|
202
|
+
* lane work or transaction work by its owner, never by node kind. #3662
|
|
203
|
+
* flagged effects only, and a memo's lane pass parked a transaction zombie
|
|
204
|
+
* that queued and stamped the memo as the action's pending node, so its
|
|
205
|
+
* next mainline recompute re-entered the hold. */ const CONFIG_LANE_FRAME = 1 << 26;
|
|
197
206
|
|
|
198
207
|
/** In-flight async node whose inputs were PUBLISHED while it was pending: a
|
|
199
208
|
* batch or transaction committed with the node still `STATUS_PENDING` (an
|
package/dist/prod/core/core.js
CHANGED
|
@@ -146,8 +146,8 @@ function recompute(e, n = false) {
|
|
|
146
146
|
bumpNotifyEpoch();
|
|
147
147
|
const t = e.je;
|
|
148
148
|
// Lane posture is resolved BEFORE the previous frame is parked below: a
|
|
149
|
-
// lane pass
|
|
150
|
-
// the decision must be known there. `lane` is applied to
|
|
149
|
+
// lane pass parks a lane frame, not a transaction zombie (#3662, #3698;
|
|
150
|
+
// see the parking site), so the decision must be known there. `lane` is applied to
|
|
151
151
|
// `currentOptimisticLane` further down, once the previous posture is saved.
|
|
152
152
|
let i = !!(e.oe & REACTIVE_OPTIMISTIC_DIRTY);
|
|
153
153
|
let u = null;
|
|
@@ -195,20 +195,30 @@ function recompute(e, n = false) {
|
|
|
195
195
|
// Tracked effects run after finalizePureQueue, so dispose immediately
|
|
196
196
|
// instead of deferring. Children built by an uncommitted recompute
|
|
197
197
|
// (CONFIG_HELD_CHILDREN) die immediately too: no frame ever showed them.
|
|
198
|
-
// Everything else is the committed frame's
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// the
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
198
|
+
// Everything else is the committed frame's. Which frame retires it is
|
|
199
|
+
// decided by who owns the pass, never by the node's kind (A15 lane work
|
|
200
|
+
// and transaction work, ruled 2026-09-28, #3698). Transaction work — a
|
|
201
|
+
// pass under a held transaction — defers them as zombies until this
|
|
202
|
+
// node's commit, a transaction-owned node included (#3404): a parked
|
|
203
|
+
// node's children predate the hold, and tearing them down when the
|
|
204
|
+
// source lands ran cleanups before the transaction's atomic reveal.
|
|
205
|
+
// Lane work — a pass over a lane, effect or memo alike — parks a LANE
|
|
206
|
+
// frame instead (CONFIG_LANE_FRAME, #3662, #3698): the frame it replaces
|
|
207
|
+
// leaves the screen when the lane's queue applies this pass (A30) — not
|
|
208
|
+
// at the action's commit — and a held lane defers that with the frame
|
|
209
|
+
// still displayed. The drain is the lane's first render entry for this
|
|
210
|
+
// pass, pushed before the pass builds the new frame: cleanups before
|
|
211
|
+
// side effects — the retired frame's `onCleanup`s run ahead of every
|
|
212
|
+
// effect callback of the new frame. While the frame waits, the live
|
|
213
|
+
// children were never shown: a superseding pass disposes them here like
|
|
214
|
+
// held children, and the parked frame stays. Zombies exist for
|
|
215
|
+
// transaction work only: #3662 gated the lane frame on `isEffect`, and a
|
|
216
|
+
// memo's lane pass (its value a derived override, A17) parked a
|
|
217
|
+
// transaction zombie that made the memo a pending node of the action,
|
|
218
|
+
// stamped it, and sent its next mainline recompute through the
|
|
219
|
+
// stamped-memo arm above — a `Show` whose `when` getter owns a
|
|
220
|
+
// compiler-emitted memo held an unrelated sync write for the action's
|
|
221
|
+
// lifetime, against the #3460 ruling that a lane never holds a sync write.
|
|
212
222
|
if (t === EFFECT_TRACKED || e.C & (CONFIG_HELD_CHILDREN | CONFIG_LANE_FRAME)) disposeChildren(e); else if (e.tn !== null || e.qe !== null) {
|
|
213
223
|
markDisposal(e);
|
|
214
224
|
const n = ext(e);
|
|
@@ -217,7 +227,7 @@ function recompute(e, n = false) {
|
|
|
217
227
|
e.qe = null;
|
|
218
228
|
e.tn = null;
|
|
219
229
|
e.En = 0;
|
|
220
|
-
if (
|
|
230
|
+
if (u) {
|
|
221
231
|
e.C |= CONFIG_LANE_FRAME;
|
|
222
232
|
findLane(u).dn[0].push(() => {
|
|
223
233
|
e.C &= ~CONFIG_LANE_FRAME;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, REACTIVE_DIRTY, REACTIVE_CHECK, CONFIG_HAS_LANE, OVERRIDE_UNDEFINED, STATUS_PENDING, CONFIG_DERIVED_OVERRIDE, CONFIG_OPTIMISTIC, CONFIG_OVERRIDE_SUPERSEDED, EFFECT_RENDER, CONFIG_AUTHORITATIVE_OBSERVED, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, LANE_RUN, EFFECT_USER } from "./constants.js";
|
|
2
2
|
|
|
3
|
-
import { ext, hasActiveOverride, stale, currentOptimisticLane, enterStagedRead, latestReadActive } from "./core.js";
|
|
3
|
+
import { ext, hasActiveOverride, stale, currentOptimisticLane, enterStagedRead, setSignal, latestReadActive } from "./core.js";
|
|
4
4
|
|
|
5
5
|
import { NotReadyError } from "./error.js";
|
|
6
6
|
|
|
@@ -25,17 +25,17 @@ import { GlobalQueue, activeTransition, globalQueue, origin, clock, insertSubs,
|
|
|
25
25
|
*/
|
|
26
26
|
/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */ function optimisticWrite(e, n) {
|
|
27
27
|
const i = e.o?.Fe !== NOT_PENDING;
|
|
28
|
-
const
|
|
29
|
-
if (typeof n === "function") n = n(
|
|
30
|
-
const
|
|
28
|
+
const r = i ? unwrapOverride(e.o?.Fe) : e.ce;
|
|
29
|
+
if (typeof n === "function") n = n(r);
|
|
30
|
+
const t = !!(e.h & STATUS_UNINITIALIZED) ||
|
|
31
31
|
// A dirty node's _value is stale (its queued recompute hasn't run — e.g.
|
|
32
32
|
// a latest() shadow marked by the previous landing's companion snap), so
|
|
33
33
|
// equality against it must not swallow the write. Without this, a sync
|
|
34
34
|
// push returning the shadow to that stale value was dropped, the snap
|
|
35
35
|
// recompute then committed the parent's old value, and the banner showed
|
|
36
36
|
// the previous transition's target (#3041 follow-up).
|
|
37
|
-
!!((e.oe ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) || !e.xe || !e.xe(
|
|
38
|
-
if (!
|
|
37
|
+
!!((e.oe ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) || !e.xe || !e.xe(r, n);
|
|
38
|
+
if (!t) {
|
|
39
39
|
// Same-value write with an active override still entangles the current
|
|
40
40
|
// action's transition — the hold must outlast all overlapping actions —
|
|
41
41
|
// and renews the override's PROVENANCE: the newer action re-asks the
|
|
@@ -162,7 +162,7 @@ function resolveOptimisticNodes(e) {
|
|
|
162
162
|
// override-covered authoritative values hold in _pendingValue and
|
|
163
163
|
// elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
|
|
164
164
|
if (!(n.h & STATUS_PENDING)) n.h &= ~STATUS_UNINITIALIZED;
|
|
165
|
-
const
|
|
165
|
+
const r = n.o?.Fe;
|
|
166
166
|
// A derived override (lanes stage, #3479) has no truth of its own: the
|
|
167
167
|
// slot disarms — the memo is plain again — and the override PROMOTES to
|
|
168
168
|
// `_value`. Not superseded, nothing it derives from told it otherwise
|
|
@@ -172,15 +172,15 @@ function resolveOptimisticNodes(e) {
|
|
|
172
172
|
// the override IS what a recompute from the truth yields. Re-deriving
|
|
173
173
|
// instead re-asked an async member's flight and held the transaction on
|
|
174
174
|
// it — a waterfall after the reveal.
|
|
175
|
-
const
|
|
176
|
-
ext(n).Fe =
|
|
175
|
+
const t = n.C & CONFIG_DERIVED_OVERRIDE;
|
|
176
|
+
ext(n).Fe = t && !(n.C & CONFIG_OPTIMISTIC) ? undefined : NOT_PENDING;
|
|
177
177
|
// A superseded override's subscribers already re-derived from the truth
|
|
178
178
|
// when it arrived (#3331) — the drop changes nothing they read. Everyone
|
|
179
179
|
// else learns of the correction here: this drop IS their notification.
|
|
180
180
|
const u = (n.C & CONFIG_OVERRIDE_SUPERSEDED) !== 0;
|
|
181
181
|
n.C &= -8912897;
|
|
182
|
-
if (!u &&
|
|
183
|
-
if (
|
|
182
|
+
if (!u && r !== NOT_PENDING && n.ce !== unwrapOverride(r)) {
|
|
183
|
+
if (t) n.ce = unwrapOverride(r); else {
|
|
184
184
|
insertSubs(n, true);
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -192,9 +192,9 @@ function resolveOptimisticNodes(e) {
|
|
|
192
192
|
// transition that produced them (A19 — pending is a property of the data).
|
|
193
193
|
for (let i = 0; i < n; i++) {
|
|
194
194
|
const n = e[i];
|
|
195
|
-
if (n.o?.Ze || n.o?.He) GlobalQueue.
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
195
|
+
if (n.o?.Ze || n.o?.He) GlobalQueue.ri(n);
|
|
196
|
+
const r = n.o?.kn;
|
|
197
|
+
if (r && (r.o?.Ze === n || r.o?.He === n)) GlobalQueue.ri(r);
|
|
198
198
|
}
|
|
199
199
|
e.splice(0, n);
|
|
200
200
|
}
|
|
@@ -248,14 +248,14 @@ function resolveOptimisticNodes(e) {
|
|
|
248
248
|
const n = e.o?.me;
|
|
249
249
|
if (n) {
|
|
250
250
|
const i = findLane(n);
|
|
251
|
-
const
|
|
252
|
-
while (
|
|
253
|
-
const e =
|
|
251
|
+
const r = [ e ];
|
|
252
|
+
while (r.length) {
|
|
253
|
+
const e = r.pop();
|
|
254
254
|
const n = e.o?.me;
|
|
255
255
|
if (!n || findLane(n) !== i) continue;
|
|
256
256
|
e.o.me = undefined;
|
|
257
257
|
i.Qe.delete(e);
|
|
258
|
-
for (let n = e.u; n !== null; n = n.Ae)
|
|
258
|
+
for (let n = e.u; n !== null; n = n.Ae) r.push(n.ge);
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
}
|
|
@@ -297,7 +297,7 @@ function resolveOptimisticNodes(e) {
|
|
|
297
297
|
* the optimistic path so it flushes ahead of the hold — but they answer for
|
|
298
298
|
* their owner and snap at settlement (`_snapCompanions`), not here.
|
|
299
299
|
*/ function endOptimism(e) {
|
|
300
|
-
if (!e.Te || e.pe.length || !e.cn.length || e.
|
|
300
|
+
if (!e.Te || e.pe.length || !e.cn.length || e.ti.size || transitionBlocked(e)) return false;
|
|
301
301
|
for (const n of e.le.keys()) if (sourceObserved(e, n, e) && n.o?.ue?.has(n) && !resolveLane(n)) return false;
|
|
302
302
|
let n = false;
|
|
303
303
|
for (const i of e.cn) {
|
|
@@ -393,7 +393,49 @@ function resolveOptimisticNodes(e) {
|
|
|
393
393
|
* action provenance, or an A17-silent confirmation (#3331). Before this the
|
|
394
394
|
* landing took setSignal's plain path: a differing truth staged silently
|
|
395
395
|
* under the override and the graph never moved until the commit.
|
|
396
|
+
*
|
|
397
|
+
* Slot arm (list-matrix F1): `mapArray`'s writes to its per-slot signals —
|
|
398
|
+
* the row accessors of index mode, the index accessors of keyed mode; never
|
|
399
|
+
* CONFIG_OPTIMISTIC, which is how the arm tells them from the store landings
|
|
400
|
+
* above — once a lane pass has run over the map. The list's frame lives in
|
|
401
|
+
* these writes as much as in the computed's result. Under a LANE pass the
|
|
402
|
+
* write is the lane's frame, as the computed's result is (lanes stage,
|
|
403
|
+
* `laneOverride`): the slot joins the lane carrying a DERIVED override —
|
|
404
|
+
* `_value` stays the committed row, the lane's readers and untracked reads
|
|
405
|
+
* see the override (A17), an off-lane render effect sees the committed frame
|
|
406
|
+
* (#3460), the revert promotes or drops it with the lane's transaction. The
|
|
407
|
+
* gate compares against the slot the pass publishes to (INV-11): the override
|
|
408
|
+
* when armed, the committed row otherwise. A plain `setSignal` here staged
|
|
409
|
+
* the write into the ACTION's transaction: `<For>` without `keyed` showed the
|
|
410
|
+
* pre-action list for the whole action while the keyed modes showed the
|
|
411
|
+
* optimistic one. A PLAIN pass over a slot still carrying the lane's frame —
|
|
412
|
+
* the landing, the reversion, a mainline re-pass — is the truth arriving
|
|
413
|
+
* under an override and takes the landing path below: a differing truth
|
|
414
|
+
* supersedes, an equal one confirms and the revert promotes. Every pass
|
|
415
|
+
* rewrites every slot whose value differs from the previous frame's (index
|
|
416
|
+
* mode rewrites every surviving slot), so a slot the landing does NOT write
|
|
417
|
+
* holds, by construction, what the truth yields. A slot with neither a lane
|
|
418
|
+
* nor an override is the plain write.
|
|
396
419
|
*/ function landOnOverride(e, n) {
|
|
420
|
+
if (!(e.C & CONFIG_OPTIMISTIC)) {
|
|
421
|
+
const i = currentOptimisticLane;
|
|
422
|
+
if (i === null) {
|
|
423
|
+
if (!hasActiveOverride(e)) return setSignal(e, n);
|
|
424
|
+
} else {
|
|
425
|
+
// INV-11: the gate compares against the slot this pass publishes to —
|
|
426
|
+
// the override when one is armed, the committed row otherwise.
|
|
427
|
+
if (!e.xe || !e.xe(hasActiveOverride(e) ? unwrapOverride(e.o.Fe) : e.ce, n)) {
|
|
428
|
+
// Membership before the publish: `laneOverride` files the slot under
|
|
429
|
+
// the lane's transaction, and the read path routes a tracked reader
|
|
430
|
+
// of a lane member through `overrideRead` (CONFIG_HAS_LANE).
|
|
431
|
+
assignOrMergeLane(e, i);
|
|
432
|
+
laneOverride(e, n, i);
|
|
433
|
+
insertSubs(e, true);
|
|
434
|
+
schedule();
|
|
435
|
+
}
|
|
436
|
+
return n;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
397
439
|
const i = e._e === NOT_PENDING ? e.ce : e._e;
|
|
398
440
|
if (typeof n === "function") n = n(i);
|
|
399
441
|
if (e._e === NOT_PENDING) queuePendingNode(e);
|
|
@@ -583,7 +625,7 @@ function cleanupCompletedLanes(e) {
|
|
|
583
625
|
|
|
584
626
|
function trackOptimisticStore(e) {
|
|
585
627
|
// After initTransition, globalQueue._batch IS activeTransition (same reference)
|
|
586
|
-
globalQueue.m.
|
|
628
|
+
globalQueue.m.ti.add(e);
|
|
587
629
|
schedule();
|
|
588
630
|
}
|
|
589
631
|
|
package/dist/prod/core/owner.js
CHANGED
|
@@ -58,13 +58,13 @@ function disposeChildren(e, n = false, t) {
|
|
|
58
58
|
// edge). Snap runs after the DISPOSED flag is set so the oracle reads
|
|
59
59
|
// false, and notifies subscribers still watching the companion.
|
|
60
60
|
const n = e;
|
|
61
|
-
if (n.o?.Ze || n.o?.He) GlobalQueue.
|
|
61
|
+
if (n.o?.Ze || n.o?.He) GlobalQueue.ri(n);
|
|
62
62
|
// A firewall's leaves have no lifecycle of their own, so a companion on
|
|
63
63
|
// one of them outlives its source the same way (INV-9's rationale). The
|
|
64
64
|
// firewall knows which leaves carry companions (CONFIG_CHILD_COMPANIONS,
|
|
65
65
|
// #3038): snap them with it — the snap retires a shadow whose firewall is
|
|
66
66
|
// disposed (spec O5).
|
|
67
|
-
if (n.C & CONFIG_CHILD_COMPANIONS) n.o.Un.forEach(GlobalQueue.
|
|
67
|
+
if (n.C & CONFIG_CHILD_COMPANIONS) n.o.Un.forEach(GlobalQueue.ri);
|
|
68
68
|
// A pending reader parked in a transaction may be the only thing holding
|
|
69
69
|
// it (#3372): its death is a completion event the transaction must be
|
|
70
70
|
// re-judged for, and nothing else re-enters a parked transaction.
|