@solidjs/signals 2.0.0-rc.1 → 2.0.0-rc.10
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 +7138 -0
- package/dist/dev.attribution.js +3307 -0
- package/dist/dev.js +5691 -4491
- package/dist/observe/affects.js +129 -0
- package/dist/observe/attribution.js +7 -0
- package/dist/observe/boundaries.js +793 -0
- package/dist/observe/core/action.js +177 -0
- package/dist/observe/core/async.js +849 -0
- package/dist/observe/core/attribution-costs.js +66 -0
- package/dist/observe/core/attribution-feedback.js +280 -0
- package/dist/observe/core/attribution-hooks.js +83 -0
- package/dist/observe/core/attribution-queries.js +43 -0
- package/dist/observe/core/attribution.js +2904 -0
- package/dist/observe/core/constants.js +283 -0
- package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
- package/dist/observe/core/core.js +1999 -0
- package/dist/observe/core/dev.js +343 -0
- package/dist/observe/core/effect.js +200 -0
- package/dist/observe/core/error-hooks.js +71 -0
- package/dist/observe/core/error.js +80 -0
- package/dist/observe/core/external.js +98 -0
- package/dist/observe/core/graph.js +155 -0
- package/dist/observe/core/heap.js +147 -0
- package/dist/observe/core/invariants.js +45 -0
- package/dist/observe/core/lanes.js +205 -0
- package/dist/observe/core/optimistic.js +627 -0
- package/dist/observe/core/owner.js +353 -0
- package/dist/observe/core/scheduler.js +1547 -0
- package/dist/observe/core/verdict.js +674 -0
- package/dist/observe/index.js +59 -0
- package/dist/observe/map.js +484 -0
- package/dist/observe/signals.js +731 -0
- package/dist/observe/store/index.js +44 -0
- package/dist/observe/store/next/optimistic.js +717 -0
- package/dist/observe/store/next/projection.js +266 -0
- package/dist/observe/store/next/reconcile.js +375 -0
- package/dist/observe/store/next/store.js +2480 -0
- package/dist/observe/store/next/target.js +51 -0
- package/dist/observe/store/store.js +331 -0
- package/dist/observe/store/storePath.js +103 -0
- package/dist/observe/store/utils.js +1070 -0
- package/dist/prod/affects.js +10 -9
- package/dist/prod/attribution.js +45 -0
- package/dist/prod/boundaries.js +388 -178
- package/dist/prod/core/action.js +65 -37
- package/dist/prod/core/async.js +378 -163
- package/dist/prod/core/attribution-hooks.js +3 -0
- package/dist/prod/core/constants.js +186 -1
- package/dist/prod/core/context.js +10 -16
- package/dist/prod/core/core.js +1546 -416
- package/dist/prod/core/dev.js +18 -1
- package/dist/prod/core/effect.js +119 -79
- package/dist/prod/core/error-hooks.js +71 -0
- package/dist/prod/core/error.js +13 -1
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +105 -54
- package/dist/prod/core/heap.js +72 -65
- package/dist/prod/core/invariants.js +3 -2
- package/dist/prod/core/lanes.js +117 -52
- package/dist/prod/core/optimistic.js +458 -104
- package/dist/prod/core/owner.js +131 -84
- package/dist/prod/core/scheduler.js +998 -241
- package/dist/prod/core/verdict.js +417 -112
- package/dist/prod/index.js +20 -8
- package/dist/prod/map.js +312 -151
- package/dist/prod/signals.js +363 -32
- package/dist/prod/store/index.js +2 -0
- package/dist/prod/store/next/optimistic.js +530 -197
- package/dist/prod/store/next/projection.js +142 -57
- package/dist/prod/store/next/reconcile.js +149 -103
- package/dist/prod/store/next/store.js +1470 -346
- package/dist/prod/store/next/target.js +40 -9
- package/dist/prod/store/store.js +23 -19
- package/dist/prod/store/utils.js +997 -130
- package/dist/types/attribution.d.ts +19 -0
- package/dist/types/attribution.prod.d.ts +22 -0
- package/dist/types/boundaries.d.ts +78 -5
- package/dist/types/core/action.d.ts +12 -5
- package/dist/types/core/async.d.ts +3 -1
- package/dist/types/core/attribution-costs.d.ts +35 -0
- package/dist/types/core/attribution-feedback.d.ts +133 -0
- package/dist/types/core/attribution-hooks.d.ts +304 -0
- package/dist/types/core/attribution-queries.d.ts +24 -0
- package/dist/types/core/attribution.d.ts +892 -0
- package/dist/types/core/constants.d.ts +179 -0
- package/dist/types/core/core.d.ts +299 -25
- package/dist/types/core/dev.d.ts +421 -8
- package/dist/types/core/effect.d.ts +8 -1
- package/dist/types/core/error-hooks.d.ts +71 -0
- package/dist/types/core/error.d.ts +9 -0
- package/dist/types/core/graph.d.ts +23 -0
- package/dist/types/core/heap.d.ts +5 -3
- package/dist/types/core/index.d.ts +6 -3
- package/dist/types/core/invariants.d.ts +5 -1
- package/dist/types/core/lanes.d.ts +51 -19
- package/dist/types/core/owner.d.ts +15 -3
- package/dist/types/core/scheduler.d.ts +248 -1
- package/dist/types/core/types.d.ts +124 -41
- package/dist/types/index.d.ts +18 -5
- package/dist/types/map.d.ts +2 -0
- package/dist/types/signals.d.ts +145 -13
- package/dist/types/store/index.d.ts +7 -6
- package/dist/types/store/next/optimistic.d.ts +18 -13
- package/dist/types/store/next/projection.d.ts +5 -2
- package/dist/types/store/next/reconcile.d.ts +7 -0
- package/dist/types/store/next/store.d.ts +83 -11
- package/dist/types/store/next/target.d.ts +108 -11
- package/dist/types/store/store.d.ts +20 -11
- package/dist/types/store/utils.d.ts +188 -0
- package/package.json +31 -26
- package/dist/node.cjs +0 -8137
- package/dist/types/store/optimistic.d.ts +0 -45
- package/dist/types/store/projection.d.ts +0 -70
- package/dist/types/store/reconcile.d.ts +0 -46
- package/dist/types-cjs/affects.d.cts +0 -47
- package/dist/types-cjs/boundaries.d.cts +0 -175
- package/dist/types-cjs/core/action.d.cts +0 -64
- package/dist/types-cjs/core/async.d.cts +0 -21
- package/dist/types-cjs/core/constants.d.cts +0 -71
- package/dist/types-cjs/core/core.d.cts +0 -156
- package/dist/types-cjs/core/dev.d.cts +0 -64
- package/dist/types-cjs/core/effect.d.cts +0 -30
- package/dist/types-cjs/core/error.d.cts +0 -47
- package/dist/types-cjs/core/external.d.cts +0 -15
- package/dist/types-cjs/core/graph.d.cts +0 -5
- package/dist/types-cjs/core/heap.d.cts +0 -22
- package/dist/types-cjs/core/index.d.cts +0 -13
- package/dist/types-cjs/core/invariants.d.cts +0 -59
- package/dist/types-cjs/core/lanes.d.cts +0 -56
- package/dist/types-cjs/core/optimistic.d.cts +0 -6
- package/dist/types-cjs/core/owner.d.cts +0 -124
- package/dist/types-cjs/core/scheduler.d.cts +0 -172
- package/dist/types-cjs/core/types.d.cts +0 -148
- package/dist/types-cjs/core/verdict.d.cts +0 -2
- package/dist/types-cjs/index.d.cts +0 -10
- package/dist/types-cjs/map.d.cts +0 -69
- package/dist/types-cjs/package.json +0 -3
- package/dist/types-cjs/signals.d.cts +0 -624
- package/dist/types-cjs/store/index.d.cts +0 -19
- package/dist/types-cjs/store/next/optimistic.d.cts +0 -20
- package/dist/types-cjs/store/next/projection.d.cts +0 -24
- package/dist/types-cjs/store/next/reconcile.d.cts +0 -3
- package/dist/types-cjs/store/next/store.d.cts +0 -71
- package/dist/types-cjs/store/next/target.d.cts +0 -99
- package/dist/types-cjs/store/optimistic.d.cts +0 -45
- package/dist/types-cjs/store/projection.d.cts +0 -70
- package/dist/types-cjs/store/reconcile.d.cts +0 -46
- package/dist/types-cjs/store/store.d.cts +0 -138
- package/dist/types-cjs/store/storePath.d.cts +0 -58
- package/dist/types-cjs/store/utils.d.cts +0 -74
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { unwrapOverride, $REFRESH, CONFIG_OWNED_WRITE, NOT_PENDING, STATUS_UNINITIALIZED, STATUS_ERROR,
|
|
1
|
+
import { unwrapOverride, $REFRESH, CONFIG_CHILDREN_FORBIDDEN, CONFIG_OWNED_WRITE, NOT_PENDING, CONFIG_OPTIMISTIC, STATUS_UNINITIALIZED, STATUS_ERROR, REACTIVE_RECOMPUTING_DEPS, CONFIG_OVERRIDE_SUPERSEDED, CONFIG_AUTHORITATIVE_READ } from "../../core/constants.js";
|
|
2
2
|
|
|
3
|
-
import { setSignal, isEqual, read, pendingCheckActive, readNodeFast, READ_SLOW, signal } from "../../core/core.js";
|
|
3
|
+
import { setSlotUnobserved, setSignal, isEqual, rederiveHeld, suppressComputedRecompute, read, hasActiveOverride, pendingCheckActive, latestReadActive, readNodeFast, READ_SLOW, signal, unlinkFirewallChild, ext, visibleOverride, ownsHold, serve, setLatestReadActive, prepareComputed, slotSignal, stale, recordStaleReplay, enterStagedRead, context, currentOptimisticLane, heldDerivation } from "../../core/core.js";
|
|
4
4
|
|
|
5
|
-
import { projectionWriteActive, setProjectionWriteActive, schedule, setStoreCommitHook } from "../../core/scheduler.js";
|
|
5
|
+
import { projectionWriteActive, setProjectionWriteActive, activeTransition, schedule, currentTransition, deferSlotRelease, setStoreCommitHook } from "../../core/scheduler.js";
|
|
6
|
+
|
|
7
|
+
import "../../core/invariants.js";
|
|
6
8
|
|
|
7
9
|
import { getObserver, getOwner } from "../../core/owner.js";
|
|
8
10
|
|
|
9
|
-
import { $TARGET, isWrappable, markRawIngest, $PROXY, getWriteOverride, markRawOne, witnessAffectsMark, $TRACK, affectsScopesLive, inheritAffectsMarks, $AFFECTS, rawValuesUsed, isRawValue, setNextAffectsNodeResolver } from "../store.js";
|
|
11
|
+
import { $TARGET, isWrappable, markRawIngest, $PROXY, getWriteOverride, markRawOne, $RECORD, witnessAffectsMark, $TRACK, affectsScopesLive, inheritAffectsMarks, $AFFECTS, rawValuesUsed, isRawValue, setNextAffectsNodeResolver } from "../store.js";
|
|
10
12
|
|
|
11
|
-
import {
|
|
13
|
+
import { lookupTarget, optHooks, storeNextLookup, $OWNER, isOwned, markDescendants, devAssertNeverUserMutation } from "./target.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Store rewrite — increment 2: plain deep stores with pending-backing writes.
|
|
@@ -30,13 +32,56 @@ import { storeNextLookup, optHooks, ownedRaw, devAssertNeverUserMutation } from
|
|
|
30
32
|
*/
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
32
34
|
// wrap / dedupe
|
|
35
|
+
/** Pre-shaped constructor for OBJECT proxy targets: V8 tips a bare `{}` into
|
|
36
|
+
* dictionary mode once ~19 named properties are assigned onto it (the #3044
|
|
37
|
+
* `ovl`/`del` fields crossed that line — every trap's field read became a
|
|
38
|
+
* hash lookup, a 15% deep-dbmon tick regression). Declaring every field in a
|
|
39
|
+
* constructor pre-allocates in-object slots so the map stays fast, with
|
|
40
|
+
* headroom for future fields. The prototype is reset to `Object.prototype`
|
|
41
|
+
* so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
|
|
42
|
+
* plain object's. Array targets keep the bare-`[]` path — they must carry
|
|
43
|
+
* the array exotic class for `Array.isArray(proxy)`.
|
|
44
|
+
*
|
|
45
|
+
* ARRAY SHAPE RULE: arrays normalize their named properties to dictionary
|
|
46
|
+
* mode as the count grows (V8 13.x: counts ≡ 0 mod 3 from 18 up), so the
|
|
47
|
+
* target's named field count is capped at 20 — any future write-side state
|
|
48
|
+
* beyond `wk` must ride an extension object (see target.ts), never new
|
|
49
|
+
* named fields here. */
|
|
50
|
+
function TargetShape() {
|
|
51
|
+
this.v = undefined;
|
|
52
|
+
this.ch = undefined;
|
|
53
|
+
this.pb = undefined;
|
|
54
|
+
this.n = undefined;
|
|
55
|
+
this.h = undefined;
|
|
56
|
+
this.k = undefined;
|
|
57
|
+
this.dk = undefined;
|
|
58
|
+
this.u = undefined;
|
|
59
|
+
this.pk = undefined;
|
|
60
|
+
this.px = undefined;
|
|
61
|
+
this.d = undefined;
|
|
62
|
+
this.a = undefined;
|
|
63
|
+
this.sc = undefined;
|
|
64
|
+
this.kc = undefined;
|
|
65
|
+
this.nc = undefined;
|
|
66
|
+
this.ab = undefined;
|
|
67
|
+
this.fam = undefined;
|
|
68
|
+
this.s = undefined;
|
|
69
|
+
this.ovl = undefined;
|
|
70
|
+
this.del = undefined;
|
|
71
|
+
this.wk = undefined;
|
|
72
|
+
this.hv = undefined;
|
|
73
|
+
this.ht = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
TargetShape.prototype = Object.prototype;
|
|
77
|
+
|
|
33
78
|
function createTarget(e, t, n, r = t?.fam ?? null) {
|
|
34
79
|
// The proxy target carries the array exotic class when the value is an
|
|
35
80
|
// array, so Array.isArray(proxy) is true; the fields live on it directly.
|
|
36
81
|
// Direct field assignment in one fixed order (no Object.assign literal
|
|
37
82
|
// copy): every target shares a hidden-class transition chain — createTarget
|
|
38
83
|
// was the #2 store cost in the uibench creation profile.
|
|
39
|
-
const i = Array.isArray(e) ? [] :
|
|
84
|
+
const i = Array.isArray(e) ? [] : new TargetShape;
|
|
40
85
|
i.v = e;
|
|
41
86
|
// Chained-backing flag (backing IS another store's proxy, §7b) — cached so
|
|
42
87
|
// the hot read path never does a per-read symbol lookup on the backing.
|
|
@@ -46,16 +91,22 @@ function createTarget(e, t, n, r = t?.fam ?? null) {
|
|
|
46
91
|
i.h = null;
|
|
47
92
|
i.k = null;
|
|
48
93
|
i.dk = null;
|
|
94
|
+
i.wk = null;
|
|
49
95
|
i.u = t;
|
|
50
96
|
i.pk = n;
|
|
51
97
|
i.px = null;
|
|
52
98
|
i.d = false;
|
|
53
99
|
i.a = false;
|
|
54
|
-
i.sc =
|
|
100
|
+
i.sc = 0;
|
|
101
|
+
i.kc = 0;
|
|
55
102
|
i.nc = 0;
|
|
56
|
-
i.
|
|
103
|
+
i.ab = null;
|
|
57
104
|
i.fam = r;
|
|
58
105
|
i.s = false;
|
|
106
|
+
i.ovl = false;
|
|
107
|
+
i.del = null;
|
|
108
|
+
i.hv = null;
|
|
109
|
+
i.ht = null;
|
|
59
110
|
i.px = new Proxy(i, traps);
|
|
60
111
|
// Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
|
|
61
112
|
// proxy off looked-up targets as a field.
|
|
@@ -68,13 +119,13 @@ function wrapNext(e, t = null, n = null, r = t?.fam ?? null) {
|
|
|
68
119
|
// markRaw'd values never wrap through ANY store (R42; sticky raw-marking
|
|
69
120
|
// is one half of the never-both-wrapped-and-raw invariant, RUL-12).
|
|
70
121
|
if (rawValuesUsed && isRawValue(e)) return e;
|
|
71
|
-
const i = (r
|
|
122
|
+
const i = lookupTarget(e, r);
|
|
72
123
|
if (i !== undefined) return i.px;
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
124
|
+
const l = e[$TARGET];
|
|
125
|
+
if (l !== undefined && l.px === e) {
|
|
75
126
|
// Foreign-family proxies re-wrap into THIS family (writes stay isolated);
|
|
76
127
|
// same-family and plain-store proxies pass through.
|
|
77
|
-
if (r === null ||
|
|
128
|
+
if (r === null || l.fam === r) return e;
|
|
78
129
|
return createTarget(e, t, n, r).px;
|
|
79
130
|
}
|
|
80
131
|
return createTarget(e, t, n, r).px;
|
|
@@ -83,98 +134,248 @@ function wrapNext(e, t = null, n = null, r = t?.fam ?? null) {
|
|
|
83
134
|
/** Unwrap our own proxies to their current backing; leave everything else. */ function unwrapValue(e) {
|
|
84
135
|
if (e == null || typeof e !== "object") return e;
|
|
85
136
|
const t = e[$TARGET];
|
|
86
|
-
if (t !== undefined && t.px === e && t.v !== undefined)
|
|
137
|
+
if (t !== undefined && t.px === e && t.v !== undefined) {
|
|
138
|
+
// A draft escaping into other storage must be a REAL container that
|
|
139
|
+
// becomes this target's committed backing at fold (the shared-raw
|
|
140
|
+
// contract) — a prototype overlay is neither.
|
|
141
|
+
if (t.ovl) materializePB(t);
|
|
142
|
+
return t.pb ?? t.v;
|
|
143
|
+
}
|
|
87
144
|
return e;
|
|
88
145
|
}
|
|
89
146
|
|
|
90
147
|
// ---------------------------------------------------------------------------
|
|
91
148
|
// nodes: pure subscription points (values used only for equality gating)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
e.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
// Shared slot-node equality (create-floor diet): ONE function for every
|
|
150
|
+
// store node — `this` is the node (method-call convention at every _equals
|
|
151
|
+
// site), `_host` is the baked-in target backref. Logical-slot equality:
|
|
152
|
+
// values resolving to the same child target are the same slot
|
|
153
|
+
// (privatization/adoption swap raw identity without changing the logical
|
|
154
|
+
// value — only changed leaves notify, R9).
|
|
155
|
+
const slotNodeEquals = function(e, t) {
|
|
156
|
+
return isEqual(e, t) || sameLogicalSlot(this.Bn, e, t);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Shared slot-node unobserved handler (create-floor diet): registered once;
|
|
160
|
+
// the core sweep dispatches CONFIG_SLOT_NODE nodes here instead of holding a
|
|
161
|
+
// per-node closure in a per-node NodeExtension.
|
|
162
|
+
setSlotUnobserved(e => {
|
|
163
|
+
// A live affects() mark keeps the node addressable (sweep parity).
|
|
164
|
+
if (e.o?.t) return;
|
|
165
|
+
// An active override or a staged write is state only the node holds (an
|
|
166
|
+
// optimistic signal keeps its override whether or not anything reads it —
|
|
167
|
+
// store parity, posture-store-parity S7): defer the release to the flush
|
|
168
|
+
// that resolves it (the scheduler's transient-node sweep).
|
|
169
|
+
if (hasActiveOverride(e) || e._e !== NOT_PENDING) return deferSlotRelease(e);
|
|
170
|
+
const t = e.Bn;
|
|
171
|
+
const n = e.Kn;
|
|
172
|
+
if (t.n && t.n[n] === e) {
|
|
173
|
+
delete t.n[n];
|
|
174
|
+
t.nc--;
|
|
175
|
+
// Projection leaves also leave the firewall child chain (#3351): a
|
|
176
|
+
// dropped node is unreachable through the store — a fresh read makes a
|
|
177
|
+
// fresh node — so the chain would only retain it and its last value.
|
|
178
|
+
unlinkFirewallChild(e);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
function getNode(e, t, n,
|
|
183
|
+
// First-read dedupe (create-floor slice 2): the get trap probes
|
|
184
|
+
// accessor-ness right before creating the node — pass the verdict through
|
|
185
|
+
// so creation skips the second descriptor scan. -1 = unknown (other
|
|
186
|
+
// callers), 0/1 = probed.
|
|
187
|
+
r = -1) {
|
|
188
|
+
const i = e.n ??= Object.create(null);
|
|
189
|
+
let l = i[t];
|
|
190
|
+
if (l === undefined) {
|
|
191
|
+
// Born holding (#3330 store twin): a key first read while a live
|
|
192
|
+
// transaction holds an ADOPTION on this target (adoptPB's `ht`) is
|
|
193
|
+
// created as if it had existed when the hold was notified — the adopted
|
|
194
|
+
// value already swapped into `v`, so committed is the held view
|
|
195
|
+
// `hv[key]` and the adopted `v[key]` is staged under the transaction
|
|
196
|
+
// (stageHeldKey). Its held-adoption notification (stageHeldAdoptions)
|
|
197
|
+
// ran before the node existed and the drain has nothing left to say.
|
|
198
|
+
// Without this the node was born from whichever view its first reader
|
|
199
|
+
// saw and never learned the other. Two kinds of hold, one rule (#3336):
|
|
200
|
+
// - a held FOLD (pb): a setter's write to an unobserved key landed only
|
|
201
|
+
// in the pending backing; committed is `v[key]`, staged is `pb[key]`
|
|
202
|
+
// (undefined for a deleted key);
|
|
203
|
+
// - a held ADOPTION (ht): the adopted value already swapped into `v`;
|
|
204
|
+
// committed is the held view `hv[key]`, staged is `v[key]`.
|
|
205
|
+
const o = heldFoldTransition(e);
|
|
206
|
+
let f = heldAdoptionTransition(e);
|
|
207
|
+
if (f !== null) n = e.hv[t]; else if ((f = o) !== null) n = e.v[t];
|
|
208
|
+
// Create-floor diet: slotSignal bakes the whole node into one literal —
|
|
209
|
+
// no options object, no equals/unobserved closures, no NodeExtension,
|
|
210
|
+
// no post-construction expandos (acc + the wrap cache px/pxv are
|
|
211
|
+
// pre-shaped fields: the proxy last served for this key and the raw it
|
|
212
|
+
// wrapped — one pointer compare replaces the per-read WeakMap lookup in
|
|
213
|
+
// wrapNext). ownedWrite rides the literal's config: the setter carries
|
|
214
|
+
// the owned-scope write guard; node-level setSignals are internal
|
|
215
|
+
// notification machinery. Projection nodes carry the projection
|
|
216
|
+
// computed as their firewall: reads through them link the derive's
|
|
217
|
+
// status/lifecycle (§7b).
|
|
218
|
+
const s = l = slotSignal(n, slotNodeEquals, e, t,
|
|
219
|
+
// Accessor-ness resolved ONCE per node (no per-object descriptor
|
|
220
|
+
// scan on reads): accessor keys serve through Reflect.get with the
|
|
221
|
+
// proxy receiver.
|
|
222
|
+
r === -1 ? isOwnAccessor(e.pb ?? e.v, t) : r === 1, e.fam?.node ?? undefined);
|
|
127
223
|
// Optimistic families: arm the override slot — setSignal routes armed
|
|
128
224
|
// nodes through the core engine (lanes, ownership, reverts all native).
|
|
129
|
-
if (e.fam?.opt)
|
|
225
|
+
if (e.fam?.opt) {
|
|
226
|
+
ext(s).Fe = NOT_PENDING;
|
|
227
|
+
s.C |= CONFIG_OPTIMISTIC;
|
|
228
|
+
}
|
|
130
229
|
// A node born inside a live mark's identity scope inherits the mark
|
|
131
230
|
// (the declaration walk could only cover nodes existing then).
|
|
132
|
-
if (t !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(
|
|
133
|
-
|
|
231
|
+
if (t !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(s, e.v, t);
|
|
232
|
+
if (f !== null) stageHeldKey(s, o !== null ? e.del !== null && e.del.has(t) ? undefined : e.pb[t] : e.v[t], f);
|
|
233
|
+
i[t] = l;
|
|
134
234
|
e.nc++;
|
|
135
235
|
markDescendants(e);
|
|
136
236
|
}
|
|
137
|
-
return
|
|
237
|
+
return l;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The live transaction holding an adoption on `target` (adoptPB's `ht`;
|
|
241
|
+
* a latest()-pull PLAIN_HOLD is not a transaction), else null. */ function heldAdoptionTransition(e) {
|
|
242
|
+
if (e.ht === null || e.ht === PLAIN_HOLD || heldMaskView(e) === null) return null;
|
|
243
|
+
const t = currentTransition(e.ht);
|
|
244
|
+
return t.Rn === false ? t : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Materialization under a hold. A node created by a first tracked read while
|
|
249
|
+
* a transaction holds this target must carry the same state a node that
|
|
250
|
+
* existed when the hold was notified carries — a transaction-stamped
|
|
251
|
+
* `_pendingValue` that core read() serves by ITS rules (a stale render reader
|
|
252
|
+
* of a foreign transaction's write sees committed, an owner-context reader
|
|
253
|
+
* inside the transaction sees the write) — or the reader's answer depends on
|
|
254
|
+
* whether some OTHER reader had materialized the key before the hold. Stage
|
|
255
|
+
* `nv` (the held value for the key — see getNode for which view it comes
|
|
256
|
+
* from) as the holding transaction's write — directly, not through
|
|
257
|
+
* setSignal: it is not a new write (it staged with the adoption's batch) and
|
|
258
|
+
* it walks no subscriber (the node has none yet). Transition-stamped now, as
|
|
259
|
+
* `runFolded` does — no parked-flush pass will stamp it.
|
|
260
|
+
*/
|
|
261
|
+
/** The live transaction holding `target`'s pending backing (the #3089
|
|
262
|
+
* write-time stamp, resolved through merges), else null. */ function liveFoldTransition(e) {
|
|
263
|
+
if (e.pb === null) return null;
|
|
264
|
+
const t = foldBatches.get(e);
|
|
265
|
+
if (t === undefined) return null;
|
|
266
|
+
const n = currentTransition(t);
|
|
267
|
+
return n.Rn === false ? n : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** liveFoldTransition for node materialization (stageHeldKey). Inside the
|
|
271
|
+
* draft the backing is the setter's working copy, not a flushed hold — its
|
|
272
|
+
* nodes take their writes at setter exit (notifyWrites). Optimistic families
|
|
273
|
+
* hold at the backing: tentative writes are node overrides over a discarded
|
|
274
|
+
* clone, and a truth-staged landing is served through the fold's own hold
|
|
275
|
+
* (heldTruthMasked for lane passes, pendingBackingVisible's hold arms for
|
|
276
|
+
* the rest) — neither is a plain staged write to mirror. Chained
|
|
277
|
+
* backings serve the inner store's live value, never a node value. */ function heldFoldTransition(e) {
|
|
278
|
+
if (e.ch || e.fam?.opt === true || inDraft(e)) return null;
|
|
279
|
+
return liveFoldTransition(e);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Rule 1 at the backing, the hold half (core serve()'s stale-of-foreign and
|
|
284
|
+
* A29 arms, for a container instead of a node): given the transaction
|
|
285
|
+
* holding what a reader `c` (a pass; callers serve context-free readers the
|
|
286
|
+
* committed container themselves) is about to be served, is the STAGED
|
|
287
|
+
* container its to see? Its own hold, or none — yes. A foreign hold — a
|
|
288
|
+
* stale pass (render effect) keeps the committed frame and is recorded for
|
|
289
|
+
* replay at the hold's commit (recordStaleReplay, A15 / A26); a deriving
|
|
290
|
+
* pass takes the staged world and enters the transaction (enterStagedRead,
|
|
291
|
+
* A29). Shared by both hold kinds: a setter's fold (committed `v`, staged
|
|
292
|
+
* `pb`, the write-time stamp) and an adoption under a transaction
|
|
293
|
+
* (committed = the held view `hv`, staged = the adopted `v`, #3074).
|
|
294
|
+
*/ function holdVisible(e, t) {
|
|
295
|
+
if (e === null || ownsHold(e)) return true;
|
|
296
|
+
if (stale) {
|
|
297
|
+
recordStaleReplay(e, t);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
enterStagedRead(null, e);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function stageHeldKey(e, t, n) {
|
|
305
|
+
if (slotNodeEquals.call(e, e.ce, t)) return;
|
|
306
|
+
e._e = t;
|
|
307
|
+
e.we = n;
|
|
308
|
+
n.Gn.push(e);
|
|
138
309
|
}
|
|
139
310
|
|
|
140
311
|
function sameLogicalSlot(e, t, n) {
|
|
141
312
|
if (t === null || typeof t !== "object" || n === null || typeof n !== "object") return false;
|
|
142
|
-
const r = e.fam
|
|
143
|
-
|
|
144
|
-
return i !== undefined && i === r.get(n);
|
|
313
|
+
const r = lookupTarget(t, e.fam);
|
|
314
|
+
return r !== undefined && r === lookupTarget(n, e.fam);
|
|
145
315
|
}
|
|
146
316
|
|
|
147
317
|
function getHasNode(e, t, n) {
|
|
148
318
|
const r = e.h ??= Object.create(null);
|
|
149
319
|
let i = r[t];
|
|
150
320
|
if (i === undefined) {
|
|
151
|
-
const
|
|
321
|
+
const l = i = signal(n, {
|
|
152
322
|
equals: isEqual,
|
|
153
323
|
unobserved() {
|
|
154
|
-
if (o
|
|
155
|
-
|
|
324
|
+
if (l.o?.t) return;
|
|
325
|
+
// The structural twin of the value slot's rule (setSlotUnobserved,
|
|
326
|
+
// S7): an optimistic add/delete lives on this presence node as its
|
|
327
|
+
// override — releasing it with the override on would let `in`,
|
|
328
|
+
// `Object.keys` and descriptors fall back to committed structure
|
|
329
|
+
// while the action is live. Defer to the flush that resolves it.
|
|
330
|
+
if (hasActiveOverride(l) || l._e !== NOT_PENDING) return deferSlotRelease(l);
|
|
331
|
+
if (e.h && e.h[t] === l) {
|
|
332
|
+
delete e.h[t];
|
|
333
|
+
unlinkFirewallChild(l);
|
|
334
|
+
}
|
|
156
335
|
}
|
|
157
336
|
}, e.fam?.node ?? undefined);
|
|
158
|
-
|
|
159
|
-
if (e.fam?.opt)
|
|
160
|
-
|
|
337
|
+
l.C |= CONFIG_OWNED_WRITE;
|
|
338
|
+
if (e.fam?.opt) {
|
|
339
|
+
ext(l).Fe = NOT_PENDING;
|
|
340
|
+
l.C |= CONFIG_OPTIMISTIC;
|
|
341
|
+
}
|
|
342
|
+
if (affectsScopesLive()) inheritAffectsMarks(l, e.v, t);
|
|
161
343
|
r[t] = i;
|
|
162
344
|
markDescendants(e);
|
|
163
345
|
}
|
|
164
346
|
return i;
|
|
165
347
|
}
|
|
166
348
|
|
|
349
|
+
/** Did `obs` link `target`'s key-set node in its CURRENT pass? The mirror of
|
|
350
|
+
* link()'s O(1) repeat-touch check: the node's newest subscriber link is
|
|
351
|
+
* this observer's, and — during a recompute — carries this pass's dep
|
|
352
|
+
* generation (a link left from a previous pass is stale: the observer may
|
|
353
|
+
* not re-read the key set this time). No dep-list scan, no allocation. A
|
|
354
|
+
* probe observer (isPending()/latest() sentinel) never links, so it always
|
|
355
|
+
* falls through to the presence read. */ function observerHoldsKeySet(e, t) {
|
|
356
|
+
const n = e.k;
|
|
357
|
+
if (n === null) return false;
|
|
358
|
+
const r = n.mn;
|
|
359
|
+
return r !== null && r.ge === t && (!(t.oe & REACTIVE_RECOMPUTING_DEPS) || r.Be === t.Ye);
|
|
360
|
+
}
|
|
361
|
+
|
|
167
362
|
function getKeySetNode(e) {
|
|
168
363
|
let t = e.k;
|
|
169
364
|
if (t === null) {
|
|
170
365
|
const n = t = signal(0, {
|
|
171
366
|
equals: false,
|
|
172
367
|
unobserved() {
|
|
173
|
-
if (e.k === n)
|
|
368
|
+
if (e.k === n) {
|
|
369
|
+
e.k = null;
|
|
370
|
+
unlinkFirewallChild(n);
|
|
371
|
+
}
|
|
174
372
|
}
|
|
175
373
|
}, e.fam?.node ?? undefined);
|
|
176
|
-
n.
|
|
177
|
-
if (e.fam?.opt)
|
|
374
|
+
n.C |= CONFIG_OWNED_WRITE;
|
|
375
|
+
if (e.fam?.opt) {
|
|
376
|
+
ext(n).Fe = NOT_PENDING;
|
|
377
|
+
n.C |= CONFIG_OPTIMISTIC;
|
|
378
|
+
}
|
|
178
379
|
e.k = t;
|
|
179
380
|
markDescendants(e);
|
|
180
381
|
}
|
|
@@ -187,11 +388,17 @@ function getDeepNode(e) {
|
|
|
187
388
|
const n = t = signal(0, {
|
|
188
389
|
equals: false,
|
|
189
390
|
unobserved() {
|
|
190
|
-
if (e.dk === n)
|
|
391
|
+
if (e.dk === n) {
|
|
392
|
+
e.dk = null;
|
|
393
|
+
unlinkFirewallChild(n);
|
|
394
|
+
}
|
|
191
395
|
}
|
|
192
396
|
}, e.fam?.node ?? undefined);
|
|
193
|
-
n.
|
|
194
|
-
if (e.fam?.opt)
|
|
397
|
+
n.C |= CONFIG_OWNED_WRITE;
|
|
398
|
+
if (e.fam?.opt) {
|
|
399
|
+
ext(n).Fe = NOT_PENDING;
|
|
400
|
+
n.C |= CONFIG_OPTIMISTIC;
|
|
401
|
+
}
|
|
195
402
|
if (affectsScopesLive()) inheritAffectsMarks(n, e.v, $TRACK);
|
|
196
403
|
e.dk = t;
|
|
197
404
|
markDescendants(e);
|
|
@@ -204,41 +411,141 @@ function getDeepNode(e) {
|
|
|
204
411
|
if (e.dk !== null) setSignal(e.dk, 1);
|
|
205
412
|
}
|
|
206
413
|
|
|
207
|
-
function markDescendants(e) {
|
|
208
|
-
let t = e;
|
|
209
|
-
while (t && !t.d) {
|
|
210
|
-
t.d = true;
|
|
211
|
-
t = t.u;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
414
|
// ---------------------------------------------------------------------------
|
|
216
415
|
// pending backing + fold (the single mutation point)
|
|
217
416
|
/** target → committed backing at batch start (the fold diff's old side). */ const foldOlds = new Map;
|
|
218
417
|
|
|
219
418
|
let hookInstalled = false;
|
|
220
419
|
|
|
221
|
-
|
|
420
|
+
/** Shallow-clone `source` as a backing OWNED by `t` (stamped `$OWNER`, see
|
|
421
|
+
* target.ts). Callers always pass their own committed backing as `source`. */ function cloneRaw(e, t) {
|
|
422
|
+
// Plain-data fast path (#3360): a scanned `Object.prototype` container
|
|
423
|
+
// whose own keys are all enumerable data clones by spread — the same
|
|
424
|
+
// result as the descriptor walk below (own enumerable string+symbol keys,
|
|
425
|
+
// normalized writable+configurable), at ~1/50th the cost.
|
|
426
|
+
t.sc || scanAccessorsOnce(t);
|
|
427
|
+
if (t.sc === 2) {
|
|
428
|
+
const n = {
|
|
429
|
+
...e
|
|
430
|
+
};
|
|
431
|
+
n[$OWNER] = t;
|
|
432
|
+
return n;
|
|
433
|
+
}
|
|
222
434
|
// Descriptor-preserving shallow clone (R29: installed getters stay live;
|
|
223
435
|
// ruled 2026-08-17: frozen sources clone unfrozen — theirs stays frozen).
|
|
224
436
|
// Data descriptors normalize to writable+configurable (the clone is OURS to
|
|
225
437
|
// mutate — R51's "source-non-configurable is writable through the store");
|
|
226
438
|
// enumerability and accessors are preserved. The scan doubles as the
|
|
227
439
|
// accessor-flag detector (free — we're enumerating descriptors anyway).
|
|
228
|
-
|
|
440
|
+
const n = Object.getOwnPropertyDescriptors(e);
|
|
229
441
|
for (const r of Reflect.ownKeys(n)) {
|
|
230
442
|
const i = n[r];
|
|
231
443
|
if (r === "length" && Array.isArray(e)) continue;
|
|
232
444
|
i.configurable = true;
|
|
233
|
-
if (!i.get && !i.set) i.writable = true; else
|
|
445
|
+
if (!i.get && !i.set) i.writable = true; else t.a = true;
|
|
234
446
|
}
|
|
235
|
-
|
|
447
|
+
const r = Array.isArray(e) ? Object.defineProperties([], n) : Object.create(Object.getPrototypeOf(e), n);
|
|
448
|
+
r[$OWNER] = t;
|
|
449
|
+
return r;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Copy own `key` from `from` onto `to`. A plain data slot (enumerable,
|
|
453
|
+
* writable, configurable, no accessor) is a bare assignment — the common case
|
|
454
|
+
* and the cheap one; anything else goes through defineProperty so accessors
|
|
455
|
+
* and attribute flags survive the copy. */ function copyOwn(e, t, n) {
|
|
456
|
+
const r = Object.getOwnPropertyDescriptor(t, n);
|
|
457
|
+
if (r.get || r.set || !r.enumerable || !r.writable || !r.configurable) Object.defineProperty(e, n, r); else e[n] = r.value;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
|
|
461
|
+
* returns true when the container is plain data (overlay-safe). Also grades
|
|
462
|
+
* the container for the plain-data fast paths (`sc` = 2): `Object.prototype`
|
|
463
|
+
* and every own key an enumerable data property — what a spread copies
|
|
464
|
+
* exactly and what a bare assignment lands exactly. */ function scanAccessorsOnce(e) {
|
|
465
|
+
const t = e.v;
|
|
466
|
+
const n = Reflect.ownKeys(t);
|
|
467
|
+
let r = Object.getPrototypeOf(t) === Object.prototype;
|
|
468
|
+
for (const i of n) {
|
|
469
|
+
// Own keys shadow prototype accessors, so the lookups are exact here.
|
|
470
|
+
if (lookupGetter.call(t, i) !== undefined || lookupSetter.call(t, i) !== undefined) {
|
|
471
|
+
e.a = true;
|
|
472
|
+
r = false;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
if (r && !propertyIsEnumerable.call(t, i)) r = false;
|
|
476
|
+
}
|
|
477
|
+
e.sc = r ? 2 : 1;
|
|
478
|
+
e.kc = n.length;
|
|
479
|
+
return !e.a;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Own-key count above which a draft opens as a prototype overlay rather
|
|
483
|
+
* than a clone (#3360). Below it a spread clone of a plain container is
|
|
484
|
+
* cheaper than the overlay's `Object.create` (V8 converts the committed
|
|
485
|
+
* backing into a prototype, and every later flatten writes into that
|
|
486
|
+
* prototype); above it the clone's O(keys) copy dominates (#3044). */ const OVERLAY_MIN_KEYS = 32;
|
|
487
|
+
|
|
488
|
+
/** Downgrade a prototype-overlay pending backing to the clone path: builds
|
|
489
|
+
* the real container (committed + overlay writes − deletes) that fold will
|
|
490
|
+
* SWAP in as the committed backing, exactly as if the draft had started on
|
|
491
|
+
* the clone path. Consumers that need a complete container (reconcile's
|
|
492
|
+
* diff walks, drafts escaping into other storage) call this. */ function materializePB(e) {
|
|
493
|
+
if (!e.ovl) return;
|
|
494
|
+
const t = e.pb;
|
|
495
|
+
const n = cloneRaw(e.v, e);
|
|
496
|
+
for (const e of Reflect.ownKeys(t)) copyOwn(n, t, e);
|
|
497
|
+
if (e.del !== null) {
|
|
498
|
+
for (const t of e.del) delete n[t];
|
|
499
|
+
e.del = null;
|
|
500
|
+
}
|
|
501
|
+
e.pb = n;
|
|
502
|
+
e.ovl = false;
|
|
236
503
|
}
|
|
237
504
|
|
|
238
505
|
function ensurePB(e) {
|
|
239
506
|
let t = e.pb;
|
|
507
|
+
// Truth-staged backing hand-off (#3164 fold): a TENTATIVE draft opening on
|
|
508
|
+
// a target whose pending backing is truth-staged (a landing folded into a
|
|
509
|
+
// retaining transaction — it carries a foldBatches stamp) must not share
|
|
510
|
+
// the container. Tentative writes would pollute staged truth, and the
|
|
511
|
+
// tentative discard (notifyOptimisticWrites nulls pb) would destroy the
|
|
512
|
+
// landing. Park the staged backing and open a fresh draft seeded from the
|
|
513
|
+
// optimistic view below; the tentative discard restores it. The
|
|
514
|
+
// tentativePBs guard scopes this to draft OPEN: the draft's own backing
|
|
515
|
+
// (foldBatches-stamped by its first write when an action's transition is
|
|
516
|
+
// ambient) must not be parked by its own later writes.
|
|
517
|
+
if (t !== null && !tentativePBs.has(t) && e.fam?.opt === true && !projectionWriteActive && !getWriteOverride() && foldBatches.has(e)) {
|
|
518
|
+
stagedTruthPB.set(e, t);
|
|
519
|
+
t = e.pb = null;
|
|
520
|
+
}
|
|
521
|
+
if (activeTransition !== null) foldBatches.set(e, activeTransition);
|
|
240
522
|
if (t === null) {
|
|
241
|
-
|
|
523
|
+
// Prototype-chain overlay (#3044): plain-data non-array containers open
|
|
524
|
+
// drafts in O(1) — own keys are the writes, reads fall through to
|
|
525
|
+
// committed. Projection and derived-store families take it too (#3352:
|
|
526
|
+
// a derive touching one root key of a wide keyed record paid an
|
|
527
|
+
// O(keys) clone per recompute). Everything else keeps the descriptor
|
|
528
|
+
// clone: arrays (splice/length semantics), OPTIMISTIC families (the
|
|
529
|
+
// view-seeding below writes and deletes on the draft container, and the
|
|
530
|
+
// tentative discard/truth-park hand whole backings around), chained
|
|
531
|
+
// backings (the committed layer is another store's proxy — an overlay
|
|
532
|
+
// would route every read-through into its traps), accessor containers
|
|
533
|
+
// (live getters).
|
|
534
|
+
// The overlay pays off only for a WIDE container over an OWNED committed
|
|
535
|
+
// backing (#3360). Narrow containers clone cheaper than they overlay (a
|
|
536
|
+
// spread is a fast-path copy; `Object.create` turns the backing into a
|
|
537
|
+
// V8 prototype and every later flatten writes into that prototype). An
|
|
538
|
+
// unowned backing has to be privatized (cloned) at commit anyway, so an
|
|
539
|
+
// overlay there would cost the create PLUS the clone PLUS a per-key
|
|
540
|
+
// copy — the clone path does the one clone and swaps it in. The first
|
|
541
|
+
// write on a fresh store is exactly that case.
|
|
542
|
+
const n = e.v;
|
|
543
|
+
if (!e.fam?.opt && !e.ch && !Array.isArray(n) && (e.sc !== 0 ? !e.a : scanAccessorsOnce(e)) && e.kc > OVERLAY_MIN_KEYS && isOwned(n)) {
|
|
544
|
+
// Inherits `v`'s $OWNER stamp — the overlay resolves and reads as
|
|
545
|
+
// owned without a registration of its own.
|
|
546
|
+
t = e.pb = Object.create(n);
|
|
547
|
+
e.ovl = true;
|
|
548
|
+
} else t = e.pb = cloneRaw(n, e);
|
|
242
549
|
// Optimistic families: seed USER drafts from the OPTIMISTIC VIEW
|
|
243
550
|
// (committed + active node overrides), so follow-up writes compose on
|
|
244
551
|
// optimism instead of clobbering from base (#2951's compose half).
|
|
@@ -246,28 +553,46 @@ function ensurePB(e) {
|
|
|
246
553
|
// seed from committed truth — seeding overrides there would fold a lane
|
|
247
554
|
// value into the committed home ("authority wins at reveal" would break).
|
|
248
555
|
if (e.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
|
|
556
|
+
tentativePBs.add(t);
|
|
249
557
|
const n = e.n;
|
|
250
558
|
if (n !== null) {
|
|
251
559
|
for (const e of Reflect.ownKeys(n)) {
|
|
252
560
|
const r = n[e];
|
|
253
|
-
if (hasActiveOverride(r)) t[e] = unwrapOverride(r.
|
|
561
|
+
if (hasActiveOverride(r)) t[e] = unwrapOverride(r.o?.Fe);
|
|
254
562
|
}
|
|
255
563
|
}
|
|
256
564
|
const r = e.h;
|
|
257
565
|
if (r !== null) {
|
|
258
566
|
for (const e of Reflect.ownKeys(r)) {
|
|
259
567
|
const n = r[e];
|
|
260
|
-
if (hasActiveOverride(n) && !unwrapOverride(n.
|
|
568
|
+
if (hasActiveOverride(n) && !unwrapOverride(n.o?.Fe)) delete t[e];
|
|
261
569
|
}
|
|
262
570
|
}
|
|
263
571
|
}
|
|
264
|
-
ownedRaw.add(t);
|
|
265
|
-
(e.fam?.map ?? storeNextLookup).set(t, e);
|
|
266
572
|
queueFold(e);
|
|
267
573
|
}
|
|
268
574
|
return t;
|
|
269
575
|
}
|
|
270
576
|
|
|
577
|
+
/** Sentinel holder for `t.ht`: a latest()-pull staged this adoption outside
|
|
578
|
+
* any transition — the hold lasts until the fold commit (drainFolds). */ const PLAIN_HOLD = Symbol("plainHold");
|
|
579
|
+
|
|
580
|
+
/** True while a latest() read is pulling the projection computed up to date
|
|
581
|
+
* (see the get trap): adoptions landing during the pull are speculative
|
|
582
|
+
* against the un-flushed batch and stage a held view. (Not injectable — the
|
|
583
|
+
* derived createStore overload retains projection machinery in every store
|
|
584
|
+
* bundle, see treeshake.test.ts.) */ let latestPullActive = false;
|
|
585
|
+
|
|
586
|
+
/** Resolve the held committed view (#3074): answers the masked old backing
|
|
587
|
+
* while the hold is live, and lazily clears a hold whose transition has
|
|
588
|
+
* committed (transitions merge — resolve through currentTransition, same as
|
|
589
|
+
* foldHeld's node stamps). */ function heldMaskView(e) {
|
|
590
|
+
const t = e.ht;
|
|
591
|
+
if (t === null) return null;
|
|
592
|
+
if (t !== PLAIN_HOLD && currentTransition(t)?.Rn === true) return e.ht = e.hv = null;
|
|
593
|
+
return e.hv;
|
|
594
|
+
}
|
|
595
|
+
|
|
271
596
|
/**
|
|
272
597
|
* Adoption (2026-08-16c): the incoming object becomes the committed backing
|
|
273
598
|
* IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
|
|
@@ -282,85 +607,302 @@ function ensurePB(e) {
|
|
|
282
607
|
if (!n) {
|
|
283
608
|
queueFold(e);
|
|
284
609
|
// records the pre-batch old before we swap
|
|
285
|
-
|
|
610
|
+
// Diff base = the view the nodes were last told (#3296). A draft's
|
|
611
|
+
// setter-exit notifications already moved them to its pending backing,
|
|
612
|
+
// so a later adoption diffs against THAT — against committed, a key the
|
|
613
|
+
// draft changed and the adoption restores would never re-notify. An
|
|
614
|
+
// adoption with no draft leaves nodes where they were: keep an existing
|
|
615
|
+
// base, else the pre-batch committed (foldOlds' entry itself stays the
|
|
616
|
+
// committed identity for the path-copy CAS). Eager callers read t.pb
|
|
617
|
+
// directly; this hand-off exists because pb is gone by drain time.
|
|
618
|
+
if (e.pb !== null) {
|
|
619
|
+
if (e.ovl) materializePB(e);
|
|
620
|
+
e.ab = e.pb;
|
|
621
|
+
} else e.ab ??= foldOlds.get(e);
|
|
622
|
+
}
|
|
623
|
+
// #3074/#3075: a projection recompute deriving from uncommitted inputs
|
|
624
|
+
// swaps the backing SPECULATIVELY — committed-visibility readers must
|
|
625
|
+
// keep the pre-hold view until the hold resolves (a source held by a
|
|
626
|
+
// live transition, or a latest()-pull ahead of the flush). Post-await
|
|
627
|
+
// landings (write-override) stay immediately visible — landed truth —
|
|
628
|
+
// and clear any hold. Optimistic families hold too (#3330 store twin):
|
|
629
|
+
// their tentative edits ride the lane machinery, but a sync derive
|
|
630
|
+
// adopting under a transaction is held TRUTH like any projection's —
|
|
631
|
+
// unheld, handlers read it early and an optimistic write equal to it
|
|
632
|
+
// compared as a no-op against the swapped-in backing. A plain store's
|
|
633
|
+
// reconcile inside an action holds the same way: its nodes stage under the
|
|
634
|
+
// transaction (the inline notify), and the backing must not show handlers
|
|
635
|
+
// and stale readers what the tracked read masks (signal parity, #3336).
|
|
636
|
+
if (getWriteOverride()) {
|
|
637
|
+
e.ht = e.hv = null;
|
|
638
|
+
} else if (activeTransition !== null || !n && latestPullActive) {
|
|
639
|
+
if (heldMaskView(e) === null) e.hv = e.v;
|
|
640
|
+
e.ht = activeTransition ?? PLAIN_HOLD;
|
|
641
|
+
if (!n && activeTransition !== null) heldAdoptions.add(e);
|
|
286
642
|
}
|
|
287
643
|
e.pb = null;
|
|
288
|
-
|
|
644
|
+
// Overlay and accessor-scan state describe the OUTGOING backing — a
|
|
645
|
+
// swapped container must not inherit them: a stale `ovl` beside a nulled
|
|
646
|
+
// pb crashes materializePB (unwrapValue consults ovl before the
|
|
647
|
+
// null-coalesce), a stale `del` would read the adoptee's keys as deleted
|
|
648
|
+
// in the next draft, and a stale plain-data verdict (`sc`/`a`) could
|
|
649
|
+
// admit an accessor-bearing adoptee to the overlay path. Reset; the next
|
|
650
|
+
// draft rescans once (#3044 audit follow-up).
|
|
651
|
+
e.ovl = false;
|
|
652
|
+
e.del = null;
|
|
653
|
+
e.sc = 0;
|
|
654
|
+
e.a = false;
|
|
655
|
+
e.wk = null;
|
|
656
|
+
// adoption supersedes staged trap writes
|
|
657
|
+
e.v = t;
|
|
289
658
|
e.ch = t[$TARGET] !== undefined;
|
|
290
|
-
|
|
659
|
+
// An adoptee we own within this family (a draft aliasing one of the
|
|
660
|
+
// family's own backings) re-stamps to its new owner — the stamp is the
|
|
661
|
+
// family's registration for it; everything else registers in the map.
|
|
662
|
+
const r = t[$OWNER];
|
|
663
|
+
if (r !== undefined && r.fam === e.fam) t[$OWNER] = e; else (e.fam?.map ?? storeNextLookup).set(t, e);
|
|
291
664
|
}
|
|
292
665
|
|
|
666
|
+
/** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
|
|
667
|
+
* array length write implicitly deleted indices) — consumers full-scan. */ const WK_ALL = new Set;
|
|
668
|
+
|
|
669
|
+
const plainProto = e => {
|
|
670
|
+
const t = Object.getPrototypeOf(e);
|
|
671
|
+
return t === Object.prototype || t === Array.prototype || t === null;
|
|
672
|
+
};
|
|
673
|
+
|
|
293
674
|
function queueFold(e) {
|
|
294
675
|
if (foldOlds.has(e)) return;
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
676
|
+
if (!hookInstalled) {
|
|
677
|
+
hookInstalled = true;
|
|
678
|
+
setStoreCommitHook(drainFolds);
|
|
679
|
+
}
|
|
680
|
+
// Always arm — "map non-empty ⇒ drain scheduled" is NOT an invariant: a
|
|
681
|
+
// held re-queue, or an incomplete-transition flush (which skips
|
|
682
|
+
// commitPendingNodes entirely), leaves entries behind after `scheduled`
|
|
683
|
+
// was consumed. A size-gated arm then strands every LATER fold — queued
|
|
684
|
+
// silently, never drained, committed base frozen at stale state while its
|
|
685
|
+
// nodes commit (#3089). schedule() early-returns when already armed.
|
|
300
686
|
schedule();
|
|
301
|
-
// once per batch — drain clears the map
|
|
302
|
-
}
|
|
303
687
|
foldOlds.set(e, e.v);
|
|
304
688
|
}
|
|
305
689
|
|
|
690
|
+
/** Fold write-attribution (#3089): a draft written while a transition is
|
|
691
|
+
* active belongs to that transition — its fold must not commit before the
|
|
692
|
+
* transition settles. Observed keys already defer through the held check in
|
|
693
|
+
* drainFolds (their nodes carry _pendingValue); this write-time stamp is the
|
|
694
|
+
* equivalent hold for UNOBSERVED keys, which have no node to consult.
|
|
695
|
+
* Refreshed on every write; resolved through currentTransition at drain
|
|
696
|
+
* (transitions merge — same rule as heldMaskView). */ const foldBatches = new WeakMap;
|
|
697
|
+
|
|
698
|
+
/** Parked truth-staged pending backings (#3164 fold): a tentative draft that
|
|
699
|
+
* opens while a folded landing's backing is live moves the staged container
|
|
700
|
+
* here (see ensurePB); the tentative discard in notifyOptimisticWrites
|
|
701
|
+
* restores it in place of the usual null. */ const stagedTruthPB = new WeakMap;
|
|
702
|
+
|
|
703
|
+
/** Backings opened by TENTATIVE drafts (optimistic user setters): ensurePB's
|
|
704
|
+
* truth-park must not fire against the draft's own container on its second
|
|
705
|
+
* and later writes (the first write stamps foldBatches whenever an action's
|
|
706
|
+
* transition is ambient). Entries die with their draft — tentative backings
|
|
707
|
+
* are consumed at setter exit. */ const tentativePBs = new WeakSet;
|
|
708
|
+
|
|
709
|
+
/** A draft read composes the live optimistic view until the draft has opened
|
|
710
|
+
* its OWN view-seeded backing (ensurePB seeds that clone from the view and
|
|
711
|
+
* registers it in tentativePBs; from then on reads must see the draft's
|
|
712
|
+
* writes, not the overrides they superseded). A pending backing that exists
|
|
713
|
+
* for any other reason is not that clone — a truth landing staged into a
|
|
714
|
+
* retaining transaction (#3164 fold) is authoritative truth WITHOUT the
|
|
715
|
+
* live overrides. ensurePB parks such a backing on the draft's first WRITE
|
|
716
|
+
* and reseeds from the view, but the reads that precede that write went to
|
|
717
|
+
* the staged truth: `votes++` read base, wrote base+1, and the override it
|
|
718
|
+
* emitted landed on the value already displayed — a second in-flight
|
|
719
|
+
* increment made after a sibling's landing was invisible (#2951's compose
|
|
720
|
+
* half, one landing later). */ function draftSeesOverrides(e) {
|
|
721
|
+
return e.pb === null || !tentativePBs.has(e.pb);
|
|
722
|
+
}
|
|
723
|
+
|
|
306
724
|
/** Committed-time privatization for parent-chain slot updates (path copying). */ function privatizeCommitted(e) {
|
|
307
|
-
if (
|
|
308
|
-
const t =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
e.v = t;
|
|
725
|
+
if (isOwned(e.v)) return;
|
|
726
|
+
const t = e.v;
|
|
727
|
+
const n = cloneRaw(t, e);
|
|
728
|
+
e.v = n;
|
|
312
729
|
e.ch = false;
|
|
313
730
|
if (e.u) {
|
|
314
731
|
privatizeCommitted(e.u);
|
|
315
732
|
devAssertNeverUserMutation(e.u.v);
|
|
316
|
-
|
|
733
|
+
// CAS, same as drainFolds' path copy: re-point the parent slot only
|
|
734
|
+
// while it still holds the raw we cloned. A parent that folded EARLIER
|
|
735
|
+
// in this drain may have replaced or deleted this slot (the same batch
|
|
736
|
+
// wrote the child and then `parent.row = fresh` / `parent.length = 0`)
|
|
737
|
+
// — an unconditional write resurrected the dropped child over it.
|
|
738
|
+
const n = e.u.v;
|
|
739
|
+
const r = parentSlotKey(e, t);
|
|
740
|
+
if (n[r] === t) n[r] = e.v;
|
|
317
741
|
}
|
|
318
742
|
}
|
|
319
743
|
|
|
744
|
+
/** Resolve the slot this child currently occupies in its parent's committed
|
|
745
|
+
* backing (#3282). `pk` is stamped at wrap time and arrays MOVE: a reverse/
|
|
746
|
+
* unshift/splice relocates the raw, and a fold that re-points the wrap-time
|
|
747
|
+
* slot writes the clone over whichever row lives there now. Objects never
|
|
748
|
+
* move keys, so the stamp is authoritative; for arrays, verify and re-locate
|
|
749
|
+
* by identity when stale (fold-time only — never on a read path). */ function parentSlotKey(e, t) {
|
|
750
|
+
const n = e.pk;
|
|
751
|
+
const r = e.u.v;
|
|
752
|
+
if (r[n] === t || !Array.isArray(r)) return n;
|
|
753
|
+
const i = r.indexOf(t);
|
|
754
|
+
if (i === -1) return n;
|
|
755
|
+
e.pk = i;
|
|
756
|
+
return i;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Overlay commit (#3044): apply the batch's writes onto an OWNED committed
|
|
760
|
+
* backing in place — O(written), not O(container). Unowned backings
|
|
761
|
+
* privatize first (clone once, parents re-slotted) — the never-mutate-user-
|
|
762
|
+
* data contract holds. Shared by the deferred fold (drainFolds) and the
|
|
763
|
+
* projection landing's immediate commit (notifyWrites). */ function flattenOverlay(e, t) {
|
|
764
|
+
privatizeCommitted(e);
|
|
765
|
+
const n = e.v;
|
|
766
|
+
// Plain-data grade (sc 2): every own key on the overlay is an enumerable
|
|
767
|
+
// writable data slot (set-trap writes; a non-plain defineProperty
|
|
768
|
+
// downgrades the grade) — a bare assignment lands it without the
|
|
769
|
+
// descriptor round trip.
|
|
770
|
+
for (const r of Reflect.ownKeys(t)) e.sc === 2 ? n[r] = t[r] : copyOwn(n, t, r);
|
|
771
|
+
if (e.del !== null) {
|
|
772
|
+
for (const t of e.del) delete n[t];
|
|
773
|
+
e.del = null;
|
|
774
|
+
}
|
|
775
|
+
e.pb = null;
|
|
776
|
+
e.ovl = false;
|
|
777
|
+
e.wk = null;
|
|
778
|
+
// written-keys window closes with the commit
|
|
779
|
+
}
|
|
780
|
+
|
|
320
781
|
function drainFolds() {
|
|
321
782
|
if (foldOlds.size === 0) return;
|
|
322
783
|
const e = [ ...foldOlds ];
|
|
323
784
|
foldOlds.clear();
|
|
324
785
|
for (const [t, n] of e) {
|
|
786
|
+
// A latest()-pull staging holds only until the fold commit: this flush
|
|
787
|
+
// is committing the batch the pull ran ahead of. Transition holds stay —
|
|
788
|
+
// they clear when their transition is done (heldMaskView).
|
|
789
|
+
if (t.ht === PLAIN_HOLD) t.ht = t.hv = null;
|
|
325
790
|
if (t.pb !== null) {
|
|
791
|
+
// #3089: a fold written under a still-running transition defers to
|
|
792
|
+
// that transition's settle (the write-time stamp covers unobserved
|
|
793
|
+
// keys; observed keys also hit the pending-node held check below).
|
|
794
|
+
const e = foldBatches.get(t);
|
|
795
|
+
if (e !== undefined) {
|
|
796
|
+
if (currentTransition(e).Rn === false) {
|
|
797
|
+
foldOlds.set(t, n);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
foldBatches.delete(t);
|
|
801
|
+
}
|
|
326
802
|
// Setter path: nodes were setSignal'd at setter exit (write-time
|
|
327
803
|
// notification — transitions/holds ride core machinery). Commit the
|
|
328
804
|
// backing only for keys whose nodes have committed; a still-pending
|
|
329
805
|
// node (transition-held) re-queues the target for the settling flush.
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
806
|
+
let r = false;
|
|
807
|
+
const i = t.pb;
|
|
808
|
+
const l = t.n;
|
|
809
|
+
if (l !== null) {
|
|
810
|
+
// Only written keys can hold (their nodes took the setSignal); the
|
|
811
|
+
// wk bound keeps this O(written) — see notifyWrites. Same fallback
|
|
812
|
+
// rules as the notify (WK_ALL / accessors / non-plain prototypes).
|
|
813
|
+
const e = t.wk;
|
|
814
|
+
const n = e === null || e === WK_ALL || t.a === true ||
|
|
815
|
+
// Overlay pbs chain to the COMMITTED object (#3044) — plainness is
|
|
816
|
+
// the committed container's prototype, not the overlay's.
|
|
817
|
+
!plainProto(t.ovl ? t.v : i) ? Reflect.ownKeys(l) : e;
|
|
818
|
+
for (const e of n) {
|
|
819
|
+
const t = l[e];
|
|
820
|
+
if (t !== undefined && t._e !== NOT_PENDING) {
|
|
821
|
+
r = true;
|
|
338
822
|
break;
|
|
339
823
|
}
|
|
340
824
|
}
|
|
341
825
|
}
|
|
342
|
-
if (
|
|
826
|
+
if (r) {
|
|
343
827
|
foldOlds.set(t, n);
|
|
344
828
|
// re-queue: commit happens when the hold settles
|
|
345
829
|
continue;
|
|
346
830
|
}
|
|
347
|
-
t.
|
|
348
|
-
|
|
831
|
+
if (t.ovl) {
|
|
832
|
+
// Overlay flatten (#3044): the backing keeps its identity, so the
|
|
833
|
+
// `t.v === old` gate below skips path copying (the parent slot
|
|
834
|
+
// already points here) and the adopted-notify (setter notifications
|
|
835
|
+
// happened at write time).
|
|
836
|
+
flattenOverlay(t, i);
|
|
837
|
+
} else if (t.v !== n) {
|
|
838
|
+
// Privatized mid-batch (#3271): an earlier fold in this drain
|
|
839
|
+
// path-copied THROUGH this target — privatizeCommitted cloned the
|
|
840
|
+
// committed backing, re-pointed the parent slot at the clone, and
|
|
841
|
+
// stitched the descendant's fold into it. The draft's pb predates
|
|
842
|
+
// that: swapping it in would clobber the descendant's fold, and the
|
|
843
|
+
// parent CAS below (still comparing against `old`) would fail and
|
|
844
|
+
// orphan this fold entirely — a projection draft writing descendant-
|
|
845
|
+
// then-ancestor silently lost the ancestor write. Merge the batch's
|
|
846
|
+
// writes onto the current container instead (the parent slot already
|
|
847
|
+
// points at it). privatize first: an adopt-then-write batch can land
|
|
848
|
+
// here with an unowned adoptee as t.v.
|
|
849
|
+
privatizeCommitted(t);
|
|
850
|
+
const e = t.v;
|
|
851
|
+
const r = t.wk;
|
|
852
|
+
if (r !== null && r !== WK_ALL) {
|
|
853
|
+
// The trap records every write/delete key — apply exactly those.
|
|
854
|
+
for (const t of r) {
|
|
855
|
+
if (hasOwn.call(i, t)) copyOwn(e, i, t); else delete e[t];
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
858
|
+
// Array length write poisoned the bound (WK_ALL) — value-diff
|
|
859
|
+
// against the pre-batch old. Slots the draft never touched hold
|
|
860
|
+
// the same raw reference in both, so descendant folds stay put.
|
|
861
|
+
// Not copyOwn: the plain-value write is GATED on "the draft changed
|
|
862
|
+
// this slot" — an untouched slot in pb holds the pre-batch reference,
|
|
863
|
+
// and writing it back would clobber a descendant fold stitched into v.
|
|
864
|
+
for (const t of Reflect.ownKeys(i)) {
|
|
865
|
+
const r = Object.getOwnPropertyDescriptor(i, t);
|
|
866
|
+
if (r.get || r.set || !r.enumerable || !r.writable || !r.configurable) Object.defineProperty(e, t, r); else if (r.value !== n[t] || !hasOwn.call(n, t)) e[t] = r.value;
|
|
867
|
+
}
|
|
868
|
+
for (const t of Reflect.ownKeys(n)) {
|
|
869
|
+
if (!hasOwn.call(i, t)) delete e[t];
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
t.pb = null;
|
|
873
|
+
t.wk = null;
|
|
874
|
+
// written-keys window closes with the fold commit
|
|
875
|
+
} else {
|
|
876
|
+
t.v = i;
|
|
877
|
+
t.ch = false;
|
|
349
878
|
// pb is always a plain clone
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Path copying (CAS: see the eager-fold twin above).
|
|
355
|
-
if (t.u && t.u.v[t.pk] === n) {
|
|
356
|
-
privatizeCommitted(t.u);
|
|
357
|
-
devAssertNeverUserMutation(t.u.v);
|
|
358
|
-
t.u.v[t.pk] = t.v;
|
|
879
|
+
t.pb = null;
|
|
880
|
+
t.wk = null;
|
|
881
|
+
// written-keys window closes with the fold commit
|
|
882
|
+
}
|
|
359
883
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
884
|
+
const e = t.ab;
|
|
885
|
+
t.ab = null;
|
|
886
|
+
if (t.v !== n) {
|
|
887
|
+
// Path copying (CAS: see the eager-fold twin above). Slot resolved by
|
|
888
|
+
// identity (#3282): an array move relocated the raw, so the wrap-time
|
|
889
|
+
// pk may point at a sibling — a raw-slot CAS there both failed to
|
|
890
|
+
// re-point AND (via privatizeCommitted's unguarded write) clobbered
|
|
891
|
+
// the sibling.
|
|
892
|
+
if (t.u) {
|
|
893
|
+
const e = parentSlotKey(t, n);
|
|
894
|
+
if (t.u.v[e] === n) {
|
|
895
|
+
privatizeCommitted(t.u);
|
|
896
|
+
devAssertNeverUserMutation(t.u.v);
|
|
897
|
+
t.u.v[e] = t.v;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
363
900
|
}
|
|
901
|
+
// Adoption notify against the base the nodes were last told (#3296). A
|
|
902
|
+
// no-op adoption (A -> B -> A before flush, no draft) has base === v and
|
|
903
|
+
// nothing to say; a draft superseded by an adoption back to the SAME raw
|
|
904
|
+
// still has (base = pending backing) !== v and must notify.
|
|
905
|
+
if (e !== null && e !== t.v) notifyFold(t, e, t.v);
|
|
364
906
|
}
|
|
365
907
|
}
|
|
366
908
|
|
|
@@ -372,7 +914,7 @@ function drainFolds() {
|
|
|
372
914
|
* "pending home = the node when a node exists"). Unobserved keys stay in the
|
|
373
915
|
* pending backing and fold directly at commit.
|
|
374
916
|
*/ function notifyWrites(e) {
|
|
375
|
-
|
|
917
|
+
let t = e.pb;
|
|
376
918
|
if (t === null) return;
|
|
377
919
|
// Optimistic channel: user writes on an optimistic family become node-level
|
|
378
920
|
// engine writes (armed nodes route setSignal through optimisticWrite) — the
|
|
@@ -402,50 +944,90 @@ function drainFolds() {
|
|
|
402
944
|
}
|
|
403
945
|
const n = e.v;
|
|
404
946
|
const r = e.n;
|
|
947
|
+
// Written-keys bound: trap writes record their keys, so the notify visits
|
|
948
|
+
// O(written) nodes instead of every subscription on the record (a selection
|
|
949
|
+
// map with thousands of per-key subscribers pays two visits per select,
|
|
950
|
+
// not a full scan). Falls back to the full node scan when the bound can't
|
|
951
|
+
// hold: no trap granularity (wk null), an array length write (WK_ALL —
|
|
952
|
+
// implicit index deletes), accessors on the record (t.a — a getter node's
|
|
953
|
+
// value can change when ANY key is written), or a non-plain prototype
|
|
954
|
+
// (class instances: prototype getters derive from arbitrary fields).
|
|
955
|
+
const i = e.wk;
|
|
956
|
+
// Overlay pbs chain to the COMMITTED object (#3044): a prototype-overlay
|
|
957
|
+
// draft is plain data on its own layer, but its getPrototypeOf is the
|
|
958
|
+
// committed container — judge plainness by the COMMITTED prototype or the
|
|
959
|
+
// bound never engages for overlay writes (every plain-object setter batch
|
|
960
|
+
// would full-scan: the exact selection-map workload wk exists for; jf
|
|
961
|
+
// `select` regressed 2x on this).
|
|
962
|
+
const l = i === WK_ALL || e.a === true || !plainProto(e.ovl ? e.v : t) ? null : i;
|
|
405
963
|
if (r !== null) {
|
|
406
|
-
|
|
407
|
-
|
|
964
|
+
const i = l ?? Reflect.ownKeys(r);
|
|
965
|
+
for (const l of i) {
|
|
966
|
+
const i = r[l];
|
|
967
|
+
if (i === undefined) continue;
|
|
408
968
|
// Per-key accessor handling: the node's cached flag plus ONE getter
|
|
409
969
|
// probe on the incoming side (getters arriving via merge/adoption).
|
|
410
970
|
// Setter-only props read as data (value undefined) so lookupSetter is
|
|
411
971
|
// not consulted on this hot path; prototype getters never own nodes.
|
|
412
|
-
if (i.acc === true || hasOwn.call(t,
|
|
413
|
-
i.acc = isOwnAccessor(t,
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
if (
|
|
972
|
+
if (i.acc === true || hasOwn.call(t, l) && lookupGetter.call(t, l) !== undefined) {
|
|
973
|
+
i.acc = isOwnAccessor(t, l);
|
|
974
|
+
const e = Object.getOwnPropertyDescriptor(n, l);
|
|
975
|
+
const r = Object.getOwnPropertyDescriptor(t, l);
|
|
976
|
+
if (e && (e.get || e.set) || r && (r.get || r.set)) {
|
|
977
|
+
if (e?.get !== r?.get || e?.set !== r?.set || e?.value !== r?.value) setSignal(i, () => FORCE);
|
|
418
978
|
continue;
|
|
419
979
|
}
|
|
420
|
-
if (!isEqual(
|
|
980
|
+
if (!isEqual(e?.value, r?.value)) setSignal(i, () => r?.value);
|
|
421
981
|
continue;
|
|
422
982
|
}
|
|
423
983
|
// No old-side pre-compare: t.v lags across multi-batch windows (a
|
|
424
984
|
// projection recompute can run before the prior fold commits) — the
|
|
425
985
|
// node's OWN current value is the true old side, and setSignal's
|
|
426
986
|
// internal equality already checks exactly that.
|
|
427
|
-
const o = t[
|
|
987
|
+
const o = e.del !== null && e.del.has(l) ? undefined : t[l];
|
|
988
|
+
// The derived store's setter reaching a leaf its own fold staged under
|
|
989
|
+
// another transaction: not a proposal — the fold re-runs (#3612).
|
|
990
|
+
if (derivedSetter !== null && i.De === derivedSetter && heldDerivation(i)) heldDerivationHit = true;
|
|
428
991
|
setSignal(i, () => o);
|
|
429
992
|
}
|
|
430
993
|
}
|
|
431
|
-
const
|
|
432
|
-
if (
|
|
433
|
-
|
|
994
|
+
const o = e.h;
|
|
995
|
+
if (o !== null) {
|
|
996
|
+
const n = l ?? Reflect.ownKeys(o);
|
|
997
|
+
for (const r of n) {
|
|
998
|
+
const n = o[r];
|
|
999
|
+
if (n !== undefined) setSignal(n, r in t && !(e.del !== null && e.del.has(r)));
|
|
1000
|
+
}
|
|
434
1001
|
}
|
|
435
1002
|
// Deep-witness (dk): setter writes must notify a deep() subscriber even on
|
|
436
|
-
// keys with no node. O(pb keys) equality only when a witness exists.
|
|
1003
|
+
// keys with no node. O(written/pb keys) equality only when a witness exists.
|
|
437
1004
|
if (e.dk !== null) {
|
|
438
|
-
for (const r of Reflect.ownKeys(t)) {
|
|
1005
|
+
if (e.del !== null && e.del.size !== 0) bumpDeep(e); else for (const r of l ?? Reflect.ownKeys(t)) {
|
|
1006
|
+
if (r === $OWNER) continue;
|
|
439
1007
|
const i = t[r];
|
|
440
|
-
const
|
|
441
|
-
if (i !== null && typeof i === "object" ? !targetsEqual(
|
|
1008
|
+
const l = n[r];
|
|
1009
|
+
if (i !== null && typeof i === "object" ? !targetsEqual(l, i) : !isEqual(l, i)) {
|
|
442
1010
|
bumpDeep(e);
|
|
443
1011
|
break;
|
|
444
1012
|
}
|
|
445
1013
|
}
|
|
446
1014
|
}
|
|
447
1015
|
if (e.k !== null) {
|
|
448
|
-
|
|
1016
|
+
let r;
|
|
1017
|
+
if (e.ovl) {
|
|
1018
|
+
// Overlay membership: only NEW own keys or deletes can change it.
|
|
1019
|
+
r = e.del !== null && e.del.size !== 0;
|
|
1020
|
+
if (!r) {
|
|
1021
|
+
for (const e of Reflect.ownKeys(t)) {
|
|
1022
|
+
if (!hasOwn.call(n, e)) {
|
|
1023
|
+
r = true;
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
r = Array.isArray(t) && Array.isArray(n) ? arrayStructureChanged(n, t) : membershipChanged(n, t);
|
|
1030
|
+
}
|
|
449
1031
|
if (r) setSignal(e.k, e => e + 1);
|
|
450
1032
|
}
|
|
451
1033
|
// Projection backing folds split by channel (two pinned contracts):
|
|
@@ -458,15 +1040,32 @@ function drainFolds() {
|
|
|
458
1040
|
// IMMEDIATE — landed truth shows to untracked readers even while a
|
|
459
1041
|
// downstream consumer's own async still holds the effect-level reveal
|
|
460
1042
|
// (spec-async "verdicts never inherit consumers' in-flight state").
|
|
461
|
-
|
|
1043
|
+
// EXCEPT under an active transaction (#3164 fold): a landing riding a
|
|
1044
|
+
// retaining transaction (the optimistic module's aroundWrite binds it)
|
|
1045
|
+
// stages instead — ensurePB stamped foldBatches, so the backing commits
|
|
1046
|
+
// with the transaction and the reveal is atomic at settle. The pinned
|
|
1047
|
+
// immediate-commit contract is stated over the no-transaction microtask
|
|
1048
|
+
// posture, which `activeTransition === null` is exactly.
|
|
1049
|
+
if (e.fam !== null && e.pb !== null && getWriteOverride() && activeTransition === null) {
|
|
1050
|
+
// Landed truth (post-await write-override): immediately visible to every
|
|
1051
|
+
// reader — any staged held view is superseded.
|
|
1052
|
+
if (e.ht !== null) e.ht = e.hv = null;
|
|
1053
|
+
// Overlay landing (#3352): flatten in place — identity-stable, and
|
|
1054
|
+
// privatizeCommitted re-slots the parent itself when it has to clone an
|
|
1055
|
+
// unowned seed, so no path copy is needed.
|
|
1056
|
+
if (e.ovl) return flattenOverlay(e, t);
|
|
462
1057
|
const n = e.v;
|
|
463
1058
|
e.pb = null;
|
|
464
1059
|
e.v = t;
|
|
465
1060
|
e.ch = false;
|
|
466
|
-
if (e.u
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
e.u.v[
|
|
1061
|
+
if (e.u) {
|
|
1062
|
+
// Identity-resolved slot (#3282) — see drainFolds' path-copy twin.
|
|
1063
|
+
const r = parentSlotKey(e, n);
|
|
1064
|
+
if (e.u.v[r] === n) {
|
|
1065
|
+
privatizeCommitted(e.u);
|
|
1066
|
+
devAssertNeverUserMutation(e.u.v);
|
|
1067
|
+
e.u.v[r] = t;
|
|
1068
|
+
}
|
|
470
1069
|
}
|
|
471
1070
|
}
|
|
472
1071
|
}
|
|
@@ -475,9 +1074,9 @@ const FORCE = Symbol();
|
|
|
475
1074
|
|
|
476
1075
|
/** Same logical slot: both values resolve to one (re-pointed) child target —
|
|
477
1076
|
* adoption preserved identity, so the slot did not change (R9). */ function targetsEqual(e, t) {
|
|
478
|
-
if (e === null || typeof e !== "object") return false;
|
|
479
|
-
const n =
|
|
480
|
-
return n !== undefined && n ===
|
|
1077
|
+
if (e === null || typeof e !== "object" || t === null || typeof t !== "object") return false;
|
|
1078
|
+
const n = lookupTarget(e, null);
|
|
1079
|
+
return n !== undefined && n === lookupTarget(t, null);
|
|
481
1080
|
}
|
|
482
1081
|
|
|
483
1082
|
function arrayStructureChanged(e, t) {
|
|
@@ -492,8 +1091,9 @@ function arrayStructureChanged(e, t) {
|
|
|
492
1091
|
|
|
493
1092
|
function membershipChanged(e, t) {
|
|
494
1093
|
const n = Reflect.ownKeys(t);
|
|
495
|
-
|
|
496
|
-
|
|
1094
|
+
// The $OWNER stamp is not membership: an owned side counts one key more.
|
|
1095
|
+
if (Reflect.ownKeys(e).length - +isOwned(e) !== n.length - +isOwned(t)) return true;
|
|
1096
|
+
for (const t of n) if (t !== $OWNER && !(t in e)) return true;
|
|
497
1097
|
return false;
|
|
498
1098
|
}
|
|
499
1099
|
|
|
@@ -516,23 +1116,23 @@ i = true) {
|
|
|
516
1116
|
if (e.acc === true || i && hasOwn.call(r, t) && lookupGetter.call(r, t) !== undefined) {
|
|
517
1117
|
e.acc = isOwnAccessor(r, t);
|
|
518
1118
|
const i = Object.getOwnPropertyDescriptor(n, t);
|
|
519
|
-
const
|
|
520
|
-
if (i && (i.get || i.set) ||
|
|
1119
|
+
const l = Object.getOwnPropertyDescriptor(r, t);
|
|
1120
|
+
if (i && (i.get || i.set) || l && (l.get || l.set)) {
|
|
521
1121
|
// Accessor involved: never invoke; force-notify on shape change so
|
|
522
1122
|
// subscribers re-read (and re-track) through the trap.
|
|
523
|
-
if (i?.get !==
|
|
1123
|
+
if (i?.get !== l?.get || i?.set !== l?.set || i?.value !== l?.value) setSignal(e, () => FORCE);
|
|
524
1124
|
return;
|
|
525
1125
|
}
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
if (!isEqual(
|
|
1126
|
+
const o = i?.value;
|
|
1127
|
+
const f = l?.value;
|
|
1128
|
+
if (!isEqual(o, f) && !targetsEqual(o, f)) setSignal(e, typeof f === "function" ? () => f : f);
|
|
529
1129
|
} else {
|
|
530
1130
|
const i = n[t];
|
|
531
|
-
const
|
|
1131
|
+
const l = r[t];
|
|
532
1132
|
// Direct value write when not a function (setSignal treats functions as
|
|
533
1133
|
// updaters) — saves a closure allocation per changed key on the fold
|
|
534
1134
|
// hot path.
|
|
535
|
-
if (!isEqual(i,
|
|
1135
|
+
if (!isEqual(i, l) && !targetsEqual(i, l)) setSignal(e, typeof l === "function" ? () => l : l);
|
|
536
1136
|
}
|
|
537
1137
|
}
|
|
538
1138
|
|
|
@@ -544,9 +1144,9 @@ i = true) {
|
|
|
544
1144
|
/** Fused-walk per-key notification with values already in hand: the caller
|
|
545
1145
|
* fetched both sides and handled the identity skip; this applies the
|
|
546
1146
|
* accessor branch (cached flag only — reconcile channel) or the plain
|
|
547
|
-
* equality/identity-gated write. */ function notifyKeyValue(e, t, n, r, i,
|
|
1147
|
+
* equality/identity-gated write. */ function notifyKeyValue(e, t, n, r, i, l) {
|
|
548
1148
|
if (e.acc === true) {
|
|
549
|
-
notifyKeyDiff(e, t, i,
|
|
1149
|
+
notifyKeyDiff(e, t, i, l, false);
|
|
550
1150
|
return;
|
|
551
1151
|
}
|
|
552
1152
|
// The pre-compare is NOT redundant with the node's equals: setSignal parks
|
|
@@ -641,17 +1241,46 @@ function inDraft(e) {
|
|
|
641
1241
|
|
|
642
1242
|
/** Targets written during the current (outermost) setter — notified at exit. */ const pendingNotify = new Set;
|
|
643
1243
|
|
|
1244
|
+
/** Targets adopted under a live transaction this setter (adoptPB set a
|
|
1245
|
+
* transaction hold) — their nodes are notified at the outermost exit. */ const heldAdoptions = new Set;
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Write-time notification for a transaction-held adoption (#3330 store twin;
|
|
1249
|
+
* the setter path's notifyWrites twin for adoptions). A projection's fold
|
|
1250
|
+
* normally notifies its nodes at the drain — and the drain of a batch parked
|
|
1251
|
+
* in a live transaction is the transaction's COMMIT, so the nodes took the
|
|
1252
|
+
* adopted values as fresh writes at commit time: every subscriber was
|
|
1253
|
+
* re-marked and re-ran against a frame the lane had already published (a
|
|
1254
|
+
* third `v=1 d=2`), where a signal's write had staged at write time and
|
|
1255
|
+
* promoted silently. Staging here, inside the transaction's batch, makes the
|
|
1256
|
+
* two paths one: the nodes carry transition-stamped `_pendingValue`s, their
|
|
1257
|
+
* subscribers recompute in this flush and park with the transaction, and the
|
|
1258
|
+
* commit promotes without re-notifying. `ab` moves to the adopted backing —
|
|
1259
|
+
* the view the nodes were last told (#3296) — so the drain has nothing left
|
|
1260
|
+
* to say and only path-copies.
|
|
1261
|
+
*/ function stageHeldAdoptions() {
|
|
1262
|
+
const e = [ ...heldAdoptions ];
|
|
1263
|
+
heldAdoptions.clear();
|
|
1264
|
+
for (const t of e) {
|
|
1265
|
+
const e = t.ab;
|
|
1266
|
+
if (e === null || e === t.v) continue;
|
|
1267
|
+
notifyFold(t, e, t.v);
|
|
1268
|
+
t.ab = t.v;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
644
1272
|
const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
|
|
645
1273
|
|
|
646
1274
|
/** Mirror of core read()'s context rule: the OWNER context (not the tracking
|
|
647
1275
|
* observer) decides pending visibility, with Roots resolving to their parent
|
|
648
1276
|
* computed (#2687 — untracked reads inside mapArray Roots see in-flight
|
|
649
1277
|
* values mid-flush). CHILDREN_FORBIDDEN execution scopes (createTrackedEffect
|
|
650
|
-
* / onSettled callbacks) get COMMITTED visibility (#3006), same as core. */
|
|
1278
|
+
* / onSettled callbacks) get COMMITTED visibility (#3006), same as core. */
|
|
1279
|
+
/** Core read()'s reader: the current computation, a root reading as its
|
|
1280
|
+
* parent computed (`context` persists under untrack — an untracked read
|
|
1281
|
+
* inside an effect is still that effect's read). */ function readerContext() {
|
|
651
1282
|
const e = getOwner();
|
|
652
|
-
|
|
653
|
-
const t = e.Tt ? e.It : e;
|
|
654
|
-
return t != null && !(t.T & CONFIG_CHILDREN_FORBIDDEN);
|
|
1283
|
+
return e === null ? null : e.Wn ? e.qn ?? null : e;
|
|
655
1284
|
}
|
|
656
1285
|
|
|
657
1286
|
/** A pending fold is transition-held when any written node's parked value is
|
|
@@ -661,24 +1290,82 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
|
|
|
661
1290
|
if (t === null) return false;
|
|
662
1291
|
for (const e of Reflect.ownKeys(t)) {
|
|
663
1292
|
const n = t[e];
|
|
664
|
-
if (n._e !== NOT_PENDING && n.
|
|
1293
|
+
if (n._e !== NOT_PENDING && n.we != null && n.we.Rn !== true) return true;
|
|
665
1294
|
}
|
|
666
1295
|
return false;
|
|
667
1296
|
}
|
|
668
1297
|
|
|
669
1298
|
function readSource(e) {
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
if (
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1299
|
+
// Adoption hold first (#3074): an adoption staged under a live transaction
|
|
1300
|
+
// (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
|
|
1301
|
+
// committed-visibility readers — context-free and children-forbidden ones,
|
|
1302
|
+
// and stale passes off a foreign hold. Drafts, write-override and latest()
|
|
1303
|
+
// see the adopted backing.
|
|
1304
|
+
const t = e.ht;
|
|
1305
|
+
if (t !== null && !latestReadActive && !inDraft(e) && !getWriteOverride()) {
|
|
1306
|
+
const n = heldMaskView(e);
|
|
1307
|
+
if (n !== null) {
|
|
1308
|
+
const e = readerContext();
|
|
1309
|
+
if (e === null || e.C & CONFIG_CHILDREN_FORBIDDEN || !holdVisible(t === PLAIN_HOLD ? null : currentTransition(t), e)) return n;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return pendingBackingVisible(e, false) ? e.pb : e.v;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** The single pb-vs-committed visibility decision (#3147), shared by per-key
|
|
1316
|
+
* backing reads (readSource) and deep()/snapshot composition (snapshotWalk)
|
|
1317
|
+
* so the two reader families can never disagree about a HELD landing.
|
|
1318
|
+
*
|
|
1319
|
+
* Signal-parity visibility (core read(): owner-context reads serve
|
|
1320
|
+
* _pendingValue, context-free reads serve committed — effects recompute
|
|
1321
|
+
* BEFORE commitPendingNodes in the flush, so the pending view must be
|
|
1322
|
+
* servable). Drafts (setter window OR projection write-override) and
|
|
1323
|
+
* owner-context reads see the pending backing; context-free reads see
|
|
1324
|
+
* committed. Node reads apply the same rule, so all homes agree.
|
|
1325
|
+
*
|
|
1326
|
+
* `speculative` is deep()/snapshot's posture: an untrack/deep PEEK that sees
|
|
1327
|
+
* ordinary pending staging regardless of owner context (the documented
|
|
1328
|
+
* divergence from context-free per-key reads) — but never through a hold:
|
|
1329
|
+
* held truth stays masked exactly as it is for per-key readers. */ function pendingBackingVisible(e, t) {
|
|
1330
|
+
if (e.pb === null) return false;
|
|
1331
|
+
// The writer's own channels compose on the pending backing regardless.
|
|
1332
|
+
if (inDraft(e) || getWriteOverride()) return true;
|
|
1333
|
+
// HELD truth on an optimistic family (#3164 fold) is masked from LANE
|
|
1334
|
+
// passes until the transaction's reveal (the backing-level twin of core
|
|
1335
|
+
// serve()'s CONFIG_HELD_TRUTH arm; authoritative postures and latest()
|
|
1336
|
+
// tunnel through inside heldTruthMasked). Every other reader takes the
|
|
1337
|
+
// hold arms below: committed for no pass, the staged backing for the
|
|
1338
|
+
// holding pass, stale-of-foreign or A29 for a foreign one.
|
|
1339
|
+
if (heldTruthMasked(e)) return false;
|
|
1340
|
+
const n = readerContext();
|
|
1341
|
+
if (n === null || n.C & CONFIG_CHILDREN_FORBIDDEN) {
|
|
1342
|
+
// No pass, or a children-forbidden one: the committed frame (A32) —
|
|
1343
|
+
// except the speculative peek (deep()/snapshot()), which sees ordinary
|
|
1344
|
+
// pending staging but never through a live foreign hold, and a
|
|
1345
|
+
// projection's pending backing, authoritative-elect for context-free
|
|
1346
|
+
// readers UNLESS a transition holds the node commits (downstream async
|
|
1347
|
+
// hold — stale committed is the contract; the write-time stamp covers
|
|
1348
|
+
// keys with no node, #3336) or the scope is children-forbidden (#3082).
|
|
1349
|
+
const r = liveFoldTransition(e);
|
|
1350
|
+
if (t) return r === null || ownsHold(r);
|
|
1351
|
+
return e.fam !== null && n === null && !foldHeld(e) && r === null;
|
|
1352
|
+
}
|
|
1353
|
+
return holdVisible(liveFoldTransition(e), n);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
1357
|
+
* stamped by a live transition that retains optimism — is masked from LANE
|
|
1358
|
+
* passes only (a lane paints display-ahead at the park, so it keeps
|
|
1359
|
+
* committed until the transaction's reveal — owning transaction or not);
|
|
1360
|
+
* the authoritative postures and latest() tunnel through, and every other
|
|
1361
|
+
* reader is served by the ordinary hold arms (a deriving pass is held with
|
|
1362
|
+
* the truth, A29). Un-stamped backings and optimism-free transitions keep
|
|
1363
|
+
* ordinary mid-batch/speculation visibility. */ function heldTruthMasked(e) {
|
|
1364
|
+
if (e.fam?.opt !== true || currentOptimisticLane === null || latestReadActive || authoritativeServe()) return false;
|
|
1365
|
+
const t = foldBatches.get(e);
|
|
1366
|
+
// opt families are only created by createOptimisticStore, whose module
|
|
1367
|
+
// install populates optHooks — the assertion holds by construction.
|
|
1368
|
+
return t !== undefined && optHooks.retainsOptimism(t);
|
|
682
1369
|
}
|
|
683
1370
|
|
|
684
1371
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
@@ -691,6 +1378,8 @@ const lookupGetter = Object.prototype.__lookupGetter__;
|
|
|
691
1378
|
|
|
692
1379
|
const lookupSetter = Object.prototype.__lookupSetter__;
|
|
693
1380
|
|
|
1381
|
+
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
|
|
1382
|
+
|
|
694
1383
|
function isOwnAccessor(e, t) {
|
|
695
1384
|
return hasOwn.call(e, t) && (lookupGetter.call(e, t) !== undefined || lookupSetter.call(e, t) !== undefined);
|
|
696
1385
|
}
|
|
@@ -708,9 +1397,24 @@ function isOwnAccessor(e, t) {
|
|
|
708
1397
|
}
|
|
709
1398
|
}
|
|
710
1399
|
|
|
711
|
-
/**
|
|
712
|
-
*
|
|
713
|
-
|
|
1400
|
+
/** The reading computation is until()'s authoritative-view predicate — same
|
|
1401
|
+
* source of truth as core read()'s A17 carve-out (`context`, which persists
|
|
1402
|
+
* under untrack). optimisticView()'s composition gate consults exactly this:
|
|
1403
|
+
* write-side machinery (patch emission, tentative re-application) must keep
|
|
1404
|
+
* composing even when it runs inside an authoritative-write bracket. */ function authoritativeRead() {
|
|
1405
|
+
const e = context;
|
|
1406
|
+
return e !== null && (e.C & CONFIG_AUTHORITATIVE_READ) !== 0;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/** Serve-side authoritative gate: until()'s predicate PLUS truth authors —
|
|
1410
|
+
* the projection derive's draft (wrapDraft trap brackets, runAuthoritative;
|
|
1411
|
+
* the same posture pair ensurePB classifies drafts by). A source computing
|
|
1412
|
+
* the next truth must never read its callers' tentative overlays: a derive
|
|
1413
|
+
* continuation's `store.push` computing its index from an action's
|
|
1414
|
+
* optimistic row landed truth in the wrong slot and corrupted committed
|
|
1415
|
+
* state (#3108). Trap-level overlay serves gate on this so values, length,
|
|
1416
|
+
* membership, and keys leave the authoritative view together. */ function authoritativeServe() {
|
|
1417
|
+
return projectionWriteActive || getWriteOverride() || authoritativeRead();
|
|
714
1418
|
}
|
|
715
1419
|
|
|
716
1420
|
/** Context-aware node view for reads outside tracking: active override >
|
|
@@ -720,61 +1424,138 @@ function isOwnAccessor(e, t) {
|
|
|
720
1424
|
* reader that forced it (backing commits eagerly; node values fold at flush).
|
|
721
1425
|
* FORCE sentinels never surface (they only bump subscribers of accessor
|
|
722
1426
|
* keys, which are served by the trap, not the node). */ function nodeValue(e, t) {
|
|
723
|
-
|
|
1427
|
+
// Store-only tunnels, ahead of Rule 1: truth authors (authoritativeServe —
|
|
1428
|
+
// the projection derive's draft, the write-override continuation) see
|
|
1429
|
+
// staged truth and never an override; latest() reaching this untracked
|
|
1430
|
+
// path for a store key sees the in-flight parked value like an
|
|
1431
|
+
// owner-context reader does (#3075), the visible override first.
|
|
1432
|
+
let n;
|
|
1433
|
+
if (authoritativeServe()) n = e._e !== NOT_PENDING ? e._e : t; else if (latestReadActive) n = visibleOverride(e) ? unwrapOverride(e.o?.Fe) : e._e !== NOT_PENDING ? e._e : t;
|
|
1434
|
+
// Otherwise the one slow selection core read() uses (serve): override,
|
|
1435
|
+
// lane gate, A28, readerSeesCommitted / A29 — with the BACKING as the
|
|
1436
|
+
// committed value (single-home rule, O6).
|
|
1437
|
+
else n = serve(e, readerContext(), e.De || e, t);
|
|
724
1438
|
return n === FORCE ? t : n;
|
|
725
1439
|
}
|
|
726
1440
|
|
|
1441
|
+
/** The override a composed READER view (keys, descriptors, snapshot/deep,
|
|
1442
|
+
* optimisticView) takes from an armed node whose override is active — the
|
|
1443
|
+
* override itself, unless the node's own source superseded it (#3331,
|
|
1444
|
+
* CONFIG_OVERRIDE_SUPERSEDED): then the reader-aware selection `serve`
|
|
1445
|
+
* makes through nodeValue, so the composed view agrees with what `get`,
|
|
1446
|
+
* `in` and `length` serve the same reader (a deriving pass: the staged
|
|
1447
|
+
* truth; a lane pass or a context-free read: the override, A18). Composing
|
|
1448
|
+
* the raw override left Object.keys / snapshot() / deep() one row behind the
|
|
1449
|
+
* traps at a landing whose shape differed from the optimistic frame (F5
|
|
1450
|
+
* parity cases). Draft and authoritative callers never reach here — the
|
|
1451
|
+
* writer composes on hasActiveOverride, truth authors on the backing. */ function readerOverride(e, t) {
|
|
1452
|
+
return e.C & CONFIG_OVERRIDE_SUPERSEDED ? nodeValue(e, t) : unwrapOverride(e.o?.Fe);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** §7b: a chained target's child found as a RAW — from its pending backing
|
|
1456
|
+
* (a cloneRaw of the inner proxy, whose descriptors yield the inner store's
|
|
1457
|
+
* raws) or from the deep() walk's descriptor read through the chain — must
|
|
1458
|
+
* resolve to the inner family's proxy for that object before this family
|
|
1459
|
+
* wraps it, so the overlay serves the same chained targets as the settled
|
|
1460
|
+
* state. Otherwise every row re-wrapped as a fresh non-chained target keyed
|
|
1461
|
+
* by the raw: identities churned for the life of an optimistic action and
|
|
1462
|
+
* snapped back at settle, and writes landed on those orphans while deep()'s
|
|
1463
|
+
* witnesses sat on the chained targets (#3323). The inner family owns the
|
|
1464
|
+
* raw iff it has served it or its backing holds that exact object at `key`;
|
|
1465
|
+
* anything else is a draft's own replacement object — view-owned, correctly
|
|
1466
|
+
* non-chained — and is returned as is. Recurses down further chains. */ function resolveChainedRaw(e, t, n) {
|
|
1467
|
+
const r = e.v[$TARGET];
|
|
1468
|
+
if (r.ch) {
|
|
1469
|
+
const e = resolveChainedRaw(r, t, n);
|
|
1470
|
+
return e === n ? n : wrapNext(e, r, t);
|
|
1471
|
+
}
|
|
1472
|
+
const i = lookupTarget(n, r.fam);
|
|
1473
|
+
if (i !== undefined) return i.px;
|
|
1474
|
+
if ((r.v[t] === n || r.pb?.[t] === n) && isWrappable(n)) return wrapNext(n, r, t);
|
|
1475
|
+
return n;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
727
1478
|
/** Serve an own data key: node-first when a node exists (pending visibility,
|
|
728
1479
|
* holds, lanes ride the node); backing otherwise. Chained backings (§7b: the
|
|
729
1480
|
* backing IS another store's proxy) serve the read-through value — the outer
|
|
730
1481
|
* node is linked only for adoption-swap notification, its value never
|
|
731
|
-
* shadows the live chain. */ function serveDataKey(e, t, n, r, i) {
|
|
1482
|
+
* shadows the live chain. */ function serveDataKey(e, t, n, r, i, l = -1) {
|
|
732
1483
|
const o = e.ch && r === e.v;
|
|
733
1484
|
let f = n;
|
|
734
1485
|
// §6: on optimistic arrays LENGTH IS A VIEW, not a node value — one home
|
|
735
1486
|
// (backing ± presence overrides) for both length and indices makes torn
|
|
736
1487
|
// iteration impossible (a length node's value rides different visibility
|
|
737
|
-
// rails than index overrides mid-settle). The node
|
|
738
|
-
//
|
|
1488
|
+
// rails than index overrides mid-settle). The node carries subscriptions;
|
|
1489
|
+
// its committed `_value` is never served here.
|
|
739
1490
|
if (t === "length" && e.fam?.opt === true && !o && Array.isArray(r)) {
|
|
740
1491
|
if (!inDraft(e)) {
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
743
|
-
|
|
1492
|
+
const i = e.n?.length;
|
|
1493
|
+
if (i !== undefined) {
|
|
1494
|
+
// An override-covered length node answers through the node's
|
|
1495
|
+
// reader-aware selection (serve: A17, the lane gate, A18
|
|
1496
|
+
// supersession) with the backing's length as committed — the rule
|
|
1497
|
+
// every index node takes through `get`, so `length` and indices
|
|
1498
|
+
// resolve against ONE rule for this reader. The view composition
|
|
1499
|
+
// below used to answer here and knows nothing of supersession
|
|
1500
|
+
// (#3331): at a landing whose length differs from the optimistic
|
|
1501
|
+
// frame, mapArray's tracked `length` (the get trap's node path) saw
|
|
1502
|
+
// the staged truth while its untracked `slice` inside its owner saw
|
|
1503
|
+
// the override, so `_items` came up short and the next pass keyed an
|
|
1504
|
+
// undefined row (F5, optimistic-list-mutation-matrix `differ`).
|
|
1505
|
+
if (getObserver() !== null) {
|
|
1506
|
+
const e = read(i);
|
|
1507
|
+
if (hasActiveOverride(i) && !authoritativeServe()) return e === FORCE ? r.length : e;
|
|
1508
|
+
} else if (hasActiveOverride(i) && !authoritativeServe()) {
|
|
1509
|
+
return nodeValue(i, r.length);
|
|
1510
|
+
}
|
|
744
1511
|
} else if (getObserver() !== null) {
|
|
745
1512
|
read(getNode(e, t, n));
|
|
746
1513
|
}
|
|
747
1514
|
}
|
|
748
|
-
|
|
1515
|
+
// Truth authors read the backing's own length — an optimistic row from
|
|
1516
|
+
// the caller's transaction must not shift where the author's next write
|
|
1517
|
+
// lands (#3108). A tentative draft that has seeded its backing from the
|
|
1518
|
+
// view already carries the overrides in `src` (the draft arm every other
|
|
1519
|
+
// channel gates on draftSeesOverrides — `get`, `has`, visibleKeys,
|
|
1520
|
+
// snapshotWalk, #3665): composing them again put a slot the draft had
|
|
1521
|
+
// just spliced out back on top of its shrunken backing, so `length` read
|
|
1522
|
+
// one too long mid-splice and the second splice left a hole (F3,
|
|
1523
|
+
// optimistic-list-mutation-matrix "move head->tail + move middle").
|
|
1524
|
+
return (authoritativeServe() || inDraft(e) && !draftSeesOverrides(e) ? r : optHooks.optimisticView(e, r, inDraft(e))).length;
|
|
749
1525
|
}
|
|
750
1526
|
if (inDraft(e)) {
|
|
751
1527
|
// Optimistic drafts before their first write have no pending backing yet;
|
|
752
1528
|
// reads must still see the live optimistic view (compose, not clobber —
|
|
753
1529
|
// #2951). Once ensurePB runs, the seeded clone carries the view.
|
|
754
|
-
|
|
1530
|
+
// AUTHORITATIVE drafts (projection derive) never overlay — ensurePB's
|
|
1531
|
+
// seeding rule, applied to the read side (#3108).
|
|
1532
|
+
if (e.fam?.opt && draftSeesOverrides(e) && !authoritativeServe()) {
|
|
755
1533
|
const n = e.n?.[t];
|
|
756
|
-
if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.
|
|
1534
|
+
if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.o?.Fe);
|
|
757
1535
|
}
|
|
758
1536
|
} else {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1537
|
+
// §7b: a lane value on the outer node SHADOWS read-through — an active
|
|
1538
|
+
// override pierces the chained gate; otherwise chained backings always
|
|
1539
|
+
// serve the live inner value.
|
|
1540
|
+
if (getObserver() !== null) {
|
|
1541
|
+
// First tracked read: create + link (the wrap-cache branch below
|
|
1542
|
+
// populates px/pxv so read #2 skips wrapNext, slice 2). The value is
|
|
1543
|
+
// served THROUGH the node from this read on — a node born under a held
|
|
1544
|
+
// fold carries the hold (getNode, #3336), and the backing it was read
|
|
1545
|
+
// from does not.
|
|
1546
|
+
if (i === undefined) i = getNode(e, t, n, l);
|
|
1547
|
+
// read()'s plain-signal fast path hoisted over the call (legacy trap
|
|
1548
|
+
// parity): READ_SLOW = a global read window or non-plain node.
|
|
1549
|
+
let r = readNodeFast(i);
|
|
1550
|
+
if (r === READ_SLOW) r = read(i);
|
|
1551
|
+
if (!o || hasActiveOverride(i)) f = r === FORCE ? n : r;
|
|
1552
|
+
} else if (i !== undefined && (!o || hasActiveOverride(i))) {
|
|
1553
|
+
f = nodeValue(i, n);
|
|
774
1554
|
}
|
|
775
1555
|
}
|
|
776
1556
|
// Shallow stores serve data raw; store-proxy slots get boundary wrappers.
|
|
777
1557
|
if (e.s) return serveShallow(e, t, f);
|
|
1558
|
+
if (e.ch && !o && f !== null && typeof f === "object" && f[$TARGET] === undefined) f = resolveChainedRaw(e, t, f);
|
|
778
1559
|
if (i !== undefined) {
|
|
779
1560
|
// Wrap cache (see getNode): only wrappables are ever cached, so a hit
|
|
780
1561
|
// skips isWrappable too — pointer-compare replaces both checks.
|
|
@@ -796,8 +1577,38 @@ function isOwnAccessor(e, t) {
|
|
|
796
1577
|
* read() links the node and throws the firewall's error itself (the node
|
|
797
1578
|
* link is what wakes async-memo readers when the landing writes values;
|
|
798
1579
|
* the firewall link rides the same read). */ function firewallGate(e) {
|
|
1580
|
+
// Own-draft ops are exempt: an async derive's continuation (generator body
|
|
1581
|
+
// after an `await`/`yield`) runs OUTSIDE the sync write scope (inDraft is
|
|
1582
|
+
// already false), but its draft-proxy traps mark every op with the write
|
|
1583
|
+
// override. Those reads are the derive working its own draft (state.push
|
|
1584
|
+
// reading .length) — gating them throws NotReadyError back into the derive
|
|
1585
|
+
// itself, which the post-await read diagnostic (#2987) then escalates to a
|
|
1586
|
+
// reactivity halt. The gate exists for EXTERNAL readers (seed invisibility,
|
|
1587
|
+
// proj R23); the derive is the author.
|
|
1588
|
+
if (projectionWriteActive || getWriteOverride()) return;
|
|
799
1589
|
const t = e.fam?.node;
|
|
800
|
-
if (t != null && t.
|
|
1590
|
+
if (t != null && t.h & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(t);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** latest() pull (#3075): bring the projection computed up to date so the
|
|
1594
|
+
* read serves the IN-FLIGHT derivation — signal/memo parity, where core
|
|
1595
|
+
* read() routes latest() through a companion that recomputes speculatively.
|
|
1596
|
+
* The latest flag is suspended for the recompute (the derive's own reads
|
|
1597
|
+
* are normal reads), and latestPullActive marks any adoption it commits as
|
|
1598
|
+
* staged (see adoptPB) — the speculative swap must not leak to
|
|
1599
|
+
* committed-visibility readers before the flush. */ function pullProjectionForLatest(e) {
|
|
1600
|
+
const t = e.fam.node;
|
|
1601
|
+
if (t == null) return;
|
|
1602
|
+
const n = latestReadActive;
|
|
1603
|
+
setLatestReadActive(false);
|
|
1604
|
+
const r = latestPullActive;
|
|
1605
|
+
latestPullActive = true;
|
|
1606
|
+
try {
|
|
1607
|
+
prepareComputed(t, true);
|
|
1608
|
+
} finally {
|
|
1609
|
+
latestPullActive = r;
|
|
1610
|
+
setLatestReadActive(n);
|
|
1611
|
+
}
|
|
801
1612
|
}
|
|
802
1613
|
|
|
803
1614
|
const traps = {
|
|
@@ -807,6 +1618,10 @@ const traps = {
|
|
|
807
1618
|
if (typeof t !== "string") {
|
|
808
1619
|
if (t === $TARGET) return e;
|
|
809
1620
|
if (t === $PROXY) return n;
|
|
1621
|
+
if (t === $OWNER) return undefined;
|
|
1622
|
+
// ownership stamp: never a user key
|
|
1623
|
+
if (t === $RECORD) return undefined;
|
|
1624
|
+
// a store is no view (see `viewOf`)
|
|
810
1625
|
// refresh()/isPending resolve the projection computed through $REFRESH.
|
|
811
1626
|
if (t === $REFRESH) return e.fam?.node ?? undefined;
|
|
812
1627
|
if (t === $TRACK) {
|
|
@@ -827,13 +1642,26 @@ const traps = {
|
|
|
827
1642
|
}
|
|
828
1643
|
if (pendingCheckActive) witnessAffectsMark(e, t);
|
|
829
1644
|
if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
|
|
1645
|
+
// latest() pull (#3075): store traps never reach core read() without an
|
|
1646
|
+
// observer, so bring the projection computed up to date here — signal/
|
|
1647
|
+
// memo parity for latest() reads through a projection.
|
|
1648
|
+
if (e.fam !== null && latestReadActive && !inDraft(e) && !getWriteOverride()) pullProjectionForLatest(e);
|
|
830
1649
|
const r = readSource(e);
|
|
1650
|
+
// Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
|
|
1651
|
+
// deleted keys are tracked aside and read as absent in the pending view.
|
|
1652
|
+
if (e.del !== null && r === e.pb && e.del.has(t)) {
|
|
1653
|
+
if (!inDraft(e) && getObserver() !== null) read(getNode(e, t, undefined));
|
|
1654
|
+
return undefined;
|
|
1655
|
+
}
|
|
831
1656
|
// Hot inline case: existing PLAIN node (non-accessor), unchained backing,
|
|
832
1657
|
// tracked read of a present data key — the dbmon/uibench effect re-read
|
|
833
1658
|
// shape. Skips serveDataKey's frame, the FORCE compare (only accessor
|
|
834
1659
|
// keys ever hold the sentinel), and isWrappable for primitives.
|
|
835
|
-
|
|
836
|
-
|
|
1660
|
+
// ONE node-map lookup serves this block and the accessor probe below
|
|
1661
|
+
// (nothing between them creates nodes).
|
|
1662
|
+
const i = e.n?.[t];
|
|
1663
|
+
if (e.ch === false && writeScopes === null) {
|
|
1664
|
+
const n = i;
|
|
837
1665
|
if (n !== undefined && n.acc !== true && getObserver() !== null) {
|
|
838
1666
|
let r = readNodeFast(n);
|
|
839
1667
|
if (r === READ_SLOW) r = read(n);
|
|
@@ -855,11 +1683,25 @@ const traps = {
|
|
|
855
1683
|
// the node's cached flag; the first TRACKED read (which creates the
|
|
856
1684
|
// node) probes once — untracked node-less reads take the plain path,
|
|
857
1685
|
// where a raw-receiver getter still returns correct committed values.
|
|
858
|
-
|
|
1686
|
+
// Tracking suppression is PER-TARGET (inDraft), never global: `writing`
|
|
1687
|
+
// counts every open setter anywhere, and a projection derive runs its
|
|
1688
|
+
// whole body inside one — a global gate silently swallowed EXTERNAL
|
|
1689
|
+
// absent-key/accessor subscriptions for every store read during any
|
|
1690
|
+
// derive, leaving nested projections permanently dependency-less when
|
|
1691
|
+
// their sources hadn't materialized yet (#3037).
|
|
1692
|
+
// First-read dedupe (create-floor slice 2): remember the probe verdict —
|
|
1693
|
+
// node creation downstream reuses it instead of re-scanning the
|
|
1694
|
+
// descriptor, but only when the probed object IS the one getNode would
|
|
1695
|
+
// scan (pb ?? v).
|
|
1696
|
+
let l = -1;
|
|
859
1697
|
{
|
|
860
|
-
|
|
1698
|
+
let o;
|
|
1699
|
+
if (i !== undefined) o = i.acc === true; else if (!inDraft(e) && getObserver() !== null) {
|
|
1700
|
+
o = isOwnAccessor(r, t);
|
|
1701
|
+
if (r === (e.pb ?? e.v)) l = o ? 1 : 0;
|
|
1702
|
+
} else o = false;
|
|
861
1703
|
if (o) {
|
|
862
|
-
if (!
|
|
1704
|
+
if (!inDraft(e) && getObserver() !== null) read(i ?? getNode(e, t, undefined, l));
|
|
863
1705
|
const o = Reflect.get(r, t, n);
|
|
864
1706
|
if (e.s) return serveShallow(e, t, o);
|
|
865
1707
|
return isWrappable(o) ? draftServe(e, wrapNext(o, e, t)) : o;
|
|
@@ -868,52 +1710,77 @@ const traps = {
|
|
|
868
1710
|
// Plain-data fast path: no descriptor allocation per read.
|
|
869
1711
|
// Inherited pollution keys are never served (core R30) — checked before
|
|
870
1712
|
// the proto-function branch can leak `constructor`. Interned-string
|
|
871
|
-
// compares beat a Set hash on this per-read path.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1713
|
+
// compares beat a Set hash on this per-read path. Overlay pending
|
|
1714
|
+
// backings chain to the committed backing, so "own in the view" means
|
|
1715
|
+
// own on either layer (ownInView) — a genuine prototype method is one
|
|
1716
|
+
// that is own on NEITHER.
|
|
1717
|
+
const o = e.ovl && r === e.pb;
|
|
1718
|
+
if ((t === "constructor" || t === "__proto__" || t === "prototype") && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return undefined;
|
|
1719
|
+
let f = r[t];
|
|
1720
|
+
if (f === undefined ? !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t)) : false) {
|
|
875
1721
|
// Inherited: prototype getters/methods run with the proxy receiver.
|
|
876
|
-
|
|
877
|
-
if (typeof
|
|
1722
|
+
f = Reflect.get(r, t, n);
|
|
1723
|
+
if (typeof f === "function") return f;
|
|
878
1724
|
// proto methods untracked
|
|
879
|
-
// Reading a currently-absent own key subscribes to it (R12)
|
|
880
|
-
|
|
881
|
-
|
|
1725
|
+
// Reading a currently-absent own key subscribes to it (R12) — for any
|
|
1726
|
+
// target OUTSIDE its own draft scope, even mid-setter (#3037, above).
|
|
1727
|
+
if (f === undefined && !inDraft(e)) {
|
|
1728
|
+
if (getObserver() !== null) read(getNode(e, t, undefined, l));
|
|
882
1729
|
const n = e.n?.[t];
|
|
883
1730
|
if (n) {
|
|
884
1731
|
const r = nodeValue(n, undefined);
|
|
885
1732
|
if (e.s) return serveShallow(e, t, r);
|
|
886
1733
|
return isWrappable(r) ? draftServe(e, wrapNext(r, e, t)) : r;
|
|
887
1734
|
}
|
|
888
|
-
} else if (
|
|
1735
|
+
} else if (f === undefined && inDraft(e) && e.fam?.opt && draftSeesOverrides(e) &&
|
|
1736
|
+
// AUTHORITATIVE drafts (landing folds) never seed from overrides —
|
|
1737
|
+
// the caller's optimism is not truth (has-trap twin below).
|
|
1738
|
+
!authoritativeServe()) {
|
|
889
1739
|
const n = e.n?.[t];
|
|
890
|
-
|
|
1740
|
+
// The draft is a WRITER channel (hasActiveOverride's rule): it
|
|
1741
|
+
// composes on the tick's own unflushed adds, as the has trap's draft
|
|
1742
|
+
// arm, visibleKeys and optimisticView do. Reader-gating this arm
|
|
1743
|
+
// (visibleOverride) made a second setter in the same action read
|
|
1744
|
+
// `undefined` where the first had pushed a row (#3665, rc.9).
|
|
1745
|
+
if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.o?.Fe);
|
|
891
1746
|
}
|
|
892
|
-
if (e.s) return serveShallow(e, t,
|
|
893
|
-
return isWrappable(
|
|
1747
|
+
if (e.s) return serveShallow(e, t, f);
|
|
1748
|
+
return isWrappable(f) ? draftServe(e, wrapNext(f, e, t)) : f;
|
|
894
1749
|
}
|
|
895
|
-
if (typeof
|
|
1750
|
+
if (typeof f === "function" && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return f;
|
|
896
1751
|
// proto method
|
|
897
|
-
return serveDataKey(e, t,
|
|
1752
|
+
return serveDataKey(e, t, f, r, i, l);
|
|
898
1753
|
},
|
|
899
1754
|
has(e, t) {
|
|
900
1755
|
if (t === $TARGET || t === $PROXY || t === $TRACK) return true;
|
|
1756
|
+
if (t === $OWNER || t === $RECORD) return false;
|
|
901
1757
|
if (pendingCheckActive) witnessAffectsMark(e, t);
|
|
902
1758
|
if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
|
|
903
1759
|
const n = readSource(e);
|
|
904
1760
|
let r = t in n;
|
|
1761
|
+
// Overlay deletes read as absent in the pending view (#3044).
|
|
1762
|
+
if (r && e.del !== null && n === e.pb && e.del.has(t)) r = false;
|
|
905
1763
|
if (!inDraft(e)) {
|
|
906
1764
|
if (getObserver() !== null) {
|
|
907
1765
|
const n = getHasNode(e, t, r);
|
|
908
|
-
|
|
1766
|
+
// Authoritative-view readers get the right answer for free: core read()
|
|
1767
|
+
// skips the override arm for them, so nv is authoritative presence.
|
|
1768
|
+
const i = read(n);
|
|
909
1769
|
if (hasActiveOverride(n)) r = !!i;
|
|
910
|
-
} else {
|
|
1770
|
+
} else if (!authoritativeServe()) {
|
|
911
1771
|
const n = e.h?.[t];
|
|
912
|
-
|
|
1772
|
+
// The get trap's untracked selection (nodeValue → serve), so `in`
|
|
1773
|
+
// agrees with the tracked branch above and with `get`: a presence
|
|
1774
|
+
// override the landing superseded (#3331 — an optimistic delete
|
|
1775
|
+
// whose slot the truth refilled) answers the staged truth for a
|
|
1776
|
+
// deriving reader. Reading the override directly said "absent" while
|
|
1777
|
+
// `get` served the landed row, and HasProperty-driven copies (slice,
|
|
1778
|
+
// spread, map) left a hole (F5 `differ: delete *`).
|
|
1779
|
+
if (n !== undefined && hasActiveOverride(n)) r = !!nodeValue(n, r);
|
|
913
1780
|
}
|
|
914
|
-
} else if (e.fam?.opt && e
|
|
1781
|
+
} else if (e.fam?.opt && draftSeesOverrides(e) && !authoritativeServe()) {
|
|
915
1782
|
const n = e.h?.[t];
|
|
916
|
-
if (n !== undefined && hasActiveOverride(n)) r = !!unwrapOverride(n.
|
|
1783
|
+
if (n !== undefined && hasActiveOverride(n)) r = !!unwrapOverride(n.o?.Fe);
|
|
917
1784
|
}
|
|
918
1785
|
return r;
|
|
919
1786
|
},
|
|
@@ -921,47 +1788,45 @@ const traps = {
|
|
|
921
1788
|
if (pendingCheckActive) witnessAffectsMark(e);
|
|
922
1789
|
if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
|
|
923
1790
|
if (!inDraft(e) && getObserver() !== null) read(getKeySetNode(e));
|
|
924
|
-
|
|
925
|
-
// Optimistic membership overlay: presence-node overrides add/remove keys
|
|
926
|
-
// (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
|
|
927
|
-
// Draft reads before the first write overlay too (pb, once created, is
|
|
928
|
-
// seeded with the view).
|
|
929
|
-
if (e.fam?.opt && e.h !== null && (!inDraft(e) || e.pb === null)) {
|
|
930
|
-
let n = null;
|
|
931
|
-
for (const r of Reflect.ownKeys(e.h)) {
|
|
932
|
-
const i = e.h[r];
|
|
933
|
-
if (!hasActiveOverride(i)) continue;
|
|
934
|
-
n ??= new Set(t);
|
|
935
|
-
if (unwrapOverride(i.De)) n.add(r); else n.delete(r);
|
|
936
|
-
}
|
|
937
|
-
if (n !== null) return [ ...n ];
|
|
938
|
-
}
|
|
939
|
-
return t;
|
|
1791
|
+
return visibleKeys(e, readSource(e));
|
|
940
1792
|
},
|
|
941
1793
|
getOwnPropertyDescriptor(e, t) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1794
|
+
if (t === $OWNER || t === $RECORD) return undefined;
|
|
1795
|
+
// A descriptor read is a PRESENCE read: it subscribes to the key's
|
|
1796
|
+
// presence node and witnesses affects()/isPending() exactly as `in` does
|
|
1797
|
+
// (structural oracle, 2026-09-17 — the trap read no node before, so a
|
|
1798
|
+
// render effect inspecting a key through getOwnPropertyDescriptor never
|
|
1799
|
+
// re-ran for an optimistic add or delete, and an isPending() probe over
|
|
1800
|
+
// it witnessed nothing). The value it reports rides the value node's
|
|
1801
|
+
// view through visibleDescriptor.
|
|
1802
|
+
|
|
1803
|
+
// EXCEPT for an enumerator (#3664): Object.keys / for...in / spread /
|
|
1804
|
+
// Object.entries / JSON.stringify take this trap once per key right
|
|
1805
|
+
// after `ownKeys`, which already subscribed the observer to the key-set
|
|
1806
|
+
// node — and that node bumps on every membership change, committed or
|
|
1807
|
+
// optimistic, so a presence node per key adds nothing the enumerator
|
|
1808
|
+
// can observe (rc.9 birthed one per key per object: ~640 B and a graph
|
|
1809
|
+
// node each, 10x the memory of a 30-key row's reader). When the
|
|
1810
|
+
// observer holds the key-set node in THIS pass, skip the presence read;
|
|
1811
|
+
// a lone descriptor read keeps its per-key precision.
|
|
1812
|
+
if (pendingCheckActive) witnessAffectsMark(e, t);
|
|
1813
|
+
const n = getObserver();
|
|
1814
|
+
if (e.fam !== null && n === null && !inDraft(e)) firewallGate(e);
|
|
1815
|
+
const r = readSource(e);
|
|
1816
|
+
const i = visibleDescriptor(e, r, t);
|
|
1817
|
+
if (!inDraft(e) && n !== null && !observerHoldsKeySet(e, n)) {
|
|
1818
|
+
// The node is born from the source's presence (as `has` births it),
|
|
1819
|
+
// not the override-adjusted answer.
|
|
1820
|
+
let n = t in r;
|
|
1821
|
+
if (n && e.del !== null && r === e.pb && e.del.has(t)) n = false;
|
|
1822
|
+
read(getHasNode(e, t, n));
|
|
958
1823
|
}
|
|
959
|
-
if (
|
|
1824
|
+
if (i === undefined) return undefined;
|
|
960
1825
|
// Array targets carry a real non-configurable `length` the proxy
|
|
961
1826
|
// invariant forces us to report faithfully; everything else reports
|
|
962
1827
|
// configurable via target indirection (core R51).
|
|
963
|
-
if (!(t === "length" && Array.isArray(e)))
|
|
964
|
-
return
|
|
1828
|
+
if (!(t === "length" && Array.isArray(e))) i.configurable = true;
|
|
1829
|
+
return i;
|
|
965
1830
|
},
|
|
966
1831
|
set(e, t, n) {
|
|
967
1832
|
// Writes require the target's draft scope OR the projection write
|
|
@@ -972,27 +1837,61 @@ const traps = {
|
|
|
972
1837
|
if (!r && !i) return true;
|
|
973
1838
|
if (t === "__proto__") return true;
|
|
974
1839
|
// pollution guard (core R30)
|
|
975
|
-
|
|
1840
|
+
// Unwrap BEFORE ensurePB: unwrapValue materializes a self-referencing
|
|
1841
|
+
// draft's overlay (replacing target.pb), so a pb local captured earlier
|
|
1842
|
+
// would be the abandoned overlay and the write would vanish.
|
|
1843
|
+
// Shallow slots store what was written VERBATIM — another store's proxy
|
|
1844
|
+
// passes through by reference (#2932; markRawOne skips proxies), while
|
|
1845
|
+
// deep stores unwrap to raw backings.
|
|
1846
|
+
const l = e.s ? n : unwrapValue(n);
|
|
1847
|
+
const o = ensurePB(e);
|
|
976
1848
|
pendingNotify.add(e);
|
|
1849
|
+
// Array length writes implicitly delete indices — the written-keys bound
|
|
1850
|
+
// can't see them, so poison to the full scan for this batch. Index
|
|
1851
|
+
// writes implicitly GROW length, so arrays always record it alongside.
|
|
1852
|
+
if (Array.isArray(o)) {
|
|
1853
|
+
if (t === "length") e.wk = WK_ALL; else if (e.wk !== WK_ALL) {
|
|
1854
|
+
const n = e.wk ??= new Set;
|
|
1855
|
+
n.add(t);
|
|
1856
|
+
n.add("length");
|
|
1857
|
+
}
|
|
1858
|
+
} else {
|
|
1859
|
+
if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
|
|
1860
|
+
// Live own-key estimate for the overlay/clone choice (#3360): `in`
|
|
1861
|
+
// sees through an overlay to the committed keys, so this counts keys
|
|
1862
|
+
// NEW to the container. Deletes are not un-counted (a stale high
|
|
1863
|
+
// count only picks the overlay a little early).
|
|
1864
|
+
if (!(t in o)) e.kc++;
|
|
1865
|
+
}
|
|
977
1866
|
// Own data keys literally named "prototype"/"constructor" land as data —
|
|
978
1867
|
// defineProperty sidesteps a proto-chain setter named the same.
|
|
979
1868
|
if (UNSAFE_KEYS.has(t)) {
|
|
980
1869
|
Object.defineProperty(o, t, {
|
|
981
|
-
value:
|
|
1870
|
+
value: l,
|
|
982
1871
|
writable: true,
|
|
983
1872
|
enumerable: true,
|
|
984
1873
|
configurable: true
|
|
985
1874
|
});
|
|
1875
|
+
if (e.del !== null) e.del.delete(t);
|
|
986
1876
|
return true;
|
|
987
1877
|
}
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
1878
|
+
// Overlay first-write DEFINES the own key: assignment through the proto
|
|
1879
|
+
// chain would reject on a non-writable committed property (the clone
|
|
1880
|
+
// path normalized descriptors for exactly this — R51 parity). A
|
|
1881
|
+
// plain-data-graded backing (sc 2, owned: every slot writable) takes the
|
|
1882
|
+
// bare assignment — it lands as an own key on the overlay all the same.
|
|
1883
|
+
if (e.ovl && e.sc !== 2 && !hasOwn.call(o, t)) {
|
|
1884
|
+
Object.defineProperty(o, t, {
|
|
1885
|
+
value: l,
|
|
1886
|
+
writable: true,
|
|
1887
|
+
enumerable: true,
|
|
1888
|
+
configurable: true
|
|
1889
|
+
});
|
|
1890
|
+
} else o[t] = l;
|
|
1891
|
+
if (e.del !== null) e.del.delete(t);
|
|
993
1892
|
// Shallow ingest: written records are sticky raw-marked (one entity is
|
|
994
1893
|
// never both deep-wrapped and raw — R41/#2932, shared invariant).
|
|
995
|
-
if (e.s &&
|
|
1894
|
+
if (e.s && l !== null && typeof l === "object") markRawOne(l);
|
|
996
1895
|
// Override-mode (post-await draft) writes have no setter exit — notify
|
|
997
1896
|
// per-op (setSignal equality-gates repeats).
|
|
998
1897
|
if (i) notifyWrites(e);
|
|
@@ -1004,13 +1903,20 @@ const traps = {
|
|
|
1004
1903
|
if (!r && !i) return true;
|
|
1005
1904
|
if (t === "__proto__") return true;
|
|
1006
1905
|
if (n.get || n.set) e.a = true;
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
if ("value" in n) n = {
|
|
1906
|
+
// Unwrap before ensurePB (see the set trap: self-reference materializes).
|
|
1907
|
+
if ("value" in n) n = {
|
|
1010
1908
|
...n,
|
|
1011
1909
|
value: unwrapValue(n.value)
|
|
1012
1910
|
};
|
|
1013
|
-
|
|
1911
|
+
const l = ensurePB(e);
|
|
1912
|
+
// A non-default data descriptor (or an accessor) leaves the plain-data
|
|
1913
|
+
// grade: the key reaches the committed backing as defined, so the spread
|
|
1914
|
+
// clone and bare-assignment paths no longer describe it.
|
|
1915
|
+
if (e.a || !(n.enumerable && n.writable && n.configurable)) e.sc = 1;
|
|
1916
|
+
pendingNotify.add(e);
|
|
1917
|
+
if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
|
|
1918
|
+
Object.defineProperty(l, t, n);
|
|
1919
|
+
if (e.del !== null) e.del.delete(t);
|
|
1014
1920
|
if (i) notifyWrites(e);
|
|
1015
1921
|
return true;
|
|
1016
1922
|
},
|
|
@@ -1020,7 +1926,11 @@ const traps = {
|
|
|
1020
1926
|
if (!n && !r) return true;
|
|
1021
1927
|
const i = ensurePB(e);
|
|
1022
1928
|
pendingNotify.add(e);
|
|
1929
|
+
if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
|
|
1023
1930
|
delete i[t];
|
|
1931
|
+
// A prototype overlay cannot shadow a delete of a committed key —
|
|
1932
|
+
// record it aside (#3044); reads/has/ownKeys/commit consult the set.
|
|
1933
|
+
if (e.ovl && hasOwn.call(e.v, t)) (e.del ??= new Set).add(t);
|
|
1024
1934
|
if (r) notifyWrites(e);
|
|
1025
1935
|
return true;
|
|
1026
1936
|
}
|
|
@@ -1028,20 +1938,46 @@ const traps = {
|
|
|
1028
1938
|
|
|
1029
1939
|
/** Low-level setter primitive: opens write mode on a next proxy, runs `fn`,
|
|
1030
1940
|
* emits write-time notifications at outermost exit, applies returned
|
|
1031
|
-
* replacements as adoptions. `guard=false` skips the owned-scope
|
|
1032
|
-
* projection recomputes legitimately write from
|
|
1941
|
+
* replacements as adoptions. `guard=false` skips the dev guards (owned-scope
|
|
1942
|
+
* write, thenable result) — projection recomputes legitimately write from
|
|
1943
|
+
* inside their computed, and their async derive is handled by the recompute,
|
|
1944
|
+
* not returned through here. */
|
|
1945
|
+
/** The derived store (`createStore(fn)`) whose user setter is running, and
|
|
1946
|
+
* whether one of its leaf notifications hit a staging another transaction
|
|
1947
|
+
* holds as the fold's result (A34 amendment, #3612; core heldDerivation). */ let derivedSetter = null;
|
|
1948
|
+
|
|
1949
|
+
let heldDerivationHit = false;
|
|
1950
|
+
|
|
1951
|
+
/** The derived store's setter (CS-R31): within a synchronous frame the manual
|
|
1952
|
+
* write wins — the projection's recompute is masked for the tick. Across a
|
|
1953
|
+
* hold the write is not a proposal: a leaf the fold staged under another
|
|
1954
|
+
* transaction re-runs the fold under it, the write being the draft's prior
|
|
1955
|
+
* state (#3612). Decided from the leaf notifications, so the mask lands
|
|
1956
|
+
* after them (in the `finally`: a throwing setter's writes before the throw
|
|
1957
|
+
* were notified, and are masked as before). */ function derivedStoreWrite(e, t, n) {
|
|
1958
|
+
derivedSetter = e;
|
|
1959
|
+
heldDerivationHit = false;
|
|
1960
|
+
try {
|
|
1961
|
+
storeSetterNext(t, n);
|
|
1962
|
+
} finally {
|
|
1963
|
+
derivedSetter = null;
|
|
1964
|
+
heldDerivationHit ? rederiveHeld(e) : suppressComputedRecompute(e);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function storeSetterNext(e, t, n = true) {
|
|
1033
1969
|
const r = e[$TARGET];
|
|
1034
1970
|
const i = writeScopes;
|
|
1035
1971
|
writeScopes = new Set;
|
|
1036
1972
|
writeScopes.add(scopeKey(r));
|
|
1037
1973
|
writing++;
|
|
1038
|
-
let
|
|
1974
|
+
let l;
|
|
1039
1975
|
try {
|
|
1040
1976
|
// No untrack: the writing flag already disables store-node linking
|
|
1041
1977
|
// (draft reads never self-track, proj R2), while EXTERNAL reads (signals
|
|
1042
1978
|
// inside a projection derive) must keep tracking — they are the derive's
|
|
1043
1979
|
// dependencies.
|
|
1044
|
-
|
|
1980
|
+
l = t(e);
|
|
1045
1981
|
} finally {
|
|
1046
1982
|
writing--;
|
|
1047
1983
|
writeScopes = i;
|
|
@@ -1053,17 +1989,18 @@ const traps = {
|
|
|
1053
1989
|
for (const t of e) notifyWrites(t);
|
|
1054
1990
|
}
|
|
1055
1991
|
}
|
|
1056
|
-
if (
|
|
1992
|
+
if (l !== undefined && l !== e && isWrappable(l)) {
|
|
1057
1993
|
// Returned replacement: on an optimistic family (outside authoritative
|
|
1058
1994
|
// writes) the replacement is itself an optimistic edit — diff it against
|
|
1059
1995
|
// the visible view as engine writes (reverts at settle). Otherwise it is
|
|
1060
1996
|
// an adoption of the incoming object (unowned).
|
|
1061
1997
|
if (r.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
|
|
1062
|
-
optHooks.notifyOptimisticWrites(r, unwrapValue(
|
|
1998
|
+
optHooks.notifyOptimisticWrites(r, unwrapValue(l));
|
|
1063
1999
|
} else {
|
|
1064
|
-
adoptPB(r, unwrapValue(
|
|
2000
|
+
adoptPB(r, unwrapValue(l));
|
|
1065
2001
|
}
|
|
1066
2002
|
}
|
|
2003
|
+
if (writing === 0 && heldAdoptions.size) stageHeldAdoptions();
|
|
1067
2004
|
}
|
|
1068
2005
|
|
|
1069
2006
|
// Affects integration: the legacy affects machinery reads next targets
|
|
@@ -1080,8 +2017,131 @@ function createStoreNext(e, t = false) {
|
|
|
1080
2017
|
return [ n, setter ];
|
|
1081
2018
|
}
|
|
1082
2019
|
|
|
2020
|
+
// ---------------------------------------------------------------------------
|
|
2021
|
+
// snapshot (next targets): the backing IS the plain raw graph — zero copy.
|
|
2022
|
+
// Sees pending (R27) by reading pb. Chained/owned-copy caching lands with the
|
|
2023
|
+
// utilities increment; this covers the createStore-suite contract.
|
|
2024
|
+
/** True when `proxy` is a SHALLOW store (children served verbatim, slots
|
|
2025
|
+
* replaced by reference — #2932). The list driver uses this to choose the
|
|
2026
|
+
* slot-patch channel (collected row bodies) over per-record registration. */ function storeIsShallow(e) {
|
|
2027
|
+
const t = e?.[$TARGET];
|
|
2028
|
+
return t !== undefined && t.s === true;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/** True when `proxy` belongs to a projection/optimistic FAMILY. The list
|
|
2032
|
+
* driver must DECLINE family arrays (external audit finding): family
|
|
2033
|
+
* structural changes never emit row/slot ops (the setter channel is
|
|
2034
|
+
* fam-gated; optimistic writes ride node overrides), and the proxy identity
|
|
2035
|
+
* is stable so the each-watch cannot catch the change either — an engaged
|
|
2036
|
+
* list would freeze on optimistic/projection structural updates. Record-
|
|
2037
|
+
* level family patches are unaffected (they have their own emission). */ function storeHasFamily(e) {
|
|
2038
|
+
const t = e?.[$TARGET];
|
|
2039
|
+
return t !== undefined && t.fam !== null;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/** True when `proxy` belongs to an OPTIMISTIC family specifically. The list
|
|
2043
|
+
* driver declines these (audit finding, narrowed): optimistic user writes
|
|
2044
|
+
* ride node-level overrides — they never enter the reconcile walk, so no
|
|
2045
|
+
* row/slot ops are emitted and an engaged list would freeze on optimistic
|
|
2046
|
+
* structural changes. PROJECTION (non-optimistic) families are drivable:
|
|
2047
|
+
* their recomputes go through the reconcile walk, whose emissions are
|
|
2048
|
+
* transition-stamped in the apply queue like any other (equivalence-matrix
|
|
2049
|
+
* gated). Re-admitting optimistic families requires a lane-timed structural
|
|
2050
|
+
* emission mirroring emitPatchOptimistic, plus revert resync. */ function storeHasOptimisticFamily(e) {
|
|
2051
|
+
const t = e?.[$TARGET];
|
|
2052
|
+
return t !== undefined && t.fam?.opt === true;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// ---------------------------------------------------------------------------
|
|
2056
|
+
// visibility: what a reader sees on a record — ONE rule for the ownKeys /
|
|
2057
|
+
// getOwnPropertyDescriptor traps and the deep() walk (#3323). The walk used
|
|
2058
|
+
// to re-derive the trap rules over raw backings and missed each new one in
|
|
2059
|
+
// turn (the #3044 overlay merge, then #3323's chain, then optimistic presence
|
|
2060
|
+
// and value overrides); sharing the body makes parity structural.
|
|
2061
|
+
/** Keys visible on `target` served from `src` (= readSource(target)): the
|
|
2062
|
+
* pending-overlay merge (#3044) and, on optimistic families, presence-node
|
|
2063
|
+
* overrides (§6, FINDING-2's fix). Draft reads before the first write overlay
|
|
2064
|
+
* too (pb, once created, is seeded with the view). Authoritative-view reads
|
|
2065
|
+
* (until()'s predicate, truth-author drafts) skip the overlay. */ function visibleKeys(e, t) {
|
|
2066
|
+
let n;
|
|
2067
|
+
if (e.ovl && t === e.pb) {
|
|
2068
|
+
// Overlay merge (#3044): committed keys in their order, then this
|
|
2069
|
+
// batch's NEW keys, minus deletes.
|
|
2070
|
+
n = Reflect.ownKeys(e.v);
|
|
2071
|
+
const r = e.del;
|
|
2072
|
+
if (r !== null && r.size !== 0) n = n.filter(e => !r.has(e));
|
|
2073
|
+
for (const r of Reflect.ownKeys(t)) {
|
|
2074
|
+
if (!hasOwn.call(e.v, r)) n.push(r);
|
|
2075
|
+
}
|
|
2076
|
+
} else n = Reflect.ownKeys(t);
|
|
2077
|
+
// Drop the $OWNER stamp (owned backings carry it as an own enumerable
|
|
2078
|
+
// symbol). Symbols enumerate last, so the scan stops at the first string.
|
|
2079
|
+
for (let e = n.length - 1; e >= 0 && typeof n[e] === "symbol"; e--) {
|
|
2080
|
+
if (n[e] === $OWNER) {
|
|
2081
|
+
n.splice(e, 1);
|
|
2082
|
+
break;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
if (!authoritativeServe() && e.fam?.opt && e.h !== null && (!inDraft(e) || draftSeesOverrides(e))) {
|
|
2086
|
+
let t = null;
|
|
2087
|
+
const r = inDraft(e);
|
|
2088
|
+
for (const i of Reflect.ownKeys(e.h)) {
|
|
2089
|
+
const l = e.h[i];
|
|
2090
|
+
if (!(r ? hasActiveOverride(l) : visibleOverride(l))) continue;
|
|
2091
|
+
t ??= new Set(n);
|
|
2092
|
+
// Reader arm: a superseded presence override answers as `in` does
|
|
2093
|
+
// (readerOverride, #3331) — Object.keys listed one row fewer than the
|
|
2094
|
+
// traps served after a landing refilled an optimistically deleted slot.
|
|
2095
|
+
const o = r ? unwrapOverride(l.o?.Fe) : readerOverride(l, t.has(i));
|
|
2096
|
+
if (o) t.add(i); else t.delete(i);
|
|
2097
|
+
}
|
|
2098
|
+
if (t !== null) return [ ...t ];
|
|
2099
|
+
}
|
|
2100
|
+
return n;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
/** The data/accessor descriptor visible for `key` on `target` served from
|
|
2104
|
+
* `src`: overlay (#3044 — unwritten keys live on the committed backing,
|
|
2105
|
+
* deleted keys are absent) and optimistic presence overrides (an opt delete
|
|
2106
|
+
* hides the key; an opt add synthesizes a data descriptor from the value
|
|
2107
|
+
* node). `configurable` is the trap's concern (proxy invariant). */ function visibleDescriptor(e, t, n) {
|
|
2108
|
+
let r = Object.getOwnPropertyDescriptor(t, n);
|
|
2109
|
+
if (e.ovl && t === e.pb) {
|
|
2110
|
+
if (e.del !== null && e.del.has(n)) return undefined;
|
|
2111
|
+
if (r === undefined) r = Object.getOwnPropertyDescriptor(e.v, n);
|
|
2112
|
+
}
|
|
2113
|
+
// Draft arm, the twin of visibleKeys' (#3665): a draft still composing on
|
|
2114
|
+
// the live view (no view-seeded backing yet) serves the writer rule
|
|
2115
|
+
// (hasActiveOverride); once ensurePB has seeded the draft's own backing,
|
|
2116
|
+
// `src` carries the view. Without it ownKeys listed a key the descriptor
|
|
2117
|
+
// reported absent, and every enumerator — Object.keys, spread, entries,
|
|
2118
|
+
// JSON.stringify, deep() — dropped the row a previous setter had added.
|
|
2119
|
+
const i = inDraft(e);
|
|
2120
|
+
if (!authoritativeServe() && e.fam?.opt && (!i || draftSeesOverrides(e))) {
|
|
2121
|
+
const t = e.h?.[n];
|
|
2122
|
+
if (t !== undefined && (i ? hasActiveOverride(t) : visibleOverride(t))) {
|
|
2123
|
+
// Reader arm: presence as `in` serves it (readerOverride, #3331).
|
|
2124
|
+
const l = i ? unwrapOverride(t.o?.Fe) : readerOverride(t, r !== undefined);
|
|
2125
|
+
if (!l) return undefined;
|
|
2126
|
+
// opt delete
|
|
2127
|
+
if (r === undefined) {
|
|
2128
|
+
const t = e.n?.[n];
|
|
2129
|
+
return {
|
|
2130
|
+
// In a draft the value is the override itself (nodeValue routes
|
|
2131
|
+
// through serve — the reader rule — and would answer undefined).
|
|
2132
|
+
value: t === undefined ? undefined : i ? hasActiveOverride(t) ? unwrapOverride(t.o?.Fe) : undefined : nodeValue(t, undefined),
|
|
2133
|
+
writable: true,
|
|
2134
|
+
enumerable: true,
|
|
2135
|
+
configurable: true
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return r;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1083
2143
|
/** Tracking deep snapshot (`deep()` for next targets): subscribes to the
|
|
1084
|
-
* key-set and
|
|
2144
|
+
* key-set and deep-witness node at every reachable level, then returns the
|
|
1085
2145
|
* plain view. Shared references and cycles handled via the visited set. */ function deepNext(e) {
|
|
1086
2146
|
const t = e?.[$TARGET];
|
|
1087
2147
|
if (t === undefined || t.px !== e) return e;
|
|
@@ -1091,34 +2151,69 @@ function createStoreNext(e, t = false) {
|
|
|
1091
2151
|
// in per-key nodes, and it walks TARGETS directly — no per-child proxy
|
|
1092
2152
|
// round-trip (wrapNext → proxy → $TARGET trap) on the re-walk every
|
|
1093
2153
|
// effect run performs.
|
|
1094
|
-
|
|
2154
|
+
// Resolve `child` (a raw or a stored proxy found under `t`) to the target
|
|
2155
|
+
// that `t`'s family serves for it — created on first visit, as the get trap
|
|
2156
|
+
// would. Undefined = leaf (unwrappable or raw-marked).
|
|
2157
|
+
const childTarget = (e, t, n) => {
|
|
2158
|
+
let r = lookupTarget(t, e.fam);
|
|
2159
|
+
if (r === undefined) {
|
|
2160
|
+
if (!isWrappable(t)) return undefined;
|
|
2161
|
+
wrapNext(t, e, n);
|
|
2162
|
+
// Stored proxies (chained slots) that this family passes through
|
|
2163
|
+
// resolve to their own target.
|
|
2164
|
+
r = lookupTarget(t, e.fam) ?? t[$TARGET];
|
|
2165
|
+
}
|
|
2166
|
+
return r;
|
|
2167
|
+
};
|
|
2168
|
+
const walkT = e => {
|
|
1095
2169
|
const t = readSource(e);
|
|
1096
2170
|
if (n.has(t)) return;
|
|
1097
2171
|
n.add(t);
|
|
1098
2172
|
read(getKeySetNode(e));
|
|
1099
2173
|
read(getDeepNode(e));
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
2174
|
+
// Chained backing (§7b): the committed backing IS another store's proxy.
|
|
2175
|
+
// Writes to the inner store bump the INNER record's witnesses; this
|
|
2176
|
+
// target's k/dk hear only its own family's writes (optimistic overrides).
|
|
2177
|
+
// Read through the whole chain — the $TRACK trap's rule (#2864 / R21)
|
|
2178
|
+
// applied to deep() (#3323). From `t.v`, not `src`: a pending backing is a
|
|
2179
|
+
// raw clone and would hide the chain. Non-chained targets pay one flag.
|
|
2180
|
+
for (let t = e; t.ch; ) {
|
|
2181
|
+
t = t.v[$TARGET];
|
|
2182
|
+
read(getKeySetNode(t));
|
|
2183
|
+
read(getDeepNode(t));
|
|
2184
|
+
}
|
|
2185
|
+
// Keys and children exactly as the traps serve them — the overlay merge
|
|
2186
|
+
// (#3044/#3283: a bare ownKeys over a pending backing mid-flush dropped
|
|
2187
|
+
// every untouched child from the re-subscribing effect's dependencies)
|
|
2188
|
+
// and optimistic presence overrides (a row added under a held action
|
|
2189
|
+
// lives in `h`/`n`, not the committed backing; the walk never reached its
|
|
2190
|
+
// record, so deep() was deaf to every write on it until settle).
|
|
2191
|
+
const r = e.fam?.opt === true;
|
|
2192
|
+
for (const n of visibleKeys(e, t)) {
|
|
2193
|
+
const i = visibleDescriptor(e, t, n);
|
|
1103
2194
|
if (i === undefined) continue;
|
|
1104
2195
|
if (i.get || i.set) {
|
|
1105
2196
|
e.a = true;
|
|
1106
2197
|
continue;
|
|
1107
2198
|
// accessors track through their own reads when invoked
|
|
1108
2199
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
if (!
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
2200
|
+
let l = i.value;
|
|
2201
|
+
// An active value override on an optimistic node shadows the backing's
|
|
2202
|
+
// child (an optimistic replacement `d[i] = {...}`) — follow what reads
|
|
2203
|
+
// serve, as nodeValue's leading arm does.
|
|
2204
|
+
if (r) {
|
|
2205
|
+
const t = e.n?.[n];
|
|
2206
|
+
if (t !== undefined && hasActiveOverride(t) && !authoritativeServe()) l = unwrapOverride(t.o.Fe);
|
|
2207
|
+
}
|
|
2208
|
+
if (l === null || typeof l !== "object") continue;
|
|
2209
|
+
// Through a chain the descriptor yields the INNERMOST raw: resolve it to
|
|
2210
|
+
// the inner family's proxy so this level wraps the chained `view[i]` the
|
|
2211
|
+
// get trap serves, never a fresh non-chained wrapper of the base raw.
|
|
2212
|
+
if (e.ch && l[$TARGET] === undefined) l = resolveChainedRaw(e, n, l);
|
|
2213
|
+
const o = childTarget(e, l, n);
|
|
2214
|
+
if (o === undefined) continue;
|
|
1119
2215
|
// raw-marked: leaf by contract
|
|
1120
|
-
|
|
1121
|
-
walkT(f);
|
|
2216
|
+
walkT(o);
|
|
1122
2217
|
}
|
|
1123
2218
|
};
|
|
1124
2219
|
walkT(t);
|
|
@@ -1148,60 +2243,89 @@ function snapshotWalk(e, t, n) {
|
|
|
1148
2243
|
// encountered and compose their views over the resolved base, innermost
|
|
1149
2244
|
// outward.
|
|
1150
2245
|
let i = null;
|
|
1151
|
-
for (
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (
|
|
1155
|
-
if (
|
|
1156
|
-
if (
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
2246
|
+
for (let e = true; ;e = false) {
|
|
2247
|
+
const t = r?.[$TARGET]?.v !== undefined;
|
|
2248
|
+
let l = t ? r[$TARGET] : undefined;
|
|
2249
|
+
if (l === undefined && n !== null) l = lookupTarget(r, n);
|
|
2250
|
+
if (l === undefined) l = lookupTarget(r, null);
|
|
2251
|
+
if (l === undefined) break;
|
|
2252
|
+
// Entering a level from a RAW below a chained family: the raw resolves to
|
|
2253
|
+
// the INNER store's target, but this family's wrapper for it — keyed by
|
|
2254
|
+
// the inner proxy, §7b — is where the outer overrides live. Start from
|
|
2255
|
+
// the wrapper so they compose (#3323: a nested optimistic write on a view
|
|
2256
|
+
// row was served by reads but missing from deep()/snapshot()). Entry only:
|
|
2257
|
+
// the descent then runs wrapper → inner → raw and terminates.
|
|
2258
|
+
if (e && !t && n !== null && l.fam !== n) {
|
|
2259
|
+
const e = n.map.get(l.px);
|
|
2260
|
+
if (e !== undefined) l = e;
|
|
2261
|
+
}
|
|
2262
|
+
if (l.fam !== null) n = l.fam;
|
|
2263
|
+
if (l.fam?.opt === true) (i ??= []).push(l);
|
|
2264
|
+
// The shared visibility decision (#3147): the speculative peek serves
|
|
2265
|
+
// pending staging, but a HELD landing is masked to committed exactly as
|
|
2266
|
+
// it is for per-key readers — the two families must answer alike while
|
|
2267
|
+
// a transaction holds store landings.
|
|
2268
|
+
const o = pendingBackingVisible(l, true);
|
|
2269
|
+
// Snapshot runs mid-flush (tracked memos execute before commit), so a
|
|
2270
|
+
// pending prototype overlay must present as a REAL merged container.
|
|
2271
|
+
if (o && l.ovl) materializePB(l);
|
|
2272
|
+
const f = o ? l.pb : l.v;
|
|
2273
|
+
if (f === r) break;
|
|
2274
|
+
r = f;
|
|
1161
2275
|
}
|
|
1162
2276
|
if (!isWrappable(r)) return r;
|
|
1163
2277
|
// Optimistic families: compose the visible view; a composed view is a fresh
|
|
1164
2278
|
// object and snapshots via the owned/copy path (pinned `not.toBe` identity).
|
|
1165
2279
|
if (i !== null) {
|
|
1166
2280
|
let e = r;
|
|
1167
|
-
for (let t = i.length - 1; t >= 0; t--)
|
|
2281
|
+
for (let t = i.length - 1; t >= 0; t--) {
|
|
2282
|
+
const n = i[t];
|
|
2283
|
+
// Draft twin (#3665): deep()/snapshot() inside a setter is the writer's
|
|
2284
|
+
// channel and composes on the tick's own unflushed adds
|
|
2285
|
+
// (hasActiveOverride). A draft that has seeded its own backing from
|
|
2286
|
+
// the view already carries them in `src` — composing again would
|
|
2287
|
+
// clobber the draft's later writes with the overrides they superseded.
|
|
2288
|
+
const r = inDraft(n);
|
|
2289
|
+
if (r && !draftSeesOverrides(n)) continue;
|
|
2290
|
+
e = optHooks.optimisticView(n, e, r);
|
|
2291
|
+
}
|
|
1168
2292
|
if (e !== r) {
|
|
1169
2293
|
const i = t.get(r);
|
|
1170
2294
|
if (i !== undefined) return i;
|
|
1171
|
-
const
|
|
1172
|
-
const
|
|
1173
|
-
t.set(r,
|
|
2295
|
+
const l = Array.isArray(e);
|
|
2296
|
+
const o = l ? [] : Object.create(Object.getPrototypeOf(e));
|
|
2297
|
+
t.set(r, o);
|
|
1174
2298
|
for (const r of Reflect.ownKeys(e)) {
|
|
1175
|
-
if (
|
|
2299
|
+
if (l && r === "length" || r === $OWNER) continue;
|
|
1176
2300
|
const i = e[r];
|
|
1177
|
-
|
|
2301
|
+
o[r] = i !== null && typeof i === "object" ? snapshotWalk(i, t, n) : i;
|
|
1178
2302
|
}
|
|
1179
|
-
if (
|
|
1180
|
-
return
|
|
2303
|
+
if (l) o.length = e.length;
|
|
2304
|
+
return o;
|
|
1181
2305
|
}
|
|
1182
2306
|
}
|
|
1183
|
-
const
|
|
1184
|
-
if (
|
|
2307
|
+
const l = t.get(r);
|
|
2308
|
+
if (l !== undefined) return l;
|
|
1185
2309
|
// OWNED (written) subtrees snapshot as copies (§7b: identity is only for
|
|
1186
2310
|
// subtrees "unmodified relative to source"): non-enumerable symbols are
|
|
1187
2311
|
// excluded (recon-snap R29), and the copy registers BEFORE descent so
|
|
1188
2312
|
// cycles keep identity (FINDING-3).
|
|
1189
|
-
if (
|
|
2313
|
+
if (isOwned(r)) {
|
|
1190
2314
|
const e = Array.isArray(r);
|
|
1191
2315
|
const i = e ? [] : Object.create(Object.getPrototypeOf(r));
|
|
1192
2316
|
t.set(r, i);
|
|
1193
|
-
for (const
|
|
1194
|
-
if (e &&
|
|
1195
|
-
const
|
|
1196
|
-
if (typeof
|
|
1197
|
-
if (
|
|
1198
|
-
Object.defineProperty(i,
|
|
2317
|
+
for (const l of Reflect.ownKeys(r)) {
|
|
2318
|
+
if (e && l === "length" || l === $OWNER) continue;
|
|
2319
|
+
const o = Object.getOwnPropertyDescriptor(r, l);
|
|
2320
|
+
if (typeof l === "symbol" && !o.enumerable) continue;
|
|
2321
|
+
if (o.get || o.set) {
|
|
2322
|
+
Object.defineProperty(i, l, o);
|
|
1199
2323
|
continue;
|
|
1200
2324
|
}
|
|
1201
|
-
const
|
|
1202
|
-
const s =
|
|
1203
|
-
if (
|
|
1204
|
-
...
|
|
2325
|
+
const f = o.value;
|
|
2326
|
+
const s = f !== null && typeof f === "object" ? snapshotWalk(f, t, n) : f;
|
|
2327
|
+
if (o.enumerable && o.writable && o.configurable) i[l] = s; else Object.defineProperty(i, l, {
|
|
2328
|
+
...o,
|
|
1205
2329
|
value: s
|
|
1206
2330
|
});
|
|
1207
2331
|
}
|
|
@@ -1211,22 +2335,22 @@ function snapshotWalk(e, t, n) {
|
|
|
1211
2335
|
// UNOWNED (shared/user) subtrees keep identity unless a descendant
|
|
1212
2336
|
// substituted; copy-on-substitution preserves the documented CoW contract.
|
|
1213
2337
|
t.set(r, r);
|
|
1214
|
-
let
|
|
2338
|
+
let o = null;
|
|
1215
2339
|
for (const e of Reflect.ownKeys(r)) {
|
|
1216
2340
|
const i = Object.getOwnPropertyDescriptor(r, e);
|
|
1217
2341
|
if (!i || i.get || i.set) continue;
|
|
1218
|
-
const
|
|
1219
|
-
if (
|
|
1220
|
-
const
|
|
1221
|
-
if (
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
1224
|
-
t.set(r,
|
|
2342
|
+
const l = i.value;
|
|
2343
|
+
if (l === null || typeof l !== "object") continue;
|
|
2344
|
+
const f = snapshotWalk(l, t, n);
|
|
2345
|
+
if (f !== l) {
|
|
2346
|
+
if (o === null) {
|
|
2347
|
+
o = Array.isArray(r) ? [ ...r ] : Object.create(Object.getPrototypeOf(r), Object.getOwnPropertyDescriptors(r));
|
|
2348
|
+
t.set(r, o);
|
|
1225
2349
|
}
|
|
1226
|
-
|
|
2350
|
+
o[e] = f;
|
|
1227
2351
|
}
|
|
1228
2352
|
}
|
|
1229
|
-
return
|
|
2353
|
+
return o ?? r;
|
|
1230
2354
|
}
|
|
1231
2355
|
|
|
1232
|
-
export { adoptPB, bumpDeep, createStoreNext, deepNext, getHasNode, getKeySetNode, getNode, hasAccessorFlag, hasActiveOverride, notifyFold, notifyFoldTail, notifyKeyDiff, notifyKeyValue, runAuthoritative, snapshotNext, storeSetterNext, targetsEqual, unwrapValue, wrapNext };
|
|
2356
|
+
export { adoptPB, arrayStructureChanged, authoritativeRead, authoritativeServe, bumpDeep, createStoreNext, deepNext, derivedStoreWrite, getHasNode, getKeySetNode, getNode, hasAccessorFlag, hasActiveOverride, heldMaskView, materializePB, membershipChanged, notifyFold, notifyFoldTail, notifyKeyDiff, notifyKeyValue, readerOverride, runAuthoritative, snapshotNext, stagedTruthPB, storeHasFamily, storeHasOptimisticFamily, storeIsShallow, storeSetterNext, targetsEqual, unwrapValue, visibleOverride, wrapNext };
|