@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
package/dist/dev.js
CHANGED
|
@@ -42,11 +42,11 @@ import {
|
|
|
42
42
|
K as currentOptimisticLane,
|
|
43
43
|
L as enterStagedRead,
|
|
44
44
|
M as currentTransition,
|
|
45
|
-
P as
|
|
46
|
-
Q as
|
|
47
|
-
T as
|
|
48
|
-
U as
|
|
49
|
-
V as
|
|
45
|
+
P as assignOrMergeLane,
|
|
46
|
+
Q as queuePendingNode,
|
|
47
|
+
T as latestReadActive,
|
|
48
|
+
U as REACTIVE_MANUAL_WRITE,
|
|
49
|
+
V as REACTIVE_OPTIMISTIC_DIRTY,
|
|
50
50
|
W as LANE_RUN,
|
|
51
51
|
X as attrHooks,
|
|
52
52
|
Y as EFFECT_USER,
|
|
@@ -750,8 +750,53 @@ function overrideRead(el, c) {
|
|
|
750
750
|
* action provenance, or an A17-silent confirmation (#3331). Before this the
|
|
751
751
|
* landing took setSignal's plain path: a differing truth staged silently
|
|
752
752
|
* under the override and the graph never moved until the commit.
|
|
753
|
+
*
|
|
754
|
+
* Slot arm (list-matrix F1): `mapArray`'s writes to its per-slot signals —
|
|
755
|
+
* the row accessors of index mode, the index accessors of keyed mode; never
|
|
756
|
+
* CONFIG_OPTIMISTIC, which is how the arm tells them from the store landings
|
|
757
|
+
* above — once a lane pass has run over the map. The list's frame lives in
|
|
758
|
+
* these writes as much as in the computed's result. Under a LANE pass the
|
|
759
|
+
* write is the lane's frame, as the computed's result is (lanes stage,
|
|
760
|
+
* `laneOverride`): the slot joins the lane carrying a DERIVED override —
|
|
761
|
+
* `_value` stays the committed row, the lane's readers and untracked reads
|
|
762
|
+
* see the override (A17), an off-lane render effect sees the committed frame
|
|
763
|
+
* (#3460), the revert promotes or drops it with the lane's transaction. The
|
|
764
|
+
* gate compares against the slot the pass publishes to (INV-11): the override
|
|
765
|
+
* when armed, the committed row otherwise. A plain `setSignal` here staged
|
|
766
|
+
* the write into the ACTION's transaction: `<For>` without `keyed` showed the
|
|
767
|
+
* pre-action list for the whole action while the keyed modes showed the
|
|
768
|
+
* optimistic one. A PLAIN pass over a slot still carrying the lane's frame —
|
|
769
|
+
* the landing, the reversion, a mainline re-pass — is the truth arriving
|
|
770
|
+
* under an override and takes the landing path below: a differing truth
|
|
771
|
+
* supersedes, an equal one confirms and the revert promotes. Every pass
|
|
772
|
+
* rewrites every slot whose value differs from the previous frame's (index
|
|
773
|
+
* mode rewrites every surviving slot), so a slot the landing does NOT write
|
|
774
|
+
* holds, by construction, what the truth yields. A slot with neither a lane
|
|
775
|
+
* nor an override is the plain write.
|
|
753
776
|
*/
|
|
754
777
|
function landOnOverride(el, v) {
|
|
778
|
+
if (!(el._config & CONFIG_OPTIMISTIC)) {
|
|
779
|
+
const lane = currentOptimisticLane;
|
|
780
|
+
if (lane === null) {
|
|
781
|
+
if (!hasActiveOverride(el)) return setSignal(el, v);
|
|
782
|
+
} else {
|
|
783
|
+
// INV-11: the gate compares against the slot this pass publishes to —
|
|
784
|
+
// the override when one is armed, the committed row otherwise.
|
|
785
|
+
if (
|
|
786
|
+
!el._equals ||
|
|
787
|
+
!el._equals(hasActiveOverride(el) ? unwrapOverride(el._x._overrideValue) : el._value, v)
|
|
788
|
+
) {
|
|
789
|
+
// Membership before the publish: `laneOverride` files the slot under
|
|
790
|
+
// the lane's transaction, and the read path routes a tracked reader
|
|
791
|
+
// of a lane member through `overrideRead` (CONFIG_HAS_LANE).
|
|
792
|
+
assignOrMergeLane(el, lane);
|
|
793
|
+
laneOverride(el, v, lane);
|
|
794
|
+
insertSubs(el, true);
|
|
795
|
+
schedule();
|
|
796
|
+
}
|
|
797
|
+
return v;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
755
800
|
const currentValue = el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
756
801
|
if (typeof v === "function") v = v(currentValue);
|
|
757
802
|
if (attrHooks !== null) attrHooks.write(el, currentValue, v);
|
|
@@ -4749,6 +4794,8 @@ function getNode(
|
|
|
4749
4794
|
// committed is the held view `hv[key]`, staged is `v[key]`.
|
|
4750
4795
|
const fold = heldFoldTransition(target);
|
|
4751
4796
|
let held = heldAdoptionTransition(target);
|
|
4797
|
+
// A key the adoption left unchanged is not born holding (#3706).
|
|
4798
|
+
if (held !== null && !heldKey(target, key)) held = null;
|
|
4752
4799
|
if (held !== null) current = target.hv[key];
|
|
4753
4800
|
else if ((held = fold) !== null) current = target.v[key];
|
|
4754
4801
|
// Create-floor diet: slotSignal bakes the whole node into one literal —
|
|
@@ -4871,6 +4918,45 @@ function holdVisible(txn, c) {
|
|
|
4871
4918
|
enterStagedRead(null, txn);
|
|
4872
4919
|
return true;
|
|
4873
4920
|
}
|
|
4921
|
+
// #3706: the keys an adoption under a live hold changed against the held
|
|
4922
|
+
// view — the adoption twin of `wk` (#3688), keyed on the target. adoptPB
|
|
4923
|
+
// records `[adopted object]`; the keys are diffed against IT, once, on the
|
|
4924
|
+
// first held read (its children are re-pointed by then), and replace the
|
|
4925
|
+
// entry. Never against the live backing — a mainline write during the hold
|
|
4926
|
+
// replaces that, and its key is not the adoption's. WK_ALL holds the whole
|
|
4927
|
+
// container.
|
|
4928
|
+
const heldKeys = new WeakMap();
|
|
4929
|
+
function heldKey(target, key) {
|
|
4930
|
+
let keys = heldKeys.get(target);
|
|
4931
|
+
if (Array.isArray(keys)) heldKeys.set(target, (keys = adoptionChangedKeys(target, keys[0])));
|
|
4932
|
+
return keys === WK_ALL || keys.has(key);
|
|
4933
|
+
}
|
|
4934
|
+
function adoptionChangedKeys(target, v) {
|
|
4935
|
+
const hv = target.hv;
|
|
4936
|
+
// A chained backing, an optimistic family, a chained held view, a swapped
|
|
4937
|
+
// or non-plain prototype (inherited accessors read through `this`): whole.
|
|
4938
|
+
if (
|
|
4939
|
+
v[$TARGET] !== undefined ||
|
|
4940
|
+
target.fam?.opt === true ||
|
|
4941
|
+
hv[$TARGET] !== undefined ||
|
|
4942
|
+
Object.getPrototypeOf(hv) !== Object.getPrototypeOf(v) ||
|
|
4943
|
+
!plainProto(v)
|
|
4944
|
+
)
|
|
4945
|
+
return WK_ALL;
|
|
4946
|
+
const keys = new Set();
|
|
4947
|
+
// Accessors are never invoked: they and a flipped enumerability are changes.
|
|
4948
|
+
for (const key of Reflect.ownKeys(hv))
|
|
4949
|
+
if (
|
|
4950
|
+
!hasOwn.call(v, key) ||
|
|
4951
|
+
isOwnAccessor(hv, key) ||
|
|
4952
|
+
isOwnAccessor(v, key) ||
|
|
4953
|
+
propertyIsEnumerable.call(hv, key) !== propertyIsEnumerable.call(v, key) ||
|
|
4954
|
+
!(isEqual(hv[key], v[key]) || sameLogicalSlot(target, hv[key], v[key]))
|
|
4955
|
+
)
|
|
4956
|
+
keys.add(key);
|
|
4957
|
+
for (const key of Reflect.ownKeys(v)) if (!hasOwn.call(hv, key)) keys.add(key);
|
|
4958
|
+
return keys;
|
|
4959
|
+
}
|
|
4874
4960
|
function stageHeldKey(node, nv, txn) {
|
|
4875
4961
|
if (slotNodeEquals.call(node, node._value, nv)) return;
|
|
4876
4962
|
node._pendingValue = nv;
|
|
@@ -5071,6 +5157,23 @@ function cloneRaw(source, t) {
|
|
|
5071
5157
|
clone[$OWNER] = t;
|
|
5072
5158
|
return clone;
|
|
5073
5159
|
}
|
|
5160
|
+
/** Shallow copy of a WIDE plain-data (sc 2) backing for the overlay rebuild
|
|
5161
|
+
* (#3689). Own keys land on a null-prototype object — V8 creates those in
|
|
5162
|
+
* dictionary mode, so the copy is O(keys) hash inserts — and the prototype
|
|
5163
|
+
* is attached after. A spread builds a fast-mode object one map transition
|
|
5164
|
+
* per key; from a source in dictionary mode (a backing that took deletes)
|
|
5165
|
+
* whose key order differs from the last copy's, every transition is a fresh
|
|
5166
|
+
* map with an O(keys) descriptor copy: measured 270 µs vs 28 µs at 400 keys,
|
|
5167
|
+
* 1.3 ms vs 75 µs at 1000. Narrow, stable-shaped backings keep the spread
|
|
5168
|
+
* (cloneRaw): there the clone IC copies the property store at once and is
|
|
5169
|
+
* 3–8× cheaper than any per-key loop. */
|
|
5170
|
+
function wideClone(source, t) {
|
|
5171
|
+
const clone = Object.create(null);
|
|
5172
|
+
for (const key of Reflect.ownKeys(source)) clone[key] = source[key];
|
|
5173
|
+
Object.setPrototypeOf(clone, Object.prototype);
|
|
5174
|
+
clone[$OWNER] = t;
|
|
5175
|
+
return clone;
|
|
5176
|
+
}
|
|
5074
5177
|
/** Copy own `key` from `from` onto `to`. A plain data slot (enumerable,
|
|
5075
5178
|
* writable, configurable, no accessor) is a bare assignment — the common case
|
|
5076
5179
|
* and the cheap one; anything else goes through defineProperty so accessors
|
|
@@ -5109,22 +5212,68 @@ function scanAccessorsOnce(target) {
|
|
|
5109
5212
|
* backing into a prototype, and every later flatten writes into that
|
|
5110
5213
|
* prototype); above it the clone's O(keys) copy dominates (#3044). */
|
|
5111
5214
|
const OVERLAY_MIN_KEYS = 32;
|
|
5215
|
+
/** Overlay commit choice (#3689): flatten in place, or rebuild the backing
|
|
5216
|
+
* (materializePB, then the clone path's swap)? Measured on V8 (Node 26): a
|
|
5217
|
+
* committed backing that has served as an overlay prototype is kept as a
|
|
5218
|
+
* FAST-MODE prototype object up to V8's descriptor limit (1020 own
|
|
5219
|
+
* properties), and there every in-place DELETE is O(keys) — normalize to
|
|
5220
|
+
* dictionary plus re-optimize as a prototype, ~0.08 µs × keys — and every
|
|
5221
|
+
* in-place ADD is a map transition copying the descriptors, ~0.006 µs ×
|
|
5222
|
+
* keys. Detaching the overlay first does not help: prototype-ness is a
|
|
5223
|
+
* property of the object's map. One rebuild (wideClone, deletes, writes) is
|
|
5224
|
+
* ~0.07–0.09 µs × keys — the price of ONE in-place delete. So a fold that
|
|
5225
|
+
* deleted anything, or added more than ~16 keys, commits cheaper rebuilt (a
|
|
5226
|
+
* 400-key record churning 100 keys per commit, 10,000 membership readers:
|
|
5227
|
+
* 4.7 ms → 0.22 ms per step); a pure rewrite of existing keys — the
|
|
5228
|
+
* overlay's home case — stays in place at O(written).
|
|
5229
|
+
* Past the descriptor limit V8 keeps the backing in dictionary mode where
|
|
5230
|
+
* every in-place op is O(1), and a rebuild would be a pure O(keys) loss on
|
|
5231
|
+
* exactly the growing-record pattern the overlay exists for (#3044) — the
|
|
5232
|
+
* count gate keeps those in place. The gate reads `kc`, which the set trap
|
|
5233
|
+
* counts up for keys new to the container and the delete commit counts down. */
|
|
5234
|
+
const OVERLAY_REBUILD_MAX_KEYS = 1024;
|
|
5235
|
+
const OVERLAY_REBUILD_MIN_ADDS = 16;
|
|
5236
|
+
function overlayRebuilds(t, pb) {
|
|
5237
|
+
if (t.kc > OVERLAY_REBUILD_MAX_KEYS) return false;
|
|
5238
|
+
if (t.del !== null && t.del.size !== 0) return true;
|
|
5239
|
+
// Every trap write records its key, so the written-key count bounds the
|
|
5240
|
+
// adds — the common few-key fold (#3044's growing record) answers here
|
|
5241
|
+
// without enumerating the overlay.
|
|
5242
|
+
const wk = t.wk;
|
|
5243
|
+
if (wk !== null && wk !== WK_ALL && wk.size <= OVERLAY_REBUILD_MIN_ADDS) return false;
|
|
5244
|
+
const v = t.v;
|
|
5245
|
+
let adds = 0;
|
|
5246
|
+
for (const key of Reflect.ownKeys(pb))
|
|
5247
|
+
if (!hasOwn.call(v, key) && ++adds > OVERLAY_REBUILD_MIN_ADDS) return true;
|
|
5248
|
+
return false;
|
|
5249
|
+
}
|
|
5112
5250
|
/** Downgrade a prototype-overlay pending backing to the clone path: builds
|
|
5113
5251
|
* the real container (committed + overlay writes − deletes) that fold will
|
|
5114
5252
|
* SWAP in as the committed backing, exactly as if the draft had started on
|
|
5115
5253
|
* the clone path. Consumers that need a complete container (reconcile's
|
|
5116
|
-
* diff walks, drafts escaping into other storage) call this
|
|
5254
|
+
* diff walks, drafts escaping into other storage) call this, and so does the
|
|
5255
|
+
* commit itself when the fold changed the key set (overlayRebuilds). Returns
|
|
5256
|
+
* the pending backing (the clone, or the pb as-is when not an overlay). */
|
|
5117
5257
|
function materializePB(target) {
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
const clone = cloneRaw(target.v, target);
|
|
5121
|
-
|
|
5258
|
+
const pb = target.pb;
|
|
5259
|
+
if (!target.ovl) return pb;
|
|
5260
|
+
const clone = target.sc === 2 ? wideClone(target.v, target) : cloneRaw(target.v, target);
|
|
5261
|
+
// Deletes before writes (#3689): on a fast-mode clone (the descriptor
|
|
5262
|
+
// path) the first delete normalizes it once, and the writes then land as
|
|
5263
|
+
// O(1) dictionary adds instead of O(keys) map transitions each. Order is
|
|
5264
|
+
// free semantically — a rewritten key left `del` at write time, so no
|
|
5265
|
+
// write targets a deleted key.
|
|
5122
5266
|
if (target.del !== null) {
|
|
5123
5267
|
for (const key of target.del) delete clone[key];
|
|
5268
|
+
target.kc -= target.del.size;
|
|
5124
5269
|
target.del = null;
|
|
5125
5270
|
}
|
|
5271
|
+
// Plain-data grade (sc 2): bare assignment, as flattenOverlay does.
|
|
5272
|
+
for (const key of Reflect.ownKeys(pb))
|
|
5273
|
+
target.sc === 2 ? (clone[key] = pb[key]) : copyOwn(clone, pb, key);
|
|
5126
5274
|
target.pb = clone;
|
|
5127
5275
|
target.ovl = false;
|
|
5276
|
+
return clone;
|
|
5128
5277
|
}
|
|
5129
5278
|
function ensurePB(target) {
|
|
5130
5279
|
let pb = target.pb;
|
|
@@ -5276,6 +5425,7 @@ function adoptPB(target, incoming, eager = false) {
|
|
|
5276
5425
|
if (heldMaskView(target) === null) target.hv = target.v;
|
|
5277
5426
|
target.ht = activeTransition ?? PLAIN_HOLD;
|
|
5278
5427
|
if (!eager && activeTransition !== null) heldAdoptions.add(target);
|
|
5428
|
+
heldKeys.set(target, [incoming]);
|
|
5279
5429
|
}
|
|
5280
5430
|
target.pb = null;
|
|
5281
5431
|
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
@@ -5405,6 +5555,7 @@ function flattenOverlay(t, pb) {
|
|
|
5405
5555
|
for (const key of Reflect.ownKeys(pb)) t.sc === 2 ? (v[key] = pb[key]) : copyOwn(v, pb, key);
|
|
5406
5556
|
if (t.del !== null) {
|
|
5407
5557
|
for (const key of t.del) delete v[key];
|
|
5558
|
+
t.kc -= t.del.size;
|
|
5408
5559
|
t.del = null;
|
|
5409
5560
|
}
|
|
5410
5561
|
t.pb = null;
|
|
@@ -5465,11 +5616,13 @@ function drainFolds() {
|
|
|
5465
5616
|
foldOlds.set(t, old); // re-queue: commit happens when the hold settles
|
|
5466
5617
|
continue;
|
|
5467
5618
|
}
|
|
5468
|
-
if (t.ovl) {
|
|
5619
|
+
if (t.ovl && (t.v !== old || !overlayRebuilds(t, pb))) {
|
|
5469
5620
|
// Overlay flatten (#3044): the backing keeps its identity, so the
|
|
5470
5621
|
// `t.v === old` gate below skips path copying (the parent slot
|
|
5471
5622
|
// already points here) and the adopted-notify (setter notifications
|
|
5472
|
-
// happened at write time).
|
|
5623
|
+
// happened at write time). A backing privatized mid-batch is a fresh
|
|
5624
|
+
// clone, never a prototype — in-place writes into it are cheap, and
|
|
5625
|
+
// the merge branch below is what a materialized pb would need anyway.
|
|
5473
5626
|
flattenOverlay(t, pb);
|
|
5474
5627
|
} else if (t.v !== old) {
|
|
5475
5628
|
// Privatized mid-batch (#3271): an earlier fold in this drain
|
|
@@ -5512,7 +5665,10 @@ function drainFolds() {
|
|
|
5512
5665
|
t.pb = null;
|
|
5513
5666
|
t.wk = null; // written-keys window closes with the fold commit
|
|
5514
5667
|
} else {
|
|
5515
|
-
|
|
5668
|
+
// An overlay whose fold changed the key set rebuilds (#3689) and
|
|
5669
|
+
// takes the swap like a clone-path draft: the parent slot re-points
|
|
5670
|
+
// in the path copy below.
|
|
5671
|
+
t.v = t.ovl ? materializePB(t) : pb;
|
|
5516
5672
|
t.ch = false; // pb is always a plain clone
|
|
5517
5673
|
t.pb = null;
|
|
5518
5674
|
t.wk = null; // written-keys window closes with the fold commit
|
|
@@ -5778,8 +5934,12 @@ function notifyWrites(t) {
|
|
|
5778
5934
|
if (t.ht !== null) t.ht = t.hv = null;
|
|
5779
5935
|
// Overlay landing (#3352): flatten in place — identity-stable, and
|
|
5780
5936
|
// privatizeCommitted re-slots the parent itself when it has to clone an
|
|
5781
|
-
// unowned seed, so no path copy is needed.
|
|
5782
|
-
|
|
5937
|
+
// unowned seed, so no path copy is needed. A landing that changed the
|
|
5938
|
+
// key set rebuilds instead (#3689) and swaps like a clone-path landing.
|
|
5939
|
+
if (t.ovl) {
|
|
5940
|
+
if (!overlayRebuilds(t, pb)) return flattenOverlay(t, pb);
|
|
5941
|
+
pb = materializePB(t);
|
|
5942
|
+
}
|
|
5783
5943
|
const oldBacking = t.v;
|
|
5784
5944
|
t.pb = null;
|
|
5785
5945
|
t.v = pb;
|
|
@@ -6037,7 +6197,10 @@ function foldHeld(target) {
|
|
|
6037
6197
|
}
|
|
6038
6198
|
return false;
|
|
6039
6199
|
}
|
|
6040
|
-
|
|
6200
|
+
/** The backing a reader is served. `key` (the get/has/descriptor traps)
|
|
6201
|
+
* scopes a fold hold to the keys the fold touched — see pendingBackingVisible —
|
|
6202
|
+
* and an adoption hold to the keys the adoption changed (heldKey). */
|
|
6203
|
+
function readSource(target, key) {
|
|
6041
6204
|
// Adoption hold first (#3074): an adoption staged under a live transaction
|
|
6042
6205
|
// (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
|
|
6043
6206
|
// committed-visibility readers — context-free and children-forbidden ones,
|
|
@@ -6046,7 +6209,9 @@ function readSource(target) {
|
|
|
6046
6209
|
const ht = target.ht;
|
|
6047
6210
|
if (ht !== null && !latestReadActive && !inDraft(target) && !getWriteOverride()) {
|
|
6048
6211
|
const hv = heldMaskView(target);
|
|
6049
|
-
|
|
6212
|
+
// A key the adoption left unchanged derives nothing from the hold, for
|
|
6213
|
+
// any reader (#3706): the backing serves it.
|
|
6214
|
+
if (hv !== null && (key === undefined || heldKey(target, key))) {
|
|
6050
6215
|
const c = readerContext();
|
|
6051
6216
|
if (
|
|
6052
6217
|
c === null ||
|
|
@@ -6056,7 +6221,7 @@ function readSource(target) {
|
|
|
6056
6221
|
return hv;
|
|
6057
6222
|
}
|
|
6058
6223
|
}
|
|
6059
|
-
return pendingBackingVisible(target, false) ? target.pb : target.v;
|
|
6224
|
+
return pendingBackingVisible(target, false, key) ? target.pb : target.v;
|
|
6060
6225
|
}
|
|
6061
6226
|
/** The single pb-vs-committed visibility decision (#3147), shared by per-key
|
|
6062
6227
|
* backing reads (readSource) and deep()/snapshot composition (snapshotWalk)
|
|
@@ -6073,7 +6238,7 @@ function readSource(target) {
|
|
|
6073
6238
|
* ordinary pending staging regardless of owner context (the documented
|
|
6074
6239
|
* divergence from context-free per-key reads) — but never through a hold:
|
|
6075
6240
|
* held truth stays masked exactly as it is for per-key readers. */
|
|
6076
|
-
function pendingBackingVisible(target, speculative) {
|
|
6241
|
+
function pendingBackingVisible(target, speculative, key) {
|
|
6077
6242
|
if (target.pb === null) return false;
|
|
6078
6243
|
// The writer's own channels compose on the pending backing regardless.
|
|
6079
6244
|
if (inDraft(target) || getWriteOverride()) return true;
|
|
@@ -6097,7 +6262,26 @@ function pendingBackingVisible(target, speculative) {
|
|
|
6097
6262
|
if (speculative) return txn === null || ownsHold(txn);
|
|
6098
6263
|
return target.fam !== null && c === null && !foldHeld(target) && txn === null;
|
|
6099
6264
|
}
|
|
6100
|
-
|
|
6265
|
+
const txn = liveFoldTransition(target);
|
|
6266
|
+
// A hold changes what a reader of ONE key sees only through the fold's
|
|
6267
|
+
// writes to that key. A key the fold left alone reads the same from either
|
|
6268
|
+
// backing, so a pass reading it derives nothing from the hold: serve
|
|
6269
|
+
// committed, with no transaction entry and no stale replay (#3688 — a memo
|
|
6270
|
+
// reading an unchanged key beside an independent signal was held with
|
|
6271
|
+
// someone else's action). Per-node reads already have this precision (a
|
|
6272
|
+
// node with nothing staged enters nothing; stageHeldKey skips an equal
|
|
6273
|
+
// value); this is the same rule at the container gate. `wk` is the trap's
|
|
6274
|
+
// record of every write and delete this batch (deletes included, on the
|
|
6275
|
+
// overlay and clone paths alike); null (a fold with no trap writes) or
|
|
6276
|
+
// WK_ALL (an array length write) leaves the whole container held. Same
|
|
6277
|
+
// exclusions as heldFoldTransition: an optimistic family's draft is seeded
|
|
6278
|
+
// from node overrides the trap never saw, and a chained backing's
|
|
6279
|
+
// committed layer is a live proxy, not a frozen twin of the clone.
|
|
6280
|
+
if (txn !== null && key !== undefined && !target.ch && target.fam?.opt !== true) {
|
|
6281
|
+
const wk = target.wk;
|
|
6282
|
+
if (wk != null && wk !== WK_ALL && !wk.has(key)) return false;
|
|
6283
|
+
}
|
|
6284
|
+
return holdVisible(txn, c);
|
|
6101
6285
|
}
|
|
6102
6286
|
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
6103
6287
|
* stamped by a live transition that retains optimism — is masked from LANE
|
|
@@ -6411,7 +6595,7 @@ const traps = {
|
|
|
6411
6595
|
// memo parity for latest() reads through a projection.
|
|
6412
6596
|
if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
|
|
6413
6597
|
pullProjectionForLatest(target);
|
|
6414
|
-
const src = readSource(target);
|
|
6598
|
+
const src = readSource(target, key);
|
|
6415
6599
|
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
6416
6600
|
// deleted keys are tracked aside and read as absent in the pending view.
|
|
6417
6601
|
if (target.del !== null && src === target.pb && target.del.has(key)) {
|
|
@@ -6587,7 +6771,7 @@ const traps = {
|
|
|
6587
6771
|
if (key === $OWNER || key === $RECORD) return false;
|
|
6588
6772
|
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
6589
6773
|
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
6590
|
-
const src = readSource(target);
|
|
6774
|
+
const src = readSource(target, key);
|
|
6591
6775
|
let present = key in src;
|
|
6592
6776
|
// Overlay deletes read as absent in the pending view (#3044).
|
|
6593
6777
|
if (present && target.del !== null && src === target.pb && target.del.has(key)) present = false;
|
|
@@ -6644,7 +6828,7 @@ const traps = {
|
|
|
6644
6828
|
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
6645
6829
|
const obs = getObserver();
|
|
6646
6830
|
if (target.fam !== null && obs === null && !inDraft(target)) firewallGate(target);
|
|
6647
|
-
const src = readSource(target);
|
|
6831
|
+
const src = readSource(target, key);
|
|
6648
6832
|
const desc = visibleDescriptor(target, src, key);
|
|
6649
6833
|
if (!inDraft(target) && obs !== null && !observerHoldsKeySet(target, obs)) {
|
|
6650
6834
|
// The node is born from the source's presence (as `has` births it),
|
|
@@ -6690,10 +6874,12 @@ const traps = {
|
|
|
6690
6874
|
}
|
|
6691
6875
|
} else {
|
|
6692
6876
|
if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
|
|
6693
|
-
// Live own-key estimate for the overlay/clone choice (#3360)
|
|
6694
|
-
// sees through an overlay to the
|
|
6695
|
-
//
|
|
6696
|
-
//
|
|
6877
|
+
// Live own-key estimate for the overlay/clone choice (#3360) and the
|
|
6878
|
+
// overlay's commit choice (#3689): `in` sees through an overlay to the
|
|
6879
|
+
// committed keys, so this counts keys NEW to the container. Overlay
|
|
6880
|
+
// deletes are counted down when they commit (`del` is exact); clone-
|
|
6881
|
+
// path deletes are not (a stale high count on a narrow container only
|
|
6882
|
+
// picks the overlay a little early).
|
|
6697
6883
|
if (!(key in pb)) target.kc++;
|
|
6698
6884
|
}
|
|
6699
6885
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
@@ -8960,6 +9146,30 @@ function updateKeyedMap() {
|
|
|
8960
9146
|
// entered rows, for the list-identity census.
|
|
8961
9147
|
removedItems,
|
|
8962
9148
|
createdItems;
|
|
9149
|
+
// The pass's write to a per-slot signal (a row accessor in index mode,
|
|
9150
|
+
// an index accessor in keyed mode). The list's frame lives in these
|
|
9151
|
+
// writes as much as in the computed's result, so under a LANE pass they
|
|
9152
|
+
// are the lane's frame too (F1): a plain `setSignal` staged them into
|
|
9153
|
+
// the action's transaction and the row readers were served the
|
|
9154
|
+
// committed value until the action landed — `<For>` without `keyed`
|
|
9155
|
+
// showed the pre-action list for the whole action while the keyed modes
|
|
9156
|
+
// showed the optimistic one. The engine publishes the write as a
|
|
9157
|
+
// derived override on the slot (lanes stage, A17) and lands or
|
|
9158
|
+
// supersedes it when a plain pass later writes a slot still carrying
|
|
9159
|
+
// one (A18) — `landOnOverride`'s slot arm. Decided once per pass, not
|
|
9160
|
+
// per slot: a lane pass marks the map (`_laneSlots`: its slots may
|
|
9161
|
+
// carry overrides), a marked map routes every slot write through the
|
|
9162
|
+
// engine while any lane is live — a slot's override resolves with its
|
|
9163
|
+
// lane's transaction, or with the batch for an orphan lane
|
|
9164
|
+
// (resolveOptimisticNodes, then cleanupCompletedLanes, both before the
|
|
9165
|
+
// completion wakes any recompute), so a pass that finds no lane live
|
|
9166
|
+
// knows the slots are clean and drops the mark — and an unmarked map
|
|
9167
|
+
// takes `setSignal` directly. The engine is installed whenever a lane
|
|
9168
|
+
// is active.
|
|
9169
|
+
const write = (this._laneSlots =
|
|
9170
|
+
currentOptimisticLane !== null || (this._laneSlots && activeLanes.size !== 0))
|
|
9171
|
+
? GlobalQueue._landOnOverride
|
|
9172
|
+
: setSignal;
|
|
8963
9173
|
// skip common prefix
|
|
8964
9174
|
for (
|
|
8965
9175
|
start = 0, end = Math.min(this._len, newLen);
|
|
@@ -8968,7 +9178,7 @@ function updateKeyedMap() {
|
|
|
8968
9178
|
(this._rows && compare(this._key, this._items[start], newItems[start])));
|
|
8969
9179
|
start++
|
|
8970
9180
|
) {
|
|
8971
|
-
if (this._rows)
|
|
9181
|
+
if (this._rows) write(this._rows[start], newItems[start]);
|
|
8972
9182
|
}
|
|
8973
9183
|
// skip common suffix — counted only; retained entries land in one pass
|
|
8974
9184
|
// at commit instead of being staged and copied twice
|
|
@@ -9069,19 +9279,19 @@ function updateKeyedMap() {
|
|
|
9069
9279
|
indexes && (indexes[i] = this._indexes[i]);
|
|
9070
9280
|
}
|
|
9071
9281
|
for (j = start; j <= newEnd; j++) {
|
|
9072
|
-
if (rows)
|
|
9073
|
-
if (indexes)
|
|
9282
|
+
if (rows) write(rows[j], newItems[j]);
|
|
9283
|
+
if (indexes) write(indexes[j], j);
|
|
9074
9284
|
}
|
|
9075
9285
|
for (j = newEnd + 1; j < newLen; j++) {
|
|
9076
9286
|
temp[j] = this._mappings[j - dif];
|
|
9077
9287
|
tempNodes[j] = this._nodes[j - dif];
|
|
9078
9288
|
if (rows) {
|
|
9079
9289
|
rows[j] = this._rows[j - dif];
|
|
9080
|
-
|
|
9290
|
+
write(rows[j], newItems[j]);
|
|
9081
9291
|
}
|
|
9082
9292
|
if (indexes) {
|
|
9083
9293
|
indexes[j] = this._indexes[j - dif];
|
|
9084
|
-
if (dif !== 0)
|
|
9294
|
+
if (dif !== 0) write(indexes[j], j);
|
|
9085
9295
|
}
|
|
9086
9296
|
}
|
|
9087
9297
|
this._mappings = temp;
|
|
@@ -7,12 +7,13 @@ import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, i
|
|
|
7
7
|
/** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */ const FALLBACK_FLASH_MS = 150;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Under an owner the observer marked as its own (`OBSERVE.exclude
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* exists, before that a flag
|
|
10
|
+
* Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
|
|
11
|
+
* re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
|
|
12
|
+
* (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
|
|
13
|
+
* body, which is nobody's node): the engine records nothing about the node.
|
|
14
|
+
* Plumbing is a bit read and excludes the node alone, not what it owns; the
|
|
15
|
+
* observer exclusion is cached per node once any exists, before that a flag
|
|
16
|
+
* read.
|
|
16
17
|
*/ function excludedNode(e) {
|
|
17
18
|
if ((e.C & CONFIG_PLUMBING) !== 0) return true;
|
|
18
19
|
if (!anyExcluded()) return false;
|
|
@@ -373,15 +374,22 @@ function popFrame(e) {
|
|
|
373
374
|
return;
|
|
374
375
|
}
|
|
375
376
|
}
|
|
376
|
-
|
|
377
|
+
// The initial navigation is the document's: its request is the time origin
|
|
378
|
+
// unless the router says otherwise, and it left nowhere.
|
|
379
|
+
const n = e.initial === true;
|
|
380
|
+
const t = {
|
|
377
381
|
kind: e.kind,
|
|
378
|
-
at: e.at ?? now()
|
|
382
|
+
at: e.at ?? (n ? 0 : now())
|
|
379
383
|
};
|
|
380
|
-
if (e.from !== undefined)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
384
|
+
if (!n && e.from !== undefined) t.from = e.from;
|
|
385
|
+
// Declared beats ambient: a router that awaited before writing hands back
|
|
386
|
+
// the origin it captured in the request (`undefined` when the request ran
|
|
387
|
+
// under none — still a declaration); what is on the stack now is whatever
|
|
388
|
+
// happened to be running.
|
|
389
|
+
const o = "interaction" in e ? interactionOf(e.interaction) : enclosingInteraction();
|
|
390
|
+
if (o !== undefined) t.interaction = o;
|
|
391
|
+
originFrames.push(t);
|
|
392
|
+
openNavigation(t, e);
|
|
385
393
|
}
|
|
386
394
|
|
|
387
395
|
function originEnd() {
|
|
@@ -415,7 +423,8 @@ function originEnd() {
|
|
|
415
423
|
if (e.to !== undefined && e.to !== n) t.push(e.to);
|
|
416
424
|
const o = navStates.get(e)?.event.redirects;
|
|
417
425
|
if (o !== undefined) t.push(`redirected from ${o.map(e => e.to ?? e.name ?? "?").join(" → ")}`);
|
|
418
|
-
|
|
426
|
+
const i = navStates.get(e)?.event.initial === true;
|
|
427
|
+
return `${i ? "initial " : ""}navigation to ${n}${t.length > 0 ? ` (${t.join(", ")})` : ""}`;
|
|
419
428
|
}
|
|
420
429
|
|
|
421
430
|
default:
|
|
@@ -1778,8 +1787,10 @@ function checkLongHold(e, n) {
|
|
|
1778
1787
|
// derived override promotes rather than reverts. Same predicate the hold
|
|
1779
1788
|
// census uses to skip them.
|
|
1780
1789
|
if (!options.optimisticReverts || isCompanion(e)) return;
|
|
1781
|
-
|
|
1782
|
-
|
|
1790
|
+
// Method call, as every commit path does: store slot nodes share one
|
|
1791
|
+
// comparator that reads `this._host` (#3687).
|
|
1792
|
+
const i = e.Fe;
|
|
1793
|
+
if (i && i.call(e, n, t)) return;
|
|
1783
1794
|
const s = nodeName(e);
|
|
1784
1795
|
// The two values are user data: quoted only under `values: "full"`; the
|
|
1785
1796
|
// other levels say what happened without saying what was shown.
|
|
@@ -1841,17 +1852,17 @@ function checkLongHold(e, n) {
|
|
|
1841
1852
|
for (const t of e) {
|
|
1842
1853
|
n.owners++;
|
|
1843
1854
|
// Iterative: a deep tree must not grow the call stack.
|
|
1844
|
-
let e = t.
|
|
1855
|
+
let e = t.On;
|
|
1845
1856
|
for (;;) {
|
|
1846
1857
|
while (e !== null) {
|
|
1847
1858
|
n.owners++;
|
|
1848
1859
|
if (typeof e.Se === "function") meet(e);
|
|
1849
|
-
if (e.
|
|
1850
|
-
e = e.
|
|
1860
|
+
if (e.On !== null) i.push(e);
|
|
1861
|
+
e = e._n;
|
|
1851
1862
|
}
|
|
1852
1863
|
const t = i.pop();
|
|
1853
1864
|
if (t === undefined) break;
|
|
1854
|
-
e = t.
|
|
1865
|
+
e = t.On;
|
|
1855
1866
|
}
|
|
1856
1867
|
}
|
|
1857
1868
|
for (let e = 0; e < o.length; e++) {
|
|
@@ -2070,6 +2081,7 @@ function openNavigation(e, n) {
|
|
|
2070
2081
|
writes: 0,
|
|
2071
2082
|
origin: e
|
|
2072
2083
|
};
|
|
2084
|
+
if (n.initial === true) t.initial = true;
|
|
2073
2085
|
if (e.from !== undefined) t.from = e.from;
|
|
2074
2086
|
if (e.interaction !== undefined) t.interaction = e.interaction;
|
|
2075
2087
|
const o = {
|
|
@@ -2295,7 +2307,10 @@ function settleNavigation(e, n, t) {
|
|
|
2295
2307
|
o.settledMs = now() - o.at;
|
|
2296
2308
|
o.outcome = n;
|
|
2297
2309
|
if (t !== undefined) o.hold = t;
|
|
2298
|
-
|
|
2310
|
+
// The initial declaration is not a navigation the person waited on: its
|
|
2311
|
+
// `settledMs` is document start → router built, which would read as the
|
|
2312
|
+
// route's responsiveness in the feedback tables. Consumers get the record.
|
|
2313
|
+
if (o.initial !== true) for (const e of folds) e.navigation?.(o);
|
|
2299
2314
|
records.emit("navigation", o, undefined);
|
|
2300
2315
|
trackGraph(o);
|
|
2301
2316
|
// The interaction that performed it may have been waiting only on this.
|
|
@@ -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
|