@solidjs/signals 2.0.0-rc.14 → 2.0.0-rc.16
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 +882 -405
- package/dist/dev.attribution.js +172 -66
- package/dist/dev.js +1529 -745
- package/dist/observe/affects-marks.js +86 -0
- package/dist/observe/affects.js +32 -80
- package/dist/observe/boundaries.js +202 -166
- package/dist/observe/core/async.js +192 -141
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +315 -214
- package/dist/observe/core/constants.js +23 -4
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +532 -360
- package/dist/observe/core/dev.js +63 -35
- package/dist/observe/core/effect.js +47 -47
- package/dist/observe/core/error.js +5 -3
- package/dist/observe/core/external.js +5 -6
- package/dist/observe/core/graph.js +56 -42
- package/dist/observe/core/heap.js +33 -33
- package/dist/observe/core/identities.js +62 -0
- package/dist/observe/core/lanes.js +242 -229
- package/dist/observe/core/owner.js +62 -50
- package/dist/observe/core/scheduler.js +322 -168
- package/dist/observe/core/truth.js +45 -0
- package/dist/observe/core/verdict.js +114 -64
- package/dist/observe/deferred.js +184 -0
- package/dist/observe/index.js +7 -1
- package/dist/observe/map.js +339 -254
- package/dist/observe/reveal.js +77 -77
- package/dist/observe/signals.js +54 -33
- package/dist/observe/store/affects.js +76 -69
- package/dist/observe/store/edges.js +39 -0
- package/dist/observe/store/hooks.js +13 -0
- package/dist/observe/store/index.js +0 -2
- package/dist/observe/store/optimistic.js +82 -44
- package/dist/observe/store/projection.js +83 -64
- package/dist/observe/store/reconcile.js +32 -30
- package/dist/observe/store/store.js +219 -123
- package/dist/prod/affects-marks.js +86 -0
- package/dist/prod/affects.js +33 -79
- package/dist/prod/boundaries.js +185 -149
- package/dist/prod/core/async.js +176 -135
- package/dist/prod/core/constants.js +23 -4
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +521 -353
- package/dist/prod/core/effect.js +66 -66
- package/dist/prod/core/error.js +5 -3
- package/dist/prod/core/external.js +5 -6
- package/dist/prod/core/graph.js +56 -42
- package/dist/prod/core/heap.js +38 -38
- package/dist/prod/core/identities.js +62 -0
- package/dist/prod/core/lanes.js +237 -224
- package/dist/prod/core/owner.js +62 -51
- package/dist/prod/core/scheduler.js +312 -158
- package/dist/prod/core/truth.js +45 -0
- package/dist/prod/core/verdict.js +113 -63
- package/dist/prod/deferred.js +182 -0
- package/dist/prod/index.js +7 -1
- package/dist/prod/map.js +366 -281
- package/dist/prod/reveal.js +77 -77
- package/dist/prod/signals.js +59 -38
- package/dist/prod/store/affects.js +76 -69
- package/dist/prod/store/edges.js +39 -0
- package/dist/prod/store/hooks.js +13 -0
- package/dist/prod/store/index.js +0 -2
- package/dist/prod/store/optimistic.js +121 -84
- package/dist/prod/store/projection.js +46 -34
- package/dist/prod/store/reconcile.js +32 -30
- package/dist/prod/store/store.js +205 -116
- package/dist/types/affects-marks.d.ts +14 -0
- package/dist/types/boundaries.d.ts +5 -1
- package/dist/types/core/async.d.ts +10 -0
- package/dist/types/core/attribution.d.ts +6 -2
- package/dist/types/core/constants.d.ts +18 -1
- package/dist/types/core/core.d.ts +77 -5
- package/dist/types/core/dev.d.ts +43 -13
- package/dist/types/core/graph.d.ts +11 -0
- package/dist/types/core/identities.d.ts +17 -0
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/lanes.d.ts +2 -4
- package/dist/types/core/owner.d.ts +5 -3
- package/dist/types/core/scheduler.d.ts +59 -34
- package/dist/types/core/truth.d.ts +16 -0
- package/dist/types/core/types.d.ts +21 -6
- package/dist/types/core/verdict.d.ts +32 -0
- package/dist/types/deferred.d.ts +28 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/map.d.ts +2 -2
- package/dist/types/signals.d.ts +28 -7
- package/dist/types/store/affects.d.ts +11 -0
- package/dist/types/store/edges.d.ts +9 -0
- package/dist/types/store/hooks.d.ts +48 -0
- package/dist/types/store/index.d.ts +10 -4
- package/dist/types/store/optimistic.d.ts +3 -2
- package/dist/types/store/projection.d.ts +4 -3
- package/dist/types/store/store.d.ts +4 -39
- package/dist/types/store/target.d.ts +16 -8
- package/dist/types/store/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -1,163 +1,228 @@
|
|
|
1
1
|
import {
|
|
2
2
|
C as CONFIG_GUESS,
|
|
3
|
-
s as schedule,
|
|
4
3
|
N as NOT_PENDING,
|
|
5
|
-
a as
|
|
4
|
+
a as NotReadyError,
|
|
5
|
+
b as CONFIG_AUTHORITATIVE,
|
|
6
|
+
e as enqueueSub,
|
|
7
|
+
c as CONFIG_OVERRIDE,
|
|
8
|
+
s as schedule,
|
|
6
9
|
q as question,
|
|
7
10
|
n as nextQuestion,
|
|
11
|
+
i as installLaneHooks,
|
|
8
12
|
G as GlobalQueue,
|
|
9
|
-
R as REACTIVE_DISPOSED,
|
|
10
|
-
b as CONFIG_VERDICT,
|
|
11
|
-
c as REACTIVE_PROBE_UNANSWERED,
|
|
12
|
-
d as blocked,
|
|
13
|
-
e as enqueueSub,
|
|
14
13
|
r as resolveTx,
|
|
15
|
-
|
|
14
|
+
d as newTransaction,
|
|
15
|
+
o as ownFlights,
|
|
16
|
+
R as REACTIVE_DISPOSED,
|
|
17
|
+
f as CONFIG_VERDICT,
|
|
18
|
+
g as REACTIVE_PROBE_UNANSWERED,
|
|
19
|
+
h as blocked,
|
|
20
|
+
j as REACTIVE_LANE_DIRTY,
|
|
21
|
+
k as CONFIG_HELD,
|
|
16
22
|
t as txOf,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
p as
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
23
|
+
l as clock,
|
|
24
|
+
m as insertSubs,
|
|
25
|
+
p as laneDirty,
|
|
26
|
+
u as REACTIVE_LANE_READ,
|
|
27
|
+
v as ext,
|
|
28
|
+
S as STATUS_UNINITIALIZED,
|
|
29
|
+
w as REACTIVE_RECOMPUTING_DEPS,
|
|
30
|
+
x as queuePendingNode,
|
|
31
|
+
y as list,
|
|
32
|
+
z as STATUS_ERROR,
|
|
33
|
+
A as tracking,
|
|
34
|
+
B as CONFIG_CHILDREN_FORBIDDEN,
|
|
35
|
+
D as passLane,
|
|
36
|
+
E as STATUS_PENDING,
|
|
37
|
+
F as EFFECT_RENDER,
|
|
38
|
+
H as sameLane,
|
|
39
|
+
I as flushTransaction,
|
|
40
|
+
J as REACTIVE_SCREEN_READ,
|
|
41
|
+
K as REACTIVE_FRAME_READ,
|
|
42
|
+
L as attrHooks,
|
|
43
|
+
M as commitPendingNode,
|
|
44
|
+
O as REACTIVE_IN_HEAP,
|
|
45
|
+
P as REACTIVE_DIRTY,
|
|
46
|
+
Q as REACTIVE_CHECK,
|
|
47
|
+
T as CONFIG_INPUTS_PUBLISHED,
|
|
48
|
+
U as releaseQueues,
|
|
49
|
+
V as CONFIG_SLOT_NODE,
|
|
50
|
+
W as holdNode,
|
|
51
|
+
X as joinFuture,
|
|
52
|
+
Y as reruns,
|
|
53
|
+
Z as CONFIG_STAGED,
|
|
54
|
+
_ as merge$1,
|
|
55
|
+
$ as setPassLane,
|
|
56
|
+
a0 as REACTIVE_JOINED,
|
|
57
|
+
a1 as REACTIVE_STAGED_READ,
|
|
58
|
+
a2 as stagedRead,
|
|
59
|
+
a3 as setVerdict,
|
|
60
|
+
a4 as context,
|
|
61
|
+
a5 as installVerdictHooks,
|
|
62
|
+
a6 as REACTIVE_PROBED,
|
|
63
|
+
a7 as verdict,
|
|
64
|
+
a8 as link,
|
|
65
|
+
a9 as pullComputed,
|
|
66
|
+
aa as strictRead,
|
|
67
|
+
ab as warnStrictReadUntracked,
|
|
68
|
+
ac as staleReader,
|
|
69
|
+
ad as globalQueue,
|
|
70
|
+
ae as joinPass,
|
|
71
|
+
af as markLateLinker,
|
|
72
|
+
ag as spectating,
|
|
73
|
+
ah as unflushedValue,
|
|
74
|
+
ai as CONFIG_STAGED_ERROR,
|
|
75
|
+
aj as stagedReaders,
|
|
76
|
+
ak as bornInto,
|
|
77
|
+
al as enterAction,
|
|
78
|
+
am as setEffectCallback,
|
|
79
|
+
an as exitAction,
|
|
80
|
+
ao as isThenable,
|
|
81
|
+
ap as actionDepth,
|
|
82
|
+
aq as flush,
|
|
83
|
+
ar as setQuestion,
|
|
84
|
+
as as emitDiagnostic,
|
|
85
|
+
at as enterCallback,
|
|
86
|
+
au as exitCallback,
|
|
87
|
+
av as getOwner,
|
|
88
|
+
aw as inEffectCallback,
|
|
89
|
+
ax as setExternalHooks,
|
|
90
|
+
ay as signal,
|
|
91
|
+
az as setSignal,
|
|
92
|
+
aA as cleanup,
|
|
93
|
+
aB as read,
|
|
94
|
+
aC as DEV$1,
|
|
95
|
+
aD as CONFIG_TRANSPARENT,
|
|
96
|
+
aE as NoOwnerError,
|
|
97
|
+
aF as ContextNotFoundError,
|
|
98
|
+
aG as createEffectNode,
|
|
99
|
+
aH as recompute,
|
|
100
|
+
aI as EFFECT_USER,
|
|
101
|
+
aJ as reportDiagnostic,
|
|
102
|
+
aK as computed,
|
|
103
|
+
aL as staleValues,
|
|
104
|
+
aM as CONFIG_AUTO_DISPOSE,
|
|
105
|
+
aN as EFFECT_TRACKED,
|
|
106
|
+
aO as heldRunHook,
|
|
107
|
+
aP as unwrapStatusError,
|
|
108
|
+
aQ as haltReactivity,
|
|
109
|
+
aR as setStrictRead,
|
|
110
|
+
aS as StatusError,
|
|
111
|
+
aT as trimStaleDeps,
|
|
112
|
+
aU as setTrackedQueueCallback,
|
|
113
|
+
aV as setEffectStatusNotify,
|
|
114
|
+
aW as _hitUnhandledAsync,
|
|
115
|
+
aX as resetUnhandledAsync,
|
|
116
|
+
aY as ROOT_ERROR_HOOK,
|
|
117
|
+
aZ as registerGraph,
|
|
118
|
+
a_ as runWithOwner,
|
|
119
|
+
a$ as setMemo,
|
|
120
|
+
b0 as $REFRESH,
|
|
121
|
+
b1 as markRefresh,
|
|
122
|
+
b2 as createRoot,
|
|
123
|
+
b3 as getObserver,
|
|
124
|
+
b4 as dispose,
|
|
125
|
+
b5 as TimeoutError,
|
|
126
|
+
b6 as untrack,
|
|
127
|
+
b7 as dirtyQueue,
|
|
128
|
+
b8 as CONFIG_REDERIVE,
|
|
129
|
+
b9 as passTx,
|
|
130
|
+
ba as CONFIG_FRESH_READ,
|
|
131
|
+
bb as createOwner,
|
|
132
|
+
bc as writeHook,
|
|
133
|
+
bd as REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE,
|
|
134
|
+
be as ownedScopeWriteMessage,
|
|
135
|
+
bf as isEqual,
|
|
136
|
+
bg as CONFIG_MANUAL_WRITE,
|
|
137
|
+
bh as snapshotCaptureActive,
|
|
138
|
+
bi as captureWriteSnapshot,
|
|
139
|
+
bj as notePromotedWrite,
|
|
140
|
+
bk as slotSignal,
|
|
141
|
+
bl as stagedScreen,
|
|
142
|
+
bm as CONFIG_PROMOTED,
|
|
143
|
+
bn as setSlotUnobserved,
|
|
144
|
+
bo as installStoreHooks,
|
|
145
|
+
bp as joinPassTx,
|
|
146
|
+
bq as asyncTailFlights,
|
|
147
|
+
br as untrackDepth,
|
|
148
|
+
bs as checkPostAwaitRead,
|
|
149
|
+
bt as disposalDepth,
|
|
150
|
+
bu as insertIntoHeap,
|
|
151
|
+
bv as isDisposed,
|
|
152
|
+
bw as handleAsync,
|
|
153
|
+
bx as addPendingSource,
|
|
154
|
+
by as propagateStatus,
|
|
155
|
+
bz as forEachDependent,
|
|
156
|
+
bA as settlePendingSource,
|
|
157
|
+
bB as notifyEpoch,
|
|
158
|
+
bC as SUPPORTS_PROXY,
|
|
159
|
+
bD as releaseMarksHook,
|
|
160
|
+
bE as installMarkHooks,
|
|
161
|
+
bF as CONFIG_LAGS,
|
|
162
|
+
bG as REACTIVE_REASK,
|
|
163
|
+
bH as heldUnder,
|
|
164
|
+
bI as unhold,
|
|
165
|
+
bJ as laneStageHook,
|
|
166
|
+
bK as REACTIVE_ZOMBIE,
|
|
167
|
+
bL as installBoundaryHooks,
|
|
168
|
+
bM as CAUGHT_REARMED,
|
|
169
|
+
bN as OBSERVE$1
|
|
148
170
|
} from "./dev-shared.js";
|
|
149
171
|
export {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
bO as clearSnapshots,
|
|
173
|
+
bP as enforceLoadingBoundary,
|
|
174
|
+
bQ as getNextChildId,
|
|
175
|
+
bR as markSnapshotScope,
|
|
176
|
+
bS as peekNextChildId,
|
|
177
|
+
bT as releaseSnapshotScope,
|
|
178
|
+
bU as resetErrorHalt,
|
|
179
|
+
bV as setConsoleFooter,
|
|
180
|
+
bW as setSnapshotCapture,
|
|
181
|
+
bX as setWriteHook
|
|
159
182
|
} from "./dev-shared.js";
|
|
160
183
|
|
|
184
|
+
// Truth reads (`until`, `refresh`; CONFIG_AUTHORITATIVE) over lane nodes.
|
|
185
|
+
// Side-effect free and apart from lanes.ts so a truth reader alone does not
|
|
186
|
+
// retain the lane engine: its reads reach these only through `laneRead`,
|
|
187
|
+
// which exists once something installs lanes (`createOptimistic`, verdicts).
|
|
188
|
+
/** A lane node's current value: its guess, or the latest it derived — the
|
|
189
|
+
* staging a shown lane holds for its next reveal, else its slot; the
|
|
190
|
+
* committed value while it has none (its first lane pass in flight). */
|
|
191
|
+
function laneValueOf(el) {
|
|
192
|
+
if (!(el._config & CONFIG_GUESS) && el._pendingValue !== NOT_PENDING) return el._pendingValue;
|
|
193
|
+
const v = el._x._lane;
|
|
194
|
+
return v !== NOT_PENDING ? v : el._value;
|
|
195
|
+
}
|
|
196
|
+
/** Truth readers (`until`, `refresh`; CONFIG_AUTHORITATIVE) — pay-for-use:
|
|
197
|
+
* the first one installs these on GlobalQueue (signals.ts `watch`).
|
|
198
|
+
*
|
|
199
|
+
* A truth reader's read of a lane node: the lane's value when it is the
|
|
200
|
+
* truth (#3943), else the truth staged beneath a guess, else the committed
|
|
201
|
+
* value. */
|
|
202
|
+
function truthRead(el) {
|
|
203
|
+
if (truth(el)) return laneValueOf(el);
|
|
204
|
+
if (!(el._config & CONFIG_GUESS)) throw new NotReadyError(null);
|
|
205
|
+
return el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
|
|
206
|
+
}
|
|
207
|
+
/** A guess confirmed: its lane's truth readers re-read — of the lane's
|
|
208
|
+
* derivations too, which may be the truth now (#3943). */
|
|
209
|
+
function truthConfirmed(l) {
|
|
210
|
+
for (let i = 0; i < l._nodes.length; i++)
|
|
211
|
+
for (let s = l._nodes[i]._subs; s !== null; s = s._nextSub)
|
|
212
|
+
if (s._sub._config & CONFIG_AUTHORITATIVE) enqueueSub(s._sub);
|
|
213
|
+
}
|
|
214
|
+
/** A lane node's value is the truth when every guess beneath it is
|
|
215
|
+
* confirmed — its source staged a value equal to the guess — and nothing
|
|
216
|
+
* beneath it is held in the parent: same inputs, same value. */
|
|
217
|
+
function truth(n) {
|
|
218
|
+
if (n._config & CONFIG_GUESS)
|
|
219
|
+
return n._pendingValue !== NOT_PENDING && n._equals && n._equals(n._x._lane, n._pendingValue);
|
|
220
|
+
if (n._pendingValue !== NOT_PENDING) return;
|
|
221
|
+
if (n._config & CONFIG_OVERRIDE)
|
|
222
|
+
for (let d = n._deps; d; d = d._nextDep) if (!truth(d._dep)) return;
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
161
226
|
// Lanes (plan sec. 28 replay, 2026-10-02; maintainer rulings 2026-10-01/02). A lane
|
|
162
227
|
// is "a new base of a transition": the sub-frame an optimistic write opens
|
|
163
228
|
// in the transition of the frame it is made in. It sees the screen plus its
|
|
@@ -190,19 +255,13 @@ export {
|
|
|
190
255
|
// carries none of this.
|
|
191
256
|
/** Live lanes (parented). Scanned at every seam while non-empty. */
|
|
192
257
|
const lanes = [];
|
|
258
|
+
/** A pass left a shown slot the equality gate can no longer see (#3892). */
|
|
259
|
+
const LEFT_SHOWN = 1 << 20;
|
|
193
260
|
function newLane(parent) {
|
|
194
261
|
const l = newTransaction(true, parent);
|
|
195
262
|
lanes.push(l);
|
|
196
263
|
return l;
|
|
197
264
|
}
|
|
198
|
-
/** A lane node's current value: its guess, or the latest it derived — the
|
|
199
|
-
* staging a shown lane holds for its next reveal, else its slot; the
|
|
200
|
-
* committed value while it has none (its first lane pass in flight). */
|
|
201
|
-
function laneValueOf(el) {
|
|
202
|
-
if (!(el._config & CONFIG_GUESS) && el._pendingValue !== NOT_PENDING) return el._pendingValue;
|
|
203
|
-
const v = el._x._lane;
|
|
204
|
-
return v !== NOT_PENDING ? v : el._value;
|
|
205
|
-
}
|
|
206
265
|
/** What the screen shows for a lane's node: the lane's value once the lane
|
|
207
266
|
* has revealed, the committed truth before. */
|
|
208
267
|
function display(el) {
|
|
@@ -267,23 +326,28 @@ function applyGuesses(parent) {
|
|
|
267
326
|
const n = pendingGuesses[i];
|
|
268
327
|
const v = pendingGuesses[i + 1];
|
|
269
328
|
let changed = true;
|
|
329
|
+
// The frame already holding the slot: a guess over it is a second
|
|
330
|
+
// suggestion for the slot, and two cannot finish apart (A34 (1)) — the
|
|
331
|
+
// frames entangle.
|
|
332
|
+
let holder = null;
|
|
270
333
|
if (n._config & CONFIG_OVERRIDE) {
|
|
271
334
|
// A lane's node already: a guess re-asked, or a derivation guessed
|
|
272
|
-
// over (its staging is void — the guess is the value now).
|
|
273
|
-
//
|
|
274
|
-
// entangle. The same value re-asked renews the question; nothing to
|
|
275
|
-
// notify.
|
|
335
|
+
// over (its staging is void — the guess is the value now). The same
|
|
336
|
+
// value re-asked renews the question; nothing to notify.
|
|
276
337
|
changed = !n._equals || !n._equals(laneValueOf(n), v);
|
|
277
338
|
if (!(n._config & CONFIG_GUESS)) {
|
|
278
339
|
n._pendingValue = NOT_PENDING;
|
|
279
340
|
n._config |= CONFIG_GUESS;
|
|
280
341
|
}
|
|
281
342
|
n._x._lane = v;
|
|
282
|
-
|
|
283
|
-
if (parent !== null && holder !== null) merge$1(parent, holder);
|
|
343
|
+
holder = txOf(n)._parent;
|
|
284
344
|
} else {
|
|
285
345
|
let l;
|
|
286
|
-
|
|
346
|
+
// A held node: a truth staged there, or what a corrected guess showed.
|
|
347
|
+
// The guess composes on it, so it ends with that hold — and with its
|
|
348
|
+
// own frame's (F-11): it lands only when both have (the board's rapid
|
|
349
|
+
// adds — a later add's row reverted with an earlier add's landing).
|
|
350
|
+
if (n._config & CONFIG_HELD) l = newLane((holder = txOf(n)));
|
|
287
351
|
else {
|
|
288
352
|
if (parent === null && inFlight$1(n)) parent = newTransaction(false);
|
|
289
353
|
if (parent === null) continue;
|
|
@@ -299,6 +363,7 @@ function applyGuesses(parent) {
|
|
|
299
363
|
n._config |= CONFIG_OVERRIDE | CONFIG_GUESS;
|
|
300
364
|
list(n, l);
|
|
301
365
|
}
|
|
366
|
+
if (parent !== null && holder !== null) merge$1(parent, holder);
|
|
302
367
|
ext(n)._q = pendingGuesses[i + 2];
|
|
303
368
|
if (changed) {
|
|
304
369
|
// The lane's members re-derive as its work (REACTIVE_LANE_DIRTY): a
|
|
@@ -326,8 +391,10 @@ function enterLane(l, c) {
|
|
|
326
391
|
// The pass read a held write before its first lane read (it joined the
|
|
327
392
|
// frame's future as mainline would): lane work sees the screen — the
|
|
328
393
|
// seam re-derives it on the committed world next round (`stagedReaders`,
|
|
329
|
-
// the park's repair), as it does a lane pass that read a staging.
|
|
330
|
-
|
|
394
|
+
// the park's repair), as it does a lane pass that read a staging. Not a
|
|
395
|
+
// pass whose held reads were all of nodes born held (no staged read,
|
|
396
|
+
// `stagedRead`): it has no committed world to re-derive on (#3969).
|
|
397
|
+
if (c._flags & REACTIVE_JOINED && c._flags & REACTIVE_STAGED_READ) stagedRead(c);
|
|
331
398
|
return;
|
|
332
399
|
}
|
|
333
400
|
if (sameLane(p, l)) return setPassLane(l);
|
|
@@ -402,17 +469,22 @@ function verdictLane(t) {
|
|
|
402
469
|
* propagation (async.ts `propagateStatus`), the node gone pending. */
|
|
403
470
|
function laneStage(el, l, create, errored) {
|
|
404
471
|
if (!(create || errored || el._flags & REACTIVE_LANE_READ || el._config & CONFIG_GUESS)) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
472
|
+
const x = el._x;
|
|
473
|
+
if (x !== null) {
|
|
474
|
+
const tx = x._transaction;
|
|
475
|
+
// Cleared before equality, so a return to the committed value notifies
|
|
476
|
+
// nobody. The seam wakes it unless the frame is still held (#3892).
|
|
477
|
+
if (tx?._shown && !blocked(resolveTx(tx._parent))) el._flags |= LEFT_SHOWN;
|
|
478
|
+
if (tx?._lane) x._transaction = null;
|
|
479
|
+
x._lane = NOT_PENDING;
|
|
480
|
+
}
|
|
481
|
+
el._config &= -1050881;
|
|
410
482
|
return false;
|
|
411
483
|
}
|
|
412
484
|
if (!el._type) {
|
|
413
485
|
if (!(el._config & CONFIG_OVERRIDE)) {
|
|
414
486
|
el._pendingValue = NOT_PENDING;
|
|
415
|
-
el._config = (el._config &
|
|
487
|
+
el._config = (el._config & -1048833) | CONFIG_OVERRIDE;
|
|
416
488
|
ext(el);
|
|
417
489
|
}
|
|
418
490
|
// A written guess whose own pass is the lane's work — its source is a
|
|
@@ -464,6 +536,11 @@ function supersede(n, value, changed) {
|
|
|
464
536
|
// lane opened (two actions guessing one slot entangle, A34 (1)) — the
|
|
465
537
|
// truth is held by the transaction that lands, not the merged-away one.
|
|
466
538
|
const parent = resolveTx(l._parent ?? l);
|
|
539
|
+
// A store row answered by a new object the store would reconcile into
|
|
540
|
+
// the guessed one unchanged is an echo, not a correction (#3898): the
|
|
541
|
+
// server's object never matches the guess by reference.
|
|
542
|
+
if (changed && n._config & CONFIG_SLOT_NODE && GlobalQueue._echoes?.(n._x._lane, value))
|
|
543
|
+
changed = false;
|
|
467
544
|
if (changed) {
|
|
468
545
|
// Observe: a displayed guess is being replaced by a differing truth — a
|
|
469
546
|
// landing's (superseded), or the value it covered (reverted: the body
|
|
@@ -476,17 +553,15 @@ function supersede(n, value, changed) {
|
|
|
476
553
|
if (l._shown) n._value = n._x._lane;
|
|
477
554
|
n._config &= -10241;
|
|
478
555
|
n._x._lane = NOT_PENDING;
|
|
479
|
-
n._pendingValue = value;
|
|
480
556
|
holdNode(n, parent);
|
|
481
|
-
joinFuture(parent);
|
|
482
557
|
if (n._subs !== null) insertSubs(n);
|
|
483
558
|
} else {
|
|
484
|
-
n._pendingValue = value;
|
|
485
559
|
n._config |= CONFIG_HELD;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (s._sub._config & CONFIG_AUTHORITATIVE) enqueueSub(s._sub);
|
|
560
|
+
// A confirmation is silent (A17); only truth readers are told.
|
|
561
|
+
GlobalQueue._confirmed?.(l);
|
|
489
562
|
}
|
|
563
|
+
n._pendingValue = value;
|
|
564
|
+
joinFuture(parent);
|
|
490
565
|
}
|
|
491
566
|
/** A lane's end, two ways. `into === null`: the parent landed — the lane's
|
|
492
567
|
* values are the truth now: a guess lands the truth staged beneath it or
|
|
@@ -586,7 +661,7 @@ function dissolveLane(l, into, except) {
|
|
|
586
661
|
} else {
|
|
587
662
|
if (x._pendingFirstChild !== null || x._pendingDisposal !== null)
|
|
588
663
|
GlobalQueue._dispose(n, false, true);
|
|
589
|
-
n._config &= -
|
|
664
|
+
n._config &= -1048961;
|
|
590
665
|
}
|
|
591
666
|
}
|
|
592
667
|
}
|
|
@@ -621,11 +696,12 @@ function dissolveLane(l, into, except) {
|
|
|
621
696
|
* member throws like any. */
|
|
622
697
|
function laneRead(c, el) {
|
|
623
698
|
const guess = el._config & CONFIG_GUESS;
|
|
624
|
-
if (c !== null && c._config & CONFIG_AUTHORITATIVE)
|
|
625
|
-
|
|
699
|
+
if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth(el);
|
|
700
|
+
const status = el._statusFlags;
|
|
701
|
+
// An errored member's lane value predates its error; the error is the answer (#4106).
|
|
702
|
+
if (!guess && status & STATUS_ERROR) return NOT_PENDING;
|
|
626
703
|
if (c === null) return laneValueOf(el);
|
|
627
704
|
const l = txOf(el);
|
|
628
|
-
const status = el._statusFlags;
|
|
629
705
|
if (!tracking || c._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
630
706
|
// An untracked read inside lane work derives from the lane like a
|
|
631
707
|
// tracked one, minus the subscription — `untrack` is about
|
|
@@ -734,6 +810,10 @@ function laneSeam(l, leaked) {
|
|
|
734
810
|
reruns(l);
|
|
735
811
|
for (let i = 0; i < l._nodes.length; i++) {
|
|
736
812
|
const n = l._nodes[i];
|
|
813
|
+
if (n._flags & LEFT_SHOWN) {
|
|
814
|
+
n._flags ^= LEFT_SHOWN;
|
|
815
|
+
insertSubs(n);
|
|
816
|
+
}
|
|
737
817
|
const x = n._x;
|
|
738
818
|
if (x._transaction !== l) continue;
|
|
739
819
|
if (!(n._config & CONFIG_GUESS) && !n._type && n._pendingValue !== NOT_PENDING) {
|
|
@@ -848,20 +928,16 @@ function answered(el) {
|
|
|
848
928
|
}
|
|
849
929
|
return q;
|
|
850
930
|
}
|
|
851
|
-
GlobalQueue._laneRead = laneRead;
|
|
852
|
-
GlobalQueue._laneStage = laneStage;
|
|
853
|
-
GlobalQueue._laneOutcome = laneOutcome;
|
|
854
|
-
GlobalQueue._laneWrite = laneWrite;
|
|
855
931
|
/** The lanes alive before this seam's guesses opened theirs (a prefix of
|
|
856
932
|
* `lanes`; nothing dissolves one between here and the seam loop): a lane
|
|
857
933
|
* born at the seam has no work to judge yet (its passes run next round) —
|
|
858
934
|
* it is neither shown nor blocked until it has. */
|
|
859
935
|
let judged = 0;
|
|
860
|
-
|
|
936
|
+
const applyGuessesAt = parent => {
|
|
861
937
|
judged = lanes.length;
|
|
862
938
|
if (pendingGuesses.length !== 0) applyGuesses(parent);
|
|
863
939
|
};
|
|
864
|
-
|
|
940
|
+
const laneSeams = leaks => {
|
|
865
941
|
// Passes that read a write the parked frame holds as the screen
|
|
866
942
|
// (`stagedReaders`: lane work, a verdict reader before the verdict): they
|
|
867
943
|
// re-derive on the committed world next round (and register as stale
|
|
@@ -886,14 +962,30 @@ GlobalQueue._laneSeams = leaks => {
|
|
|
886
962
|
: !blocked(l._parent)))
|
|
887
963
|
)
|
|
888
964
|
continue;
|
|
965
|
+
// A leaked effect re-derives seated in its lane from its first read: it
|
|
966
|
+
// carries no lane value (`laneStage`), so without this its pass starts
|
|
967
|
+
// as mainline, joins the parked frame on its first held read and reads
|
|
968
|
+
// the staging, then re-enters the lane at its guess read and leaks
|
|
969
|
+
// again — every round (#4014). A computed is already seated by its
|
|
970
|
+
// override, and a written guess's own pass stays the frame's.
|
|
971
|
+
if (l != null && l._lane && r._type) r._flags |= REACTIVE_LANE_DIRTY;
|
|
889
972
|
enqueueSub(r);
|
|
890
973
|
if (l != null && l._lane && leaked.indexOf(l) === -1) leaked.push(l);
|
|
891
974
|
}
|
|
892
975
|
for (let k = Math.min(judged, lanes.length) - 1; k >= 0; k--)
|
|
893
976
|
laneSeam(lanes[k], leaked.indexOf(lanes[k]) !== -1);
|
|
894
977
|
};
|
|
895
|
-
|
|
896
|
-
|
|
978
|
+
installLaneHooks(
|
|
979
|
+
laneRead,
|
|
980
|
+
laneStage,
|
|
981
|
+
laneOutcome,
|
|
982
|
+
laneWrite,
|
|
983
|
+
applyGuessesAt,
|
|
984
|
+
laneSeams,
|
|
985
|
+
() => lanes.length !== 0 && laneCorrections(),
|
|
986
|
+
endLanes,
|
|
987
|
+
lanesBlocked
|
|
988
|
+
);
|
|
897
989
|
GlobalQueue._laneGuesses = t => {
|
|
898
990
|
const out = [];
|
|
899
991
|
for (let i = 0; i < lanes.length; i++) {
|
|
@@ -905,7 +997,6 @@ GlobalQueue._laneGuesses = t => {
|
|
|
905
997
|
}
|
|
906
998
|
return out;
|
|
907
999
|
};
|
|
908
|
-
GlobalQueue._lanesBlocked = lanesBlocked;
|
|
909
1000
|
GlobalQueue._verdictLane = verdictLane;
|
|
910
1001
|
|
|
911
1002
|
// Verdicts (plan sec. 28 replay, 2026-10-02; maintainer, 2026-10-01: verdicts are
|
|
@@ -957,9 +1048,12 @@ function route(c, t) {
|
|
|
957
1048
|
* every flush (fuzzer F12). True when the reader is served the committed
|
|
958
1049
|
* value — not over a flight committed beneath inputs already on screen (A15
|
|
959
1050
|
* reveal corollary, #3305): that value would tear against them, and the
|
|
960
|
-
* reveal observes the flight.
|
|
1051
|
+
* reveal observes the flight. Nor for a reader born into the flight's
|
|
1052
|
+
* transaction (`bornInto`, #4010): it has no screen — it waits on the flight
|
|
1053
|
+
* with the rest of its frame. */
|
|
961
1054
|
function observeFlight(c, el) {
|
|
962
|
-
if (el._config & CONFIG_INPUTS_PUBLISHED
|
|
1055
|
+
if (el._config & CONFIG_INPUTS_PUBLISHED || (el._config & CONFIG_HELD && bornInto(c, txOf(el))))
|
|
1056
|
+
return false;
|
|
963
1057
|
if (globalQueue._running) {
|
|
964
1058
|
joinFuture(null);
|
|
965
1059
|
if (!(el._config & CONFIG_HELD)) queuePendingNode(el);
|
|
@@ -983,9 +1077,10 @@ let probeFound = false;
|
|
|
983
1077
|
* reader re-derives there. `latest` shows the proposal that lands — a
|
|
984
1078
|
* rewrite or a landing reaches its reader through the link it holds — so a
|
|
985
1079
|
* re-run would only repeat the frame. */
|
|
986
|
-
function verdictRead(c, t,
|
|
1080
|
+
function verdictRead(c, t, plain) {
|
|
987
1081
|
route(c, t);
|
|
988
|
-
if (
|
|
1082
|
+
if (plain) staleReader(c, t);
|
|
1083
|
+
else if (probing && t._reruns[t._reruns.length - 1] !== c) t._reruns.push(c);
|
|
989
1084
|
}
|
|
990
1085
|
/** A verdict read of a node staged or in flight but not held yet: whether it
|
|
991
1086
|
* will be is the seam's.
|
|
@@ -1055,7 +1150,7 @@ function heldNotFinal(owner) {
|
|
|
1055
1150
|
!latestActive &&
|
|
1056
1151
|
!owner._x._reask &&
|
|
1057
1152
|
!(owner._statusFlags & STATUS_UNINITIALIZED) &&
|
|
1058
|
-
(!owner._equals || !owner._equals(owner._value, owner._pendingValue))
|
|
1153
|
+
(!!owner._x._shownError || !owner._equals || !owner._equals(owner._value, owner._pendingValue))
|
|
1059
1154
|
);
|
|
1060
1155
|
}
|
|
1061
1156
|
/** `isPending`'s answer for an unheld node whose flight is up (cause ii). */
|
|
@@ -1068,11 +1163,14 @@ function pendingVerdict(c, el) {
|
|
|
1068
1163
|
}
|
|
1069
1164
|
}
|
|
1070
1165
|
/** The pass entering a verdict window is a verdict reader (CONFIG_VERDICT):
|
|
1071
|
-
* a dependency going pending re-derives it, the verdict having changed.
|
|
1166
|
+
* a dependency going pending re-derives it, the verdict having changed. Not
|
|
1167
|
+
* an untracked window: that is a snapshot, like any untracked read (#3946 —
|
|
1168
|
+
* a router's link claim probing inside the computation that creates the
|
|
1169
|
+
* anchor). A throw still reaches the pass through `untrack`. */
|
|
1072
1170
|
function markVerdictReader(window) {
|
|
1073
1171
|
let c = context;
|
|
1074
1172
|
if (c?._root) c = c._parentComputed;
|
|
1075
|
-
if (c !== null) {
|
|
1173
|
+
if (c !== null && tracking) {
|
|
1076
1174
|
c._config |= CONFIG_VERDICT;
|
|
1077
1175
|
c._flags |= REACTIVE_PROBED;
|
|
1078
1176
|
// Observe tier: which windows this pass entered (1 `isPending`, 2
|
|
@@ -1096,6 +1194,38 @@ function latest(fn) {
|
|
|
1096
1194
|
setVerdict(prevVerdict);
|
|
1097
1195
|
}
|
|
1098
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Reports whether what `fn` reads is still waiting on an update. Read the
|
|
1199
|
+
* values in question inside `fn`: the probe answers `true` while any of
|
|
1200
|
+
* them depends on a change that is in flight — a write or an async source
|
|
1201
|
+
* that has started producing a new value whose result has not landed yet —
|
|
1202
|
+
* and `false` once the latest value is the one on screen. The answer is
|
|
1203
|
+
* tracked like any read, so a memo or effect that calls `isPending` re-runs
|
|
1204
|
+
* when it flips.
|
|
1205
|
+
*
|
|
1206
|
+
* A read that has no value yet is loading, not pending: inside `fn` it
|
|
1207
|
+
* follows the surrounding `Loading` path like any other read, and the probe
|
|
1208
|
+
* reports only updates to a value that already exists. A bare `refresh()`
|
|
1209
|
+
* does not flip `isPending`; pair it with `affects()` to make the re-ask
|
|
1210
|
+
* visible.
|
|
1211
|
+
*
|
|
1212
|
+
* @param fn expression to probe; read the values in question inside it
|
|
1213
|
+
*
|
|
1214
|
+
* @returns `true` while a read inside `fn` depends on a change that has not committed yet
|
|
1215
|
+
*
|
|
1216
|
+
* @example
|
|
1217
|
+
* ```ts
|
|
1218
|
+
* const [query, setQuery] = createSignal("");
|
|
1219
|
+
* const results = createMemo(() => searchProducts(query()));
|
|
1220
|
+
* const stale = createMemo(() => isPending(results));
|
|
1221
|
+
*
|
|
1222
|
+
* setQuery("shoes");
|
|
1223
|
+
* // stale() is true until the new results land, while results() keeps
|
|
1224
|
+
* // serving the previous list — dim it instead of showing a spinner.
|
|
1225
|
+
* ```
|
|
1226
|
+
*
|
|
1227
|
+
* @description https://v2.solidjs.com/reference/solid-js/reactivity/is-pending
|
|
1228
|
+
*/
|
|
1099
1229
|
function isPending(fn) {
|
|
1100
1230
|
markVerdictReader(1);
|
|
1101
1231
|
const prevProbing = probing;
|
|
@@ -1122,12 +1252,17 @@ function isPending(fn) {
|
|
|
1122
1252
|
setVerdict(prevVerdict);
|
|
1123
1253
|
}
|
|
1124
1254
|
}
|
|
1255
|
+
/** The staging, or the staged error thrown (D-41). */
|
|
1256
|
+
function staged(el) {
|
|
1257
|
+
if (el._config & CONFIG_STAGED_ERROR) throw el._pendingValue;
|
|
1258
|
+
return el._pendingValue;
|
|
1259
|
+
}
|
|
1125
1260
|
/** `latest()` of a held node: the flushed staging — the proposal — and, for
|
|
1126
1261
|
* a tracked reader of a rewrite not yet flushed, the late-linker re-run in
|
|
1127
1262
|
* the flush that carries it. */
|
|
1128
1263
|
function heldLatest(el, c) {
|
|
1129
1264
|
const u = unflushedValue(el);
|
|
1130
|
-
if (u === NOT_PENDING) return el
|
|
1265
|
+
if (u === NOT_PENDING) return staged(el);
|
|
1131
1266
|
if (c !== null) markLateLinker(c);
|
|
1132
1267
|
return u;
|
|
1133
1268
|
}
|
|
@@ -1154,7 +1289,8 @@ function heldLatest(el, c) {
|
|
|
1154
1289
|
* - an errored derivation throws for every reader (#2897). */
|
|
1155
1290
|
function verdictValue(el, c) {
|
|
1156
1291
|
const owner = el;
|
|
1157
|
-
const
|
|
1292
|
+
const owned = c !== null && !(c._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
1293
|
+
const tracked = owned && tracking;
|
|
1158
1294
|
if (c !== null && tracking) {
|
|
1159
1295
|
link(el, c);
|
|
1160
1296
|
if (owner._fn !== undefined) {
|
|
@@ -1174,7 +1310,10 @@ function verdictValue(el, c) {
|
|
|
1174
1310
|
probeFound = true;
|
|
1175
1311
|
if (tracked && flushTransaction !== null) verdictRead(c, flushTransaction);
|
|
1176
1312
|
}
|
|
1177
|
-
if (
|
|
1313
|
+
if (
|
|
1314
|
+
el._config & CONFIG_OVERRIDE &&
|
|
1315
|
+
!(owner._statusFlags & STATUS_ERROR && !(el._config & CONFIG_GUESS))
|
|
1316
|
+
) {
|
|
1178
1317
|
const l = txOf(el);
|
|
1179
1318
|
if (tracked) verdictRead(c, l._parent ?? l);
|
|
1180
1319
|
const flying = (owner._statusFlags & STATUS_PENDING) !== 0 && !quietPending(owner);
|
|
@@ -1233,9 +1372,14 @@ function verdictValue(el, c) {
|
|
|
1233
1372
|
}
|
|
1234
1373
|
return el._value;
|
|
1235
1374
|
}
|
|
1236
|
-
// Born into the future: its staging is its only value.
|
|
1237
|
-
|
|
1238
|
-
|
|
1375
|
+
// Born into the future: its staging is its only value. It has no
|
|
1376
|
+
// committed value, so no verdict: `isPending` reads it as its read does —
|
|
1377
|
+
// the owner joins the hold (held with it, or caught by a boundary that
|
|
1378
|
+
// has not shown content) and answers for the commit, where it is final.
|
|
1379
|
+
if (el._pendingValue !== NOT_PENDING && !(owner._statusFlags & STATUS_PENDING)) {
|
|
1380
|
+
if (probing && owned) joinPass(c, el);
|
|
1381
|
+
return staged(el);
|
|
1382
|
+
}
|
|
1239
1383
|
} else if (el._pendingValue !== NOT_PENDING) {
|
|
1240
1384
|
if (owner._statusFlags & STATUS_PENDING && !uninitialized)
|
|
1241
1385
|
pendingVerdict(tracked ? c : null, owner);
|
|
@@ -1251,7 +1395,7 @@ function verdictValue(el, c) {
|
|
|
1251
1395
|
// A28: an unflushed write is not what `latest` serves; a tracked
|
|
1252
1396
|
// reader derived from the flushed world runs again in the carrying
|
|
1253
1397
|
// flush.
|
|
1254
|
-
if (globalQueue._running) return el
|
|
1398
|
+
if (globalQueue._running) return staged(el);
|
|
1255
1399
|
if (c !== null) markLateLinker(c);
|
|
1256
1400
|
}
|
|
1257
1401
|
return el._value;
|
|
@@ -1273,7 +1417,7 @@ function verdictValue(el, c) {
|
|
|
1273
1417
|
// Installed at module evaluation — present exactly when something imports
|
|
1274
1418
|
// `latest` or `isPending`. (`read` dispatches to `verdictValue` through
|
|
1275
1419
|
// `verdict`, set while a window is open.)
|
|
1276
|
-
|
|
1420
|
+
installVerdictHooks(observeFlight);
|
|
1277
1421
|
/** A store read inside an untracked `isPending` probe found a live mark on
|
|
1278
1422
|
* a record with no node to carry it (store/affects.ts): not final. */
|
|
1279
1423
|
GlobalQueue._witnessMark = () => {
|
|
@@ -1519,7 +1663,7 @@ let externalSourceConfig = null;
|
|
|
1519
1663
|
* ```
|
|
1520
1664
|
*/
|
|
1521
1665
|
// Wires a freshly created computed through the active external-source bridge.
|
|
1522
|
-
// Lives here (installed
|
|
1666
|
+
// Lives here (installed as a scheduler hook while a config is active) rather than
|
|
1523
1667
|
// inline in core: esbuild cannot literal-track the mutable config binding the
|
|
1524
1668
|
// way rollup does, so an inline `if (externalSourceConfig)` block ships in
|
|
1525
1669
|
// every bundle even though only enableExternalSource() can make it reachable.
|
|
@@ -1541,8 +1685,8 @@ function externalUntrack(fn) {
|
|
|
1541
1685
|
// removed on reset) so core's presence checks stay equivalent to the old
|
|
1542
1686
|
// `externalSourceConfig` truthiness checks.
|
|
1543
1687
|
function syncExternalHooks() {
|
|
1544
|
-
|
|
1545
|
-
|
|
1688
|
+
if (externalSourceConfig) setExternalHooks(wireExternalSource, externalUntrack);
|
|
1689
|
+
else setExternalHooks(undefined, undefined);
|
|
1546
1690
|
}
|
|
1547
1691
|
function enableExternalSource(config) {
|
|
1548
1692
|
const { factory, untrack: untrackFn = fn => fn() } = config;
|
|
@@ -1568,6 +1712,61 @@ function enableExternalSource(config) {
|
|
|
1568
1712
|
syncExternalHooks();
|
|
1569
1713
|
}
|
|
1570
1714
|
|
|
1715
|
+
// Deferred hydration, dependency edges: plain-signal identities.
|
|
1716
|
+
// While on (`setSignalIdentities`; the server-components client turns it on
|
|
1717
|
+
// before hydrating), a plain signal created under an id-bearing owner is
|
|
1718
|
+
// named `<owner id>~<n>`, n counting the plain signals (and plain store
|
|
1719
|
+
// roots: `createStore` over an object, store/edges.ts) created under that
|
|
1720
|
+
// owner in its current pass (transparent owners defer to the nearest
|
|
1721
|
+
// id-bearing one, as child ids do). The server names the same signal the
|
|
1722
|
+
// same way and restarts the count on every re-run, so an edge it shipped (a
|
|
1723
|
+
// slot fill read it) matches the client node; the write hook receives the
|
|
1724
|
+
// node and looks its name up (`signalName`). The count restarts when the owner's pass
|
|
1725
|
+
// generation (`_depGen`, bumped by every recompute) moves on. Only the
|
|
1726
|
+
// frames client imports this module, so nothing here ships without it.
|
|
1727
|
+
// Names live in a side table, not on the node: frames reads them across the
|
|
1728
|
+
// package boundary, where a mangled field would not survive.
|
|
1729
|
+
let counts = null;
|
|
1730
|
+
const names = new WeakMap();
|
|
1731
|
+
/** @internal The identity of a plain signal named while identities were on. */
|
|
1732
|
+
function signalName(node) {
|
|
1733
|
+
return node === undefined ? undefined : names.get(node);
|
|
1734
|
+
}
|
|
1735
|
+
function identify(node) {
|
|
1736
|
+
let o = getOwner();
|
|
1737
|
+
if (!o) return;
|
|
1738
|
+
while (o._config & CONFIG_TRANSPARENT && o._parent) o = o._parent;
|
|
1739
|
+
if (o.id == null) return;
|
|
1740
|
+
const gen = o._depGen;
|
|
1741
|
+
let c = counts.get(o);
|
|
1742
|
+
if (c === undefined || c[0] !== gen) counts.set(o, (c = [gen, 0]));
|
|
1743
|
+
const sid = o.id + "~" + c[1]++;
|
|
1744
|
+
// A store root named twice (one object made into two stores, or a store
|
|
1745
|
+
// handed to `createStore`) is one target under two server names: its
|
|
1746
|
+
// writes cannot be attributed (an empty name).
|
|
1747
|
+
names.set(node, names.has(node) ? "" : sid);
|
|
1748
|
+
}
|
|
1749
|
+
/** @internal Name plain signals from now on (`on`), or stop. */
|
|
1750
|
+
function setSignalIdentities(on) {
|
|
1751
|
+
counts = on ? counts || new WeakMap() : null;
|
|
1752
|
+
signalHook = on ? identify : null;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* @internal Called with every plain signal `createSignal` makes, and every
|
|
1756
|
+
* plain store root `createStore` makes, while set (the namer above). Never
|
|
1757
|
+
* set unless `setSignalIdentities` is imported, so a bundle without it keeps
|
|
1758
|
+
* only a null check.
|
|
1759
|
+
*/
|
|
1760
|
+
let signalHook = null;
|
|
1761
|
+
/**
|
|
1762
|
+
* @internal Dev only: `watch` is called with every node read while set (the
|
|
1763
|
+
* dev hook `onRead`) — the frames client's identity-drift check watches the
|
|
1764
|
+
* named signals a deferred occurrence reads as it mounts. A no-op in prod.
|
|
1765
|
+
*/
|
|
1766
|
+
function setReadWatch(watch) {
|
|
1767
|
+
DEV$1.hooks.onRead = watch || undefined;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1571
1770
|
/**
|
|
1572
1771
|
* Context provides a form of dependency injection. It is used to save from needing to pass
|
|
1573
1772
|
* data as props through intermediate components. This function creates a new context object
|
|
@@ -1728,7 +1927,7 @@ function runEffect(node, type) {
|
|
|
1728
1927
|
// hidden subtree. The synchronous first render on creation is not a queued
|
|
1729
1928
|
// run and goes through. `_modified` stays set; the boundary re-queues the
|
|
1730
1929
|
// run (`release`).
|
|
1731
|
-
if (
|
|
1930
|
+
if (heldRunHook?.(node)) return;
|
|
1732
1931
|
// Error arm (#2840), user effects only: a compute-phase error that is still
|
|
1733
1932
|
// the node's settled state at effect time runs the bundle's error handler in
|
|
1734
1933
|
// this same imperative, writable scope. Unwrap the StatusError used for
|
|
@@ -1823,7 +2022,7 @@ function trackedEffect(fn, options) {
|
|
|
1823
2022
|
// pass, but several passes in one flush each enqueue `_run` into the same
|
|
1824
2023
|
// user queue, and this gate is what collapses them into one run.
|
|
1825
2024
|
if (!node._modified || node._flags & REACTIVE_DISPOSED) return;
|
|
1826
|
-
if (
|
|
2025
|
+
if (heldRunHook?.(node)) return;
|
|
1827
2026
|
setTrackedQueueCallback(true);
|
|
1828
2027
|
try {
|
|
1829
2028
|
node._modified = false;
|
|
@@ -1954,6 +2153,155 @@ function reportClientError(error, owner, thrower) {
|
|
|
1954
2153
|
}
|
|
1955
2154
|
}
|
|
1956
2155
|
|
|
2156
|
+
/**
|
|
2157
|
+
* Ownership stamp (#3360): every backing the store ALLOCATES (CoW clones,
|
|
2158
|
+
* privatized committed backings) carries its owning target under this
|
|
2159
|
+
* enumerable symbol. One property write replaces the two weak-collection
|
|
2160
|
+
* registrations (ownership set + raw→target map) a fresh object used to pay
|
|
2161
|
+
* per draft — V8's identity-hash + ephemeron cost dominated the one-key
|
|
2162
|
+
* write floor. Enumerable so a spread copy (the plain-data clone path) stays
|
|
2163
|
+
* on the fast path and carries the stamp along.
|
|
2164
|
+
*
|
|
2165
|
+
* Owned backings are never user-reachable (`snapshot` copies them, the traps
|
|
2166
|
+
* hide the key), so every raw key walk in the store must skip `$OWNER`, and
|
|
2167
|
+
* ownership is answered by `isOwned` — a user object never carries it.
|
|
2168
|
+
* Overlay drafts (`Object.create(v)` over an owned `v`) inherit the stamp.
|
|
2169
|
+
*/
|
|
2170
|
+
const $OWNER = Symbol("STORE_OWNER");
|
|
2171
|
+
/** raw → target for plain (family-less) stores. Family stores use their own
|
|
2172
|
+
* map so one raw can wrap once per family (§7b). */
|
|
2173
|
+
const storeLookup = new WeakMap();
|
|
2174
|
+
function setLookup(raw, t) {
|
|
2175
|
+
(t.fam?.map ?? storeLookup).set(raw, t);
|
|
2176
|
+
}
|
|
2177
|
+
function isOwned(raw) {
|
|
2178
|
+
return raw[$OWNER] !== undefined;
|
|
2179
|
+
}
|
|
2180
|
+
/** The target wrapping `raw` in `fam` (or the plain lookup). An owned raw
|
|
2181
|
+
* answers from its stamp; a user raw from the map. */
|
|
2182
|
+
function lookupTarget(raw, fam) {
|
|
2183
|
+
const owner = raw[$OWNER];
|
|
2184
|
+
return owner !== undefined && owner.fam === fam ? owner : (fam?.map ?? storeLookup).get(raw);
|
|
2185
|
+
}
|
|
2186
|
+
function devAssertNeverUserMutation(target) {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
/** Sticky descendants flag up the parent chain (§6d): adoption descends
|
|
2190
|
+
* into a changed child pair only where a target with nodes exists below. */
|
|
2191
|
+
function markDescendants(target) {
|
|
2192
|
+
let t = target;
|
|
2193
|
+
while (t && !t.d) {
|
|
2194
|
+
t.d = true;
|
|
2195
|
+
t = t.u;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
/** Observe tier: the owner in scope at `createStore` (attribution names and
|
|
2199
|
+
* owner paths); the `_owner` write never reaches the proxy. */
|
|
2200
|
+
const storeOwners = new WeakMap();
|
|
2201
|
+
/** Brand keys the proxy answers directly (never user data). */
|
|
2202
|
+
const $TRACK = Symbol("STORE_TRACK"),
|
|
2203
|
+
$TARGET = Symbol("STORE_TARGET"),
|
|
2204
|
+
$PROXY = Symbol("STORE_PROXY"),
|
|
2205
|
+
$RECORD = Symbol("VIEW_RECORD");
|
|
2206
|
+
// ---------------------------------------------------------------------------
|
|
2207
|
+
// raw marking (R42 / RUL-12: a value is never both wrapped and raw)
|
|
2208
|
+
const rawValues = new WeakSet();
|
|
2209
|
+
let rawValuesUsed = false;
|
|
2210
|
+
function isRawValue(value) {
|
|
2211
|
+
return rawValuesUsed && rawValues.has(value);
|
|
2212
|
+
}
|
|
2213
|
+
/** Mark a value so no store ever wraps it (sticky; it reads as a leaf). */
|
|
2214
|
+
function markRaw(value) {
|
|
2215
|
+
if (isWrappable(value)) {
|
|
2216
|
+
rawValuesUsed = true;
|
|
2217
|
+
rawValues.add(value);
|
|
2218
|
+
}
|
|
2219
|
+
return value;
|
|
2220
|
+
}
|
|
2221
|
+
/** A shallow store's ingested record: raw by contract (values served
|
|
2222
|
+
* verbatim, slots replaced by reference). */
|
|
2223
|
+
function markRawOne(v) {
|
|
2224
|
+
if (isWrappable(v)) {
|
|
2225
|
+
if (v[$TARGET] !== undefined) return;
|
|
2226
|
+
if (lookupTarget(v, null) !== undefined)
|
|
2227
|
+
throw new Error(
|
|
2228
|
+
"shallow store: an ingested record is already tracked as a deep store — one value cannot present both wrapped and raw"
|
|
2229
|
+
);
|
|
2230
|
+
rawValuesUsed = true;
|
|
2231
|
+
rawValues.add(v);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function markRawIngest(container) {
|
|
2235
|
+
if (Array.isArray(container)) {
|
|
2236
|
+
for (let i = 0, len = container.length; i < len; i++) markRawOne(container[i]);
|
|
2237
|
+
} else {
|
|
2238
|
+
for (const k in container) markRawOne(container[k]);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
// ---------------------------------------------------------------------------
|
|
2242
|
+
// wrappability
|
|
2243
|
+
const OBJECT_PROTO = Object.prototype;
|
|
2244
|
+
const wrappableProtos = new WeakMap();
|
|
2245
|
+
function isWrappable(obj) {
|
|
2246
|
+
if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) return false;
|
|
2247
|
+
const proto = Object.getPrototypeOf(obj);
|
|
2248
|
+
if (proto === OBJECT_PROTO || proto === null) return true;
|
|
2249
|
+
if (Array.isArray(obj)) return true;
|
|
2250
|
+
let wrappable = wrappableProtos.get(proto);
|
|
2251
|
+
if (wrappable === undefined) {
|
|
2252
|
+
wrappable =
|
|
2253
|
+
Object.prototype.toString.call(obj) === "[object Object]" &&
|
|
2254
|
+
(typeof Node === "undefined" || !(obj instanceof Node));
|
|
2255
|
+
wrappableProtos.set(proto, wrappable);
|
|
2256
|
+
}
|
|
2257
|
+
return wrappable;
|
|
2258
|
+
}
|
|
2259
|
+
function ownEnumerableKeys(o) {
|
|
2260
|
+
return Reflect.ownKeys(o).filter(k => Object.prototype.propertyIsEnumerable.call(o, k));
|
|
2261
|
+
}
|
|
2262
|
+
// ---------------------------------------------------------------------------
|
|
2263
|
+
// write override: a projection's draft ops carry it (the derive is the
|
|
2264
|
+
// author — its writes must land outside the sync setter scope too, in a
|
|
2265
|
+
// continuation after an `await`/`yield`). Projections return in S3; the
|
|
2266
|
+
// traps already honour it.
|
|
2267
|
+
let writeOverride = false;
|
|
2268
|
+
function setWriteOverride(value) {
|
|
2269
|
+
writeOverride = value;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/**
|
|
2273
|
+
* Deferred hydration, dependency edges for stores. Only the frames client
|
|
2274
|
+
* imports this module, so nothing here ships without it.
|
|
2275
|
+
*
|
|
2276
|
+
* A plain store root (`createStore` over an object) is named like a plain
|
|
2277
|
+
* signal (core/identities.ts: `<owner id>~<n>`, on the same count), and the
|
|
2278
|
+
* whole store goes by that one name: the server records it for any read of
|
|
2279
|
+
* the store, at any depth, and a setter call on the store — on its root or
|
|
2280
|
+
* on a proxy nested in it — is a write to it. Its readers mount at the
|
|
2281
|
+
* setter's entry, before the draft runs, as a plain signal's do.
|
|
2282
|
+
*/
|
|
2283
|
+
/**
|
|
2284
|
+
* @internal The name of the store `x` belongs to — `x` a store proxy (a
|
|
2285
|
+
* setter's entry) or one of its property nodes (the setter notifying it) —
|
|
2286
|
+
* or undefined when it has none or it cannot be placed: a projection or
|
|
2287
|
+
* optimistic family, a store chained over another store's proxy, a root
|
|
2288
|
+
* named twice, or a proxy reached through a store other than the one it
|
|
2289
|
+
* was named as the root of.
|
|
2290
|
+
*/
|
|
2291
|
+
function storeName(x) {
|
|
2292
|
+
if (x === undefined) return undefined;
|
|
2293
|
+
let t = x._config & CONFIG_SLOT_NODE ? x._host : x[$TARGET];
|
|
2294
|
+
if (t === undefined) return undefined;
|
|
2295
|
+
// Up to the root; a target on the way named as a root of its own is one
|
|
2296
|
+
// object in two stores.
|
|
2297
|
+
while (t.u !== null) {
|
|
2298
|
+
if (t.fam !== null || t.ch || signalName(t) !== undefined) return undefined;
|
|
2299
|
+
t = t.u;
|
|
2300
|
+
}
|
|
2301
|
+
if (t.fam !== null || t.ch) return undefined;
|
|
2302
|
+
return signalName(t) || undefined;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
1957
2305
|
/**
|
|
1958
2306
|
* Low-level reactive-cleanup primitive. Registers a callback that runs when
|
|
1959
2307
|
* the surrounding owner is disposed.
|
|
@@ -2042,6 +2390,7 @@ function createSignal(first, second) {
|
|
|
2042
2390
|
}
|
|
2043
2391
|
const node = signal(first, second);
|
|
2044
2392
|
registerGraph(node, getOwner());
|
|
2393
|
+
if (signalHook !== null) signalHook(node);
|
|
2045
2394
|
return [accessor(node), setSignal.bind(null, node)];
|
|
2046
2395
|
}
|
|
2047
2396
|
function createOptimistic(first, second) {
|
|
@@ -2123,7 +2472,7 @@ function createMemo(compute, options) {
|
|
|
2123
2472
|
* );
|
|
2124
2473
|
* ```
|
|
2125
2474
|
*
|
|
2126
|
-
* @description https://
|
|
2475
|
+
* @description https://v2.solidjs.com/reference/solid-js/reactivity/create-effect
|
|
2127
2476
|
*/
|
|
2128
2477
|
function createEffect(compute, effectFn, options) {
|
|
2129
2478
|
if (effectFn === undefined) {
|
|
@@ -2172,7 +2521,7 @@ function createEffect(compute, effectFn, options) {
|
|
|
2172
2521
|
* }
|
|
2173
2522
|
* ```
|
|
2174
2523
|
*
|
|
2175
|
-
* @description https://
|
|
2524
|
+
* @description https://v2.solidjs.com/reference/solid-js/advanced/specialized-reactivity/create-render-effect
|
|
2176
2525
|
*/
|
|
2177
2526
|
function createRenderEffect(compute, effectFn, options) {
|
|
2178
2527
|
effect(compute, effectFn, undefined, options);
|
|
@@ -2220,7 +2569,7 @@ function createRenderEffect(compute, effectFn, options) {
|
|
|
2220
2569
|
* });
|
|
2221
2570
|
* ```
|
|
2222
2571
|
*
|
|
2223
|
-
* @description https://
|
|
2572
|
+
* @description https://v2.solidjs.com/reference/solid-js/advanced/specialized-reactivity/create-tracked-effect
|
|
2224
2573
|
*/
|
|
2225
2574
|
function createTrackedEffect(compute, options) {
|
|
2226
2575
|
trackedEffect(compute, options);
|
|
@@ -2249,7 +2598,7 @@ function createTrackedEffect(compute, options) {
|
|
|
2249
2598
|
* setCount(1); // logs once, reaction re-armed for next change
|
|
2250
2599
|
* ```
|
|
2251
2600
|
*
|
|
2252
|
-
* @description https://
|
|
2601
|
+
* @description https://v2.solidjs.com/reference/solid-js/advanced/specialized-reactivity/create-reaction
|
|
2253
2602
|
*/
|
|
2254
2603
|
function createReaction(effectFn, options) {
|
|
2255
2604
|
let cl = undefined;
|
|
@@ -2309,21 +2658,31 @@ function createReaction(effectFn, options) {
|
|
|
2309
2658
|
* joined it (L2, born held — A29) and delivers at that commit: the
|
|
2310
2659
|
* committed view, never a foreign hold's unrevealed frame. */
|
|
2311
2660
|
function watch(fn, onValue, onError, config = 0, inPass, own = flushTransaction) {
|
|
2661
|
+
if (config & CONFIG_AUTHORITATIVE) {
|
|
2662
|
+
GlobalQueue._truth = truthRead;
|
|
2663
|
+
GlobalQueue._confirmed = truthConfirmed;
|
|
2664
|
+
}
|
|
2312
2665
|
// `config` is on the node for its first pass too (the pass runs inside
|
|
2313
2666
|
// `computed()`): `until`'s predicate is authoritative from its first read,
|
|
2314
2667
|
// or an already-displayed guess would satisfy it in a later slice. (REDERIVE
|
|
2315
2668
|
// after: the first pass's own error is the promise's rejection.)
|
|
2316
2669
|
computed(
|
|
2317
2670
|
() => {
|
|
2318
|
-
let v
|
|
2671
|
+
let v,
|
|
2672
|
+
failed = false;
|
|
2319
2673
|
try {
|
|
2320
2674
|
v = fn();
|
|
2321
2675
|
} catch (e) {
|
|
2322
2676
|
if (e instanceof NotReadyError) throw e;
|
|
2323
|
-
|
|
2324
|
-
|
|
2677
|
+
// An error is an answer (D-41): delivered by the same frame rule.
|
|
2678
|
+
v = unwrapStatusError(e);
|
|
2679
|
+
failed = true;
|
|
2325
2680
|
}
|
|
2326
2681
|
const c = getOwner();
|
|
2682
|
+
// A truth reader waits out a `createDeferred` flight its value derives
|
|
2683
|
+
// from, which serves everyone else the stale answer (D-40).
|
|
2684
|
+
const lag = config & CONFIG_AUTHORITATIVE && GlobalQueue._lagging?.(c);
|
|
2685
|
+
if (lag) throw new NotReadyError(lag);
|
|
2327
2686
|
// (Outside a flush a mainline pass joins through `passTx`.)
|
|
2328
2687
|
const joined = flushTransaction ?? passTx;
|
|
2329
2688
|
if (
|
|
@@ -2334,6 +2693,7 @@ function watch(fn, onValue, onError, config = 0, inPass, own = flushTransaction)
|
|
|
2334
2693
|
staleReader(c, resolveTx(joined));
|
|
2335
2694
|
return;
|
|
2336
2695
|
}
|
|
2696
|
+
if (failed) return void queueMicrotask(() => onError(v));
|
|
2337
2697
|
inPass?.(v);
|
|
2338
2698
|
queueMicrotask(() => onValue(v));
|
|
2339
2699
|
},
|
|
@@ -2446,17 +2806,24 @@ function refresh(target) {
|
|
|
2446
2806
|
const own = flushTransaction;
|
|
2447
2807
|
const promise = new Promise((res, rej) => {
|
|
2448
2808
|
queueMicrotask(() => {
|
|
2449
|
-
//
|
|
2450
|
-
//
|
|
2451
|
-
//
|
|
2452
|
-
// capture runs inside the watcher's
|
|
2453
|
-
// IS the watcher — exactly what
|
|
2809
|
+
// The root is load-bearing: an ownerless computed is auto-disposing,
|
|
2810
|
+
// and the settle walk releases an unobserved one instead of re-running
|
|
2811
|
+
// it, so the waiter would never see the refetch land (#3888). Settle
|
|
2812
|
+
// disposes the waiter directly; the capture runs inside the watcher's
|
|
2813
|
+
// own pass, where the ambient owner IS the watcher — exactly what
|
|
2814
|
+
// dispose() takes.
|
|
2454
2815
|
let waiter = null;
|
|
2455
2816
|
const make = () =>
|
|
2456
2817
|
watch(
|
|
2457
2818
|
() => {
|
|
2458
2819
|
if (waiter === null) waiter = getOwner();
|
|
2459
|
-
|
|
2820
|
+
// Authoritative: the truth beneath a guess (`until`'s view). A
|
|
2821
|
+
// guessed node's own re-ask is not answered by it (#3895): park
|
|
2822
|
+
// on that flight, or reject with its error.
|
|
2823
|
+
const v = read(node);
|
|
2824
|
+
if (node._config & CONFIG_GUESS && node._statusFlags & (STATUS_PENDING | STATUS_ERROR))
|
|
2825
|
+
throw node._x?._error;
|
|
2826
|
+
return v;
|
|
2460
2827
|
},
|
|
2461
2828
|
value => {
|
|
2462
2829
|
res(typeof target === "function" ? value : target);
|
|
@@ -2466,7 +2833,7 @@ function refresh(target) {
|
|
|
2466
2833
|
rej(err);
|
|
2467
2834
|
dispose(waiter);
|
|
2468
2835
|
},
|
|
2469
|
-
CONFIG_FRESH_READ,
|
|
2836
|
+
CONFIG_FRESH_READ | CONFIG_AUTHORITATIVE,
|
|
2470
2837
|
undefined,
|
|
2471
2838
|
own
|
|
2472
2839
|
);
|
|
@@ -2688,122 +3055,6 @@ function onSettled(callback) {
|
|
|
2688
3055
|
});
|
|
2689
3056
|
}
|
|
2690
3057
|
|
|
2691
|
-
/**
|
|
2692
|
-
* Ownership stamp (#3360): every backing the store ALLOCATES (CoW clones,
|
|
2693
|
-
* privatized committed backings) carries its owning target under this
|
|
2694
|
-
* enumerable symbol. One property write replaces the two weak-collection
|
|
2695
|
-
* registrations (ownership set + raw→target map) a fresh object used to pay
|
|
2696
|
-
* per draft — V8's identity-hash + ephemeron cost dominated the one-key
|
|
2697
|
-
* write floor. Enumerable so a spread copy (the plain-data clone path) stays
|
|
2698
|
-
* on the fast path and carries the stamp along.
|
|
2699
|
-
*
|
|
2700
|
-
* Owned backings are never user-reachable (`snapshot` copies them, the traps
|
|
2701
|
-
* hide the key), so every raw key walk in the store must skip `$OWNER`, and
|
|
2702
|
-
* ownership is answered by `isOwned` — a user object never carries it.
|
|
2703
|
-
* Overlay drafts (`Object.create(v)` over an owned `v`) inherit the stamp.
|
|
2704
|
-
*/
|
|
2705
|
-
const $OWNER = Symbol("STORE_OWNER");
|
|
2706
|
-
/** raw → target for plain (family-less) stores. Family stores use their own
|
|
2707
|
-
* map so one raw can wrap once per family (§7b). */
|
|
2708
|
-
const storeLookup = new WeakMap();
|
|
2709
|
-
function setLookup(raw, t) {
|
|
2710
|
-
(t.fam?.map ?? storeLookup).set(raw, t);
|
|
2711
|
-
}
|
|
2712
|
-
function isOwned(raw) {
|
|
2713
|
-
return raw[$OWNER] !== undefined;
|
|
2714
|
-
}
|
|
2715
|
-
/** The target wrapping `raw` in `fam` (or the plain lookup). An owned raw
|
|
2716
|
-
* answers from its stamp; a user raw from the map. */
|
|
2717
|
-
function lookupTarget(raw, fam) {
|
|
2718
|
-
const owner = raw[$OWNER];
|
|
2719
|
-
return owner !== undefined && owner.fam === fam ? owner : (fam?.map ?? storeLookup).get(raw);
|
|
2720
|
-
}
|
|
2721
|
-
function devAssertNeverUserMutation(target) {
|
|
2722
|
-
return;
|
|
2723
|
-
}
|
|
2724
|
-
/** Sticky descendants flag up the parent chain (§6d): adoption descends
|
|
2725
|
-
* into a changed child pair only where a target with nodes exists below. */
|
|
2726
|
-
function markDescendants(target) {
|
|
2727
|
-
let t = target;
|
|
2728
|
-
while (t && !t.d) {
|
|
2729
|
-
t.d = true;
|
|
2730
|
-
t = t.u;
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
/** Observe tier: the owner in scope at `createStore` (attribution names and
|
|
2734
|
-
* owner paths); the `_owner` write never reaches the proxy. */
|
|
2735
|
-
const storeOwners = new WeakMap();
|
|
2736
|
-
/** Brand keys the proxy answers directly (never user data). */
|
|
2737
|
-
const $TRACK = Symbol("STORE_TRACK"),
|
|
2738
|
-
$TARGET = Symbol("STORE_TARGET"),
|
|
2739
|
-
$PROXY = Symbol("STORE_PROXY"),
|
|
2740
|
-
$RECORD = Symbol("VIEW_RECORD");
|
|
2741
|
-
// ---------------------------------------------------------------------------
|
|
2742
|
-
// raw marking (R42 / RUL-12: a value is never both wrapped and raw)
|
|
2743
|
-
const rawValues = new WeakSet();
|
|
2744
|
-
let rawValuesUsed = false;
|
|
2745
|
-
function isRawValue(value) {
|
|
2746
|
-
return rawValuesUsed && rawValues.has(value);
|
|
2747
|
-
}
|
|
2748
|
-
/** Mark a value so no store ever wraps it (sticky; it reads as a leaf). */
|
|
2749
|
-
function markRaw(value) {
|
|
2750
|
-
if (isWrappable(value)) {
|
|
2751
|
-
rawValuesUsed = true;
|
|
2752
|
-
rawValues.add(value);
|
|
2753
|
-
}
|
|
2754
|
-
return value;
|
|
2755
|
-
}
|
|
2756
|
-
/** A shallow store's ingested record: raw by contract (values served
|
|
2757
|
-
* verbatim, slots replaced by reference). */
|
|
2758
|
-
function markRawOne(v) {
|
|
2759
|
-
if (isWrappable(v)) {
|
|
2760
|
-
if (v[$TARGET] !== undefined) return;
|
|
2761
|
-
if (lookupTarget(v, null) !== undefined)
|
|
2762
|
-
throw new Error(
|
|
2763
|
-
"shallow store: an ingested record is already tracked as a deep store — one value cannot present both wrapped and raw"
|
|
2764
|
-
);
|
|
2765
|
-
rawValuesUsed = true;
|
|
2766
|
-
rawValues.add(v);
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
function markRawIngest(container) {
|
|
2770
|
-
if (Array.isArray(container)) {
|
|
2771
|
-
for (let i = 0, len = container.length; i < len; i++) markRawOne(container[i]);
|
|
2772
|
-
} else {
|
|
2773
|
-
for (const k in container) markRawOne(container[k]);
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
// ---------------------------------------------------------------------------
|
|
2777
|
-
// wrappability
|
|
2778
|
-
const OBJECT_PROTO = Object.prototype;
|
|
2779
|
-
const wrappableProtos = new WeakMap();
|
|
2780
|
-
function isWrappable(obj) {
|
|
2781
|
-
if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) return false;
|
|
2782
|
-
const proto = Object.getPrototypeOf(obj);
|
|
2783
|
-
if (proto === OBJECT_PROTO || proto === null) return true;
|
|
2784
|
-
if (Array.isArray(obj)) return true;
|
|
2785
|
-
let wrappable = wrappableProtos.get(proto);
|
|
2786
|
-
if (wrappable === undefined) {
|
|
2787
|
-
wrappable =
|
|
2788
|
-
Object.prototype.toString.call(obj) === "[object Object]" &&
|
|
2789
|
-
(typeof Node === "undefined" || !(obj instanceof Node));
|
|
2790
|
-
wrappableProtos.set(proto, wrappable);
|
|
2791
|
-
}
|
|
2792
|
-
return wrappable;
|
|
2793
|
-
}
|
|
2794
|
-
function ownEnumerableKeys(o) {
|
|
2795
|
-
return Reflect.ownKeys(o).filter(k => Object.prototype.propertyIsEnumerable.call(o, k));
|
|
2796
|
-
}
|
|
2797
|
-
// ---------------------------------------------------------------------------
|
|
2798
|
-
// write override: a projection's draft ops carry it (the derive is the
|
|
2799
|
-
// author — its writes must land outside the sync setter scope too, in a
|
|
2800
|
-
// continuation after an `await`/`yield`). Projections return in S3; the
|
|
2801
|
-
// traps already honour it.
|
|
2802
|
-
let writeOverride = false;
|
|
2803
|
-
function setWriteOverride(value) {
|
|
2804
|
-
writeOverride = value;
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
3058
|
function mapArray(list, map, options) {
|
|
2808
3059
|
const keyFn = typeof options?.keyed === "function" ? options.keyed : undefined;
|
|
2809
3060
|
const indexes = map.length > 1;
|
|
@@ -2977,6 +3228,79 @@ function commitSmallMove(data, newItems, newLen, srcPos, dstPos, runs) {
|
|
|
2977
3228
|
}
|
|
2978
3229
|
return true;
|
|
2979
3230
|
}
|
|
3231
|
+
/** Stage a PURE REMOVAL without the key map: when the new window is the old
|
|
3232
|
+
* window with items dropped, in order (one-item removals, every-nth, the
|
|
3233
|
+
* table filters), a two-pointer walk lands every survivor in the window
|
|
3234
|
+
* stage with one key call per item and returns the leavers; a shape that is
|
|
3235
|
+
* not an ordered subsequence returns null with the node stage cleared, and
|
|
3236
|
+
* the map path below starts from scratch (it rewrites every window slot of
|
|
3237
|
+
* `temp` / `rows` / `indexes`, so only `tempNodes`' `!== undefined` probe
|
|
3238
|
+
* needs the reset). The stage arrays are scratch — nothing live is touched. */
|
|
3239
|
+
function stageRemoval(data, newItems, start, end, newEnd, temp, tempNodes, rows, indexes) {
|
|
3240
|
+
const key = data._key;
|
|
3241
|
+
const oldItems = data._items;
|
|
3242
|
+
const nodes = data._nodes;
|
|
3243
|
+
const removed = [];
|
|
3244
|
+
let i = start;
|
|
3245
|
+
for (let j = start; j <= newEnd; i++, j++) {
|
|
3246
|
+
const nk = key ? key(newItems[j]) : newItems[j];
|
|
3247
|
+
while (i <= end && (key ? key(oldItems[i]) : oldItems[i]) !== nk) removed.push(nodes[i++]);
|
|
3248
|
+
if (i > end) {
|
|
3249
|
+
tempNodes.fill(undefined);
|
|
3250
|
+
return null;
|
|
3251
|
+
}
|
|
3252
|
+
temp[j] = data._mappings[i];
|
|
3253
|
+
tempNodes[j - start] = nodes[i];
|
|
3254
|
+
if (rows) rows[j - start] = data._rows[i];
|
|
3255
|
+
if (indexes) indexes[j - start] = data._indexes[i];
|
|
3256
|
+
}
|
|
3257
|
+
for (; i <= end; i++) removed.push(nodes[i]);
|
|
3258
|
+
return removed;
|
|
3259
|
+
}
|
|
3260
|
+
/** Land a staged window into the live bookkeeping arrays in place: shift the
|
|
3261
|
+
* suffix by `dif` (a loop walked from the side that never overwrites an
|
|
3262
|
+
* unread slot), take the window from the stage, write survivors' row and
|
|
3263
|
+
* index signals. Its own function so the general path's body stays under
|
|
3264
|
+
* the JIT's optimization budget — the inlined commit measured ~10% slower on
|
|
3265
|
+
* full-window reorders (the small-move note has the same finding). */
|
|
3266
|
+
function commitWindow(data, newItems, newLen, start, end, newEnd, tempNodes, rows, indexes) {
|
|
3267
|
+
const nodes = data._nodes;
|
|
3268
|
+
const liveRows = data._rows;
|
|
3269
|
+
const liveIndexes = data._indexes;
|
|
3270
|
+
const oldLen = data._len;
|
|
3271
|
+
const dif = newLen - oldLen;
|
|
3272
|
+
let i, j;
|
|
3273
|
+
if (dif > 0) {
|
|
3274
|
+
// Grow first: a store past the end from the top is a third slower than
|
|
3275
|
+
// filling a pre-sized tail (measured on 1k refs, 2026-10-15).
|
|
3276
|
+
nodes.length = newLen;
|
|
3277
|
+
if (liveRows) liveRows.length = newLen;
|
|
3278
|
+
if (liveIndexes) liveIndexes.length = newLen;
|
|
3279
|
+
for (i = oldLen - 1, j = newLen - 1; i > end; i--, j--) {
|
|
3280
|
+
nodes[j] = nodes[i];
|
|
3281
|
+
if (liveRows) liveRows[j] = liveRows[i];
|
|
3282
|
+
if (liveIndexes) liveIndexes[j] = liveIndexes[i];
|
|
3283
|
+
}
|
|
3284
|
+
} else if (dif < 0) {
|
|
3285
|
+
for (i = end + 1, j = newEnd + 1; i < oldLen; i++, j++) {
|
|
3286
|
+
nodes[j] = nodes[i];
|
|
3287
|
+
if (liveRows) liveRows[j] = liveRows[i];
|
|
3288
|
+
if (liveIndexes) liveIndexes[j] = liveIndexes[i];
|
|
3289
|
+
}
|
|
3290
|
+
nodes.length = newLen;
|
|
3291
|
+
if (liveRows) liveRows.length = newLen;
|
|
3292
|
+
if (liveIndexes) liveIndexes.length = newLen;
|
|
3293
|
+
}
|
|
3294
|
+
for (j = start; j <= newEnd; j++) {
|
|
3295
|
+
nodes[j] = tempNodes[j - start];
|
|
3296
|
+
if (liveRows) setSignal((liveRows[j] = rows[j - start]), newItems[j]);
|
|
3297
|
+
if (liveIndexes) setSignal((liveIndexes[j] = indexes[j - start]), j);
|
|
3298
|
+
}
|
|
3299
|
+
for (j = newEnd + 1; j < newLen; j++) {
|
|
3300
|
+
if (liveRows) setSignal(liveRows[j], newItems[j]);
|
|
3301
|
+
if (liveIndexes && dif !== 0) setSignal(liveIndexes[j], j);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
2980
3304
|
function updateKeyedMap() {
|
|
2981
3305
|
// Untracked reads inside the internal owner resolve via _parentComputed:
|
|
2982
3306
|
// routed through the node, a store-proxy lookup sees pending writes (not a
|
|
@@ -2991,6 +3315,10 @@ function updateKeyedMap() {
|
|
|
2991
3315
|
runWithOwner(this._owner, () => {
|
|
2992
3316
|
let i,
|
|
2993
3317
|
j,
|
|
3318
|
+
// The stage slot a mapper writes its row/index signal into: `j` on the
|
|
3319
|
+
// create-all path, `j - start` on the general path, whose stage arrays
|
|
3320
|
+
// are sized to the changed WINDOW, not the list.
|
|
3321
|
+
slot,
|
|
2994
3322
|
rows,
|
|
2995
3323
|
indexes,
|
|
2996
3324
|
// Mappers write freshly-created row/index signals into the STAGE
|
|
@@ -2998,19 +3326,19 @@ function updateKeyedMap() {
|
|
|
2998
3326
|
mapper = this._rows
|
|
2999
3327
|
? this._byIndex
|
|
3000
3328
|
? () => {
|
|
3001
|
-
rows[
|
|
3002
|
-
return this._map(accessor(rows[
|
|
3329
|
+
rows[slot] = signal(newItems[j], pureOptions);
|
|
3330
|
+
return this._map(accessor(rows[slot]), j);
|
|
3003
3331
|
}
|
|
3004
3332
|
: () => {
|
|
3005
|
-
rows[
|
|
3006
|
-
indexes && (indexes[
|
|
3007
|
-
return this._map(accessor(rows[
|
|
3333
|
+
rows[slot] = signal(newItems[j], pureOptions);
|
|
3334
|
+
indexes && (indexes[slot] = signal(j, pureOptions));
|
|
3335
|
+
return this._map(accessor(rows[slot]), indexes ? accessor(indexes[slot]) : undefined);
|
|
3008
3336
|
}
|
|
3009
3337
|
: this._indexes
|
|
3010
3338
|
? () => {
|
|
3011
3339
|
const item = newItems[j];
|
|
3012
|
-
indexes[
|
|
3013
|
-
return this._map(item, accessor(indexes[
|
|
3340
|
+
indexes[slot] = signal(j, pureOptions);
|
|
3341
|
+
return this._map(item, accessor(indexes[slot]));
|
|
3014
3342
|
}
|
|
3015
3343
|
: () => {
|
|
3016
3344
|
const item = newItems[j];
|
|
@@ -3041,7 +3369,10 @@ function updateKeyedMap() {
|
|
|
3041
3369
|
rows = this._rows && new Array(newLen);
|
|
3042
3370
|
indexes = this._indexes && new Array(newLen);
|
|
3043
3371
|
try {
|
|
3044
|
-
for (j = 0; j < newLen; j++)
|
|
3372
|
+
for (j = 0; j < newLen; j++) {
|
|
3373
|
+
slot = j;
|
|
3374
|
+
mappings[j] = runWithOwner((nodes[j] = createOwner()), mapper);
|
|
3375
|
+
}
|
|
3045
3376
|
} catch (err) {
|
|
3046
3377
|
for (i = 0; i <= j; i++) nodes[i]?.dispose();
|
|
3047
3378
|
throw err;
|
|
@@ -3143,81 +3474,89 @@ function updateKeyedMap() {
|
|
|
3143
3474
|
if (k <= hi && trySmallMove(this, newItems, newLen, start)) return;
|
|
3144
3475
|
}
|
|
3145
3476
|
const dif = newLen - this._len;
|
|
3477
|
+
// WINDOW-SIZED STAGING, IN-PLACE COMMIT (2026-10-15). The strong-abort
|
|
3478
|
+
// contract (#2903) only needs the pass to touch nothing live before
|
|
3479
|
+
// every mapper has succeeded; it never needed four fresh newLen-sized
|
|
3480
|
+
// arrays. Only the changed window [start, newEnd] is staged — the
|
|
3481
|
+
// prefix and suffix are, by construction, the rows that did not move
|
|
3482
|
+
// relative to each other — and at commit the bookkeeping arrays
|
|
3483
|
+
// (`_nodes`, `_rows`, `_indexes`) take the window in place with the
|
|
3484
|
+
// suffix shifted by a plain loop (`copyWithin` is a generic
|
|
3485
|
+
// per-element builtin in V8 — 30× slower than the loop on 1k refs,
|
|
3486
|
+
// measured 2026-10-15). `_mappings` alone is
|
|
3487
|
+
// rebuilt: its fresh identity is the change signal downstream relies
|
|
3488
|
+
// on. A one-row edit on a 500-row list used to allocate ~20 KB of
|
|
3489
|
+
// staging plus a position-sized chain array per commit (uibench's
|
|
3490
|
+
// one-item tree ops at 4–6× Octane, jfb-reorder `removefirst` at
|
|
3491
|
+
// 2.5×); now it stages one slot.
|
|
3492
|
+
const wLen = newEnd - start + 1;
|
|
3146
3493
|
const temp = new Array(newLen);
|
|
3147
|
-
const tempNodes = new Array(
|
|
3148
|
-
rows = this._rows ? new Array(
|
|
3149
|
-
indexes = this._indexes ? new Array(
|
|
3150
|
-
//
|
|
3151
|
-
//
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
tempNodes[j] = this._nodes[i];
|
|
3171
|
-
rows && (rows[j] = this._rows[i]);
|
|
3172
|
-
indexes && (indexes[j] = this._indexes[i]);
|
|
3173
|
-
j = newIndicesNext[j];
|
|
3494
|
+
const tempNodes = new Array(wLen);
|
|
3495
|
+
rows = this._rows ? new Array(wLen) : undefined;
|
|
3496
|
+
indexes = this._indexes ? new Array(wLen) : undefined;
|
|
3497
|
+
// A shrinking pass is tried as a pure removal first (stageRemoval: no
|
|
3498
|
+
// key map, one key call per item); anything else takes the map path.
|
|
3499
|
+
const pure =
|
|
3500
|
+
dif < 0
|
|
3501
|
+
? stageRemoval(this, newItems, start, end, newEnd, temp, tempNodes, rows, indexes)
|
|
3502
|
+
: null;
|
|
3503
|
+
if (pure !== null) {
|
|
3504
|
+
// (The list-churn census below wants removals AND creations in one
|
|
3505
|
+
// pass; a pure removal has none, so no item ledger is kept here.)
|
|
3506
|
+
if (pure.length !== 0) removed = pure;
|
|
3507
|
+
} else {
|
|
3508
|
+
// 0) prepare a map of all indices in the changed window of newItems,
|
|
3509
|
+
// scanning backwards so we encounter them in natural order
|
|
3510
|
+
newIndices = new Map();
|
|
3511
|
+
newIndicesNext = new Array(wLen);
|
|
3512
|
+
for (j = newEnd; j >= start; j--) {
|
|
3513
|
+
item = newItems[j];
|
|
3514
|
+
key = this._key ? this._key(item) : item;
|
|
3515
|
+
i = newIndices.get(key);
|
|
3516
|
+
newIndicesNext[j - start] = i === undefined ? -1 : i;
|
|
3174
3517
|
newIndices.set(key, j);
|
|
3175
|
-
} else {
|
|
3176
|
-
(removed ??= []).push(this._nodes[i]);
|
|
3177
|
-
if (true && attrHooks !== null) (removedItems ??= []).push(item);
|
|
3178
3518
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
for (
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
tempNodes[i] = this._nodes[i];
|
|
3198
|
-
rows && (rows[i] = this._rows[i]);
|
|
3199
|
-
indexes && (indexes[i] = this._indexes[i]);
|
|
3200
|
-
}
|
|
3201
|
-
for (j = start; j <= newEnd; j++) {
|
|
3202
|
-
if (rows) write(rows[j], newItems[j]);
|
|
3203
|
-
if (indexes) write(indexes[j], j);
|
|
3204
|
-
}
|
|
3205
|
-
for (j = newEnd + 1; j < newLen; j++) {
|
|
3206
|
-
temp[j] = this._mappings[j - dif];
|
|
3207
|
-
tempNodes[j] = this._nodes[j - dif];
|
|
3208
|
-
if (rows) {
|
|
3209
|
-
rows[j] = this._rows[j - dif];
|
|
3210
|
-
write(rows[j], newItems[j]);
|
|
3519
|
+
// 1) step through the old changed window and see if items can be found
|
|
3520
|
+
// in the new set; if so, stage them at their new positions; if not,
|
|
3521
|
+
// queue them for disposal at commit
|
|
3522
|
+
for (i = start; i <= end; i++) {
|
|
3523
|
+
item = this._items[i];
|
|
3524
|
+
key = this._key ? this._key(item) : item;
|
|
3525
|
+
j = newIndices.get(key);
|
|
3526
|
+
if (j !== undefined && j !== -1) {
|
|
3527
|
+
temp[j] = this._mappings[i];
|
|
3528
|
+
tempNodes[j - start] = this._nodes[i];
|
|
3529
|
+
rows && (rows[j - start] = this._rows[i]);
|
|
3530
|
+
indexes && (indexes[j - start] = this._indexes[i]);
|
|
3531
|
+
j = newIndicesNext[j - start];
|
|
3532
|
+
newIndices.set(key, j);
|
|
3533
|
+
} else {
|
|
3534
|
+
(removed ??= []).push(this._nodes[i]);
|
|
3535
|
+
if (true && attrHooks !== null) (removedItems ??= []).push(item);
|
|
3536
|
+
}
|
|
3211
3537
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3538
|
+
// 2) create new rows into the window stage; an abort disposes only these
|
|
3539
|
+
try {
|
|
3540
|
+
for (j = start; j <= newEnd; j++) {
|
|
3541
|
+
if (tempNodes[j - start] !== undefined) continue;
|
|
3542
|
+
(created ??= []).push((tempNodes[j - start] = createOwner()));
|
|
3543
|
+
if (true && attrHooks !== null) (createdItems ??= []).push(newItems[j]);
|
|
3544
|
+
slot = j - start;
|
|
3545
|
+
temp[j] = runWithOwner(tempNodes[j - start], mapper);
|
|
3546
|
+
}
|
|
3547
|
+
} catch (err) {
|
|
3548
|
+
if (created) for (i = 0; i < created.length; i++) created[i].dispose();
|
|
3549
|
+
throw err;
|
|
3215
3550
|
}
|
|
3216
3551
|
}
|
|
3552
|
+
// 3) commit: the retained prefix and suffix land in the fresh mapped
|
|
3553
|
+
// array; the bookkeeping arrays take the staged window IN PLACE (own
|
|
3554
|
+
// function: kept out of this body on purpose, see commitWindow); then
|
|
3555
|
+
// the exited rows dispose
|
|
3556
|
+
for (i = 0; i < start; i++) temp[i] = this._mappings[i];
|
|
3557
|
+
for (j = newEnd + 1; j < newLen; j++) temp[j] = this._mappings[j - dif];
|
|
3558
|
+
commitWindow(this, newItems, newLen, start, end, newEnd, tempNodes, rows, indexes);
|
|
3217
3559
|
this._mappings = temp;
|
|
3218
|
-
this._nodes = tempNodes;
|
|
3219
|
-
rows && (this._rows = rows);
|
|
3220
|
-
indexes && (this._indexes = indexes);
|
|
3221
3560
|
this._len = newLen;
|
|
3222
3561
|
// save a copy of the mapped items for the next update
|
|
3223
3562
|
this._items = newItems.slice(0);
|
|
@@ -3247,7 +3586,7 @@ function updateKeyedMap() {
|
|
|
3247
3586
|
* const view = repeat(count, i => `Item ${i}`, { fallback: () => "empty" });
|
|
3248
3587
|
* ```
|
|
3249
3588
|
*
|
|
3250
|
-
* @description https://
|
|
3589
|
+
* @description https://v2.solidjs.com/reference/solid-js/advanced/jsx-component-primitives/repeat
|
|
3251
3590
|
*/
|
|
3252
3591
|
function repeat(count, map, options) {
|
|
3253
3592
|
const wrappedMap = options?.name
|
|
@@ -3343,6 +3682,15 @@ function compare(key, a, b) {
|
|
|
3343
3682
|
return key ? key(a) === key(b) : true;
|
|
3344
3683
|
}
|
|
3345
3684
|
|
|
3685
|
+
let optHooks = null;
|
|
3686
|
+
function installOptHooks(hooks) {
|
|
3687
|
+
optHooks = hooks;
|
|
3688
|
+
}
|
|
3689
|
+
let affectsHooks = null;
|
|
3690
|
+
function installAffectsHooks(hooks) {
|
|
3691
|
+
affectsHooks = hooks;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3346
3694
|
/**
|
|
3347
3695
|
* Store — plain stores on L2 (plan sec. 31, S1).
|
|
3348
3696
|
*
|
|
@@ -3373,14 +3721,6 @@ function compare(key, a, b) {
|
|
|
3373
3721
|
* setter (`wk`); overlay drafts for large plain records; no new named
|
|
3374
3722
|
* fields on array targets; zero allocation for adopted-but-unread objects.
|
|
3375
3723
|
*/
|
|
3376
|
-
let optHooks = null;
|
|
3377
|
-
function installOptHooks(hooks) {
|
|
3378
|
-
optHooks = hooks;
|
|
3379
|
-
}
|
|
3380
|
-
let affectsHooks = null;
|
|
3381
|
-
function installAffectsHooks(hooks) {
|
|
3382
|
-
affectsHooks = hooks;
|
|
3383
|
-
}
|
|
3384
3724
|
/** Key of the deep-witness slot node (never a user key). */
|
|
3385
3725
|
const $DEEP = Symbol("STORE_DEEP");
|
|
3386
3726
|
// ---------------------------------------------------------------------------
|
|
@@ -3570,17 +3910,22 @@ function releaseSlot(node) {
|
|
|
3570
3910
|
function noteNode(target, node, key) {
|
|
3571
3911
|
if (target.fam !== null) target.fam.live.add(target);
|
|
3572
3912
|
markDescendants(target);
|
|
3573
|
-
if (affectsHooks !== null) affectsHooks.
|
|
3913
|
+
if (affectsHooks !== null) affectsHooks._born(target, node, key);
|
|
3574
3914
|
}
|
|
3575
3915
|
// Shared slot-node release handler: registered once; the core sweep
|
|
3576
3916
|
// dispatches CONFIG_SLOT_NODE nodes here (graph.ts) instead of holding a
|
|
3577
3917
|
// per-node closure in a per-node NodeExtension. A dropped node is
|
|
3578
3918
|
// unreachable through the store — a fresh read makes a fresh node.
|
|
3919
|
+
// A staged write — or a lane's value (a guess, S4) — is state only the node
|
|
3920
|
+
// holds: its release waits for the commit that resolves it. So does a leaf of
|
|
3921
|
+
// a held container (#4146): a mainline write it carried is in the held
|
|
3922
|
+
// staging too, where it would read as the hold's until the landing.
|
|
3923
|
+
const slotKept = node =>
|
|
3924
|
+
node._pendingValue !== NOT_PENDING ||
|
|
3925
|
+
node._config & CONFIG_OVERRIDE ||
|
|
3926
|
+
node._host.k?._config & CONFIG_HELD;
|
|
3579
3927
|
setSlotUnobserved(node => {
|
|
3580
|
-
|
|
3581
|
-
// node holds: defer the release to the commit that resolves it.
|
|
3582
|
-
if (node._pendingValue !== NOT_PENDING || node._config & CONFIG_OVERRIDE)
|
|
3583
|
-
deferredReleases.add(node);
|
|
3928
|
+
if (slotKept(node)) deferredReleases.add(node);
|
|
3584
3929
|
else releaseSlot(node);
|
|
3585
3930
|
});
|
|
3586
3931
|
/** A node born while its container has a staging is born from the two
|
|
@@ -3640,7 +3985,9 @@ function getNode(
|
|
|
3640
3985
|
// the hottest store path, and the disabled cost must stay one null
|
|
3641
3986
|
// check (nodes created before enable() stay generically named).
|
|
3642
3987
|
if (attrHooks !== null) {
|
|
3643
|
-
|
|
3988
|
+
const root = storeRoot(target);
|
|
3989
|
+
created._name = (storeNames.get(root) ?? "store") + "." + String(key);
|
|
3990
|
+
created._loc = storeLocs.get(root);
|
|
3644
3991
|
stampNodeOwner(created, target);
|
|
3645
3992
|
}
|
|
3646
3993
|
nodes[key] = node;
|
|
@@ -3693,7 +4040,7 @@ function getContainerNode(target) {
|
|
|
3693
4040
|
* staging: nothing allocates, nothing is swept at the commit (dbmon's tick
|
|
3694
4041
|
* re-created, queued, swept and dropped ~7000 container nodes). If the
|
|
3695
4042
|
* flush parks, the seam materializes and holds the node then
|
|
3696
|
-
* (`
|
|
4043
|
+
* (`storeParkHook`) — the one case the staging home exists for. */
|
|
3697
4044
|
function stageOn(target, pb) {
|
|
3698
4045
|
if (
|
|
3699
4046
|
target.k === null &&
|
|
@@ -3716,7 +4063,7 @@ function deriveHolds(target) {
|
|
|
3716
4063
|
const fw = target.fam?.node;
|
|
3717
4064
|
return fw != null && !userWrite && (fw._config & CONFIG_HELD) !== 0;
|
|
3718
4065
|
}
|
|
3719
|
-
|
|
4066
|
+
const storePark = t => {
|
|
3720
4067
|
for (let i = 0; i < foldList.length; i++) {
|
|
3721
4068
|
const target = foldList[i];
|
|
3722
4069
|
if (target.k === null && target.pb !== null) holdNode(getContainerNode(target), t);
|
|
@@ -3746,6 +4093,7 @@ function notifyContainer(k, pb) {
|
|
|
3746
4093
|
if (k._config & CONFIG_OVERRIDE) setSignal(k, pb);
|
|
3747
4094
|
else {
|
|
3748
4095
|
if (k._config & CONFIG_HELD) joinFuture(txOf(k));
|
|
4096
|
+
if (snapshotCaptureActive) captureWriteSnapshot(k, k._value);
|
|
3749
4097
|
insertSubs(k);
|
|
3750
4098
|
schedule();
|
|
3751
4099
|
}
|
|
@@ -3763,11 +4111,16 @@ function stampNodeOwner(created, target) {
|
|
|
3763
4111
|
created._owner = storeRootOwner(target) ?? null;
|
|
3764
4112
|
}
|
|
3765
4113
|
const storeNames = new WeakMap();
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
4114
|
+
// The store's `loc` option, carried onto every property node beside its
|
|
4115
|
+
// label (the same gate: nodes created before the engine is installed carry
|
|
4116
|
+
// neither).
|
|
4117
|
+
const storeLocs = new WeakMap();
|
|
4118
|
+
function nameStore(proxy, name, loc) {
|
|
4119
|
+
if (attrHooks !== null) {
|
|
4120
|
+
const target = proxy[$TARGET];
|
|
4121
|
+
if (name) storeNames.set(target, name);
|
|
4122
|
+
if (loc) storeLocs.set(target, loc);
|
|
4123
|
+
}
|
|
3771
4124
|
}
|
|
3772
4125
|
// ---------------------------------------------------------------------------
|
|
3773
4126
|
// clones (CoW privatization)
|
|
@@ -3851,7 +4204,8 @@ function overlayRebuilds(t, pb) {
|
|
|
3851
4204
|
if (t.kc > OVERLAY_REBUILD_MAX_KEYS) return false;
|
|
3852
4205
|
if (t.del !== null && t.del.size !== 0) return true;
|
|
3853
4206
|
const wk = t.wk;
|
|
3854
|
-
if (wk !== null && wk !== WK_ALL && wk.size <= OVERLAY_REBUILD_MIN_ADDS)
|
|
4207
|
+
if (wk !== null && wk !== WK_ALL && wk.u === undefined && wk.size <= OVERLAY_REBUILD_MIN_ADDS)
|
|
4208
|
+
return false;
|
|
3855
4209
|
const v = t.v;
|
|
3856
4210
|
let adds = 0;
|
|
3857
4211
|
for (const key of Reflect.ownKeys(pb))
|
|
@@ -4081,11 +4435,7 @@ function drainFolds() {
|
|
|
4081
4435
|
}
|
|
4082
4436
|
if (deferredReleases.size !== 0) {
|
|
4083
4437
|
for (const node of deferredReleases) {
|
|
4084
|
-
if (
|
|
4085
|
-
node._pendingValue === NOT_PENDING &&
|
|
4086
|
-
!(node._config & CONFIG_OVERRIDE) &&
|
|
4087
|
-
node._subs === null
|
|
4088
|
-
) {
|
|
4438
|
+
if (node._subs === null && !slotKept(node)) {
|
|
4089
4439
|
deferredReleases.delete(node);
|
|
4090
4440
|
releaseSlot(node);
|
|
4091
4441
|
}
|
|
@@ -4114,10 +4464,14 @@ GlobalQueue._slotCovered = n => {
|
|
|
4114
4464
|
}
|
|
4115
4465
|
return n._value;
|
|
4116
4466
|
};
|
|
4117
|
-
|
|
4467
|
+
installStoreHooks(storePark, () => {
|
|
4118
4468
|
drainFolds();
|
|
4119
4469
|
optHooks?.sweep();
|
|
4120
|
-
};
|
|
4470
|
+
});
|
|
4471
|
+
/** The store half of `affects()` (store/affects.ts) makes its marks' nodes
|
|
4472
|
+
* and walks records through these: it never imports the engine. */
|
|
4473
|
+
GlobalQueue._storeNode = getNode;
|
|
4474
|
+
GlobalQueue._storeWrappable = isWrappable;
|
|
4121
4475
|
/** A projection's creation run commits directly (a memo's first value is
|
|
4122
4476
|
* its `_value`, not a staging): the draft still writes a clone of the seed,
|
|
4123
4477
|
* nothing is staged on the nodes, and the setter's exit folds the touched
|
|
@@ -4144,7 +4498,10 @@ function foldTarget(t, old) {
|
|
|
4144
4498
|
// the adopted object (shared ownership — never cloned).
|
|
4145
4499
|
t.pb = null;
|
|
4146
4500
|
t.wk = null;
|
|
4147
|
-
} else if (
|
|
4501
|
+
} else if (
|
|
4502
|
+
t.ovl &&
|
|
4503
|
+
(t.v !== old || (!overlayRebuilds(t, pb) && k?._x?._snapshotValue !== old))
|
|
4504
|
+
) {
|
|
4148
4505
|
// Overlay flatten (#3044): the backing keeps its identity, so the
|
|
4149
4506
|
// `t.v === old` gate below skips path copying (the parent slot
|
|
4150
4507
|
// already points here) and the adopted-notify (setter
|
|
@@ -4158,10 +4515,11 @@ function foldTarget(t, old) {
|
|
|
4158
4515
|
const v = t.v;
|
|
4159
4516
|
const wk = t.wk;
|
|
4160
4517
|
if (wk !== null && wk !== WK_ALL) {
|
|
4161
|
-
for (
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4518
|
+
for (let w = wk; w !== undefined; w = w.u)
|
|
4519
|
+
for (const key of w) {
|
|
4520
|
+
if (hasOwn$1.call(pb, key)) copyOwn(v, pb, key);
|
|
4521
|
+
else delete v[key];
|
|
4522
|
+
}
|
|
4165
4523
|
} else {
|
|
4166
4524
|
for (const key of Reflect.ownKeys(pb)) {
|
|
4167
4525
|
const d = Object.getOwnPropertyDescriptor(pb, key);
|
|
@@ -4334,7 +4692,10 @@ function notifyWrites(t) {
|
|
|
4334
4692
|
// value can change when ANY key is written), or a non-plain prototype
|
|
4335
4693
|
// (class instances: prototype getters derive from arbitrary fields).
|
|
4336
4694
|
// Overlay pbs chain to the COMMITTED object (#3044) — plainness is the
|
|
4337
|
-
// committed container's prototype, not the overlay's.
|
|
4695
|
+
// committed container's prototype, not the overlay's. THIS setter's keys
|
|
4696
|
+
// only (D-8, #3743): a key an earlier setter of the batch wrote and this
|
|
4697
|
+
// one did not is no second proposal — its node was told then, and telling
|
|
4698
|
+
// a held node its unchanged value again would join this tick to the hold.
|
|
4338
4699
|
const wk0 = t.wk;
|
|
4339
4700
|
const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
|
|
4340
4701
|
if (attrHooks !== null) reportReplacedContainers(t, old, pb, writtenKeys);
|
|
@@ -4355,16 +4716,19 @@ function notifyWrites(t) {
|
|
|
4355
4716
|
) {
|
|
4356
4717
|
// Accessor keys: the node is linked for shape-change notification,
|
|
4357
4718
|
// its value is never served (the getter runs with the proxy
|
|
4358
|
-
// receiver on read) — FORCE wakes the readers.
|
|
4719
|
+
// receiver on read) — FORCE wakes the readers. A data descriptor
|
|
4720
|
+
// falls through so the cleared-acc hot read serves it (#3949);
|
|
4721
|
+
// an own getter's descriptor has no `value`, so the compare below
|
|
4722
|
+
// would otherwise keep the getter's result (or FORCE) for `undefined`.
|
|
4359
4723
|
node.acc = isOwnAccessor(pb, key);
|
|
4360
4724
|
const od = Object.getOwnPropertyDescriptor(old, key);
|
|
4361
4725
|
const nd = Object.getOwnPropertyDescriptor(pb, key);
|
|
4362
|
-
if (
|
|
4726
|
+
if (!nd || nd.get || nd.set) {
|
|
4363
4727
|
if (od?.get !== nd?.get || od?.set !== nd?.set || od?.value !== nd?.value)
|
|
4364
4728
|
setSignal(node, () => FORCE);
|
|
4365
4729
|
continue;
|
|
4366
4730
|
}
|
|
4367
|
-
if (!isEqual(od?.value, nd
|
|
4731
|
+
if (od?.get || od?.set || !isEqual(od?.value, nd.value)) setSignal(node, () => nd.value);
|
|
4368
4732
|
continue;
|
|
4369
4733
|
}
|
|
4370
4734
|
const nv = t.del !== null && t.del.has(key) ? undefined : pb[key];
|
|
@@ -4462,14 +4826,15 @@ function notifyKeyDiff(node, key, old, neu, probe = true) {
|
|
|
4462
4826
|
node.acc = isOwnAccessor(neu, key);
|
|
4463
4827
|
const od = Object.getOwnPropertyDescriptor(old, key);
|
|
4464
4828
|
const nd = Object.getOwnPropertyDescriptor(neu, key);
|
|
4465
|
-
|
|
4829
|
+
// Data descriptors fall through, as in notifyWrites (#3949).
|
|
4830
|
+
if (!nd || nd.get || nd.set) {
|
|
4466
4831
|
if (od?.get !== nd?.get || od?.set !== nd?.set || od?.value !== nd?.value)
|
|
4467
4832
|
setSignal(node, () => FORCE);
|
|
4468
4833
|
return;
|
|
4469
4834
|
}
|
|
4470
4835
|
const ov = od?.value;
|
|
4471
4836
|
const nv = nd?.value;
|
|
4472
|
-
if (!isEqual(ov, nv) && !targetsEqual(ov, nv))
|
|
4837
|
+
if (od?.get || od?.set || (!isEqual(ov, nv) && !targetsEqual(ov, nv)))
|
|
4473
4838
|
setSignal(node, typeof nv === "function" ? () => nv : nv);
|
|
4474
4839
|
} else {
|
|
4475
4840
|
const ov = old[key];
|
|
@@ -4710,7 +5075,67 @@ function readSource(target, key, shape = false) {
|
|
|
4710
5075
|
/** Did the HELD staging (the container node's, not a mainline layer above
|
|
4711
5076
|
* it) change `key`? */
|
|
4712
5077
|
function heldKeyChanged(target, key, shape = true) {
|
|
4713
|
-
return
|
|
5078
|
+
return (
|
|
5079
|
+
changedBetween(target, target.k._value, target.k._pendingValue, key, shape) &&
|
|
5080
|
+
!adoptChild(target, key)
|
|
5081
|
+
);
|
|
5082
|
+
}
|
|
5083
|
+
/** The values of a held frame, built once per frame: neither the committed
|
|
5084
|
+
* frame nor the held staging changes while the hold stands (a draft writes a
|
|
5085
|
+
* clone), so the set stays true for every first read of the hold. */
|
|
5086
|
+
const frameRaws = new WeakMap();
|
|
5087
|
+
function rawsOf(frame) {
|
|
5088
|
+
let raws = frameRaws.get(frame);
|
|
5089
|
+
if (raws === undefined) frameRaws.set(frame, (raws = new Set(Object.values(frame))));
|
|
5090
|
+
return raws;
|
|
5091
|
+
}
|
|
5092
|
+
/** The same row at a slot, as the adoption pairs rows (reconcile.ts): two
|
|
5093
|
+
* objects whose keys agree — positionally for an unkeyed family. */
|
|
5094
|
+
function sameRow$1(pv, nv, keyFn) {
|
|
5095
|
+
return (
|
|
5096
|
+
pv !== null &&
|
|
5097
|
+
typeof pv === "object" &&
|
|
5098
|
+
nv !== null &&
|
|
5099
|
+
typeof nv === "object" &&
|
|
5100
|
+
(keyFn === null || sameKey(keyFn(pv), keyFn(nv)))
|
|
5101
|
+
);
|
|
5102
|
+
}
|
|
5103
|
+
/** A key of a held adoption whose old and new values are the same row that
|
|
5104
|
+
* nothing had proxied when the adoption ran, so `descend` never paired
|
|
5105
|
+
* them: paired now, as it would have been (#3712) — one target, the old
|
|
5106
|
+
* raw its committed frame, the new raw its staging, held by the same
|
|
5107
|
+
* transaction. Both raws resolve to it, so the slot reads as unchanged and
|
|
5108
|
+
* the row's own leaves decide: `id` the same in both frames holds no one,
|
|
5109
|
+
* `version` holds. A record's keys pair by name; an array's by the family's
|
|
5110
|
+
* key — without a family the pairing is not known here and the hold stands. */
|
|
5111
|
+
function adoptChild(target, key) {
|
|
5112
|
+
const k = target.k;
|
|
5113
|
+
const old = asBacking(k._value);
|
|
5114
|
+
const ov = old[key];
|
|
5115
|
+
const nv = k._pendingValue[key];
|
|
5116
|
+
const keyFn = Array.isArray(old) ? target.fam?.key : null;
|
|
5117
|
+
if (
|
|
5118
|
+
target.s ||
|
|
5119
|
+
keyFn === undefined ||
|
|
5120
|
+
!sameRow$1(ov, nv, keyFn) ||
|
|
5121
|
+
!isWrappable(ov) ||
|
|
5122
|
+
!isWrappable(nv) ||
|
|
5123
|
+
Array.isArray(ov) !== Array.isArray(nv) ||
|
|
5124
|
+
lookupTarget(ov, target.fam) !== undefined ||
|
|
5125
|
+
lookupTarget(nv, target.fam) !== undefined ||
|
|
5126
|
+
// A raw in both frames (the same objects swapped or reordered) is its
|
|
5127
|
+
// own row: pairing it with another would alias the two after the commit.
|
|
5128
|
+
rawsOf(old).has(nv) ||
|
|
5129
|
+
rawsOf(k._pendingValue).has(ov)
|
|
5130
|
+
)
|
|
5131
|
+
return false;
|
|
5132
|
+
// A raw-marked value wraps to itself, a proxy to its own target: neither
|
|
5133
|
+
// is a fresh target around `ov`.
|
|
5134
|
+
const ct = wrap(ov, target, key)[$TARGET];
|
|
5135
|
+
if (ct === undefined || ct.v !== ov) return false;
|
|
5136
|
+
adoptPB(ct, nv, false);
|
|
5137
|
+
holdNode(getContainerNode(ct), txOf(k));
|
|
5138
|
+
return true;
|
|
4714
5139
|
}
|
|
4715
5140
|
/** A family target's unheld staging, read with no pass (a handler). */
|
|
4716
5141
|
function familyAhead(target) {
|
|
@@ -4839,11 +5264,13 @@ function pullFamily(target) {
|
|
|
4839
5264
|
// One that already observes the flight (linked by the pass that saw it
|
|
4840
5265
|
// go up — the frame's hold, `blocked`) keeps observing it: a re-run for
|
|
4841
5266
|
// another reason (a guess it read, the seam) that dropped the link would
|
|
4842
|
-
// release the hold with the flight still up.
|
|
5267
|
+
// release the hold with the flight still up. An errored derive has no
|
|
5268
|
+
// landing to learn of: the error is what the frame shows.
|
|
4843
5269
|
const c = context;
|
|
4844
5270
|
if (
|
|
4845
5271
|
c !== null &&
|
|
4846
5272
|
c._type === EFFECT_RENDER &&
|
|
5273
|
+
!(fw._statusFlags & STATUS_ERROR) &&
|
|
4847
5274
|
fw._config & CONFIG_HELD &&
|
|
4848
5275
|
flushTransaction !== txOf(fw) &&
|
|
4849
5276
|
!linkedTo(c, fw)
|
|
@@ -4928,6 +5355,22 @@ function optRead(target) {
|
|
|
4928
5355
|
const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
4929
5356
|
// ---------------------------------------------------------------------------
|
|
4930
5357
|
// traps
|
|
5358
|
+
/** A trap write: the target is told at the setter's exit, the key recorded
|
|
5359
|
+
* in this setter's `wk`. A setter's first write to a target still staged by
|
|
5360
|
+
* an earlier setter of the batch opens a set of its own (the notify visits
|
|
5361
|
+
* it, D-8) and chains the earlier one behind for the fold's union. */
|
|
5362
|
+
function touch(target, key) {
|
|
5363
|
+
let wk = target.wk;
|
|
5364
|
+
if (!pendingNotify.has(target)) {
|
|
5365
|
+
pendingNotify.add(target);
|
|
5366
|
+
if (wk !== null && wk !== WK_ALL) {
|
|
5367
|
+
wk = new Set();
|
|
5368
|
+
wk.u = target.wk;
|
|
5369
|
+
target.wk = wk;
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
if (wk !== WK_ALL) (target.wk ??= new Set()).add(key);
|
|
5373
|
+
}
|
|
4931
5374
|
const traps = {
|
|
4932
5375
|
get(target, key, receiver) {
|
|
4933
5376
|
// One typeof gates every brand-symbol compare off the hot string path.
|
|
@@ -4955,7 +5398,7 @@ const traps = {
|
|
|
4955
5398
|
// (The witness before the pull: a mark on an uninitialized derived
|
|
4956
5399
|
// store is witnessed, then the pull throws — loading, declared pending.)
|
|
4957
5400
|
if (verdict !== null && affectsHooks !== null && getObserver() === null)
|
|
4958
|
-
affectsHooks.
|
|
5401
|
+
affectsHooks._witness(target, key);
|
|
4959
5402
|
if (target.fam !== null) pullFamily(target);
|
|
4960
5403
|
// Hot inline case: existing PLAIN node (non-accessor) read outside any
|
|
4961
5404
|
// draft — the dbmon/uibench effect re-read shape. Core's `read()` serves
|
|
@@ -5102,7 +5545,7 @@ const traps = {
|
|
|
5102
5545
|
// (The witness before the pull: a mark on an uninitialized derived
|
|
5103
5546
|
// store is witnessed, then the pull throws — loading, declared pending.)
|
|
5104
5547
|
if (verdict !== null && affectsHooks !== null && getObserver() === null)
|
|
5105
|
-
affectsHooks.
|
|
5548
|
+
affectsHooks._witness(target, key);
|
|
5106
5549
|
if (target.fam !== null) pullFamily(target);
|
|
5107
5550
|
const src = readSource(target, key);
|
|
5108
5551
|
// A tracked reader's presence node answers (born from the two frames;
|
|
@@ -5127,7 +5570,7 @@ const traps = {
|
|
|
5127
5570
|
},
|
|
5128
5571
|
ownKeys(target) {
|
|
5129
5572
|
if (verdict !== null && affectsHooks !== null && getObserver() === null)
|
|
5130
|
-
affectsHooks.
|
|
5573
|
+
affectsHooks._witness(target, undefined);
|
|
5131
5574
|
if (target.fam !== null) pullFamily(target);
|
|
5132
5575
|
return visibleKeys(target, enumerationSource(target));
|
|
5133
5576
|
},
|
|
@@ -5135,7 +5578,7 @@ const traps = {
|
|
|
5135
5578
|
if (key === $OWNER || key === $RECORD) return undefined;
|
|
5136
5579
|
const obs = getObserver();
|
|
5137
5580
|
if (verdict !== null && affectsHooks !== null && obs === null)
|
|
5138
|
-
affectsHooks.
|
|
5581
|
+
affectsHooks._witness(target, key);
|
|
5139
5582
|
if (target.fam !== null) pullFamily(target);
|
|
5140
5583
|
// An enumerator (spread, Object.entries) already holds the container
|
|
5141
5584
|
// node and reads its frame; a descriptor read on its own tracks
|
|
@@ -5175,16 +5618,23 @@ const traps = {
|
|
|
5175
5618
|
if (key === "__proto__") return true; // pollution guard (core R30)
|
|
5176
5619
|
const uv = target.s ? value : unwrapValue(value);
|
|
5177
5620
|
const pb = ensurePB(target);
|
|
5178
|
-
|
|
5621
|
+
// #4146 (R1: node existence is unobservable; #3706: the hold's unit is
|
|
5622
|
+
// the key): a user setter writing a key a HELD container's staging left
|
|
5623
|
+
// unchanged would write it into the held staging — read as the hold's
|
|
5624
|
+
// until its landing, while the same key with a leaf (a live reader)
|
|
5625
|
+
// publishes at the flush. The key gets its leaf first, born from the two
|
|
5626
|
+
// frames (`bornStaged`: a key the hold left unchanged — or added — is
|
|
5627
|
+
// unstaged and held by no one; a key the hold changed is born held, A34
|
|
5628
|
+
// (1)), and the write stages on it (A28; a write by the holder joins it,
|
|
5629
|
+
// A34) — as it would with a reader's leaf. Kept while the container is
|
|
5630
|
+
// held (`deferredReleases`).
|
|
5631
|
+
if (userWrite && target.k?._config & CONFIG_HELD) deferredReleases.add(getNode(target, key));
|
|
5179
5632
|
if (Array.isArray(pb)) {
|
|
5633
|
+
touch(target, key);
|
|
5180
5634
|
if (key === "length") target.wk = WK_ALL;
|
|
5181
|
-
else
|
|
5182
|
-
const wk = (target.wk ??= new Set());
|
|
5183
|
-
wk.add(key);
|
|
5184
|
-
wk.add("length");
|
|
5185
|
-
}
|
|
5635
|
+
else touch(target, "length");
|
|
5186
5636
|
} else {
|
|
5187
|
-
|
|
5637
|
+
touch(target, key);
|
|
5188
5638
|
if (!(key in pb)) target.kc++;
|
|
5189
5639
|
}
|
|
5190
5640
|
if (UNSAFE_KEYS.has(key)) {
|
|
@@ -5219,8 +5669,7 @@ const traps = {
|
|
|
5219
5669
|
if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
|
|
5220
5670
|
const pb = ensurePB(target);
|
|
5221
5671
|
if (target.a || !(desc.enumerable && desc.writable && desc.configurable)) target.sc = 1;
|
|
5222
|
-
|
|
5223
|
-
if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
|
|
5672
|
+
touch(target, key);
|
|
5224
5673
|
Object.defineProperty(pb, key, desc);
|
|
5225
5674
|
if (target.del !== null) target.del.delete(key);
|
|
5226
5675
|
if (override) notifyWrites(target);
|
|
@@ -5231,10 +5680,22 @@ const traps = {
|
|
|
5231
5680
|
const override = !draft && writeOverride;
|
|
5232
5681
|
if (!draft && !override) return true;
|
|
5233
5682
|
const pb = ensurePB(target);
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
delete
|
|
5237
|
-
|
|
5683
|
+
touch(target, key);
|
|
5684
|
+
// The key count (`kc`, the overlay gate and the rebuild cap) follows a
|
|
5685
|
+
// delete the way it follows an add: an own key of the draft leaving — a
|
|
5686
|
+
// clone's key, or an overlay's own add — is one fewer now. An overlay's
|
|
5687
|
+
// override of a committed key is the exception: the committed key's
|
|
5688
|
+
// deletion is recorded in `del` and counted at the rebuild, as before.
|
|
5689
|
+
// Without the decrement the natural "delete the old key, set the new
|
|
5690
|
+
// one" shape drifted the count up by one per write until a three-key
|
|
5691
|
+
// object crossed the overlay threshold and paid a rebuild on every
|
|
5692
|
+
// commit after.
|
|
5693
|
+
const committed = target.ovl && hasOwn$1.call(target.v, key);
|
|
5694
|
+
if (hasOwn$1.call(pb, key)) {
|
|
5695
|
+
delete pb[key];
|
|
5696
|
+
committed || target.kc--;
|
|
5697
|
+
}
|
|
5698
|
+
if (committed) (target.del ??= new Set()).add(key);
|
|
5238
5699
|
if (override) notifyWrites(target);
|
|
5239
5700
|
return true;
|
|
5240
5701
|
}
|
|
@@ -5262,7 +5723,7 @@ const ASYNC_STORE_SETTER_MESSAGE =
|
|
|
5262
5723
|
* write violation — the user guard (`mapArray`'s pattern: the node-level
|
|
5263
5724
|
* writes carry `ownedWrite`; the setter asks). */
|
|
5264
5725
|
function devGuardStoreSetterWrite() {
|
|
5265
|
-
if (context && !(context._config & CONFIG_CHILDREN_FORBIDDEN)) {
|
|
5726
|
+
if (context && !(context._config & CONFIG_CHILDREN_FORBIDDEN) && !disposalDepth) {
|
|
5266
5727
|
emitDiagnostic({
|
|
5267
5728
|
code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
|
|
5268
5729
|
kind: "write",
|
|
@@ -5298,6 +5759,7 @@ function devGuardStoreSetterResult(result) {
|
|
|
5298
5759
|
}
|
|
5299
5760
|
}
|
|
5300
5761
|
function storeSetter(proxy, fn, guard = true) {
|
|
5762
|
+
if (writeHook !== null) writeHook(proxy);
|
|
5301
5763
|
if (guard) devGuardStoreSetterWrite();
|
|
5302
5764
|
const target = proxy[$TARGET];
|
|
5303
5765
|
const prevScopes = writeScopes;
|
|
@@ -5365,6 +5827,8 @@ function createStore$1(initialValue, options) {
|
|
|
5365
5827
|
throw new Error("createStore({ shallow }): value is already a store proxy");
|
|
5366
5828
|
}
|
|
5367
5829
|
const proxy = wrap(initialValue);
|
|
5830
|
+
// Dependency edges (store/edges.ts): the root is named like a plain signal.
|
|
5831
|
+
if (signalHook !== null) signalHook(proxy[$TARGET]);
|
|
5368
5832
|
if (shallow) {
|
|
5369
5833
|
proxy[$TARGET].s = true;
|
|
5370
5834
|
markRawIngest(initialValue);
|
|
@@ -5379,7 +5843,7 @@ function createStore$1(initialValue, options) {
|
|
|
5379
5843
|
// disabled, buys nothing.
|
|
5380
5844
|
if (attrHooks !== null) {
|
|
5381
5845
|
storeOwners.set(proxy[$TARGET], owner);
|
|
5382
|
-
nameStore(proxy, options?.name);
|
|
5846
|
+
nameStore(proxy, options?.name, options?.loc);
|
|
5383
5847
|
}
|
|
5384
5848
|
}
|
|
5385
5849
|
const setter = fn => storeSetter(proxy, fn);
|
|
@@ -5638,150 +6102,29 @@ function updatePath(current, args, i = 0) {
|
|
|
5638
6102
|
* Path parts can be:
|
|
5639
6103
|
* - a single key — `"user"`, `0`
|
|
5640
6104
|
* - an array of keys — `[0, 1, 2]`
|
|
5641
|
-
* - a range over an array — `{ from?, to?, by? }`
|
|
5642
|
-
* - a filter `(item, index) => boolean` for arrays
|
|
5643
|
-
*
|
|
5644
|
-
* The final argument is the new value or an updater `(prev) => next`. Use
|
|
5645
|
-
* `storePath.DELETE` to remove a property.
|
|
5646
|
-
*
|
|
5647
|
-
* @example
|
|
5648
|
-
* ```ts
|
|
5649
|
-
* const [state, setState] = createStore({ user: { name: "Ada" }, todos: [] });
|
|
5650
|
-
*
|
|
5651
|
-
* setState(storePath("user", "name", "Grace"));
|
|
5652
|
-
* setState(storePath("todos", t => !t.done, "done", true)); // mark all undone as done
|
|
5653
|
-
* setState(storePath("user", "nickname", storePath.DELETE));
|
|
5654
|
-
* ```
|
|
5655
|
-
*/
|
|
5656
|
-
const storePath = /* @__PURE__ */ Object.assign(
|
|
5657
|
-
function storePath(...args) {
|
|
5658
|
-
return state => {
|
|
5659
|
-
updatePath(state, args);
|
|
5660
|
-
};
|
|
5661
|
-
},
|
|
5662
|
-
{ DELETE }
|
|
5663
|
-
);
|
|
5664
|
-
|
|
5665
|
-
/**
|
|
5666
|
-
* Store — the store half of `affects()` (plan sec. 29, plan sec. 37; A24 (4), #2882,
|
|
5667
|
-
* #2893, #2904).
|
|
5668
|
-
*
|
|
5669
|
-
* A mark on a store is a mark on nodes (affects.ts: a count on the node, a
|
|
5670
|
-
* dependency walk at probe time): `affects(record, key)` marks the slot's
|
|
5671
|
-
* leaf; `affects(record)` marks a carrier (the record's `$AFFECTS` leaf)
|
|
5672
|
-
* and every live node in the record's subtree — the edges existing readers
|
|
5673
|
-
* subscribed through. Coverage is by RAW IDENTITY, not read path (#2882: a
|
|
5674
|
-
* captured row proxy, a derived store sharing the source's raw, never read
|
|
5675
|
-
* through the declaration's proxy): the identities reachable at the
|
|
5676
|
-
* declaration are the mark's `scope`, so a node born inside the window on
|
|
5677
|
-
* a covered record inherits the mark (released with the carrier's last),
|
|
5678
|
-
* and an untracked probe through a record with no nodes is witnessed into
|
|
5679
|
-
* the verdict. A mark declared on a draft (inside a setter) or over an
|
|
5680
|
-
* optimistic family walks the view the writer sees.
|
|
5681
|
-
*
|
|
5682
|
-
* Installed by `store/index.ts`: a program with stores carries it; one
|
|
5683
|
-
* without pays nothing (affects.ts asks `GlobalQueue._storeMarks`).
|
|
5684
|
-
*/
|
|
5685
|
-
/** The keyless mark's carrier key: a leaf of the record no read serves. */
|
|
5686
|
-
const $AFFECTS = Symbol("STORE_AFFECTS");
|
|
5687
|
-
const scopes = new Map();
|
|
5688
|
-
/** The nodes an `affects(store[, key])` declaration marks (affects.ts
|
|
5689
|
-
* registers each). */
|
|
5690
|
-
function storeMarks(t, key) {
|
|
5691
|
-
if (key === undefined) {
|
|
5692
|
-
const carrier = getNode(t, $AFFECTS);
|
|
5693
|
-
let entry = scopes.get(carrier);
|
|
5694
|
-
if (entry === undefined) scopes.set(carrier, (entry = { scope: new Set(), inherited: [] }));
|
|
5695
|
-
const found = [carrier];
|
|
5696
|
-
walk(t.px, entry, found, t.fam, new Set());
|
|
5697
|
-
return found;
|
|
5698
|
-
}
|
|
5699
|
-
const node = t.n?.[key] ?? getNode(t, key);
|
|
5700
|
-
let entry = scopes.get(node);
|
|
5701
|
-
if (entry === undefined) scopes.set(node, (entry = { scope: new Set(), inherited: [], key }));
|
|
5702
|
-
entry.scope.add(t.v);
|
|
5703
|
-
if (t.pb !== null) entry.scope.add(t.pb);
|
|
5704
|
-
return [node];
|
|
5705
|
-
}
|
|
5706
|
-
/** The identities reachable from `value` into the scope (the draft's and
|
|
5707
|
-
* the committed backing both, when a draft is open; an optimistic family's
|
|
5708
|
-
* view), every live node under each record into `found`. Untracked by
|
|
5709
|
-
* construction: raws, never traps. */
|
|
5710
|
-
function walk(value, entry, found, fam, visited) {
|
|
5711
|
-
if (!isWrappable(value)) return;
|
|
5712
|
-
const t = value[$TARGET] ?? lookupTarget(value, fam);
|
|
5713
|
-
let raw = t !== undefined ? (t.pb ?? t.v) : value;
|
|
5714
|
-
if (visited.has(raw)) return;
|
|
5715
|
-
visited.add(raw);
|
|
5716
|
-
entry.scope.add(raw);
|
|
5717
|
-
if (t !== undefined) {
|
|
5718
|
-
if (t.pb !== null) entry.scope.add(t.v);
|
|
5719
|
-
// The writer's view: the tick's own unflushed guesses are in motion too.
|
|
5720
|
-
if (t.fam?.opt === true) raw = optHooks.view(t, raw, true);
|
|
5721
|
-
const nodes = t.n;
|
|
5722
|
-
if (nodes !== null)
|
|
5723
|
-
for (const k of Reflect.ownKeys(nodes))
|
|
5724
|
-
// Another mark's carrier is its own channel: counting it here would
|
|
5725
|
-
// extend that scope's lifetime to this declaration's.
|
|
5726
|
-
if (k !== $AFFECTS) found.push(nodes[k]);
|
|
5727
|
-
const has = t.h;
|
|
5728
|
-
if (has !== null) for (const k of Reflect.ownKeys(has)) found.push(has[k]);
|
|
5729
|
-
if (t.k !== null) found.push(t.k);
|
|
5730
|
-
if (t.dk !== null) found.push(t.dk);
|
|
5731
|
-
fam = t.fam ?? fam;
|
|
5732
|
-
}
|
|
5733
|
-
for (const k of Reflect.ownKeys(raw)) {
|
|
5734
|
-
if (k === $OWNER) continue;
|
|
5735
|
-
const d = Object.getOwnPropertyDescriptor(raw, k);
|
|
5736
|
-
if (d === undefined || d.get !== undefined) continue;
|
|
5737
|
-
walk(d.value, entry, found, fam, visited);
|
|
5738
|
-
}
|
|
5739
|
-
}
|
|
5740
|
-
/** A live scope covering `t`'s identity (keyed: for `key`). Chained
|
|
5741
|
-
* backings (§7b): a view's backing is another store's proxy — marks cover
|
|
5742
|
-
* the BASE raw, so every identity along the chain is checked. */
|
|
5743
|
-
function covering(t, key, skip) {
|
|
5744
|
-
for (const [carrier, entry] of scopes) {
|
|
5745
|
-
if (carrier === skip || (entry.key !== undefined && entry.key !== key)) continue;
|
|
5746
|
-
for (let r = t.v; ; ) {
|
|
5747
|
-
if (entry.scope.has(r)) return carrier;
|
|
5748
|
-
const inner = r?.[$TARGET];
|
|
5749
|
-
if (inner === undefined) break;
|
|
5750
|
-
const backing = inner.pb ?? inner.v;
|
|
5751
|
-
if (backing === r) break;
|
|
5752
|
-
r = backing;
|
|
5753
|
-
}
|
|
5754
|
-
}
|
|
5755
|
-
return null;
|
|
5756
|
-
}
|
|
5757
|
-
installAffectsHooks({
|
|
5758
|
-
// A node born on a record a live mark covers inherits it (released with
|
|
5759
|
-
// the carrier's last registration).
|
|
5760
|
-
born(t, node, key) {
|
|
5761
|
-
if (key === $AFFECTS) return;
|
|
5762
|
-
const carrier = covering(t, key, node);
|
|
5763
|
-
if (carrier === null) return;
|
|
5764
|
-
GlobalQueue._mark(node);
|
|
5765
|
-
scopes.get(carrier).inherited.push(node);
|
|
6105
|
+
* - a range over an array — `{ from?, to?, by? }`
|
|
6106
|
+
* - a filter `(item, index) => boolean` for arrays
|
|
6107
|
+
*
|
|
6108
|
+
* The final argument is the new value or an updater `(prev) => next`. Use
|
|
6109
|
+
* `storePath.DELETE` to remove a property.
|
|
6110
|
+
*
|
|
6111
|
+
* @example
|
|
6112
|
+
* ```ts
|
|
6113
|
+
* const [state, setState] = createStore({ user: { name: "Ada" }, todos: [] });
|
|
6114
|
+
*
|
|
6115
|
+
* setState(storePath("user", "name", "Grace"));
|
|
6116
|
+
* setState(storePath("todos", t => !t.done, "done", true)); // mark all undone as done
|
|
6117
|
+
* setState(storePath("user", "nickname", storePath.DELETE));
|
|
6118
|
+
* ```
|
|
6119
|
+
*/
|
|
6120
|
+
const storePath = /* @__PURE__ */ Object.assign(
|
|
6121
|
+
function storePath(...args) {
|
|
6122
|
+
return state => {
|
|
6123
|
+
updatePath(state, args);
|
|
6124
|
+
};
|
|
5766
6125
|
},
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
// verdict is told directly.
|
|
5770
|
-
witness(t, key) {
|
|
5771
|
-
const own = t.n?.[$AFFECTS];
|
|
5772
|
-
if ((own !== undefined && own._x !== null && own._x._marks !== 0) || covering(t, key) !== null)
|
|
5773
|
-
GlobalQueue._witnessMark();
|
|
5774
|
-
}
|
|
5775
|
-
});
|
|
5776
|
-
GlobalQueue._storeMarks = storeMarks;
|
|
5777
|
-
/** The carrier's last mark released (affects.ts): its scope dies, and the
|
|
5778
|
-
* nodes that inherited the mark release theirs. */
|
|
5779
|
-
GlobalQueue._releaseMarkScope = carrier => {
|
|
5780
|
-
const entry = scopes.get(carrier);
|
|
5781
|
-
if (entry === undefined) return;
|
|
5782
|
-
scopes.delete(carrier);
|
|
5783
|
-
GlobalQueue._releaseMarks(entry.inherited);
|
|
5784
|
-
};
|
|
6126
|
+
{ DELETE }
|
|
6127
|
+
);
|
|
5785
6128
|
|
|
5786
6129
|
/**
|
|
5787
6130
|
* Store — reconcile: the immutable-diff adoption channel (INTERNALS §3).
|
|
@@ -5874,17 +6217,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
|
|
|
5874
6217
|
for (const end = Math.min(plen, nlen); i < end; i++) {
|
|
5875
6218
|
const nv = nextRows[i];
|
|
5876
6219
|
const pvRaw = prevRows[i];
|
|
5877
|
-
if (
|
|
5878
|
-
pvRaw !== nv &&
|
|
5879
|
-
!(
|
|
5880
|
-
pvRaw !== null &&
|
|
5881
|
-
typeof pvRaw === "object" &&
|
|
5882
|
-
nv !== null &&
|
|
5883
|
-
typeof nv === "object" &&
|
|
5884
|
-
sameKey(keyFn(pvRaw), keyFn(nv))
|
|
5885
|
-
)
|
|
5886
|
-
)
|
|
5887
|
-
break; // misaligned: fall to the keyed remainder below
|
|
6220
|
+
if (pvRaw !== nv && !sameRow$1(pvRaw, nv, keyFn)) break; // misaligned: fall to the keyed remainder below
|
|
5888
6221
|
if (
|
|
5889
6222
|
(pvRaw !== nv || (nv !== null && typeof nv === "object" && isOwned(nv))) &&
|
|
5890
6223
|
nv !== null &&
|
|
@@ -6274,6 +6607,11 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
|
|
|
6274
6607
|
// A landing (not the sync pass's own commit): the readers held
|
|
6275
6608
|
// pending on the derive settle.
|
|
6276
6609
|
if (!first || !(owner._statusFlags & STATUS_UNINITIALIZED)) settleFamily(fam);
|
|
6610
|
+
// A commit is the derive's change for the readers that observed its
|
|
6611
|
+
// flight (linked by `pullFamily`; a settled pull does not link), as
|
|
6612
|
+
// a memo's landing is for its readers: one that caught the NotReady
|
|
6613
|
+
// (a fallback value, not pending) re-runs too (#4070).
|
|
6614
|
+
insertSubs(owner);
|
|
6277
6615
|
};
|
|
6278
6616
|
const sync = handleAsync(owner, result, commit, (error, pending) => {
|
|
6279
6617
|
// The flight rejected: the leaves' readers learn it from here —
|
|
@@ -6296,7 +6634,9 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
|
|
|
6296
6634
|
// A flight went up (NotReady out of the pass — the derive's own, with
|
|
6297
6635
|
// the derive as its source): the leaves' readers learn it from here —
|
|
6298
6636
|
// they do not subscribe to the derive.
|
|
6299
|
-
|
|
6637
|
+
// A synchronous error the same way (#3956: a rerun that finds the
|
|
6638
|
+
// entry failed throws): every live reader re-derives and meets it.
|
|
6639
|
+
if (!first) e instanceof NotReadyError ? wakeFamily(fam, e) : errorFamily(fam);
|
|
6300
6640
|
throw e;
|
|
6301
6641
|
}
|
|
6302
6642
|
// A pass that returned with a flight up (an iterator's sync first yield):
|
|
@@ -6313,11 +6653,23 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
|
|
|
6313
6653
|
return owner;
|
|
6314
6654
|
}
|
|
6315
6655
|
function createProjectionInternal(fn, seed, options) {
|
|
6656
|
+
// Seedless (`fn` takes no draft): the root starts as an empty record. It is
|
|
6657
|
+
// a draft like any seed, never observable before the first landing (A25).
|
|
6658
|
+
seed ||= {};
|
|
6659
|
+
const keyOption = options?.key === undefined ? "id" : options.key;
|
|
6316
6660
|
const fam = {
|
|
6317
6661
|
map: new WeakMap(),
|
|
6318
6662
|
node: null,
|
|
6319
6663
|
live: new Set(),
|
|
6320
|
-
shallow: !!options?.shallow
|
|
6664
|
+
shallow: !!options?.shallow,
|
|
6665
|
+
// The row key, for a child paired after the adoption (store.ts
|
|
6666
|
+
// `adoptChild`, #3712) as the adoption pairs them.
|
|
6667
|
+
key:
|
|
6668
|
+
typeof keyOption === "function"
|
|
6669
|
+
? keyOption
|
|
6670
|
+
: keyOption === null
|
|
6671
|
+
? null
|
|
6672
|
+
: row => (isWrappable(row) ? row[keyOption] : undefined)
|
|
6321
6673
|
};
|
|
6322
6674
|
const store = wrap(seed, null, null, fam);
|
|
6323
6675
|
if (fam.shallow) {
|
|
@@ -6326,24 +6678,25 @@ function createProjectionInternal(fn, seed, options) {
|
|
|
6326
6678
|
}
|
|
6327
6679
|
let nodeOptions;
|
|
6328
6680
|
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
6329
|
-
if (
|
|
6330
|
-
|
|
6331
|
-
|
|
6681
|
+
if (attrHooks !== null)
|
|
6682
|
+
// As createStore: the owner in scope stamps the projection's property
|
|
6683
|
+
// nodes, so attribution places them under it (#4035).
|
|
6684
|
+
storeOwners.set(store[$TARGET], getOwner());
|
|
6685
|
+
if (options?.name || options?.loc) {
|
|
6686
|
+
nodeOptions = { ...nodeOptions, name: options.name, loc: options.loc };
|
|
6687
|
+
nameStore(store, options.name, options.loc);
|
|
6332
6688
|
}
|
|
6333
6689
|
const node = computed(() => {
|
|
6334
6690
|
if (!fam.node) fam.node = getOwner();
|
|
6335
|
-
runProjectionComputed(store, fn,
|
|
6691
|
+
runProjectionComputed(store, fn, keyOption);
|
|
6336
6692
|
}, nodeOptions);
|
|
6337
6693
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
6338
6694
|
fam.node = node;
|
|
6695
|
+
// The derive's leaves are not its dependents: a mark's repoll
|
|
6696
|
+
// (affects-marks.ts) reaches their readers through this.
|
|
6697
|
+
node.fam = fam;
|
|
6339
6698
|
return { store, node };
|
|
6340
6699
|
}
|
|
6341
|
-
/**
|
|
6342
|
-
* A derived store: `fn` runs in a computed and writes its draft; readers of
|
|
6343
|
-
* the store's keys re-run only when the keys they read change (the
|
|
6344
|
-
* firewall). `fn` may be async or an async iterator; the seed is a draft,
|
|
6345
|
-
* never an observable value, until the first landing (A25).
|
|
6346
|
-
*/
|
|
6347
6700
|
function createProjection(fn, seed, options) {
|
|
6348
6701
|
return createProjectionInternal(fn, seed, options).store;
|
|
6349
6702
|
}
|
|
@@ -6494,8 +6847,38 @@ function rowSame(a, b, keyFn) {
|
|
|
6494
6847
|
if (keyFn === null || !isWrappable(a) || !isWrappable(b)) return false;
|
|
6495
6848
|
const ka = keyFn(unwrapValue(a));
|
|
6496
6849
|
const kb = keyFn(unwrapValue(b));
|
|
6850
|
+
// Keyless rows are identified by position — reconcile's fallback when the
|
|
6851
|
+
// key is missing. The caller already paired these by index, so two keyless
|
|
6852
|
+
// rows are the same row and the landing confirms the arrangement instead of
|
|
6853
|
+
// dissolving it. Dissolving held an external echo under the action, and a
|
|
6854
|
+
// mainline `until` then timed out on a source that already contained it
|
|
6855
|
+
// (#3898). Keyed rows still match only on the key.
|
|
6856
|
+
if (ka === undefined && kb === undefined) return true;
|
|
6497
6857
|
return ka !== undefined && sameKey(ka, kb);
|
|
6498
6858
|
}
|
|
6859
|
+
/** Whether reconcile would write anything into `a` from `b`: wrappable
|
|
6860
|
+
* pairs descend, leaves compare as its writes do. A key mismatch is a
|
|
6861
|
+
* replacement, which a content difference already says. Plain data — the
|
|
6862
|
+
* server's echo of a guessed row is a new object with the same contents. */
|
|
6863
|
+
function echoes(a, b) {
|
|
6864
|
+
if (a === b) return true;
|
|
6865
|
+
if (!isWrappable(a) || !isWrappable(b)) return false;
|
|
6866
|
+
a = unwrapValue(a);
|
|
6867
|
+
b = unwrapValue(b);
|
|
6868
|
+
const ak = Object.keys(a);
|
|
6869
|
+
if (Array.isArray(a) !== Array.isArray(b) || ak.length !== Object.keys(b).length) return false;
|
|
6870
|
+
for (const k of ak) if (!hasOwn.call(b, k) || !echoes(a[k], b[k])) return false;
|
|
6871
|
+
return true;
|
|
6872
|
+
}
|
|
6873
|
+
// A cycle (a row that references itself) is not plain data: no match, so
|
|
6874
|
+
// the landing stays a correction.
|
|
6875
|
+
GlobalQueue._echoes = (a, b) => {
|
|
6876
|
+
try {
|
|
6877
|
+
return echoes(a, b);
|
|
6878
|
+
} catch {
|
|
6879
|
+
return false;
|
|
6880
|
+
}
|
|
6881
|
+
};
|
|
6499
6882
|
function arrangementEqual(a, b, keyFn) {
|
|
6500
6883
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
6501
6884
|
if (Array.isArray(a)) {
|
|
@@ -6840,7 +7223,9 @@ function optimisticView(
|
|
|
6840
7223
|
function createOptimisticStore(first, second, third) {
|
|
6841
7224
|
const derived = typeof first === "function";
|
|
6842
7225
|
const options = derived ? third : second;
|
|
6843
|
-
|
|
7226
|
+
// Seedless derived form: the root starts as an empty record (see
|
|
7227
|
+
// createProjectionInternal).
|
|
7228
|
+
const initialValue = derived ? second || {} : first;
|
|
6844
7229
|
const keyOption = options?.key === undefined ? "id" : options.key;
|
|
6845
7230
|
const fam = {
|
|
6846
7231
|
map: new WeakMap(),
|
|
@@ -6856,7 +7241,7 @@ function createOptimisticStore(first, second, third) {
|
|
|
6856
7241
|
: row => (isWrappable(row) ? row[keyOption] : undefined)
|
|
6857
7242
|
};
|
|
6858
7243
|
const store = wrap(initialValue, null, null, fam);
|
|
6859
|
-
nameStore(store, options?.name);
|
|
7244
|
+
nameStore(store, options?.name, options?.loc);
|
|
6860
7245
|
if (fam.shallow) {
|
|
6861
7246
|
store[$TARGET].s = true;
|
|
6862
7247
|
markRawIngest(initialValue);
|
|
@@ -6865,7 +7250,8 @@ function createOptimisticStore(first, second, third) {
|
|
|
6865
7250
|
const fn = first;
|
|
6866
7251
|
let nodeOptions;
|
|
6867
7252
|
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
6868
|
-
if (options?.name
|
|
7253
|
+
if (options?.name || options?.loc)
|
|
7254
|
+
nodeOptions = { ...nodeOptions, name: options.name, loc: options.loc };
|
|
6869
7255
|
const node = computed(() => {
|
|
6870
7256
|
if (!fam.node) fam.node = getOwner();
|
|
6871
7257
|
runProjectionComputed(store, fn, keyOption);
|
|
@@ -7934,55 +8320,59 @@ function deep(value) {
|
|
|
7934
8320
|
return deep$1(value);
|
|
7935
8321
|
}
|
|
7936
8322
|
|
|
7937
|
-
//
|
|
7938
|
-
//
|
|
7939
|
-
//
|
|
7940
|
-
//
|
|
7941
|
-
//
|
|
7942
|
-
//
|
|
7943
|
-
// until the surrounding transaction lands (an ambient mark, declared outside
|
|
7944
|
-
// one, releases at the next seam). The marked values stay readable: no reader
|
|
7945
|
-
// suspends on a mark, a mark holds nothing and entangles nothing — it is not
|
|
7946
|
-
// a node of its transaction (`blocked` never sees it), and a reader of marked
|
|
7947
|
-
// data joins nothing through it. A mark is never a re-ask, so a declared
|
|
7948
|
-
// reload's own `refresh()` cannot silence it; a real error outranks it.
|
|
8323
|
+
// The mark core: a declared not-final state on a node that reads pending
|
|
8324
|
+
// (`isPending` → true) on the node and everything derived from it, without
|
|
8325
|
+
// holding anything or entangling a reader. Two declarers share it:
|
|
8326
|
+
// `affects()` (affects.ts — a mark scoped to a transaction, with a store
|
|
8327
|
+
// half) and `createDeferred` (deferred.ts — a mark whose lifetime is the
|
|
8328
|
+
// node's own flight). Split out so the second does not ship the first.
|
|
7949
8329
|
//
|
|
7950
|
-
// Representation: a count on the marked node (`_x._marks`)
|
|
7951
|
-
// scope (the transaction's `_marks`, or the ambient list). Coverage of the
|
|
8330
|
+
// Representation: a count on the marked node (`_x._marks`). Coverage of the
|
|
7952
8331
|
// derivations is pull-derived at probe time — `isPending`'s read walks the
|
|
7953
8332
|
// probed node's dependencies for a marked one (verdict.ts → `_marked`) — so
|
|
7954
8333
|
// nothing is stored downstream and a mid-window recompute strands nothing.
|
|
7955
|
-
// The push half is the verdict readers' re-derivation
|
|
7956
|
-
//
|
|
7957
|
-
|
|
7958
|
-
// Installed on `GlobalQueue` when imported: a program that never declares a
|
|
7959
|
-
// mark carries none of this.
|
|
7960
|
-
/** Live marks, every scope — the gate on the probe's walk. */
|
|
8334
|
+
// The push half is the verdict readers' re-derivation when a node's marks
|
|
8335
|
+
// start and end (`repoll`): the only notification a mark makes.
|
|
8336
|
+
/** Live marks, every declarer — the gate on the probe's walk. */
|
|
7961
8337
|
let active = 0;
|
|
7962
|
-
/** Marks declared outside a transaction: released at the next seam. */
|
|
7963
|
-
const ambient = [];
|
|
7964
8338
|
/** The verdict readers downstream of `node` — through derivations, stopping
|
|
7965
8339
|
* at effects — re-derive: the mark's one notification (a probe's answer
|
|
7966
|
-
* changed with no value notifying anyone).
|
|
7967
|
-
|
|
8340
|
+
* changed with no value notifying anyone). Truth readers (`until`, the
|
|
8341
|
+
* `refresh` waiter) too, which a lagging flight parked (deferred.ts) — but
|
|
8342
|
+
* not through a derived store, whose leaves change only once its derive
|
|
8343
|
+
* re-runs: they re-derive at that write. */
|
|
8344
|
+
function repoll(node, seen = new Set(), readers = CONFIG_VERDICT | CONFIG_AUTHORITATIVE) {
|
|
7968
8345
|
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
7969
8346
|
const sub = s._sub;
|
|
7970
8347
|
if (seen.has(sub)) continue;
|
|
7971
8348
|
seen.add(sub);
|
|
7972
|
-
if (sub._config &
|
|
7973
|
-
if (!sub._type) repoll(sub, seen);
|
|
8349
|
+
if (sub._config & readers) enqueueSub(sub);
|
|
8350
|
+
if (!sub._type) repoll(sub, seen, readers);
|
|
8351
|
+
// A derived store's derive: its leaves' readers derive from it too
|
|
8352
|
+
// (store/projection.ts; a live target's property, has and keys nodes).
|
|
8353
|
+
const fam = sub.fam;
|
|
8354
|
+
if (fam)
|
|
8355
|
+
for (const t of fam.live)
|
|
8356
|
+
for (const o of [t.n, t.h, { k: t.k, d: t.dk }])
|
|
8357
|
+
if (o) for (const k of Reflect.ownKeys(o)) o[k] && repoll(o[k], seen, CONFIG_VERDICT);
|
|
7974
8358
|
}
|
|
7975
8359
|
}
|
|
7976
8360
|
/** A node is covered by a live mark iff it carries one, or derives — through
|
|
7977
|
-
* its current dependencies — from one that does. A
|
|
7978
|
-
*
|
|
7979
|
-
*
|
|
7980
|
-
*
|
|
8361
|
+
* its current dependencies — from one that does. A store slot derives from
|
|
8362
|
+
* its family's derive (a derived store's leaves are not the derive's
|
|
8363
|
+
* dependents, store/projection.ts), so coverage hops there. A real error
|
|
8364
|
+
* outranks an inherited mark (A24 (c)): coverage does not flow through an
|
|
8365
|
+
* errored node (a direct mark on one still reads pending). Mid-recompute,
|
|
8366
|
+
* only the validated prefix of the dependency list is this pass's. */
|
|
7981
8367
|
function marked(el, seen) {
|
|
7982
8368
|
if (el._x !== null && el._x._marks !== 0) return true;
|
|
7983
8369
|
const c = el;
|
|
7984
8370
|
if (c._statusFlags & STATUS_ERROR || seen.has(el)) return false;
|
|
7985
8371
|
seen.add(el);
|
|
8372
|
+
if (c._config & CONFIG_SLOT_NODE) {
|
|
8373
|
+
const derive = c._host?.fam?.node;
|
|
8374
|
+
return derive != null && marked(derive, seen);
|
|
8375
|
+
}
|
|
7986
8376
|
const tail = c._flags & REACTIVE_RECOMPUTING_DEPS ? c._depsTail : undefined;
|
|
7987
8377
|
if (tail !== null)
|
|
7988
8378
|
for (let d = c._deps ?? null; d !== null; d = d._nextDep) {
|
|
@@ -7991,18 +8381,183 @@ function marked(el, seen) {
|
|
|
7991
8381
|
}
|
|
7992
8382
|
return false;
|
|
7993
8383
|
}
|
|
8384
|
+
/** One mark on `node`. The declarer repolls once its bookkeeping is done. */
|
|
8385
|
+
function mark(node) {
|
|
8386
|
+
ext(node)._marks++;
|
|
8387
|
+
active++;
|
|
8388
|
+
}
|
|
8389
|
+
/** Drops one mark from `node`: true when it was the last — the node's
|
|
8390
|
+
* verdict readers have re-derived. */
|
|
8391
|
+
function unmark(node) {
|
|
8392
|
+
active--;
|
|
8393
|
+
if (--node._x._marks !== 0) return false;
|
|
8394
|
+
repoll(node);
|
|
8395
|
+
return true;
|
|
8396
|
+
}
|
|
8397
|
+
GlobalQueue._marked = el => active !== 0 && marked(el, new Set());
|
|
8398
|
+
GlobalQueue._mark = mark;
|
|
8399
|
+
|
|
8400
|
+
/**
|
|
8401
|
+
* Store — the store half of `affects()` (plan sec. 29, plan sec. 37; A24 (4), #2882,
|
|
8402
|
+
* #2893, #2904).
|
|
8403
|
+
*
|
|
8404
|
+
* A mark on a store is a mark on nodes (affects.ts: a count on the node, a
|
|
8405
|
+
* dependency walk at probe time): `affects(record, key)` marks the slot's
|
|
8406
|
+
* leaf; `affects(record)` marks a carrier (the record's `$AFFECTS` leaf)
|
|
8407
|
+
* and every live node in the record's subtree — the edges existing readers
|
|
8408
|
+
* subscribed through. Coverage is by RAW IDENTITY, not read path (#2882: a
|
|
8409
|
+
* captured row proxy, a derived store sharing the source's raw, never read
|
|
8410
|
+
* through the declaration's proxy): the identities reachable at the
|
|
8411
|
+
* declaration are the mark's `scope`, so a node born inside the window on
|
|
8412
|
+
* a covered record inherits the mark (released with the carrier's last),
|
|
8413
|
+
* and an untracked probe through a record with no nodes is witnessed into
|
|
8414
|
+
* the verdict. A mark declared on a draft (inside a setter) or over an
|
|
8415
|
+
* optimistic family walks the view the writer sees.
|
|
8416
|
+
*
|
|
8417
|
+
* Imported by `affects()`, which installs it on its first store target
|
|
8418
|
+
* (#3891, after #3912): a program with stores that never declares a mark
|
|
8419
|
+
* carries none of it. It never imports store.ts — the engine's top level —
|
|
8420
|
+
* so a program declaring marks without stores carries this module alone:
|
|
8421
|
+
* the store's nodes and its wrappability test come through `GlobalQueue`
|
|
8422
|
+
* (`_storeNode`, `_storeWrappable`), and node birth and the untracked traps
|
|
8423
|
+
* reach back through `affectsHooks` (hooks.ts).
|
|
8424
|
+
*/
|
|
8425
|
+
/** The keyless mark's carrier key: a leaf of the record no read serves. */
|
|
8426
|
+
const $AFFECTS = Symbol("STORE_AFFECTS");
|
|
8427
|
+
const scopes = new Map();
|
|
8428
|
+
function scopeOf(carrier, key) {
|
|
8429
|
+
let entry = scopes.get(carrier);
|
|
8430
|
+
if (entry === undefined)
|
|
8431
|
+
scopes.set(carrier, (entry = { _scope: new Set(), _inherited: [], _key: key }));
|
|
8432
|
+
return entry;
|
|
8433
|
+
}
|
|
8434
|
+
/** The nodes an `affects(store[, key])` declaration marks (affects.ts
|
|
8435
|
+
* registers each). */
|
|
8436
|
+
function storeMarks(t, key) {
|
|
8437
|
+
const node = GlobalQueue._storeNode(t, key === undefined ? $AFFECTS : key);
|
|
8438
|
+
const entry = scopeOf(node, key);
|
|
8439
|
+
if (key === undefined) {
|
|
8440
|
+
const found = [node];
|
|
8441
|
+
walk(t.px, entry, found, t.fam, new Set());
|
|
8442
|
+
return found;
|
|
8443
|
+
}
|
|
8444
|
+
entry._scope.add(t.v);
|
|
8445
|
+
if (t.pb !== null) entry._scope.add(t.pb);
|
|
8446
|
+
return [node];
|
|
8447
|
+
}
|
|
8448
|
+
/** The identities reachable from `value` into the scope (the draft's and
|
|
8449
|
+
* the committed backing both, when a draft is open; an optimistic family's
|
|
8450
|
+
* view), every live node under each record into `found`. Untracked by
|
|
8451
|
+
* construction: raws, never traps. */
|
|
8452
|
+
function walk(value, entry, found, fam, visited) {
|
|
8453
|
+
if (!GlobalQueue._storeWrappable(value)) return;
|
|
8454
|
+
const t = value[$TARGET] ?? lookupTarget(value, fam);
|
|
8455
|
+
let raw = t !== undefined ? (t.pb ?? t.v) : value;
|
|
8456
|
+
if (visited.has(raw)) return;
|
|
8457
|
+
visited.add(raw);
|
|
8458
|
+
entry._scope.add(raw);
|
|
8459
|
+
if (t !== undefined) {
|
|
8460
|
+
if (t.pb !== null) entry._scope.add(t.v);
|
|
8461
|
+
// The writer's view: the tick's own unflushed guesses are in motion too.
|
|
8462
|
+
if (t.fam?.opt === true) raw = optHooks.view(t, raw, true);
|
|
8463
|
+
const nodes = t.n;
|
|
8464
|
+
if (nodes !== null)
|
|
8465
|
+
for (const k of Reflect.ownKeys(nodes))
|
|
8466
|
+
// Another mark's carrier is its own channel: counting it here would
|
|
8467
|
+
// extend that scope's lifetime to this declaration's.
|
|
8468
|
+
if (k !== $AFFECTS) found.push(nodes[k]);
|
|
8469
|
+
const has = t.h;
|
|
8470
|
+
if (has !== null) for (const k of Reflect.ownKeys(has)) found.push(has[k]);
|
|
8471
|
+
if (t.k !== null) found.push(t.k);
|
|
8472
|
+
if (t.dk !== null) found.push(t.dk);
|
|
8473
|
+
fam = t.fam ?? fam;
|
|
8474
|
+
}
|
|
8475
|
+
for (const k of Reflect.ownKeys(raw))
|
|
8476
|
+
if (k !== $OWNER) {
|
|
8477
|
+
const d = Object.getOwnPropertyDescriptor(raw, k);
|
|
8478
|
+
if (d.get === undefined) walk(d.value, entry, found, fam, visited);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
/** A live scope covering `t`'s identity (keyed: for `key`). Chained
|
|
8482
|
+
* backings (§7b): a view's backing is another store's proxy — marks cover
|
|
8483
|
+
* the BASE raw, so every identity along the chain is checked. */
|
|
8484
|
+
function covering(t, key, skip) {
|
|
8485
|
+
for (const [carrier, entry] of scopes) {
|
|
8486
|
+
if (carrier === skip || (entry._key !== undefined && entry._key !== key)) continue;
|
|
8487
|
+
for (let r = t.v; ; ) {
|
|
8488
|
+
if (entry._scope.has(r)) return carrier;
|
|
8489
|
+
const inner = r?.[$TARGET];
|
|
8490
|
+
if (inner === undefined) break;
|
|
8491
|
+
const backing = inner.pb ?? inner.v;
|
|
8492
|
+
if (backing === r) break;
|
|
8493
|
+
r = backing;
|
|
8494
|
+
}
|
|
8495
|
+
}
|
|
8496
|
+
return null;
|
|
8497
|
+
}
|
|
8498
|
+
/** The store's side: node birth and the untracked traps call it (store.ts,
|
|
8499
|
+
* through `affectsHooks`) — with no live mark too: an empty scope map is
|
|
8500
|
+
* no work. */
|
|
8501
|
+
const hooks = {
|
|
8502
|
+
// A node born on a record a live mark covers inherits it (released with
|
|
8503
|
+
// the carrier's last registration).
|
|
8504
|
+
_born(t, node, key) {
|
|
8505
|
+
if (scopes.size === 0 || key === $AFFECTS) return;
|
|
8506
|
+
const carrier = covering(t, key, node);
|
|
8507
|
+
if (!carrier) return;
|
|
8508
|
+
GlobalQueue._mark(node);
|
|
8509
|
+
scopes.get(carrier)._inherited.push(node);
|
|
8510
|
+
},
|
|
8511
|
+
// An untracked probe (`isPending(() => s.x)` with no observer) reading a
|
|
8512
|
+
// record a live mark covers: no node carries the mark for it — the
|
|
8513
|
+
// verdict is told directly.
|
|
8514
|
+
_witness(t, key) {
|
|
8515
|
+
if (t.n?.[$AFFECTS]?._x?._marks || covering(t, key)) GlobalQueue._witnessMark();
|
|
8516
|
+
}
|
|
8517
|
+
};
|
|
8518
|
+
/** `affects()` calls it before a store-targeted declaration (idempotent). */
|
|
8519
|
+
function installStoreAffects() {
|
|
8520
|
+
installAffectsHooks(hooks);
|
|
8521
|
+
}
|
|
8522
|
+
/** The carrier's last mark released (affects.ts): its scope dies, and the
|
|
8523
|
+
* nodes that inherited the mark release theirs. */
|
|
8524
|
+
function releaseMarkScope(carrier) {
|
|
8525
|
+
const entry = scopes.get(carrier);
|
|
8526
|
+
if (entry === undefined) return;
|
|
8527
|
+
scopes.delete(carrier);
|
|
8528
|
+
releaseMarksHook(entry._inherited);
|
|
8529
|
+
}
|
|
8530
|
+
|
|
8531
|
+
// `affects()` — the declaration verb of the pending model (A24 (4), ruled
|
|
8532
|
+
// 2026-07-13; the #2893 audit corollaries; rebuilt on L2 2026-10-02 as ruled:
|
|
8533
|
+
// transaction-inert).
|
|
8534
|
+
//
|
|
8535
|
+
// A mark is a declared in-flight change: the marked node — and everything
|
|
8536
|
+
// derived from it — reads pending (`isPending` → true) from the declaration
|
|
8537
|
+
// until the surrounding transaction lands (an ambient mark, declared outside
|
|
8538
|
+
// one, releases at the next seam). The marked values stay readable: no reader
|
|
8539
|
+
// suspends on a mark, a mark holds nothing and entangles nothing — it is not
|
|
8540
|
+
// a node of its transaction (`blocked` never sees it), and a reader of marked
|
|
8541
|
+
// data joins nothing through it. A mark is never a re-ask, so a declared
|
|
8542
|
+
// reload's own `refresh()` cannot silence it; a real error outranks it.
|
|
8543
|
+
//
|
|
8544
|
+
// Representation: the mark core (affects-marks.ts) — a count on the marked
|
|
8545
|
+
// node, coverage pull-derived at probe time, readers repolled at registration
|
|
8546
|
+
// and release. Here: each mark's scope (the transaction's `_marks`, or the
|
|
8547
|
+
// ambient list) and the store half.
|
|
8548
|
+
//
|
|
8549
|
+
// Installed on `GlobalQueue` when imported: a program that never declares a
|
|
8550
|
+
// mark carries none of this.
|
|
8551
|
+
/** Marks declared outside a transaction: released at the next seam. */
|
|
8552
|
+
const ambient = [];
|
|
7994
8553
|
/** Releases one scope's marks (a landed transaction's, the ambient list at
|
|
7995
8554
|
* the seam): a node whose last mark drops re-derives its verdict readers. */
|
|
7996
8555
|
function release$1(nodes) {
|
|
7997
8556
|
for (let i = 0; i < nodes.length; i++) {
|
|
7998
8557
|
const n = nodes[i];
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
// A store mark's carrier: its scope (and the marks the nodes born in
|
|
8003
|
-
// its window inherited) goes with it.
|
|
8004
|
-
GlobalQueue._releaseMarkScope?.(n);
|
|
8005
|
-
}
|
|
8558
|
+
// A store mark's carrier: its scope (and the marks the nodes born in
|
|
8559
|
+
// its window inherited) goes with the last mark.
|
|
8560
|
+
if (unmark(n)) releaseMarkScope(n);
|
|
8006
8561
|
}
|
|
8007
8562
|
nodes.length = 0;
|
|
8008
8563
|
}
|
|
@@ -8018,10 +8573,6 @@ function register(node) {
|
|
|
8018
8573
|
}
|
|
8019
8574
|
repoll(node);
|
|
8020
8575
|
}
|
|
8021
|
-
function mark(node) {
|
|
8022
|
-
ext(node)._marks++;
|
|
8023
|
-
active++;
|
|
8024
|
-
}
|
|
8025
8576
|
function affects(target, key) {
|
|
8026
8577
|
if (arguments.length > 2)
|
|
8027
8578
|
invalid(
|
|
@@ -8032,7 +8583,8 @@ function affects(target, key) {
|
|
|
8032
8583
|
// record's carrier and every live node under it.
|
|
8033
8584
|
const t = target?.[$TARGET];
|
|
8034
8585
|
if (t !== undefined) {
|
|
8035
|
-
|
|
8586
|
+
installStoreAffects();
|
|
8587
|
+
const nodes = storeMarks(t, key);
|
|
8036
8588
|
for (let i = 0; i < nodes.length; i++) register(nodes[i]);
|
|
8037
8589
|
schedule();
|
|
8038
8590
|
return;
|
|
@@ -8058,10 +8610,7 @@ function invalid(detail) {
|
|
|
8058
8610
|
emitDiagnostic({ code: "INVALID_AFFECTS_TARGET", kind: "write", severity: "error", message });
|
|
8059
8611
|
throw new Error(message);
|
|
8060
8612
|
}
|
|
8061
|
-
|
|
8062
|
-
GlobalQueue._mark = mark;
|
|
8063
|
-
GlobalQueue._releaseMarks = release$1;
|
|
8064
|
-
GlobalQueue._releaseAmbientMarks = parked => {
|
|
8613
|
+
installMarkHooks(release$1, parked => {
|
|
8065
8614
|
if (ambient.length === 0) return;
|
|
8066
8615
|
// The flush parked: the declaration's window is the frame's — the marks
|
|
8067
8616
|
// are its transaction's ("nothing async below ⇒ no window").
|
|
@@ -8084,7 +8633,7 @@ GlobalQueue._releaseAmbientMarks = parked => {
|
|
|
8084
8633
|
}
|
|
8085
8634
|
ambient.length = kept;
|
|
8086
8635
|
if (done.length !== 0) release$1(done);
|
|
8087
|
-
};
|
|
8636
|
+
});
|
|
8088
8637
|
function inFlight(n) {
|
|
8089
8638
|
return (
|
|
8090
8639
|
(n._statusFlags & STATUS_PENDING) !== 0 ||
|
|
@@ -8092,6 +8641,174 @@ function inFlight(n) {
|
|
|
8092
8641
|
);
|
|
8093
8642
|
}
|
|
8094
8643
|
|
|
8644
|
+
// `createDeferred` — an async memo that may lag, but never leads (D-40,
|
|
8645
|
+
// docs/create-deferred.md).
|
|
8646
|
+
//
|
|
8647
|
+
// Once the node has a committed value, its own non-final work is invisible
|
|
8648
|
+
// to the graph: its readers keep the committed value and hold nothing, its
|
|
8649
|
+
// landings are changes of their own, and only the verdict (`isPending`) and
|
|
8650
|
+
// the truth readers (`until`, `refresh`'s waiter) see the flight.
|
|
8651
|
+
//
|
|
8652
|
+
// Built from parts the engine already has, so core pays nothing for it:
|
|
8653
|
+
//
|
|
8654
|
+
// - **Read side: the loading window** (`_loading`, async.ts). Every pass of
|
|
8655
|
+
// a node with a committed value opens the window before it asks, so a
|
|
8656
|
+
// flight serves the committed value instead of throwing, and an unready
|
|
8657
|
+
// source parks the node instead of pending it. `CONFIG_REDERIVE` makes a
|
|
8658
|
+
// source going pending a question for the node's pass (which parks) rather
|
|
8659
|
+
// than a status pushed through it to its readers.
|
|
8660
|
+
// - **Verdict: a mark** (affects-marks.ts) whose lifetime is the flight —
|
|
8661
|
+
// started when a pass asks a new question (a `refresh()` re-ask is quiet, F-9),
|
|
8662
|
+
// dropped at the landing, the rejection, a sync answer or the node's death.
|
|
8663
|
+
// - **Never leads:** a flight that lands while an input it read is held
|
|
8664
|
+
// derived from that hold's world, so it lands into the hold (`holdNode`;
|
|
8665
|
+
// `setSignal` stages it there). An input that changed after the pass
|
|
8666
|
+
// would have superseded the flight, so a held input at the landing is one
|
|
8667
|
+
// the pass read held.
|
|
8668
|
+
// - **Truth readers** wait the flight out (`GlobalQueue._lagging`, signals.ts
|
|
8669
|
+
// `watch`): `await refresh(d)` delivers the landing, `until` sees only
|
|
8670
|
+
// settled values.
|
|
8671
|
+
/** Deferred nodes with a flight up. */
|
|
8672
|
+
const lagging = new Set();
|
|
8673
|
+
/** The deferred node with a flight up that `c`'s value derives from, if
|
|
8674
|
+
* any: `c` itself, or one reached through its current dependencies. */
|
|
8675
|
+
function lagOf(c, seen) {
|
|
8676
|
+
if (lagging.has(c)) return c;
|
|
8677
|
+
if (seen.has(c)) return;
|
|
8678
|
+
seen.add(c);
|
|
8679
|
+
if (c._config & CONFIG_SLOT_NODE) return lagOf(c._host?.fam?.node ?? c, seen);
|
|
8680
|
+
const tail = c._flags & REACTIVE_RECOMPUTING_DEPS ? c._depsTail : undefined;
|
|
8681
|
+
if (tail !== null)
|
|
8682
|
+
for (let d = c._deps ?? null; d !== null; d = d._nextDep) {
|
|
8683
|
+
const l = lagOf(d._dep, seen);
|
|
8684
|
+
if (l) return l;
|
|
8685
|
+
if (d === tail) break;
|
|
8686
|
+
}
|
|
8687
|
+
}
|
|
8688
|
+
GlobalQueue._lagging = c => (lagging.size !== 0 ? lagOf(c, new Set()) : undefined);
|
|
8689
|
+
/**
|
|
8690
|
+
* Creates an async memo that may lag, but never leads. Before its first
|
|
8691
|
+
* answer it is an ordinary async memo: readers suspend to the nearest
|
|
8692
|
+
* `<Loading>`. After it, a refetch holds nothing: readers keep the last
|
|
8693
|
+
* committed value until the new one lands, and the rest of the screen moves
|
|
8694
|
+
* on without it. `isPending` reports the refetch, on the node and on what
|
|
8695
|
+
* derives from it. Use it for independent widgets (dashboards, feeds,
|
|
8696
|
+
* search results) where the previous answer with a "refreshing" indicator is
|
|
8697
|
+
* better than waiting with everything else.
|
|
8698
|
+
*
|
|
8699
|
+
* - It never shows a value ahead of a held change: a refetch that read a
|
|
8700
|
+
* held value lands with that hold.
|
|
8701
|
+
* - A rejected refetch errors like any memo; the stale value never masks it.
|
|
8702
|
+
* - `refresh(d)` is a quiet re-ask, and `await refresh(d)` and `until`
|
|
8703
|
+
* wait for the landing rather than the stale value.
|
|
8704
|
+
* - Combine with `loadingValue` for a node that never suspends at all.
|
|
8705
|
+
*
|
|
8706
|
+
* @example
|
|
8707
|
+
* ```tsx
|
|
8708
|
+
* const rows = createDeferred(() => fetchRows(store.period));
|
|
8709
|
+
*
|
|
8710
|
+
* <section class={{ stale: isPending(() => rows()) }}>
|
|
8711
|
+
* <For each={rows()}>{row => <Row row={row} />}</For>
|
|
8712
|
+
* </section>
|
|
8713
|
+
* ```
|
|
8714
|
+
*/
|
|
8715
|
+
function createDeferred(fn, options) {
|
|
8716
|
+
let node;
|
|
8717
|
+
/** The flight in force: a promise, a stream, or the error an unready
|
|
8718
|
+
* source parked the node with. */
|
|
8719
|
+
let flight = null;
|
|
8720
|
+
let marked = false;
|
|
8721
|
+
let answered = false;
|
|
8722
|
+
/** The flight `r` is over. An async answer (`held`) arriving while an
|
|
8723
|
+
* input is held lands into that hold first. */
|
|
8724
|
+
const land = (r, held) => {
|
|
8725
|
+
if (r === null || flight !== r) return;
|
|
8726
|
+
flight = null;
|
|
8727
|
+
lagging.delete(node);
|
|
8728
|
+
if (held)
|
|
8729
|
+
for (let d = node._deps; d !== null; d = d._nextDep)
|
|
8730
|
+
if (d._dep._config & CONFIG_HELD) {
|
|
8731
|
+
holdNode(node, txOf(d._dep));
|
|
8732
|
+
break;
|
|
8733
|
+
}
|
|
8734
|
+
if (marked) {
|
|
8735
|
+
marked = false;
|
|
8736
|
+
unmark(node);
|
|
8737
|
+
schedule();
|
|
8738
|
+
}
|
|
8739
|
+
};
|
|
8740
|
+
const ask = r => {
|
|
8741
|
+
flight = r;
|
|
8742
|
+
lagging.add(node);
|
|
8743
|
+
if (!marked && !(node._flags & REACTIVE_REASK)) {
|
|
8744
|
+
marked = true;
|
|
8745
|
+
mark(node);
|
|
8746
|
+
repoll(node);
|
|
8747
|
+
}
|
|
8748
|
+
};
|
|
8749
|
+
/** Registers the flight and serves the pass's value: the committed one,
|
|
8750
|
+
* or over a held landing the landing itself, which a lagging question
|
|
8751
|
+
* does not replace (D-40) — restaged as the committed value, the hold's
|
|
8752
|
+
* readers would re-derive against the old input. */
|
|
8753
|
+
const serve = r => {
|
|
8754
|
+
const v = handleAsync(node, r);
|
|
8755
|
+
return node._config & CONFIG_HELD && node._pendingValue !== NOT_PENDING
|
|
8756
|
+
? node._pendingValue
|
|
8757
|
+
: v;
|
|
8758
|
+
};
|
|
8759
|
+
const end = () => node._flags & REACTIVE_DISPOSED && land(flight);
|
|
8760
|
+
return accessor(
|
|
8761
|
+
computed(prev => {
|
|
8762
|
+
node = getOwner();
|
|
8763
|
+
node._config |= CONFIG_REDERIVE;
|
|
8764
|
+
cleanup(end);
|
|
8765
|
+
// Answered: the node has a real committed answer — not a first load,
|
|
8766
|
+
// not a `loadingValue` window still open (both are loading, F-9).
|
|
8767
|
+
// Sticky, and read before this wrapper ever opens the window itself.
|
|
8768
|
+
const open = (answered ||= !(node._statusFlags & STATUS_UNINITIALIZED) && !node._loading);
|
|
8769
|
+
if (open) ((node._loading = true), (node._config |= CONFIG_LAGS));
|
|
8770
|
+
let r;
|
|
8771
|
+
try {
|
|
8772
|
+
r = fn(prev);
|
|
8773
|
+
} catch (e) {
|
|
8774
|
+
// The wrapping form: an unready source parks the node in the window,
|
|
8775
|
+
// and the wait is its flight until the source's settle re-runs it.
|
|
8776
|
+
if (open && e instanceof NotReadyError) ask(e);
|
|
8777
|
+
throw e;
|
|
8778
|
+
}
|
|
8779
|
+
if (open && typeof r === "object" && r !== null) {
|
|
8780
|
+
if (isThenable(r)) {
|
|
8781
|
+
ask(r);
|
|
8782
|
+
const done = () => land(r, true);
|
|
8783
|
+
r.then(done, done);
|
|
8784
|
+
return serve(r);
|
|
8785
|
+
}
|
|
8786
|
+
const iterate = r[Symbol.asyncIterator];
|
|
8787
|
+
if (iterate) {
|
|
8788
|
+
// A stream's flight ends at its first yield (or its end).
|
|
8789
|
+
const it = iterate.call(r);
|
|
8790
|
+
let first = true;
|
|
8791
|
+
const done = () => land(s, true);
|
|
8792
|
+
const s = {
|
|
8793
|
+
// Registered ahead of the engine's own reaction to the same
|
|
8794
|
+
// promise, as for a plain flight: verdict and value in one flush.
|
|
8795
|
+
next: v => {
|
|
8796
|
+
const p = it.next(v);
|
|
8797
|
+
if (first) ((first = false), p.then(done, done));
|
|
8798
|
+
return p;
|
|
8799
|
+
},
|
|
8800
|
+
return: v => it.return?.(v) ?? Promise.resolve({ done: true, value: v })
|
|
8801
|
+
};
|
|
8802
|
+
ask(s);
|
|
8803
|
+
return serve({ [Symbol.asyncIterator]: () => s });
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
land(flight);
|
|
8807
|
+
return r;
|
|
8808
|
+
}, options)
|
|
8809
|
+
);
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8095
8812
|
// CARVE 4: `flatten` lived in boundaries.ts but is the general children
|
|
8096
8813
|
// resolver the renderers (`web`, `universal`, `solid`'s `children()`) walk
|
|
8097
8814
|
// through — not a boundary feature. Moved verbatim so the boundary carve
|
|
@@ -8183,7 +8900,7 @@ function flattenArray(children, results = [], options) {
|
|
|
8183
8900
|
* the root holds the frame as for any reader. The tree's own status takes
|
|
8184
8901
|
* the same route from the output's pass (`read(tree)` throws it).
|
|
8185
8902
|
*
|
|
8186
|
-
* The hold (`blocked`, scheduler.ts) asks `
|
|
8903
|
+
* The hold (`blocked`, scheduler.ts) asks `hiddenBy`: a reader behind a
|
|
8187
8904
|
* fallback — any boundary on its chain whose last pass chose the fallback —
|
|
8188
8905
|
* is not on screen and holds nothing. An error boundary flipping to its
|
|
8189
8906
|
* fallback is the same as a loading one resetting: every reader under it
|
|
@@ -8218,6 +8935,11 @@ const boundaryOf = node => node._context[BOUNDARY];
|
|
|
8218
8935
|
/** Catches what reaches it: an error boundary always; a loading one until it
|
|
8219
8936
|
* has shown content, or while re-armed. */
|
|
8220
8937
|
const isCollecting = b => b._type === STATUS_ERROR || !b._initialized || b._armed;
|
|
8938
|
+
/** A reader `b` waits on: recorded for its `on`, and `b` visited at the seam. */
|
|
8939
|
+
function record(b, node) {
|
|
8940
|
+
b._readers.add(node);
|
|
8941
|
+
collecting.add(b);
|
|
8942
|
+
}
|
|
8221
8943
|
/** The reader goes to the fallback: recorded, and the output re-derives
|
|
8222
8944
|
* (a no-op while the output's own pass is running — its post-check
|
|
8223
8945
|
* decides). */
|
|
@@ -8234,8 +8956,7 @@ function caught(b, node, error) {
|
|
|
8234
8956
|
setSignal(b._error, caught);
|
|
8235
8957
|
reportClientError(caught, b._owner, node);
|
|
8236
8958
|
}
|
|
8237
|
-
b
|
|
8238
|
-
collecting.add(b);
|
|
8959
|
+
record(b, node);
|
|
8239
8960
|
if (b._armed) flip(b);
|
|
8240
8961
|
// Already showing the fallback: one more reader to wait on changes
|
|
8241
8962
|
// nothing on screen. The fallback is instantiated once per show — a later
|
|
@@ -8251,8 +8972,9 @@ function caught(b, node, error) {
|
|
|
8251
8972
|
function flip(b) {
|
|
8252
8973
|
b._initialized = false;
|
|
8253
8974
|
b._lane = b._armLane;
|
|
8254
|
-
|
|
8255
|
-
|
|
8975
|
+
const o = b._output;
|
|
8976
|
+
if (!b._lane && o._config & CONFIG_HELD && txOf(o) !== flushTransaction) unhold(o);
|
|
8977
|
+
if (b._lane !== null && b._output !== null) laneStageHook(b._output, b._lane, true, false);
|
|
8256
8978
|
}
|
|
8257
8979
|
function redraw(b) {
|
|
8258
8980
|
if (b._output !== null && !(b._output._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
|
|
@@ -8260,7 +8982,7 @@ function redraw(b) {
|
|
|
8260
8982
|
schedule();
|
|
8261
8983
|
}
|
|
8262
8984
|
}
|
|
8263
|
-
/**
|
|
8985
|
+
/** `catchHook` (scheduler.ts): status from a frame reader, nearest boundary first.
|
|
8264
8986
|
* A loading boundary on the way records a pending reader whether or not it
|
|
8265
8987
|
* catches it (its `on` may collect it later). */
|
|
8266
8988
|
function catchStatus(node, flags, error) {
|
|
@@ -8273,7 +8995,7 @@ function catchStatus(node, flags, error) {
|
|
|
8273
8995
|
// lands with its run, not a round after). Others still waiting: nothing
|
|
8274
8996
|
// on screen changes, the fallback stays as it is.
|
|
8275
8997
|
if (collecting.size !== 0)
|
|
8276
|
-
for (let b = boundaryOf(node); b
|
|
8998
|
+
for (let b = boundaryOf(node); b; b = b._parent)
|
|
8277
8999
|
if (
|
|
8278
9000
|
!(node._statusFlags & unsettled(b)) &&
|
|
8279
9001
|
b._readers.delete(node) &&
|
|
@@ -8284,28 +9006,29 @@ function catchStatus(node, flags, error) {
|
|
|
8284
9006
|
}
|
|
8285
9007
|
return false;
|
|
8286
9008
|
}
|
|
8287
|
-
for (let b = boundaryOf(node); b
|
|
9009
|
+
for (let b = boundaryOf(node); b; b = b._parent) {
|
|
8288
9010
|
if (!(b._type & flags)) continue;
|
|
8289
9011
|
if (isCollecting(b)) {
|
|
8290
9012
|
caught(b, node, error);
|
|
8291
|
-
return true;
|
|
9013
|
+
return b._armed ? CAUGHT_REARMED : true;
|
|
8292
9014
|
}
|
|
8293
|
-
b
|
|
8294
|
-
collecting.add(b);
|
|
9015
|
+
record(b, node);
|
|
8295
9016
|
}
|
|
8296
9017
|
return false;
|
|
8297
9018
|
}
|
|
8298
|
-
/** The nearest boundary
|
|
9019
|
+
/** The nearest loading boundary that would catch `node` was re-armed this
|
|
9020
|
+
* flush. */
|
|
9021
|
+
function armedOver(node) {
|
|
9022
|
+
for (let b = boundaryOf(node); b; b = b._parent)
|
|
9023
|
+
if (b._type & STATUS_PENDING && isCollecting(b)) return b._armed;
|
|
9024
|
+
}
|
|
9025
|
+
/** The nearest boundary on `r`'s chain showing its fallback, if any
|
|
9026
|
+
* (`hiddenHook`: `r` is behind a fallback). */
|
|
8299
9027
|
function hiddenBy(r) {
|
|
8300
|
-
for (let b = boundaryOf(r); b
|
|
8301
|
-
if (b._fallback) return b;
|
|
9028
|
+
for (let b = boundaryOf(r); b; b = b._parent) if (b._fallback) return b;
|
|
8302
9029
|
return undefined;
|
|
8303
9030
|
}
|
|
8304
|
-
/**
|
|
8305
|
-
function hidden(r) {
|
|
8306
|
-
return hiddenBy(r) !== undefined;
|
|
8307
|
-
}
|
|
8308
|
-
/** GlobalQueue._heldRun: a queued run behind a fallback waits for the reveal
|
|
9031
|
+
/** `heldRunHook` (scheduler.ts): a queued run behind a fallback waits for the reveal
|
|
8309
9032
|
* (maintainer, 2026-10-02: the queue is held, the synchronous first render
|
|
8310
9033
|
* goes through). A user effect's, which would read a DOM that is not
|
|
8311
9034
|
* attached ("many depend on reading the DOM, something that can't happen
|
|
@@ -8350,12 +9073,18 @@ function prune(b, pass) {
|
|
|
8350
9073
|
for (const r of b._readers) {
|
|
8351
9074
|
if (r._flags & REACTIVE_DISPOSED) b._readers.delete(r);
|
|
8352
9075
|
else if (!(r._statusFlags & mask)) {
|
|
8353
|
-
if (
|
|
9076
|
+
if (
|
|
9077
|
+
(r !== b._tree || b._armed) &&
|
|
9078
|
+
(r._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD
|
|
9079
|
+
) {
|
|
8354
9080
|
// Landed, held: the content is that transaction's — the output's
|
|
8355
9081
|
// pass enters it and the reveal lands with the reader's run. The
|
|
8356
9082
|
// seam, past its park decision, waits for the commit instead. (A
|
|
8357
|
-
// lane's reader reveals with the lane, this round.)
|
|
8358
|
-
|
|
9083
|
+
// lane's reader reveals with the lane, this round.) A boundary that
|
|
9084
|
+
// has not shown content catches it instead: the fallback stays until
|
|
9085
|
+
// the reader is committed (#3540: what a fresh boundary catches holds
|
|
9086
|
+
// nothing outside it).
|
|
9087
|
+
if (!pass || !b._initialized) continue;
|
|
8359
9088
|
joinFuture(txOf(r));
|
|
8360
9089
|
}
|
|
8361
9090
|
b._readers.delete(r);
|
|
@@ -8377,7 +9106,7 @@ function ready(b) {
|
|
|
8377
9106
|
prune(b, false) === 0)
|
|
8378
9107
|
);
|
|
8379
9108
|
}
|
|
8380
|
-
/**
|
|
9109
|
+
/** `boundarySeamHook` (scheduler.ts), end of the seam: readers that settled, died,
|
|
8381
9110
|
* or landed and committed are dropped; a fallback with none left reveals
|
|
8382
9111
|
* next round. An arm resolves: nothing collected, nothing happened. */
|
|
8383
9112
|
function boundarySeam() {
|
|
@@ -8390,6 +9119,18 @@ function boundarySeam() {
|
|
|
8390
9119
|
// A parked frame's boundary (zombie) keeps its state for its revival.
|
|
8391
9120
|
if (flags & REACTIVE_ZOMBIE) continue;
|
|
8392
9121
|
prune(b, false);
|
|
9122
|
+
const o = b._output;
|
|
9123
|
+
if (
|
|
9124
|
+
b._armed &&
|
|
9125
|
+
!b._initialized &&
|
|
9126
|
+
o._config & CONFIG_HELD &&
|
|
9127
|
+
[...b._readers].every(
|
|
9128
|
+
r => !(r._statusFlags & STATUS_PENDING) && r._config & CONFIG_HELD && txOf(r) === txOf(o)
|
|
9129
|
+
)
|
|
9130
|
+
) {
|
|
9131
|
+
b._readers.clear();
|
|
9132
|
+
b._initialized = true;
|
|
9133
|
+
}
|
|
8393
9134
|
if (b._armed && !b._initialized && !b._ahead) reportOutsideHold(b);
|
|
8394
9135
|
b._armed = false;
|
|
8395
9136
|
if (b._readers.size === 0) {
|
|
@@ -8416,15 +9157,17 @@ function reportOutsideHold(b) {
|
|
|
8416
9157
|
if (outside(s._sub, false)) return true;
|
|
8417
9158
|
return false;
|
|
8418
9159
|
}
|
|
8419
|
-
if (
|
|
8420
|
-
for (let o = boundaryOf(reader); o
|
|
8421
|
-
if (o === b) return false;
|
|
9160
|
+
if (hiddenBy(reader)) return false;
|
|
9161
|
+
for (let o = boundaryOf(reader); o; o = o._parent) if (o === b) return false;
|
|
8422
9162
|
return true;
|
|
8423
9163
|
};
|
|
8424
9164
|
for (const r of b._readers) {
|
|
8425
9165
|
const sources = r._x?._pendingSources;
|
|
8426
9166
|
if (sources === undefined) continue;
|
|
8427
9167
|
for (const source of sources) {
|
|
9168
|
+
// Only the flight the change itself started (D-16): an outside reader
|
|
9169
|
+
// of an earlier write's flight holds nothing the change waits on.
|
|
9170
|
+
if (source._time !== clock) continue;
|
|
8428
9171
|
for (let s = source._subs; s !== null; s = s._nextSub) {
|
|
8429
9172
|
if (!outside(s._sub, true)) continue;
|
|
8430
9173
|
const name = source._name;
|
|
@@ -8449,15 +9192,6 @@ function reportOutsideHold(b) {
|
|
|
8449
9192
|
}
|
|
8450
9193
|
}
|
|
8451
9194
|
}
|
|
8452
|
-
/** The `on` pass is a verdict's: it probed itself (CONFIG_VERDICT), or it
|
|
8453
|
-
* read a memo that did (#3528's `createMemo(() => isPending(m2))`) — its
|
|
8454
|
-
* swap is display-ahead either way. */
|
|
8455
|
-
function verdictDerived(c) {
|
|
8456
|
-
if (c._config & CONFIG_VERDICT) return true;
|
|
8457
|
-
for (let d = c._deps; d !== null; d = d._nextDep)
|
|
8458
|
-
if (d._dep._config & CONFIG_VERDICT) return true;
|
|
8459
|
-
return false;
|
|
8460
|
-
}
|
|
8461
9195
|
/** `on` changed after the mount: fresh again if anything under it is
|
|
8462
9196
|
* pending now; collecting for the rest of the flush either way. */
|
|
8463
9197
|
function arm(b) {
|
|
@@ -8481,20 +9215,32 @@ function arm(b) {
|
|
|
8481
9215
|
return;
|
|
8482
9216
|
}
|
|
8483
9217
|
b._armed = true;
|
|
8484
|
-
// A verdict read in `on` (`isPending
|
|
8485
|
-
//
|
|
9218
|
+
// A verdict read in `on` (`isPending`, or a memo of one — #3528's
|
|
9219
|
+
// `createMemo(() => isPending(m2))`: the mark carries, core.ts `read`)
|
|
9220
|
+
// whose frame has no transaction yet: the swap is still display-ahead — the frame gets one now (it lands
|
|
8486
9221
|
// at this very seam if nothing holds it), and the swap is its verdict
|
|
8487
9222
|
// lane's work, shown ahead of the frame's own runs (#3528).
|
|
8488
9223
|
let lane = passLane;
|
|
8489
|
-
if (lane === null && globalQueue._running &&
|
|
9224
|
+
if (lane === null && globalQueue._running && getOwner()._config & CONFIG_VERDICT) {
|
|
8490
9225
|
joinFuture(null);
|
|
8491
9226
|
lane = GlobalQueue._verdictLane(flushTransaction);
|
|
8492
9227
|
}
|
|
8493
9228
|
b._armLane = lane;
|
|
8494
9229
|
b._ahead = lane !== null;
|
|
8495
9230
|
collecting.add(b);
|
|
9231
|
+
const n = b._readers.size;
|
|
9232
|
+
heldUnder(b._owner, flushTransaction, r => record(b, r));
|
|
8496
9233
|
for (const r of b._readers) {
|
|
8497
|
-
if (
|
|
9234
|
+
if (
|
|
9235
|
+
b._readers.size !== n ||
|
|
9236
|
+
(r._statusFlags & STATUS_PENDING && !(r._flags & REACTIVE_DISPOSED))
|
|
9237
|
+
) {
|
|
9238
|
+
// What it collects was forwarded through it to the boundaries above
|
|
9239
|
+
// too (`catchStatus` records a reader in every boundary on the way).
|
|
9240
|
+
// It is this boundary's alone now (B5, the nearest catcher): an outer
|
|
9241
|
+
// re-armed by the same tick does not wait for it (D-14, U-4).
|
|
9242
|
+
for (let a = b._parent; a !== null; a = a._parent)
|
|
9243
|
+
for (const o of b._readers) a._readers.delete(o);
|
|
8498
9244
|
flip(b);
|
|
8499
9245
|
redraw(b);
|
|
8500
9246
|
return;
|
|
@@ -8541,7 +9287,8 @@ function createBoundary(type, fn, show, onFn) {
|
|
|
8541
9287
|
_gated: false,
|
|
8542
9288
|
_collapsed: false,
|
|
8543
9289
|
_error: type === STATUS_ERROR ? signal(undefined, ERROR_SIGNAL) : null,
|
|
8544
|
-
_show: show
|
|
9290
|
+
_show: show,
|
|
9291
|
+
_fb: null
|
|
8545
9292
|
};
|
|
8546
9293
|
const controller = type === STATUS_PENDING ? owner._context[REVEAL] : null;
|
|
8547
9294
|
if (getOwner())
|
|
@@ -8626,8 +9373,7 @@ function createBoundary(type, fn, show, onFn) {
|
|
|
8626
9373
|
}
|
|
8627
9374
|
// Forwarded: a loading boundary showing content keeps it and
|
|
8628
9375
|
// holds the frame like any reader (recorded for its `on`).
|
|
8629
|
-
b
|
|
8630
|
-
collecting.add(b);
|
|
9376
|
+
record(b, tree);
|
|
8631
9377
|
}
|
|
8632
9378
|
// A pending passes an error boundary by (A6); an error, a loading
|
|
8633
9379
|
// one; a loading boundary showing content forwards its pending.
|
|
@@ -8667,8 +9413,7 @@ function createBoundary(type, fn, show, onFn) {
|
|
|
8667
9413
|
}
|
|
8668
9414
|
// Forwarded: a loading boundary showing content keeps it and
|
|
8669
9415
|
// holds the frame like any reader (recorded for its `on`).
|
|
8670
|
-
b
|
|
8671
|
-
collecting.add(b);
|
|
9416
|
+
record(b, tree);
|
|
8672
9417
|
}
|
|
8673
9418
|
// A pending passes an error boundary by (A6).
|
|
8674
9419
|
throw e;
|
|
@@ -8682,6 +9427,20 @@ function createBoundary(type, fn, show, onFn) {
|
|
|
8682
9427
|
if (b._fallback && b._type & STATUS_PENDING && attrHooks !== null)
|
|
8683
9428
|
attrHooks.boundaryFallback(b, b._tree, false);
|
|
8684
9429
|
b._fallback = false;
|
|
9430
|
+
// The fallback dies when the content that replaces it is on screen —
|
|
9431
|
+
// with this flush's commits when the pass is the frame's, at the
|
|
9432
|
+
// landing when the frame parks (the run is held with it, as the old
|
|
9433
|
+
// children of a held pass are) — not now: a held swap keeps the
|
|
9434
|
+
// fallback on screen, live, until it commits. A pass back to the
|
|
9435
|
+
// fallback before then finds it still built and keeps it.
|
|
9436
|
+
const fb = b._fb;
|
|
9437
|
+
if (fb !== null)
|
|
9438
|
+
globalQueue.enqueue(EFFECT_RENDER, () => {
|
|
9439
|
+
if (!b._fallback && b._fb === fb) {
|
|
9440
|
+
dispose(fb);
|
|
9441
|
+
b._fb = null;
|
|
9442
|
+
}
|
|
9443
|
+
});
|
|
8685
9444
|
release(b);
|
|
8686
9445
|
// Shown: the slot leaves its reveal order (a later re-arm gates nobody
|
|
8687
9446
|
// else; the frontier moves on).
|
|
@@ -8724,7 +9483,22 @@ function fallback(b) {
|
|
|
8724
9483
|
if (!globalQueue._running) schedule();
|
|
8725
9484
|
}
|
|
8726
9485
|
b._fallback = true;
|
|
8727
|
-
|
|
9486
|
+
// Built in its own scope, like `Show`'s function child, once per show
|
|
9487
|
+
// (D-39: the fallback is the display until a new answer replaces the
|
|
9488
|
+
// error): a re-derive of the output while it shows — a second reader
|
|
9489
|
+
// caught, the same error propagated again through a sibling's re-run, a
|
|
9490
|
+
// retry pending after `reset()`, a later error — keeps the built
|
|
9491
|
+
// fallback, its DOM and focus with it (#3991). A later error reaches it
|
|
9492
|
+
// through the error accessor; only a fallback reading the accessor at its
|
|
9493
|
+
// top level is rebuilt by it. Created beside the output — in its owner,
|
|
9494
|
+
// outside the boundary, as the output is: its errors and runs are the
|
|
9495
|
+
// enclosing boundary's — with the output's id, so what it builds gets the
|
|
9496
|
+
// ids a build under the output would; disposed once content is on screen.
|
|
9497
|
+
if (b._fb === null) {
|
|
9498
|
+
const out = getOwner();
|
|
9499
|
+
b._fb = runWithOwner(out._parent, () => computed(() => b._show(b), { id: out.id }));
|
|
9500
|
+
}
|
|
9501
|
+
return read(b._fb);
|
|
8728
9502
|
}
|
|
8729
9503
|
const ERROR_SIGNAL = { ownedWrite: true, _noSnapshot: true };
|
|
8730
9504
|
/**
|
|
@@ -8800,12 +9574,17 @@ function createErrorBoundary(fn, fallback) {
|
|
|
8800
9574
|
}
|
|
8801
9575
|
// Installed at module evaluation — present exactly when something imports a
|
|
8802
9576
|
// boundary. An app without one pays the three null checks and nothing else.
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
9577
|
+
installBoundaryHooks(
|
|
9578
|
+
catchStatus,
|
|
9579
|
+
(node, held) =>
|
|
9580
|
+
!held &&
|
|
9581
|
+
(node._statusFlags & STATUS_UNINITIALIZED
|
|
9582
|
+
? catchStatus(node, STATUS_PENDING)
|
|
9583
|
+
: armedOver(node)),
|
|
9584
|
+
hiddenBy,
|
|
9585
|
+
boundarySeam,
|
|
9586
|
+
heldRun
|
|
9587
|
+
);
|
|
8809
9588
|
|
|
8810
9589
|
/**
|
|
8811
9590
|
* Reveal order — `createRevealOrder` (`<Reveal>`): the order in which a group
|
|
@@ -9048,6 +9827,7 @@ export {
|
|
|
9048
9827
|
affects,
|
|
9049
9828
|
configureClientErrors,
|
|
9050
9829
|
createContext,
|
|
9830
|
+
createDeferred,
|
|
9051
9831
|
createEffect,
|
|
9052
9832
|
createErrorBoundary,
|
|
9053
9833
|
createLoadingBoundary,
|
|
@@ -9093,6 +9873,9 @@ export {
|
|
|
9093
9873
|
resolvedTable,
|
|
9094
9874
|
runWithOwner,
|
|
9095
9875
|
setContext,
|
|
9876
|
+
setReadWatch,
|
|
9877
|
+
setSignalIdentities,
|
|
9878
|
+
signalName,
|
|
9096
9879
|
snapshot,
|
|
9097
9880
|
sourceGet,
|
|
9098
9881
|
sourceHas,
|
|
@@ -9101,6 +9884,7 @@ export {
|
|
|
9101
9884
|
storeHasFamily,
|
|
9102
9885
|
storeHasOptimisticFamily,
|
|
9103
9886
|
storeIsShallow,
|
|
9887
|
+
storeName,
|
|
9104
9888
|
storePath,
|
|
9105
9889
|
until,
|
|
9106
9890
|
untrack,
|