@solidjs/signals 2.0.0-rc.8 → 2.0.0-rc.9
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 +1399 -285
- package/dist/dev.attribution.js +459 -307
- package/dist/dev.js +1864 -435
- package/dist/observe/affects.js +3 -1
- package/dist/observe/attribution.js +7 -1
- package/dist/observe/boundaries.js +209 -154
- package/dist/observe/core/action.js +18 -8
- package/dist/observe/core/async.js +220 -110
- package/dist/observe/core/attribution-costs.js +66 -0
- package/dist/observe/core/attribution-feedback.js +282 -0
- package/dist/observe/core/attribution-hooks.js +23 -1
- package/dist/observe/core/attribution-queries.js +28 -0
- package/dist/observe/core/attribution.js +262 -485
- package/dist/observe/core/constants.js +34 -1
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +867 -367
- package/dist/observe/core/dev.js +78 -17
- package/dist/observe/core/effect.js +67 -51
- package/dist/observe/core/error-hooks.js +71 -0
- package/dist/observe/core/external.js +4 -4
- package/dist/observe/core/graph.js +37 -37
- package/dist/observe/core/heap.js +45 -45
- package/dist/observe/core/invariants.js +2 -0
- package/dist/observe/core/lanes.js +86 -49
- package/dist/observe/core/optimistic.js +242 -95
- package/dist/observe/core/owner.js +98 -84
- package/dist/observe/core/scheduler.js +543 -305
- package/dist/observe/core/verdict.js +247 -129
- package/dist/observe/index.js +7 -3
- package/dist/observe/map.js +126 -124
- package/dist/observe/signals.js +33 -17
- package/dist/observe/store/index.js +2 -0
- package/dist/observe/store/next/optimistic.js +141 -132
- package/dist/observe/store/next/projection.js +34 -21
- package/dist/observe/store/next/reconcile.js +58 -56
- package/dist/observe/store/next/store.js +260 -146
- package/dist/observe/store/store.js +5 -3
- package/dist/observe/store/utils.js +948 -135
- package/dist/prod/attribution.js +26 -17
- package/dist/prod/boundaries.js +128 -76
- package/dist/prod/core/action.js +16 -8
- package/dist/prod/core/async.js +251 -143
- package/dist/prod/core/constants.js +34 -1
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +843 -347
- package/dist/prod/core/dev.js +17 -1
- package/dist/prod/core/effect.js +48 -34
- package/dist/prod/core/error-hooks.js +71 -0
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +37 -37
- package/dist/prod/core/heap.js +45 -45
- package/dist/prod/core/lanes.js +90 -53
- package/dist/prod/core/optimistic.js +247 -100
- package/dist/prod/core/owner.js +57 -45
- package/dist/prod/core/scheduler.js +543 -305
- package/dist/prod/core/verdict.js +245 -127
- package/dist/prod/index.js +7 -3
- package/dist/prod/map.js +112 -112
- package/dist/prod/signals.js +28 -12
- package/dist/prod/store/next/optimistic.js +135 -128
- package/dist/prod/store/next/projection.js +31 -20
- package/dist/prod/store/next/store.js +325 -252
- package/dist/prod/store/store.js +2 -2
- package/dist/prod/store/utils.js +946 -135
- package/dist/types/attribution.d.ts +7 -2
- package/dist/types/attribution.prod.d.ts +10 -1
- package/dist/types/boundaries.d.ts +10 -1
- package/dist/types/core/action.d.ts +12 -5
- package/dist/types/core/attribution-costs.d.ts +35 -0
- package/dist/types/core/attribution-feedback.d.ts +133 -0
- package/dist/types/core/attribution-hooks.d.ts +28 -3
- package/dist/types/core/attribution-queries.d.ts +10 -0
- package/dist/types/core/attribution.d.ts +51 -172
- package/dist/types/core/constants.d.ts +33 -0
- package/dist/types/core/core.d.ts +186 -5
- package/dist/types/core/dev.d.ts +129 -9
- package/dist/types/core/error-hooks.d.ts +71 -0
- package/dist/types/core/index.d.ts +3 -1
- package/dist/types/core/invariants.d.ts +4 -0
- package/dist/types/core/lanes.d.ts +31 -4
- package/dist/types/core/scheduler.d.ts +95 -2
- package/dist/types/core/types.d.ts +3 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/signals.d.ts +8 -0
- package/dist/types/store/index.d.ts +2 -1
- package/dist/types/store/next/optimistic.d.ts +1 -1
- package/dist/types/store/next/store.d.ts +6 -5
- package/dist/types/store/next/target.d.ts +1 -1
- package/dist/types/store/utils.d.ts +177 -6
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -24,126 +24,147 @@ import {
|
|
|
24
24
|
m as insertSubs,
|
|
25
25
|
n as schedule,
|
|
26
26
|
p as STATUS_PENDING,
|
|
27
|
-
q as
|
|
28
|
-
t as
|
|
29
|
-
v as
|
|
30
|
-
w as
|
|
31
|
-
x as
|
|
32
|
-
y as
|
|
27
|
+
q as CONFIG_DERIVED_OVERRIDE,
|
|
28
|
+
t as CONFIG_OPTIMISTIC,
|
|
29
|
+
v as CONFIG_OVERRIDE_SUPERSEDED,
|
|
30
|
+
w as hasActiveOverride,
|
|
31
|
+
x as NotReadyError,
|
|
32
|
+
y as activeLanes,
|
|
33
|
+
z as signalLanes,
|
|
34
|
+
A as laneHeld,
|
|
33
35
|
E as EFFECT_RENDER,
|
|
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
|
-
|
|
36
|
+
B as CONFIG_AUTHORITATIVE_OBSERVED,
|
|
37
|
+
D as findLane,
|
|
38
|
+
F as sourceObserved,
|
|
39
|
+
H as resolveLane,
|
|
40
|
+
I as stale,
|
|
41
|
+
J as readsHeldCommitted,
|
|
42
|
+
K as enterStagedRead,
|
|
43
|
+
L as currentTransition,
|
|
44
|
+
M as queuePendingNode,
|
|
45
|
+
P as latestReadActive,
|
|
46
|
+
Q as REACTIVE_MANUAL_WRITE,
|
|
47
|
+
T as currentOptimisticLane,
|
|
48
|
+
U as REACTIVE_OPTIMISTIC_DIRTY,
|
|
49
|
+
V as assignOrMergeLane,
|
|
50
|
+
W as LANE_RUN,
|
|
51
|
+
X as attrHooks,
|
|
52
|
+
Y as EFFECT_USER,
|
|
53
|
+
Z as REACTIVE_DISPOSED,
|
|
54
|
+
_ as dispose,
|
|
55
|
+
$ as insertIntoHeap,
|
|
56
|
+
a0 as queueFor,
|
|
57
|
+
a1 as visibleOverride,
|
|
58
|
+
a2 as unflushedValue,
|
|
59
|
+
a3 as context,
|
|
60
|
+
a4 as REACTIVE_RECOMPUTING_DEPS,
|
|
61
|
+
a5 as markLateLinker,
|
|
62
|
+
a6 as prepareComputed,
|
|
63
|
+
a7 as tracking,
|
|
64
|
+
a8 as link,
|
|
65
|
+
a9 as enqueueSub,
|
|
66
|
+
aa as setLatestReadActive,
|
|
67
|
+
ab as setContextInternal,
|
|
68
|
+
ac as optimisticComputed,
|
|
69
|
+
ad as CONFIG_HAS_COMPANIONS,
|
|
70
|
+
ae as unflushed,
|
|
71
|
+
af as CONFIG_ADOPTED_UNFLUSHED,
|
|
72
|
+
ag as CONFIG_CHILD_COMPANIONS,
|
|
73
|
+
ah as runAsTransitionBatch,
|
|
74
|
+
ai as unflushedCompanions,
|
|
75
|
+
aj as setPendingCheckActive,
|
|
76
|
+
ak as setStrictRead,
|
|
77
|
+
al as strictRead,
|
|
78
|
+
am as optimisticSignal,
|
|
79
|
+
an as activeAffectsMarks,
|
|
80
|
+
ao as pendingCheckActive,
|
|
81
|
+
ap as STATUS_ERROR,
|
|
82
|
+
aq as setEffectStatusNotify,
|
|
83
|
+
ar as unwrapStatusError,
|
|
84
|
+
as as haltReactivity,
|
|
85
|
+
at as StatusError,
|
|
86
|
+
au as trimStaleDeps,
|
|
87
|
+
av as createEffectNode,
|
|
88
|
+
aw as recompute,
|
|
89
|
+
ax as reportDiagnostic,
|
|
90
|
+
ay as emitDiagnostic,
|
|
91
|
+
az as computed,
|
|
92
|
+
aA as staleValues,
|
|
93
|
+
aB as CONFIG_AUTO_DISPOSE,
|
|
94
|
+
aC as CONFIG_CHILDREN_FORBIDDEN,
|
|
95
|
+
aD as EFFECT_TRACKED,
|
|
96
|
+
aE as setEffectCallback,
|
|
97
|
+
aF as setTrackedQueueCallback,
|
|
98
|
+
aG as _hitUnhandledAsync,
|
|
99
|
+
aH as resetUnhandledAsync,
|
|
100
|
+
aI as setOrigin,
|
|
101
|
+
aJ as isThenable,
|
|
102
|
+
aK as actionStepDepth,
|
|
103
|
+
aL as flush,
|
|
104
|
+
aM as enterActionStep,
|
|
105
|
+
aN as exitActionStep,
|
|
106
|
+
aO as ROOT_ERROR_HOOK,
|
|
107
|
+
aP as registerGraph,
|
|
108
|
+
aQ as runWithOwner,
|
|
109
|
+
aR as setMemo,
|
|
110
|
+
aS as dirtyQueue,
|
|
111
|
+
aT as $REFRESH,
|
|
112
|
+
aU as installAuthoritativeRead,
|
|
113
|
+
aV as markRefresh,
|
|
114
|
+
aW as createRoot,
|
|
115
|
+
aX as getObserver,
|
|
116
|
+
aY as CONFIG_DIRECT_COMMIT,
|
|
117
|
+
aZ as CONFIG_AUTHORITATIVE_READ,
|
|
118
|
+
a_ as entangleConfirmingTransitions,
|
|
119
|
+
a$ as TimeoutError,
|
|
120
|
+
b0 as untrack,
|
|
121
|
+
b1 as Queue,
|
|
122
|
+
b2 as CONFIG_FRESH_READ,
|
|
123
|
+
b3 as forEachDependent,
|
|
124
|
+
b4 as statusNotifierOf,
|
|
125
|
+
b5 as shiftAffectsMarks,
|
|
126
|
+
b6 as SUPPORTS_PROXY,
|
|
127
|
+
b7 as setSlotUnobserved,
|
|
128
|
+
b8 as devGuardStoreSetterWrite,
|
|
129
|
+
b9 as devGuardStoreSetterResult,
|
|
130
|
+
ba as projectionWriteActive,
|
|
131
|
+
bb as setProjectionWriteActive,
|
|
132
|
+
bc as DEV$1,
|
|
133
|
+
bd as isEqual,
|
|
134
|
+
be as readNodeFast,
|
|
135
|
+
bf as READ_SLOW,
|
|
136
|
+
bg as throwPendingUntrackedRead,
|
|
137
|
+
bh as warnStrictReadUntracked,
|
|
138
|
+
bi as unlinkFirewallChild,
|
|
139
|
+
bj as CONFIG_OWNED_WRITE,
|
|
140
|
+
bk as ownsHold,
|
|
141
|
+
bl as deferSlotRelease,
|
|
142
|
+
bm as slotSignal,
|
|
143
|
+
bn as serve,
|
|
144
|
+
bo as recordStaleReplay,
|
|
145
|
+
bp as setStoreCommitHook,
|
|
146
|
+
bq as suppressComputedRecompute,
|
|
147
|
+
br as handleAsync,
|
|
148
|
+
bs as createTransition,
|
|
149
|
+
bt as CONFIG_HELD_TRUTH,
|
|
150
|
+
bu as createOwner,
|
|
151
|
+
bv as spectate,
|
|
152
|
+
bw as transitions,
|
|
153
|
+
bx as wakeParked,
|
|
154
|
+
by as REACTIVE_ZOMBIE,
|
|
155
|
+
bz as OBSERVE$1
|
|
136
156
|
} from "./dev-shared.js";
|
|
137
157
|
export {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
bA as clearSnapshots,
|
|
159
|
+
bB as enforceLoadingBoundary,
|
|
160
|
+
bC as getNextChildId,
|
|
161
|
+
bD as isDisposed,
|
|
162
|
+
bE as markSnapshotScope,
|
|
163
|
+
bF as ownerPath,
|
|
164
|
+
bG as peekNextChildId,
|
|
165
|
+
bH as releaseSnapshotScope,
|
|
166
|
+
bI as resetErrorHalt,
|
|
167
|
+
bJ as setSnapshotCapture
|
|
147
168
|
} from "./dev-shared.js";
|
|
148
169
|
|
|
149
170
|
let externalSourceConfig = null;
|
|
@@ -341,7 +362,7 @@ function optimisticWrite(el, v) {
|
|
|
341
362
|
ext(el)._optimisticLane = lane;
|
|
342
363
|
// A fresh override re-masks: whatever truth is staged, this write is the
|
|
343
364
|
// value for the graph again until the source answers it (#3331).
|
|
344
|
-
el._config = (el._config | CONFIG_HAS_LANE) &
|
|
365
|
+
el._config = (el._config | CONFIG_HAS_LANE) & -8912897;
|
|
345
366
|
// Literal undefined must not land raw: the slot doubles as the optimistic
|
|
346
367
|
// brand, and erasing it makes the write invisible and routes follow-up
|
|
347
368
|
// writes off the optimistic path into permanent commits (#2898).
|
|
@@ -356,16 +377,72 @@ function optimisticWrite(el, v) {
|
|
|
356
377
|
schedule();
|
|
357
378
|
return v;
|
|
358
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Lanes stage (#3479): a lane pass's publish for a memo. An optimistic
|
|
382
|
+
* derivation is an override — the speculative result lives in the override
|
|
383
|
+
* slot, `_value` stays the committed truth. The whole optimistic frame is then
|
|
384
|
+
* in one place: the lane's readers and untracked reads see it (A17), a render
|
|
385
|
+
* effect off the held lane sees the committed frame whole (readsHeldCommitted,
|
|
386
|
+
* #3460) — the source's shadow AND its derivations — where a speculative
|
|
387
|
+
* `_value` beside a committed shadow tore it. The node joins the
|
|
388
|
+
* transaction's optimistic nodes on its first speculative publish; the revert
|
|
389
|
+
* drops the override and re-derives it from the truth (a derived override has
|
|
390
|
+
* no truth of its own — see resolveOptimisticNodes, endOptimism).
|
|
391
|
+
*/
|
|
392
|
+
function laneOverride(el, value, lane) {
|
|
393
|
+
// The wake-only channel (#3009, see recomputeLane): a plain write to a
|
|
394
|
+
// latest()-tracked source rides a companion-sourced lane with no
|
|
395
|
+
// transaction on either side only to wake the verdict companions. Nothing
|
|
396
|
+
// is speculative — the pass commits directly, as any plain write does.
|
|
397
|
+
lane = findLane(lane);
|
|
398
|
+
if (!lane._transition && !activeTransition && lane._source._x?._parentSource !== undefined) {
|
|
399
|
+
el._value = value;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (!hasActiveOverride(el)) {
|
|
403
|
+
// It reverts with the lane's transaction (a landing runs outside any
|
|
404
|
+
// flush, where the ambient batch would revert it at its own end); an
|
|
405
|
+
// orphan lane's falls to the batch, adopted with it (initTransition) as a
|
|
406
|
+
// write's is. No `_overrideOwner`: a derived override is a plain member,
|
|
407
|
+
// its transaction its lane's (resolveTransition), and it merges lanes
|
|
408
|
+
// through itself as any shared reader does (assignOrMergeLane). No
|
|
409
|
+
// provenance stamp either: not an intent, any truth supersedes it.
|
|
410
|
+
(lane._transition
|
|
411
|
+
? currentTransition(lane._transition)
|
|
412
|
+
: globalQueue._batch
|
|
413
|
+
)._optimisticNodes.push(el);
|
|
414
|
+
}
|
|
415
|
+
// A lane pass's output is a derivation — also over a WRITTEN guess it
|
|
416
|
+
// corrects (a `createOptimistic(fn)` re-derived from fresh upstream data):
|
|
417
|
+
// the guess is gone, the slot holds fn's answer, and the revert promotes it
|
|
418
|
+
// rather than dropping to a stale `_value` and re-asking downstream (the
|
|
419
|
+
// next user write re-arms the guess: optimisticWrite clears the bit). No
|
|
420
|
+
// `_overrideTime` stamp: that marks a user WRITE unflushed until the flush
|
|
421
|
+
// that carries it (A28) and shields it from same-tick supersession — a pass's
|
|
422
|
+
// result is neither (a pulled ownerless memo publishes outside any flush).
|
|
423
|
+
// A fresh lane frame ends a supersession in force: the pass just dropped
|
|
424
|
+
// the staged truth it pointed at (recompute, INV-11 corollary) — left set,
|
|
425
|
+
// the flag served a `_value` never committed (fuzzer latest-1 #2481).
|
|
426
|
+
el._config = (el._config | CONFIG_DERIVED_OVERRIDE) & ~CONFIG_OVERRIDE_SUPERSEDED;
|
|
427
|
+
el._x._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
|
|
428
|
+
}
|
|
359
429
|
/**
|
|
360
430
|
* transitionComplete's override blockage: a settling transition stays open
|
|
361
431
|
* while one of its optimistic nodes holds an active override that is still
|
|
362
|
-
* pending on real (non-affects-sentinel) async.
|
|
432
|
+
* pending on real (non-affects-sentinel) async. A derived override's flight is
|
|
433
|
+
* the lane's own work, never authoritative — it does not hold the settle.
|
|
434
|
+
* Neither is a companion's (#3494): the `latest()` shadow backfilled under the
|
|
435
|
+
* owner's transaction (A28 (3)) is an observation of the flight, and a
|
|
436
|
+
* mainline `latest(details)` after the flight's last reader unmounted held the
|
|
437
|
+
* released write until the orphaned request landed.
|
|
363
438
|
*/
|
|
364
439
|
function transitionBlocked(transition) {
|
|
365
440
|
for (let i = 0; i < transition._optimisticNodes.length; i++) {
|
|
366
441
|
const node = transition._optimisticNodes[i];
|
|
367
442
|
if (
|
|
368
|
-
|
|
443
|
+
!(node._config & CONFIG_DERIVED_OVERRIDE) &&
|
|
444
|
+
node._x?._parentSource === undefined &&
|
|
445
|
+
hasActiveOverride(node) &&
|
|
369
446
|
"_statusFlags" in node &&
|
|
370
447
|
node._statusFlags & STATUS_PENDING &&
|
|
371
448
|
node._x?._error instanceof NotReadyError
|
|
@@ -388,14 +465,31 @@ function resolveOptimisticNodes(nodes) {
|
|
|
388
465
|
// elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
|
|
389
466
|
if (!(node._statusFlags & STATUS_PENDING)) node._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
390
467
|
const prevOverride = node._x?._overrideValue;
|
|
391
|
-
|
|
468
|
+
// A derived override (lanes stage, #3479) has no truth of its own: the
|
|
469
|
+
// slot disarms — the memo is plain again — and the override PROMOTES to
|
|
470
|
+
// `_value`. Not superseded, nothing it derives from told it otherwise
|
|
471
|
+
// (a source override that reverts to a differing truth dirties it just
|
|
472
|
+
// above — sources join this list before their derivations — and its
|
|
473
|
+
// recompute then replaces the promotion), so by the graph's invariant
|
|
474
|
+
// the override IS what a recompute from the truth yields. Re-deriving
|
|
475
|
+
// instead re-asked an async member's flight and held the transaction on
|
|
476
|
+
// it — a waterfall after the reveal.
|
|
477
|
+
const derived = node._config & CONFIG_DERIVED_OVERRIDE;
|
|
478
|
+
ext(node)._overrideValue =
|
|
479
|
+
derived && !(node._config & CONFIG_OPTIMISTIC) ? undefined : NOT_PENDING;
|
|
392
480
|
// A superseded override's subscribers already re-derived from the truth
|
|
393
481
|
// when it arrived (#3331) — the drop changes nothing they read. Everyone
|
|
394
482
|
// else learns of the correction here: this drop IS their notification.
|
|
395
483
|
const superseded = (node._config & CONFIG_OVERRIDE_SUPERSEDED) !== 0;
|
|
396
|
-
node._config &=
|
|
397
|
-
if (
|
|
398
|
-
|
|
484
|
+
node._config &= -8912897;
|
|
485
|
+
if (
|
|
486
|
+
!superseded &&
|
|
487
|
+
prevOverride !== NOT_PENDING &&
|
|
488
|
+
node._value !== unwrapOverride(prevOverride)
|
|
489
|
+
) {
|
|
490
|
+
if (derived) node._value = unwrapOverride(prevOverride);
|
|
491
|
+
else insertSubs(node, true);
|
|
492
|
+
}
|
|
399
493
|
node._transition = null;
|
|
400
494
|
if (node._x !== null) node._x._overrideOwner = null;
|
|
401
495
|
}
|
|
@@ -476,6 +570,77 @@ function supersedeOverride(el, value) {
|
|
|
476
570
|
if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
|
|
477
571
|
insertSubs(el);
|
|
478
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* The flush's pre-verdict step once the action bodies have ended (#3427).
|
|
575
|
+
* The bodies were the optimism's justification; with them over, the
|
|
576
|
+
* overrides still in force revert at the settle — unless the transaction is
|
|
577
|
+
* still waiting on AUTHORITATIVE work: an override node's own source in
|
|
578
|
+
* flight (`transitionBlocked` — that answer supersedes or confirms on
|
|
579
|
+
* arrival), or a held flight that does not derive from an override (a plain
|
|
580
|
+
* write's load the action asked for). Through that window the optimistic
|
|
581
|
+
* world stands: a co-written "saving" flag stays rendered until the page it
|
|
582
|
+
* covers lands (A17 — the optimistic world is one).
|
|
583
|
+
*
|
|
584
|
+
* The flights that DO derive from an override — routed through a live lane —
|
|
585
|
+
* are obsolete: their input is the guess that is about to revert, and nobody
|
|
586
|
+
* will read their answer. With nothing authoritative left, each override's
|
|
587
|
+
* truth is already here (the staged value an A17-silent landing left, else
|
|
588
|
+
* the committed value) and supersedes it now, exactly as an arriving
|
|
589
|
+
* differing truth does (A18, #3331): the graph re-derives from it as this
|
|
590
|
+
* transaction's held work — a lane-derived memo re-asks with the truth, its
|
|
591
|
+
* other changed inputs included — and the transaction settles when THAT
|
|
592
|
+
* lands. Before this the settle first waited for the obsolete flight,
|
|
593
|
+
* revealed the obsolete optimistic frame when it landed, and only then
|
|
594
|
+
* started the correction — a waterfall with a flash in the middle. Returns
|
|
595
|
+
* whether it superseded anything (the caller re-runs the heap).
|
|
596
|
+
*
|
|
597
|
+
* The optimistic world is one, so it ends early only when all of it can. An
|
|
598
|
+
* optimistic STORE edit cannot yet: its truth is the base layer under an
|
|
599
|
+
* overlay that `_clearOptimisticStores` folds off at settlement, with no
|
|
600
|
+
* tracked/displayed split — superseding its tracking signals alone would
|
|
601
|
+
* re-derive readers against a still-displayed overlay (and a memo reading
|
|
602
|
+
* both a signal and the store would ask a mixed question). A transaction
|
|
603
|
+
* holding one keeps the settle-then-revert order throughout. Companions
|
|
604
|
+
* (`_parentSource` set) are optimistic nodes too — a verdict written through
|
|
605
|
+
* the optimistic path so it flushes ahead of the hold — but they answer for
|
|
606
|
+
* their owner and snap at settlement (`_snapCompanions`), not here.
|
|
607
|
+
*/
|
|
608
|
+
function endOptimism(transition) {
|
|
609
|
+
if (
|
|
610
|
+
!transition._acted ||
|
|
611
|
+
transition._actions.length ||
|
|
612
|
+
!transition._optimisticNodes.length ||
|
|
613
|
+
transition._optimisticStores.size ||
|
|
614
|
+
transitionBlocked(transition)
|
|
615
|
+
)
|
|
616
|
+
return false;
|
|
617
|
+
for (const source of transition._asyncReporters.keys())
|
|
618
|
+
if (
|
|
619
|
+
sourceObserved(transition, source, transition) &&
|
|
620
|
+
source._x?._pendingSources?.has(source) &&
|
|
621
|
+
!resolveLane(source)
|
|
622
|
+
)
|
|
623
|
+
return false;
|
|
624
|
+
let superseded = false;
|
|
625
|
+
for (const node of transition._optimisticNodes) {
|
|
626
|
+
if (
|
|
627
|
+
!hasActiveOverride(node) ||
|
|
628
|
+
node._x._parentSource ||
|
|
629
|
+
// A derived override re-derives when its source's is superseded.
|
|
630
|
+
node._config & (CONFIG_OVERRIDE_SUPERSEDED | CONFIG_DERIVED_OVERRIDE) ||
|
|
631
|
+
node._statusFlags & STATUS_UNINITIALIZED
|
|
632
|
+
)
|
|
633
|
+
continue;
|
|
634
|
+
const truth = node._pendingValue !== NOT_PENDING ? node._pendingValue : node._value;
|
|
635
|
+
if (!node._equals || !node._equals(truth, unwrapOverride(node._x._overrideValue))) {
|
|
636
|
+
supersedeOverride(node, truth);
|
|
637
|
+
// Judged by the mark, not the call: a provenance refusal (an older
|
|
638
|
+
// action's window is still open) leaves the node for a later pass.
|
|
639
|
+
if (node._config & CONFIG_OVERRIDE_SUPERSEDED) superseded = true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return superseded;
|
|
643
|
+
}
|
|
479
644
|
/** read()'s value for a tracked reader of a superseded node: the truth —
|
|
480
645
|
* staged, or already committed (a mainline landing commits at the head of
|
|
481
646
|
* its flush, ahead of the heap run, and the override drops only at the
|
|
@@ -483,9 +648,35 @@ function supersedeOverride(el, value) {
|
|
|
483
648
|
* has left) — or the displayed override for a stale (render) reader of some
|
|
484
649
|
* OTHER transaction, the same visibility a foreign transaction's staged
|
|
485
650
|
* write has. */
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
651
|
+
/**
|
|
652
|
+
* A tracked read of an active override (read()'s override arm). Lanes mirror
|
|
653
|
+
* transitions (#3460): a render effect OFF the override's held lane — re-run
|
|
654
|
+
* by a sync write, or mounted mid-hold — sees the committed value, as a stale
|
|
655
|
+
* reader of a held transaction does, and publishes now; the lane's release
|
|
656
|
+
* re-runs it (readsHeldCommitted). The lane defers the override's own readers'
|
|
657
|
+
* runs, so the committed value is what is on screen — the override is the
|
|
658
|
+
* visible value only once the lane has revealed (or, demoted at body-end,
|
|
659
|
+
* A18). Otherwise the override displays, unless the node's own source
|
|
660
|
+
* answered with a DIFFERENT value (A18 supersession, #3331): the optimism is
|
|
661
|
+
* over for the graph — a tracked reader sees the staged truth — while the
|
|
662
|
+
* override remains the DISPLAYED value for untracked reads (and for a stale
|
|
663
|
+
* reader of some other transaction).
|
|
664
|
+
*/
|
|
665
|
+
function overrideRead(el, c) {
|
|
666
|
+
if (stale && readsHeldCommitted(el, c)) return el._value;
|
|
667
|
+
if (!(el._config & CONFIG_OVERRIDE_SUPERSEDED)) return unwrapOverride(el._x?._overrideValue);
|
|
668
|
+
// The owning transaction: `_overrideOwner` (#2912), not the stamp — an
|
|
669
|
+
// override written directly inside an action never passes the adoption
|
|
670
|
+
// loop that stamps `_transition`, and a body-end supersession (#3427)
|
|
671
|
+
// stages nothing that would queue it. Without the owner a stale reader of
|
|
672
|
+
// a body-ended node read the committed truth beside a display still
|
|
673
|
+
// showing the override.
|
|
674
|
+
const owner = resolveTransition(el);
|
|
675
|
+
if (stale && owner && activeTransition !== owner) return unwrapOverride(el._x?._overrideValue);
|
|
676
|
+
// A superseded read is a staged read (A29) whether the truth is staged or
|
|
677
|
+
// already committed: the pass that derives from it derives from the
|
|
678
|
+
// owning transaction's world (the override is still displayed by it).
|
|
679
|
+
enterStagedRead(el, owner);
|
|
489
680
|
return el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
|
|
490
681
|
}
|
|
491
682
|
/**
|
|
@@ -561,11 +752,16 @@ function laneSuspends(owner) {
|
|
|
561
752
|
// this is only reachable under a lane, which implies the engine.
|
|
562
753
|
if (owner._statusFlags & STATUS_UNINITIALIZED) return true;
|
|
563
754
|
// Per-lane suspension: only throw if in same lane as pending async
|
|
564
|
-
// AND the node doesn't have an active override (overrides are the
|
|
565
|
-
// downstream in the lane should read the override, not
|
|
755
|
+
// AND the node doesn't have an active WRITTEN override (overrides are the
|
|
756
|
+
// visible value, downstream in the lane should read the override, not
|
|
757
|
+
// throw). A derived override (#3479) is a previous speculative answer, not
|
|
758
|
+
// an intent: the re-ask pending behind it suspends like any lane async.
|
|
566
759
|
const pendingLane = owner._x?._optimisticLane;
|
|
567
760
|
if (!pendingLane) return false;
|
|
568
|
-
return
|
|
761
|
+
return (
|
|
762
|
+
findLane(pendingLane) === findLane(currentOptimisticLane) &&
|
|
763
|
+
(!hasActiveOverride(owner) || (owner._config & CONFIG_DERIVED_OVERRIDE) !== 0)
|
|
764
|
+
);
|
|
569
765
|
}
|
|
570
766
|
/**
|
|
571
767
|
* read()'s reveal carve-out asks whether a pending node is routed through a
|
|
@@ -722,7 +918,9 @@ function installOptimisticEngine() {
|
|
|
722
918
|
GlobalQueue._cleanupLanes = cleanupCompletedLanes;
|
|
723
919
|
GlobalQueue._runLaneEffects = runLaneEffects;
|
|
724
920
|
GlobalQueue._supersedeOverride = supersedeOverride;
|
|
725
|
-
GlobalQueue.
|
|
921
|
+
GlobalQueue._endOptimism = endOptimism;
|
|
922
|
+
GlobalQueue._overrideRead = overrideRead;
|
|
923
|
+
GlobalQueue._laneOverride = laneOverride;
|
|
726
924
|
GlobalQueue._landOnOverride = landOnOverride;
|
|
727
925
|
GlobalQueue._gatedRead = gatedRead;
|
|
728
926
|
GlobalQueue._laneSuspends = laneSuspends;
|
|
@@ -760,9 +958,10 @@ const suppressedProbes = new Map();
|
|
|
760
958
|
* the one-load gate at the call sites). The snap then iterates exactly the
|
|
761
959
|
* children someone asked verdicts of — O(companions) — never the full
|
|
762
960
|
* `_child` chain, which carries one node per materialized leaf (the
|
|
763
|
-
* O(all-leaves-ever-read)-per-update pathology). Entries
|
|
764
|
-
*
|
|
765
|
-
*
|
|
961
|
+
* O(all-leaves-ever-read)-per-update pathology). Entries live as long as the
|
|
962
|
+
* store addresses the leaf — the unobserved sweep's `unlinkFirewallChild`
|
|
963
|
+
* drops them (#3503); a store with no leaf-level isPending()/latest() reads
|
|
964
|
+
* never allocates the set or pays the walk. */
|
|
766
965
|
function markFirewallChildCompanions(el) {
|
|
767
966
|
const fw = el._firewall;
|
|
768
967
|
if (!fw) return;
|
|
@@ -778,10 +977,53 @@ function getPendingSignal(el) {
|
|
|
778
977
|
el._config |= CONFIG_HAS_COMPANIONS;
|
|
779
978
|
markFirewallChildCompanions(el);
|
|
780
979
|
ext(ps)._parentSource = el;
|
|
781
|
-
if (computePendingState(el))
|
|
980
|
+
if (computePendingState(el)) backfillCompanion(el, ps, true);
|
|
981
|
+
joinUnflushedResync(el);
|
|
782
982
|
}
|
|
783
983
|
return ps;
|
|
784
984
|
}
|
|
985
|
+
/**
|
|
986
|
+
* A lazily created companion's first write mirrors state the owner already
|
|
987
|
+
* carries — a held write, a pending verdict. The companion is created wherever
|
|
988
|
+
* the first latest()/isPending() read happens to run, but the write belongs
|
|
989
|
+
* to whatever HOLDS that state: written in the ambient window, the override
|
|
990
|
+
* would register in the ambient batch and revert when that flush's round ends
|
|
991
|
+
* — the shadow re-derived from the committed view, the verdict flipped false —
|
|
992
|
+
* while the owner's hold was still on (#3336: A and B differing only in
|
|
993
|
+
* whether a companion existed before the hold). A companion created lazily
|
|
994
|
+
* answers as if it had always existed: its backfill is registered with the
|
|
995
|
+
* owner's transaction and lives and reverts with it. A hold with no
|
|
996
|
+
* transaction (a pending async, a same-flush staged write) is ambient and the
|
|
997
|
+
* write stays ambient. (A28 (3), lifted from #3337.)
|
|
998
|
+
*/
|
|
999
|
+
function backfillCompanion(el, companion, value) {
|
|
1000
|
+
const transition = el._transition;
|
|
1001
|
+
if (transition) runAsTransitionBatch(transition, () => setSignal(companion, value));
|
|
1002
|
+
else setSignal(companion, value);
|
|
1003
|
+
}
|
|
1004
|
+
/** A28: a companion created while its source carries an UNFLUSHED write joins
|
|
1005
|
+
* the flush-start re-sync like a companion that existed at the write —
|
|
1006
|
+
* syncCompanions only reaches companions that exist at write time. Without
|
|
1007
|
+
* this the flush brings it current by a plain recompute instead of the
|
|
1008
|
+
* optimistic write: its readers are then staged under whatever transaction
|
|
1009
|
+
* the round entered (a memo over latest() of a held source was held with the
|
|
1010
|
+
* source, and its untracked reads answered the previous value until the hold
|
|
1011
|
+
* committed) rather than direct-committed as the optimistic view they are. */
|
|
1012
|
+
function joinUnflushedResync(el) {
|
|
1013
|
+
if (unflushed(el)) unflushedCompanions.push(el);
|
|
1014
|
+
}
|
|
1015
|
+
/** The staged value the verdict channels answer for (A28): while a node
|
|
1016
|
+
* carries an unflushed write its `_pendingValue` is not yet part of any
|
|
1017
|
+
* flushed world, so the channels answer for the value the last flush left
|
|
1018
|
+
* staged (a held node's stash) or for nothing (NOT_PENDING). */
|
|
1019
|
+
function flushedStaged(el) {
|
|
1020
|
+
if (!unflushed(el)) return el._pendingValue;
|
|
1021
|
+
// Ambient, or adopted before any flush (CONFIG_ADOPTED_UNFLUSHED): nothing
|
|
1022
|
+
// a flush carried is staged for it. A held rewrite: the stash.
|
|
1023
|
+
return el._transition === null || el._config & CONFIG_ADOPTED_UNFLUSHED
|
|
1024
|
+
? NOT_PENDING
|
|
1025
|
+
: el._x._flushedStaged;
|
|
1026
|
+
}
|
|
785
1027
|
function collectPendingSources(el) {
|
|
786
1028
|
if (!pendingProbe) return;
|
|
787
1029
|
pendingProbe.sources.add(el);
|
|
@@ -869,7 +1111,8 @@ function computePendingState(el) {
|
|
|
869
1111
|
const parent = parentNode._firewall || parentNode;
|
|
870
1112
|
return newQuestionInFlight(parent);
|
|
871
1113
|
}
|
|
872
|
-
|
|
1114
|
+
const staged = flushedStaged(el);
|
|
1115
|
+
if (firewall && staged !== NOT_PENDING && !hasActiveOverride(el)) {
|
|
873
1116
|
return (
|
|
874
1117
|
!!(firewall._flags & REACTIVE_MANUAL_WRITE) ||
|
|
875
1118
|
(!firewall._x?._inFlight && !(firewall._statusFlags & STATUS_PENDING)) ||
|
|
@@ -880,18 +1123,41 @@ function computePendingState(el) {
|
|
|
880
1123
|
// the window's own landing in flight to its commit — verdict-quiet like the
|
|
881
1124
|
// rest of the window (the UNINITIALIZED check suppresses exactly this frame
|
|
882
1125
|
// for windowless first loads; born-committed nodes need their own gate, #2990).
|
|
1126
|
+
// A18 (d) for a body-end supersession (#3427): the truth at hand is the
|
|
1127
|
+
// COMMITTED value — nothing staged — yet the display still shows the
|
|
1128
|
+
// override; pending iff they differ, as for a staged arrival below.
|
|
883
1129
|
if (
|
|
884
|
-
el.
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
)
|
|
888
|
-
|
|
889
|
-
|
|
1130
|
+
el._config & CONFIG_OVERRIDE_SUPERSEDED &&
|
|
1131
|
+
el._pendingValue === NOT_PENDING &&
|
|
1132
|
+
visibleOverride(el)
|
|
1133
|
+
)
|
|
1134
|
+
return !el._equals || !el._equals(el._value, unwrapOverride(el._x?._overrideValue));
|
|
1135
|
+
// A28 (2): an unflushed write is not yet observable — the verdict answers
|
|
1136
|
+
// for the flushed staged value.
|
|
1137
|
+
if (staged !== NOT_PENDING && !comp._loading) {
|
|
1138
|
+
// A18 (d): under a displayed override the observable value is the
|
|
1139
|
+
// override, so the verdict is "the arrived truth differs from it" —
|
|
1140
|
+
// even before the node's first commit. The UNINITIALIZED suppression
|
|
1141
|
+
// below is A19 exception (1), "no observable value exists to be
|
|
1142
|
+
// non-final"; an override is one (a node whose first landing was held
|
|
1143
|
+
// by a reveal it never got to commit, then superseded under its
|
|
1144
|
+
// override, read false here).
|
|
1145
|
+
if (visibleOverride(el))
|
|
1146
|
+
return !el._equals || !el._equals(staged, unwrapOverride(el._x?._overrideValue));
|
|
890
1147
|
// A quiet re-ask's held landing still answers the same question: the
|
|
891
1148
|
// classification survives the landing (asyncWrite) and dies with the
|
|
892
1149
|
// commit (commitPendingNode) — verdict-quiet through the reveal, like
|
|
893
1150
|
// the loading window above (#3178).
|
|
894
|
-
|
|
1151
|
+
// A staged value equal to the committed one is no proposal (A34, #3494): the
|
|
1152
|
+
// observable value IS final (A19). The coalesced `setShow(false);
|
|
1153
|
+
// setShow(true)` read pending through the flush that carried it — and,
|
|
1154
|
+
// stamped into a hold that flush opened, until the hold settled.
|
|
1155
|
+
if (
|
|
1156
|
+
!(comp._statusFlags & STATUS_UNINITIALIZED) &&
|
|
1157
|
+
!comp._x?._reask &&
|
|
1158
|
+
(!el._equals || !el._equals(el._value, staged))
|
|
1159
|
+
)
|
|
1160
|
+
return true;
|
|
895
1161
|
}
|
|
896
1162
|
return newQuestionInFlight(comp);
|
|
897
1163
|
}
|
|
@@ -982,6 +1248,18 @@ function snapCompanionsToState(owner) {
|
|
|
982
1248
|
}
|
|
983
1249
|
const shadow = owner._x?._latestValueComputed;
|
|
984
1250
|
if (shadow && !(shadow._flags & REACTIVE_DISPOSED)) {
|
|
1251
|
+
// A leaf whose firewall is disposed (the projection's teardown snaps its
|
|
1252
|
+
// companion-bearing leaves): the shadow's compute reads through a
|
|
1253
|
+
// disposed, possibly still-pending projection and would sit
|
|
1254
|
+
// NotReady/uninitialized forever — never derived, its backfilled override
|
|
1255
|
+
// dropped at the settle — against a leaf whose committed value differs
|
|
1256
|
+
// (INV-4 at the next quiescence; spec O5). It dies with its source;
|
|
1257
|
+
// getLatestValueComputed treats a disposed shadow as absent, so a later
|
|
1258
|
+
// read recreates it from the committed view.
|
|
1259
|
+
if (owner._firewall?._flags & REACTIVE_DISPOSED) {
|
|
1260
|
+
dispose(shadow);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
985
1263
|
if (
|
|
986
1264
|
(shadow._x?._overrideValue === undefined || shadow._x?._overrideValue === NOT_PENDING) &&
|
|
987
1265
|
shadow._pendingValue === NOT_PENDING &&
|
|
@@ -1012,7 +1290,12 @@ function getLatestValueComputed(el) {
|
|
|
1012
1290
|
setPendingCheckActive(false);
|
|
1013
1291
|
const prevContext = context;
|
|
1014
1292
|
setContextInternal(null); // Detach from owner so it isn't disposed with effects
|
|
1015
|
-
|
|
1293
|
+
GlobalQueue._verdictPull = true;
|
|
1294
|
+
try {
|
|
1295
|
+
lvc = optimisticComputed(() => read(el), { ownedWrite: true });
|
|
1296
|
+
} finally {
|
|
1297
|
+
GlobalQueue._verdictPull = false;
|
|
1298
|
+
}
|
|
1016
1299
|
ext(el)._latestValueComputed = lvc;
|
|
1017
1300
|
el._config |= CONFIG_HAS_COMPANIONS;
|
|
1018
1301
|
markFirewallChildCompanions(el);
|
|
@@ -1021,8 +1304,9 @@ function getLatestValueComputed(el) {
|
|
|
1021
1304
|
// created lazily, possibly after the write was processed — syncCompanions
|
|
1022
1305
|
// only pushes into companions that already exist, so the first latest()
|
|
1023
1306
|
// read inside a held transition showed the committed value (#3041).
|
|
1024
|
-
|
|
1025
|
-
|
|
1307
|
+
const staged = flushedStaged(el);
|
|
1308
|
+
if (staged !== NOT_PENDING && !hasActiveOverride(el)) backfillCompanion(el, lvc, staged);
|
|
1309
|
+
joinUnflushedResync(el);
|
|
1026
1310
|
setContextInternal(prevContext);
|
|
1027
1311
|
setPendingCheckActive(prevCheck);
|
|
1028
1312
|
setLatestReadActive(prevPending);
|
|
@@ -1030,67 +1314,84 @@ function getLatestValueComputed(el) {
|
|
|
1030
1314
|
return lvc;
|
|
1031
1315
|
}
|
|
1032
1316
|
/** The latest()-mode read path, installed as GlobalQueue._latestRead. */
|
|
1317
|
+
/** A7: the source has no visible value yet — judged on the OWNER, as read()
|
|
1318
|
+
* does: a store leaf behind a projection's firewall is a plain signal whose
|
|
1319
|
+
* `_value` is the seed (A25: a draft, never a value), and read() routes a
|
|
1320
|
+
* latest() read here before its own firewall/status logic. An override
|
|
1321
|
+
* displays a value even before the first commit (A17). */
|
|
1322
|
+
function uninitializedSource(el) {
|
|
1323
|
+
const owner = el._firewall || el;
|
|
1324
|
+
return !!(owner._statusFlags & STATUS_UNINITIALIZED) && !hasActiveOverride(el);
|
|
1325
|
+
}
|
|
1033
1326
|
function latestRead(el) {
|
|
1327
|
+
// A leaf of a DISPOSED projection has no flushed world left to mirror: a
|
|
1328
|
+
// shadow created for it now would read through the dead firewall, sit
|
|
1329
|
+
// NotReady/uninitialized forever, and no teardown would ever retire it (the
|
|
1330
|
+
// firewall's already ran — spec O5). Serve the committed value; create
|
|
1331
|
+
// nothing. (A read of a disposed node freezes at its last commit, #3024.)
|
|
1332
|
+
if (el._firewall?._flags & REACTIVE_DISPOSED) return el._value;
|
|
1034
1333
|
const pendingComputed = getLatestValueComputed(el);
|
|
1035
1334
|
const prevPending = latestReadActive;
|
|
1036
1335
|
setLatestReadActive(false);
|
|
1037
|
-
const visibleValue =
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1336
|
+
const visibleValue = visibleOverride(el) ? unwrapOverride(el._x?._overrideValue) : el._value;
|
|
1337
|
+
// A28: an unflushed write is not the staged value latest() serves. The
|
|
1338
|
+
// shadow was written at the source's write to mirror it (A8) — consult it
|
|
1339
|
+
// only once a flush has carried the write.
|
|
1340
|
+
const u = unflushedValue(el);
|
|
1341
|
+
if (u !== NOT_PENDING) {
|
|
1342
|
+
// The reader derived from the flushed world because of an unflushed
|
|
1343
|
+
// write: it runs again in the flush that carries it (late linker) —
|
|
1344
|
+
// a pull may have cleared the mark the write's walk set.
|
|
1345
|
+
if (context !== null && context._flags & REACTIVE_RECOMPUTING_DEPS) markLateLinker(context);
|
|
1346
|
+
// Link the reader to the shadow so the flush that carries the write
|
|
1347
|
+
// updates it (the shadow itself already mirrors the write, A8).
|
|
1348
|
+
try {
|
|
1349
|
+
read(pendingComputed);
|
|
1350
|
+
} catch {
|
|
1351
|
+
/* the flushed value answers */
|
|
1352
|
+
} finally {
|
|
1353
|
+
setLatestReadActive(prevPending);
|
|
1354
|
+
}
|
|
1355
|
+
// An ambient write: the visible value (override or committed). A rewrite
|
|
1356
|
+
// of a held node: the staged value the last flush left.
|
|
1357
|
+
return el._transition === null ? visibleValue : u;
|
|
1358
|
+
}
|
|
1041
1359
|
let value;
|
|
1042
1360
|
try {
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
const queue = queueFor(pendingComputed);
|
|
1049
|
-
if (
|
|
1050
|
-
pendingComputed._height >= queue._min &&
|
|
1051
|
-
!(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
1052
|
-
) {
|
|
1053
|
-
markHeap(queue);
|
|
1054
|
-
// Suspend probe collection during the pull (mirrors pendingCheckRead's
|
|
1055
|
-
// prepare): a probe through latest() answers for the SHADOW — the
|
|
1056
|
-
// read() dispatch collects it deliberately, so the verdict reflects
|
|
1057
|
-
// async still in flight for the latest view, not the parent's held
|
|
1058
|
-
// write. A stale shadow recomputing HERE ran its `read(parent)` with
|
|
1059
|
-
// the probe still live and collected the parent too, so the verdict
|
|
1060
|
-
// depended on whether anything had pulled the shadow current earlier
|
|
1061
|
-
// in the tick (#3104: reading latest(m) flipped a later
|
|
1062
|
-
// latest(() => isPending(x)) from true to false).
|
|
1063
|
-
const prevCheck = pendingCheckActive;
|
|
1064
|
-
setPendingCheckActive(false);
|
|
1065
|
-
try {
|
|
1066
|
-
prepareComputed(pendingComputed, true);
|
|
1067
|
-
} finally {
|
|
1068
|
-
setPendingCheckActive(prevCheck);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1361
|
+
// No mid-tick pull: writes become visible at flush (A28), so before the
|
|
1362
|
+
// flush the shadow is exactly as current as the flushed world — the read
|
|
1363
|
+
// below serves it as is. (#2922's pull, which brought the shadow current
|
|
1364
|
+
// against the unflushed write, is superseded: `flush()` first to read
|
|
1365
|
+
// your own write.)
|
|
1071
1366
|
value = read(pendingComputed);
|
|
1072
1367
|
} catch (e) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1368
|
+
// A NotReady from the shadow of an INITIALIZED source means the shadow
|
|
1369
|
+
// is mid-flight: serve the visible (committed / override) value. An
|
|
1370
|
+
// uninitialized source has no visible value — latest() throws in every
|
|
1371
|
+
// scope rather than fabricate `undefined` for a `T` that excludes it
|
|
1372
|
+
// (A7; the unowned scope used to return undefined here).
|
|
1373
|
+
if (e instanceof NotReadyError && !uninitializedSource(el)) return visibleValue;
|
|
1075
1374
|
throw e;
|
|
1076
1375
|
} finally {
|
|
1077
1376
|
setLatestReadActive(prevPending);
|
|
1078
1377
|
}
|
|
1079
|
-
if (pendingComputed._statusFlags & STATUS_PENDING)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const curLane = findLane(currentOptimisticLane);
|
|
1083
|
-
if (pcLane !== curLane && laneHeld(pcLane)) {
|
|
1084
|
-
return visibleValue;
|
|
1085
|
-
}
|
|
1378
|
+
if (pendingComputed._statusFlags & STATUS_PENDING) {
|
|
1379
|
+
if (uninitializedSource(el)) throw new NotReadyError(el);
|
|
1380
|
+
return visibleValue;
|
|
1086
1381
|
}
|
|
1382
|
+
// A render effect off the shadow's HELD lane sees the committed value and
|
|
1383
|
+
// re-runs at the release (#3460; lanes mirror transitions — see
|
|
1384
|
+
// readsHeldCommitted). Was: only a reader under ANOTHER lane; a mainline
|
|
1385
|
+
// reader, mounted or re-run by a sync write mid-hold, showed the
|
|
1386
|
+
// speculative value beside the lane's deferred readers.
|
|
1387
|
+
if (stale && context !== null && readsHeldCommitted(pendingComputed, context)) return el._value;
|
|
1087
1388
|
// A shadow recomputed by the pull above (not at creation) holds its fresh
|
|
1088
1389
|
// speculative value in _pendingValue; a contextless read() only surfaces
|
|
1089
1390
|
// _value. Overrides stay authoritative (A17), and stale readers keep the
|
|
1090
1391
|
// other transition's committed view, matching read()'s own selection.
|
|
1091
1392
|
if (
|
|
1092
1393
|
pendingComputed._pendingValue !== NOT_PENDING &&
|
|
1093
|
-
!hasActiveOverride
|
|
1394
|
+
!hasActiveOverride(pendingComputed) &&
|
|
1094
1395
|
!(stale && pendingComputed._transition && activeTransition !== pendingComputed._transition)
|
|
1095
1396
|
)
|
|
1096
1397
|
return pendingComputed._pendingValue;
|
|
@@ -1115,7 +1416,14 @@ function latestShadowWithInitializedParent(owner) {
|
|
|
1115
1416
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
1116
1417
|
function pendingCheckRead(el, c, owner, firewall) {
|
|
1117
1418
|
setPendingCheckActive(false);
|
|
1118
|
-
if (typeof el._fn === "function")
|
|
1419
|
+
if (typeof el._fn === "function") {
|
|
1420
|
+
GlobalQueue._verdictPull = true;
|
|
1421
|
+
try {
|
|
1422
|
+
prepareComputed(el, true);
|
|
1423
|
+
} finally {
|
|
1424
|
+
GlobalQueue._verdictPull = false;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1119
1427
|
const ownerStatus = owner._statusFlags;
|
|
1120
1428
|
if (
|
|
1121
1429
|
c &&
|
|
@@ -1162,10 +1470,15 @@ function heldAwaitingAsync(el) {
|
|
|
1162
1470
|
// action (#2831: a reader that saw the new value must not also see
|
|
1163
1471
|
// pending); still-computing answers are covered by the reporter scan.
|
|
1164
1472
|
if (t._actions.length && !el._fn) return true;
|
|
1165
|
-
//
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1473
|
+
// The reporter scan runs for an unstamped node too (#3457): a node staged
|
|
1474
|
+
// AFTER the transaction opened is pushed straight into the transaction's
|
|
1475
|
+
// batch (queuePendingNode, once initTransition adopted it) and only gets
|
|
1476
|
+
// its `_transition` stamp when the flush stashes the hold, but its staged
|
|
1477
|
+
// value is already the transaction's, and `t` resolved to that very
|
|
1478
|
+
// transaction above. Gating on the stamp let a memo whose recompute read
|
|
1479
|
+
// a sync memo's fresh staged value mid-flush pair "not pending" with it
|
|
1480
|
+
// (A10) while the transaction's async source was still computing, so a
|
|
1481
|
+
// memo-wrapped isPending() read false where a direct probe read true.
|
|
1169
1482
|
for (const [source, reporters] of t._asyncReporters) {
|
|
1170
1483
|
if (
|
|
1171
1484
|
reporters.size &&
|
|
@@ -1297,7 +1610,11 @@ function effect(compute, effect, error, options) {
|
|
|
1297
1610
|
options
|
|
1298
1611
|
);
|
|
1299
1612
|
recompute(node, true);
|
|
1613
|
+
// A first pass that derived from a live transaction's staged world was
|
|
1614
|
+
// staged into that transaction (recompute: born held); the transaction's
|
|
1615
|
+
// commit replays this effect. Its first run is not this creation's (A29).
|
|
1300
1616
|
!options?.defer &&
|
|
1617
|
+
node._pendingValue === NOT_PENDING &&
|
|
1301
1618
|
(node._type === EFFECT_USER || options?.schedule
|
|
1302
1619
|
? node._queue.enqueue(node._type, runEffect.bind(null, node))
|
|
1303
1620
|
: runEffect(node, LANE_RUN));
|
|
@@ -1430,6 +1747,9 @@ function runEffect(node, type) {
|
|
|
1430
1747
|
}
|
|
1431
1748
|
return;
|
|
1432
1749
|
}
|
|
1750
|
+
// Captured before the callback: its own throw errors the node below, but
|
|
1751
|
+
// the compute pass that produced `_value` was clean, so its tail still goes.
|
|
1752
|
+
const cleanPass = node._x?._error == null;
|
|
1433
1753
|
let prevStrictRead = false;
|
|
1434
1754
|
{
|
|
1435
1755
|
prevStrictRead = setStrictRead("an effect callback");
|
|
@@ -1462,6 +1782,12 @@ function runEffect(node, type) {
|
|
|
1462
1782
|
}
|
|
1463
1783
|
node._prevValue = node._value;
|
|
1464
1784
|
node._modified = false;
|
|
1785
|
+
// The run applied: this is the frame now, so the dependency tail the
|
|
1786
|
+
// compute pass left linked goes (A30, #3438 — `recompute` defers an
|
|
1787
|
+
// effect's trim while a run is owed; the twin of `commitPendingNode`'s
|
|
1788
|
+
// trim for a staged pass). An errored compute kept its full list with
|
|
1789
|
+
// `_depsTail` marking where it stopped; leave it, as the commit does.
|
|
1790
|
+
if (cleanPass) trimStaleDeps(node);
|
|
1465
1791
|
}
|
|
1466
1792
|
// Outside the try (see the rule in attribution-hooks.ts). Reached whether or
|
|
1467
1793
|
// not the callback threw — a throw that escapes the catch above halts.
|
|
@@ -1587,15 +1913,22 @@ function restoreTransition(seq, transition, fn) {
|
|
|
1587
1913
|
* `yield` is the transaction-safe suspension point: the action waits for a
|
|
1588
1914
|
* yielded promise and re-enters the transaction before running the code after
|
|
1589
1915
|
* it. A plain `await` does NOT — the runtime has no hook into an async
|
|
1590
|
-
* generator's internal await continuations, so
|
|
1591
|
-
*
|
|
1592
|
-
* immediately
|
|
1593
|
-
*
|
|
1916
|
+
* generator's internal await continuations, so code between an `await` and
|
|
1917
|
+
* the next `yield` runs OUTSIDE the transaction: writes to fresh signals
|
|
1918
|
+
* commit immediately, and anything that creates a reader there — `until()`,
|
|
1919
|
+
* `latest()`, a memo or effect, a mount — is created mainline, where a read of
|
|
1920
|
+
* this action's held state makes it born held (A29): staged with the
|
|
1921
|
+
* transaction and replayed at its commit. For `until()` that commit is the
|
|
1922
|
+
* settle its own promise holds open (#3482). `await` is still the ergonomic
|
|
1923
|
+
* choice for typed results; just put a bare `yield` before any write or
|
|
1924
|
+
* reader creation that follows it — including the expression of the next
|
|
1925
|
+
* `yield`, which is evaluated before the step re-enters:
|
|
1594
1926
|
*
|
|
1595
1927
|
* ```ts
|
|
1596
1928
|
* const saved = await api.createTodo(text); // typed result
|
|
1597
|
-
* yield; // re-enter the transaction before writing
|
|
1929
|
+
* yield; // re-enter the transaction before writing or reading
|
|
1598
1930
|
* setTodos(t => { ... });
|
|
1931
|
+
* yield until(() => todos.some(t => t.id === saved.id));
|
|
1599
1932
|
* ```
|
|
1600
1933
|
*
|
|
1601
1934
|
* (For the same reason, don't call `flush()` inside an action body — it
|
|
@@ -1658,6 +1991,7 @@ function action(genFn) {
|
|
|
1658
1991
|
globalQueue.initTransition();
|
|
1659
1992
|
let ctx = activeTransition;
|
|
1660
1993
|
ctx._actions.push(it);
|
|
1994
|
+
ctx._acted = true;
|
|
1661
1995
|
const done = (v, e, failed = false) => {
|
|
1662
1996
|
ctx = currentTransition(ctx);
|
|
1663
1997
|
const i = ctx._actions.indexOf(it);
|
|
@@ -1731,6 +2065,73 @@ function action(genFn) {
|
|
|
1731
2065
|
};
|
|
1732
2066
|
}
|
|
1733
2067
|
|
|
2068
|
+
let ambientHook;
|
|
2069
|
+
const reported = new WeakSet();
|
|
2070
|
+
/**
|
|
2071
|
+
* Registers the ambient client error hook — the one call a browser `init()`
|
|
2072
|
+
* makes to see every failure a boundary renders a fallback for, in
|
|
2073
|
+
* production. (Uncaught errors reach `reportError` / `window.onerror`.)
|
|
2074
|
+
*
|
|
2075
|
+
* ```ts
|
|
2076
|
+
* configureClientErrors({
|
|
2077
|
+
* onError: (error, { ownerPath }) =>
|
|
2078
|
+
* Sentry.captureException(error, {
|
|
2079
|
+
* mechanism: { type: "solid.error_boundary", handled: true }
|
|
2080
|
+
* })
|
|
2081
|
+
* });
|
|
2082
|
+
* ```
|
|
2083
|
+
*/
|
|
2084
|
+
function configureClientErrors(config) {
|
|
2085
|
+
if (config && config.onError !== undefined && typeof config.onError !== "function") {
|
|
2086
|
+
throw new TypeError(`Invalid onError: expected a function, received ${typeof config.onError}.`);
|
|
2087
|
+
}
|
|
2088
|
+
ambientHook = config ? config.onError : undefined;
|
|
2089
|
+
}
|
|
2090
|
+
/** The nearest root's hook above `owner` (parked under `ROOT_ERROR_HOOK`), else the ambient one. */
|
|
2091
|
+
function hookFor(owner) {
|
|
2092
|
+
for (let o = owner; o; o = o._parent) {
|
|
2093
|
+
const hook = o[ROOT_ERROR_HOOK];
|
|
2094
|
+
if (hook !== undefined) return hook;
|
|
2095
|
+
}
|
|
2096
|
+
return ambientHook;
|
|
2097
|
+
}
|
|
2098
|
+
/** Component labels up the owner chain, root first — `_name` where the runtime keeps it. */
|
|
2099
|
+
function labels(owner) {
|
|
2100
|
+
const path = [];
|
|
2101
|
+
for (let o = owner; o; o = o._parent) {
|
|
2102
|
+
const name = o._name;
|
|
2103
|
+
if (typeof name === "string" && name.length) path.push(name);
|
|
2104
|
+
}
|
|
2105
|
+
return path.length ? path.reverse() : undefined;
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Tells the client error hook about `error`, caught by the boundary whose
|
|
2109
|
+
* owner is `owner`, thrown by `thrower` (the computation the engine's status
|
|
2110
|
+
* wrapper named; unknown for a value that never crossed one) — once per
|
|
2111
|
+
* error object. A throwing hook is reported on the console and otherwise
|
|
2112
|
+
* ignored — a monitor must never take the app down.
|
|
2113
|
+
* @internal
|
|
2114
|
+
*/
|
|
2115
|
+
function reportClientError(error, owner, thrower) {
|
|
2116
|
+
const isObject = error !== null && (typeof error === "object" || typeof error === "function");
|
|
2117
|
+
if (isObject) {
|
|
2118
|
+
if (reported.has(error)) return;
|
|
2119
|
+
reported.add(error);
|
|
2120
|
+
}
|
|
2121
|
+
const hook = hookFor(owner);
|
|
2122
|
+
if (hook === undefined) return;
|
|
2123
|
+
const context = {};
|
|
2124
|
+
const boundary = labels(owner);
|
|
2125
|
+
const path = labels(thrower) ?? boundary;
|
|
2126
|
+
if (path !== undefined) context.ownerPath = path;
|
|
2127
|
+
if (boundary !== undefined) context.boundaryPath = boundary;
|
|
2128
|
+
try {
|
|
2129
|
+
hook(error, context);
|
|
2130
|
+
} catch (hookError) {
|
|
2131
|
+
console.error(hookError);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1734
2135
|
/**
|
|
1735
2136
|
* Low-level reactive-cleanup primitive. Registers a callback that runs when
|
|
1736
2137
|
* the surrounding owner is disposed.
|
|
@@ -2290,12 +2691,20 @@ function refresh(target) {
|
|
|
2290
2691
|
*
|
|
2291
2692
|
* Must be called *outside* a tracking scope.
|
|
2292
2693
|
*
|
|
2694
|
+
* Inside an action, call it from a step: after an `await`, put a bare `yield`
|
|
2695
|
+
* before `yield until(...)`. The runtime cannot hook an async generator's
|
|
2696
|
+
* `await` continuation, so the `until(...)` expression — which CREATES the
|
|
2697
|
+
* predicate's reader — would otherwise run outside the transaction; created
|
|
2698
|
+
* there it is born held (A29) and replays only at the commit its own promise
|
|
2699
|
+
* holds open (#3482). See {@link action}.
|
|
2700
|
+
*
|
|
2293
2701
|
* @example
|
|
2294
2702
|
* ```ts
|
|
2295
2703
|
* const send = action(async function* (text: string) {
|
|
2296
2704
|
* const clientId = crypto.randomUUID();
|
|
2297
2705
|
* setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
|
|
2298
2706
|
* await socket.send({ clientId, text }); // fire-and-forget transport
|
|
2707
|
+
* yield; // re-enter the transaction after the await
|
|
2299
2708
|
* // Hold until the live source echoes the write (authoritative view —
|
|
2300
2709
|
* // the optimistic row above cannot satisfy this):
|
|
2301
2710
|
* yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
|
|
@@ -2486,7 +2895,15 @@ function onSettled(callback) {
|
|
|
2486
2895
|
const owner = getOwner();
|
|
2487
2896
|
owner && !(owner._config & CONFIG_CHILDREN_FORBIDDEN)
|
|
2488
2897
|
? trackedEffect(() => untrack(callback), { name: "onSettled" })
|
|
2489
|
-
: globalQueue.enqueue(EFFECT_USER, ()
|
|
2898
|
+
: globalQueue.enqueue(EFFECT_USER, function fire() {
|
|
2899
|
+
// Settled means derived. A settle that reverts optimism (or replays
|
|
2900
|
+
// gated reads) only enqueues the affected subscribers; the pass after
|
|
2901
|
+
// the commit re-derives them. Fired in the commit pass, the callback
|
|
2902
|
+
// read the optimistic source already reverted beside a sync memo of it
|
|
2903
|
+
// still holding the optimistic value — reads do not pull (#3411). Fall
|
|
2904
|
+
// to the next pass while the heap has work; `run` swapped the queue,
|
|
2905
|
+
// so this lands there, and `enqueue` keeps the drain alive.
|
|
2906
|
+
if (dirtyQueue._max >= dirtyQueue._min) return globalQueue.enqueue(EFFECT_USER, fire);
|
|
2490
2907
|
// Unowned, out-of-band fire (no owner, or a children-forbidden one this
|
|
2491
2908
|
// one-shot must not bind to): a returned cleanup has no lifecycle to
|
|
2492
2909
|
// attach to. Reject it in dev; in production the return is simply
|
|
@@ -3011,44 +3428,825 @@ function affects(target, key) {
|
|
|
3011
3428
|
function trueFn() {
|
|
3012
3429
|
return true;
|
|
3013
3430
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3431
|
+
/** @internal What a source ENTRY is, decided once when the view is built
|
|
3432
|
+
* (`merge()` learns it while flattening; `omit()` from its argument) and
|
|
3433
|
+
* carried beside the entry — `MergeView.kinds[i]`, `OmitView.kind` — so no
|
|
3434
|
+
* read has to ask. Asking is the cost: any brand check on a Proxy is a trap
|
|
3435
|
+
* (`instanceof` is a `getPrototypeOf` trap, ~20 ns on a store — as much as
|
|
3436
|
+
* the read itself), and a merge over a store did two per read. */
|
|
3437
|
+
const SOURCE_PLAIN = 0; // a plain object: own keys fixed, data is data
|
|
3438
|
+
const SOURCE_OMIT = 1; // an `OmitView` record (only as a merge entry)
|
|
3439
|
+
const SOURCE_PROXY = 2; // a store or foreign proxy: everything is a trap
|
|
3440
|
+
const SOURCE_MEMO = 3; // a function source (merge's memo): swaps objects
|
|
3441
|
+
const SOURCE_MERGE = 4; // a `MergeView` record (only as an omit's source)
|
|
3442
|
+
const EMPTY = Object.freeze({});
|
|
3443
|
+
// The object behind a LEAF entry (any kind but OMIT): a memo is read
|
|
3444
|
+
// (tracked, as merge's own reads are) and a nullish result has no keys.
|
|
3445
|
+
function leafOf(s, kind) {
|
|
3446
|
+
return kind === SOURCE_MEMO ? ((s = s()) == null ? EMPTY : s) : s;
|
|
3447
|
+
}
|
|
3448
|
+
const $SOURCES = Symbol("MERGE_SOURCE");
|
|
3449
|
+
const $OMIT = Symbol("OMIT_VIEW");
|
|
3450
|
+
// The MergeView behind a merge proxy. `$SOURCES` answers the array; the
|
|
3451
|
+
// record itself is reached through this symbol so the table can live on it.
|
|
3452
|
+
const $VIEW = Symbol("MERGE_VIEW");
|
|
3453
|
+
/** @internal The record behind an `omit()` proxy: `source` with `hidden`
|
|
3454
|
+
* keys removed. It is the proxy's TARGET, so the shared handler reads it as
|
|
3455
|
+
* plain fields — no per-instance closures — and it is what props consumers
|
|
3456
|
+
* walk directly (`merge`, `spread`, `ssrElement`): a view never materializes
|
|
3457
|
+
* a copy, and a consumer that knows the record never goes through its traps
|
|
3458
|
+
* (a `getOwnPropertyDescriptor` trap per key allocates a descriptor and a
|
|
3459
|
+
* getter, so enumerating a proxy costs more than the copy it was avoiding).
|
|
3460
|
+
* `hidden` is a key list or a predicate (`omit(props, k => k[0] === "$")`).
|
|
3461
|
+
*
|
|
3462
|
+
* An omit over a `merge()` holds the merge's RECORD (`MergeView`, kind
|
|
3463
|
+
* `SOURCE_MERGE`) — never its proxy, so no read hops through a trap — and is
|
|
3464
|
+
* one record however many leaves the merge has. A consumer walks it as ONE
|
|
3465
|
+
* filtered entry (`sourceKeys` / `sourceGet` recurse into the merge's
|
|
3466
|
+
* sources by function call), and a later `merge()` over it carries the
|
|
3467
|
+
* record as one entry instead of copying its leaves: on a component chain of
|
|
3468
|
+
* defaults + omit + spread (`merge(omit(merge(omit(props))))`) the layers
|
|
3469
|
+
* nest as records, each a few fields, where a flatten to leaf views built a
|
|
3470
|
+
* view and a combined key list per leaf per layer — the largest allocation
|
|
3471
|
+
* of a Kobalte-shaped render. A merge leaf may be merge's memo for a
|
|
3472
|
+
* function source; it is resolved on access. */
|
|
3473
|
+
class OmitView {
|
|
3474
|
+
source;
|
|
3475
|
+
kind;
|
|
3476
|
+
hidden;
|
|
3477
|
+
/** see `MergeView.table` */
|
|
3478
|
+
table = 0;
|
|
3479
|
+
/** see `tableOwnKeys` / `tableDescriptor` */
|
|
3480
|
+
keys = undefined;
|
|
3481
|
+
descs = undefined;
|
|
3482
|
+
constructor(
|
|
3483
|
+
source,
|
|
3484
|
+
/** of `source` — PLAIN, PROXY (a store or a foreign proxy), MEMO, or
|
|
3485
|
+
* MERGE (a `MergeView` record); never OMIT, a view over a view folds
|
|
3486
|
+
* into one. */
|
|
3487
|
+
kind,
|
|
3488
|
+
hidden
|
|
3489
|
+
) {
|
|
3490
|
+
this.source = source;
|
|
3491
|
+
this.kind = kind;
|
|
3492
|
+
this.hidden = hidden;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
function isHidden(view, key) {
|
|
3496
|
+
const h = view.hidden;
|
|
3497
|
+
return typeof h === "function" ? h(key) : h.includes(key);
|
|
3498
|
+
}
|
|
3499
|
+
// Both filters as one. Two key lists stay a key list (one `includes`, no
|
|
3500
|
+
// closure); a predicate on either side needs a closure. An omit over a
|
|
3501
|
+
// merge builds one combined list per leaf, per component layer, so the
|
|
3502
|
+
// copy's form matters in every tier: `concat` runs the species/spreadable
|
|
3503
|
+
// protocol (2–3× the cost of a copy once optimized), a hand loop is 2–4×
|
|
3504
|
+
// `concat` in the interpreter and baseline tiers (a bytecode per element
|
|
3505
|
+
// against one builtin), and a presized `new Array(n)` is holey, which takes
|
|
3506
|
+
// `includes` off its fast path. `slice` + `push` of the (short) second list
|
|
3507
|
+
// is within a third of the best form in every tier, and packed.
|
|
3508
|
+
function combineHidden(a, b) {
|
|
3509
|
+
if (typeof a !== "function" && typeof b !== "function") {
|
|
3510
|
+
const out = a.slice();
|
|
3511
|
+
for (let i = 0; i < b.length; i++) out.push(b[i]);
|
|
3512
|
+
return out;
|
|
3513
|
+
}
|
|
3514
|
+
return key =>
|
|
3515
|
+
(typeof a === "function" ? a(key) : a.includes(key)) ||
|
|
3516
|
+
(typeof b === "function" ? b(key) : b.includes(key));
|
|
3517
|
+
}
|
|
3518
|
+
// The object a view filters (see `leafOf`).
|
|
3519
|
+
function viewSource(view) {
|
|
3520
|
+
return leafOf(view.source, view.kind);
|
|
3521
|
+
}
|
|
3522
|
+
// Whether a `$PROXY`-marked object is one of OUR views rather than a store
|
|
3523
|
+
// (or a foreign proxy). Asked through `$TARGET`, which a store's `get` trap
|
|
3524
|
+
// answers on its symbol fast path and the view traps answer first thing —
|
|
3525
|
+
// so the question never reaches a store's generic read path (firewall gate,
|
|
3526
|
+
// tracked key read), which is what any unknown symbol (`$SOURCES`, `$OMIT`)
|
|
3527
|
+
// would take. Call only after `$PROXY in o` is known true.
|
|
3528
|
+
function isView(o) {
|
|
3529
|
+
return o[$TARGET] === undefined && (o[$OMIT] !== undefined || o[$VIEW] !== undefined);
|
|
3530
|
+
}
|
|
3531
|
+
/** @internal The `OmitView` behind an `omit()` proxy, or undefined. */
|
|
3532
|
+
function omitView(o) {
|
|
3533
|
+
return o != null && $PROXY in o && o[$TARGET] === undefined ? o[$OMIT] : undefined;
|
|
3534
|
+
}
|
|
3535
|
+
// A props SOURCE ENTRY is a plain object, a proxy (store, merge, omit — the
|
|
3536
|
+
// last two are normally unwrapped first: `mergeView` / `omitView`), a memo,
|
|
3537
|
+
// or an `OmitView` record, and travels with its kind. These three answer for
|
|
3538
|
+
// an entry what `Object.keys` / `in` / `[]` answer for an object, so every
|
|
3539
|
+
// consumer walks entries with one code path, an `OmitView` is filtered
|
|
3540
|
+
// rather than materialized, and nothing is asked of a proxy but the read.
|
|
3541
|
+
// Own keys of a leaf: a proxy answers through ONE `ownKeys` trap (a store's
|
|
3542
|
+
// keeps the key set tracked; `Object.keys` on a proxy would add a descriptor
|
|
3543
|
+
// trap per key); a plain object its enumerable string keys. What a memo
|
|
3544
|
+
// holds is only known once read.
|
|
3545
|
+
function leafKeys(leaf, kind) {
|
|
3546
|
+
if (kind === SOURCE_PLAIN) return Object.keys(leaf);
|
|
3547
|
+
if (kind === SOURCE_PROXY || leaf[$PROXY] === leaf) return Reflect.ownKeys(leaf);
|
|
3548
|
+
return Object.keys(leaf);
|
|
3549
|
+
}
|
|
3550
|
+
/** @internal Own string keys of a source entry — every consumer skips
|
|
3551
|
+
* symbols itself. */
|
|
3552
|
+
function sourceKeys(s, kind) {
|
|
3553
|
+
if (kind === SOURCE_OMIT) {
|
|
3554
|
+
if (s.kind === SOURCE_MERGE) return mergeKeysOf(s.source, false, s);
|
|
3555
|
+
const keys = leafKeys(viewSource(s), s.kind);
|
|
3556
|
+
const out = [];
|
|
3557
|
+
for (let i = 0; i < keys.length; i++) if (!isHidden(s, keys[i])) out.push(keys[i]);
|
|
3558
|
+
return out;
|
|
3559
|
+
}
|
|
3560
|
+
return leafKeys(leafOf(s, kind), kind);
|
|
3561
|
+
}
|
|
3562
|
+
/** @internal `key in entry`. */
|
|
3563
|
+
function sourceHas(s, kind, key) {
|
|
3564
|
+
if (kind === SOURCE_OMIT) {
|
|
3565
|
+
if (isHidden(s, key)) return false;
|
|
3566
|
+
return s.kind === SOURCE_MERGE ? mergeHas(s.source, key) : key in viewSource(s);
|
|
3567
|
+
}
|
|
3568
|
+
return key in leafOf(s, kind);
|
|
3569
|
+
}
|
|
3570
|
+
/** @internal `entry[key]` — the source's getter runs once, here. */
|
|
3571
|
+
function sourceGet(s, kind, key) {
|
|
3572
|
+
if (kind === SOURCE_OMIT) {
|
|
3573
|
+
if (isHidden(s, key)) return undefined;
|
|
3574
|
+
return s.kind === SOURCE_MERGE ? mergeGet(s.source, key) : viewSource(s)[key];
|
|
3575
|
+
}
|
|
3576
|
+
return leafOf(s, kind)[key];
|
|
3577
|
+
}
|
|
3578
|
+
// An entry whose own key set is fixed: a plain object, or a view over one —
|
|
3579
|
+
// an omit of a plain object, or of a merge whose entries all are. Not a
|
|
3580
|
+
// store (its key set is a tracked signal), not a merge memo source (it
|
|
3581
|
+
// swaps whole objects), and not any proxy that declares itself with
|
|
3582
|
+
// `$PROXY in s` — a frames slot proxy answers `has` for every key and lists
|
|
3583
|
+
// none, so only the `in` walk is right for it.
|
|
3584
|
+
function entryHasStaticKeys(s, kind) {
|
|
3585
|
+
if (kind === SOURCE_PLAIN) return true;
|
|
3586
|
+
if (kind !== SOURCE_OMIT) return false;
|
|
3587
|
+
if (s.kind === SOURCE_PLAIN) return true;
|
|
3588
|
+
return s.kind === SOURCE_MERGE && mergeHasStaticKeys(s.source);
|
|
3589
|
+
}
|
|
3590
|
+
function mergeHasStaticKeys(view) {
|
|
3591
|
+
const f = view.sources,
|
|
3592
|
+
k = view.kinds;
|
|
3593
|
+
for (let i = 0; i < f.length; i++) if (!entryHasStaticKeys(f[i], k[i])) return false;
|
|
3594
|
+
return true;
|
|
3595
|
+
}
|
|
3596
|
+
/** @internal Whether the own key set of a props object cannot change
|
|
3597
|
+
* reactively: a plain object, or a merge/omit view over plain objects only.
|
|
3598
|
+
* A consumer may then decide from `Object.getOwnPropertyDescriptor` once —
|
|
3599
|
+
* "no `children` key" or "a data `children`" holds for the object's lifetime,
|
|
3600
|
+
* so no tracking scope is needed for it (#3388). For a store, or a view with
|
|
3601
|
+
* a store or memo leaf, keys can appear later and the reactive path is the
|
|
3602
|
+
* only correct one. */
|
|
3603
|
+
function hasStaticKeys(o) {
|
|
3604
|
+
if (!($PROXY in o)) return true;
|
|
3605
|
+
if (o[$TARGET] !== undefined) return false;
|
|
3606
|
+
const merged = o[$VIEW];
|
|
3607
|
+
if (merged !== undefined) return mergeHasStaticKeys(merged);
|
|
3608
|
+
const view = o[$OMIT];
|
|
3609
|
+
return view !== undefined && entryHasStaticKeys(view, SOURCE_OMIT);
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Whether `o[key]` can never change for the lifetime of `o`: the key is a
|
|
3613
|
+
* data property of a plain object, or is absent from an object whose key set
|
|
3614
|
+
* is fixed. A getter, a key on a store, a memo-backed `merge()` source, or
|
|
3615
|
+
* any key of an object whose keys can appear later (a store) is not static.
|
|
3616
|
+
*
|
|
3617
|
+
* Looks through `merge()`/`omit()` views to the leaf that owns the key. Any
|
|
3618
|
+
* object will do, but props are the case it exists for: the compiler encodes
|
|
3619
|
+
* a literal at the call site (`as="button"`) as a data property and an
|
|
3620
|
+
* expression (`as={isLink() ? "a" : "button"}`) as a getter, so a component
|
|
3621
|
+
* library reads the caller's own static/dynamic classification of a prop at
|
|
3622
|
+
* runtime — identically on server and client, the compiled shape being the
|
|
3623
|
+
* same on both — and can take a no-computation path for the literal:
|
|
3624
|
+
*
|
|
3625
|
+
* ```tsx
|
|
3626
|
+
* const Tag = dynamic(() => props.as, { static: isStatic(props, "as") });
|
|
3627
|
+
* ```
|
|
3628
|
+
*
|
|
3629
|
+
* One descriptor lookup; no read of the value, nothing tracked.
|
|
3630
|
+
*/
|
|
3631
|
+
function isStatic(o, key) {
|
|
3632
|
+
if ($PROXY in o) {
|
|
3633
|
+
// A store answers its descriptor trap with a value; through a view the
|
|
3634
|
+
// descriptor is truthful (see `sourceDescriptor`). A foreign proxy is
|
|
3635
|
+
// opaque: nothing about it is known to be fixed.
|
|
3636
|
+
if (viewOf(o) === undefined) return false;
|
|
3637
|
+
const desc = Reflect.getOwnPropertyDescriptor(o, key);
|
|
3638
|
+
return desc === undefined ? hasStaticKeys(o) : desc.get === undefined;
|
|
3639
|
+
}
|
|
3640
|
+
const desc = Reflect.getOwnPropertyDescriptor(o, key);
|
|
3641
|
+
return desc === undefined || (desc.get === undefined && desc.set === undefined);
|
|
3642
|
+
}
|
|
3643
|
+
function accessorDescriptor(get, enumerable = true) {
|
|
3644
|
+
return { configurable: true, enumerable, get, set: trueFn };
|
|
3645
|
+
}
|
|
3646
|
+
/** The descriptor a consumer should see for `key` on an entry —
|
|
3647
|
+
* the view proxies answer `getOwnPropertyDescriptor` with it, so it tells the
|
|
3648
|
+
* truth through any depth of merge/omit layers.
|
|
3649
|
+
*
|
|
3650
|
+
* A DATA descriptor means "nothing reactive can hide behind this value": the
|
|
3651
|
+
* key is a data property of a plain-object leaf — the compiler's own
|
|
3652
|
+
* encoding of a static attribute. Everything else is an accessor: a getter
|
|
3653
|
+
* on a leaf, a key on a store proxy (its "data" is a signal), or a key on a
|
|
3654
|
+
* merge memo source (the whole object is reactive). That is what lets a
|
|
3655
|
+
* consumer skip a reactive node for a static prop at the bottom of a
|
|
3656
|
+
* component chain, and it is why the store case must NOT forward the store's
|
|
3657
|
+
* own descriptor, which reports a value.
|
|
3658
|
+
*
|
|
3659
|
+
* `configurable: true` always — the target has no such property, and the
|
|
3660
|
+
* Proxy invariants forbid reporting a non-configurable one. */
|
|
3661
|
+
// `present` says the caller has already established `key in s` (a trap's
|
|
3662
|
+
// shadowing walk did), so a store is not asked a second time.
|
|
3663
|
+
function sourceDescriptor(s, kind, key, present = false) {
|
|
3664
|
+
if (kind === SOURCE_OMIT) {
|
|
3665
|
+
if (isHidden(s, key)) return undefined;
|
|
3666
|
+
return sourceDescriptor(s.source, s.kind, key, present);
|
|
3667
|
+
}
|
|
3668
|
+
// An omit's merge record: the descriptor of the last entry that has the
|
|
3669
|
+
// key, as the merge proxy's own trap answers (see `mergeDescriptor`).
|
|
3670
|
+
if (kind === SOURCE_MERGE) return mergeDescriptor(s, key);
|
|
3671
|
+
// A memo source (`merge(() => …)`) is reactive wholesale: whatever shape
|
|
3672
|
+
// the memo's current object has, the key is an accessor here.
|
|
3673
|
+
if (kind === SOURCE_MEMO) {
|
|
3674
|
+
return present || key in leafOf(s, kind)
|
|
3675
|
+
? accessorDescriptor(() => leafOf(s, kind)[key])
|
|
3676
|
+
: undefined;
|
|
3677
|
+
}
|
|
3678
|
+
if (kind === SOURCE_PROXY) {
|
|
3679
|
+
// Another view (an omit's source may be a merge proxy) already answers
|
|
3680
|
+
// truthfully. A store's reported "data" is a signal, and a foreign proxy
|
|
3681
|
+
// (frames slot props) has no own descriptors: for both, existence is
|
|
3682
|
+
// `in` and the kind is accessor — one trap, and never the store's
|
|
3683
|
+
// descriptor path.
|
|
3684
|
+
if (isView(s)) return Reflect.getOwnPropertyDescriptor(s, key);
|
|
3685
|
+
return present || key in s ? accessorDescriptor(() => s[key]) : undefined;
|
|
3686
|
+
}
|
|
3687
|
+
const desc = Reflect.getOwnPropertyDescriptor(s, key);
|
|
3688
|
+
if (desc === undefined) return undefined;
|
|
3689
|
+
if (desc.get !== undefined || desc.set !== undefined)
|
|
3690
|
+
return accessorDescriptor(() => s[key], desc.enumerable);
|
|
3691
|
+
// The proxy target has no such key, so the descriptor must be configurable;
|
|
3692
|
+
// Reflect's is a fresh object, so a configurable one is handed out as is.
|
|
3693
|
+
if (desc.configurable) return desc;
|
|
3694
|
+
return { configurable: true, enumerable: desc.enumerable, writable: true, value: desc.value };
|
|
3695
|
+
}
|
|
3696
|
+
// Own ENUMERABLE keys, symbols included, of an entry — the user-facing key
|
|
3697
|
+
// set (`Object.keys(merged)`), where enumerability matters (#2769).
|
|
3698
|
+
function sourceEnumerableKeys(s, kind) {
|
|
3699
|
+
{
|
|
3700
|
+
if (s.kind === SOURCE_MERGE) return mergeEnumerableKeys(s.source, s);
|
|
3701
|
+
const keys = ownEnumerableKeys(viewSource(s));
|
|
3702
|
+
const out = [];
|
|
3703
|
+
for (let i = 0; i < keys.length; i++) if (!isHidden(s, keys[i])) out.push(keys[i]);
|
|
3704
|
+
return out;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
// The target of a merge() proxy: the flattened sources, read by one shared
|
|
3708
|
+
// handler — like OmitView, no per-instance closures. `sources` is what
|
|
3709
|
+
// `$SOURCES` answers.
|
|
3710
|
+
/** @internal */
|
|
3711
|
+
class MergeView {
|
|
3712
|
+
sources;
|
|
3713
|
+
kinds;
|
|
3714
|
+
/** key → the plain leaf that owns it (later sources win), built by an
|
|
3715
|
+
* enumeration or once the reads have paid for it (see `resolvedTable`)
|
|
3716
|
+
* when every leaf has static keys; `null` when one doesn't. Until then
|
|
3717
|
+
* the slot counts the per-key trap reads so far. One slot rather than a
|
|
3718
|
+
* counter field of its own: a view is built per source per component
|
|
3719
|
+
* layer, and each field initializer is a measurable share of a
|
|
3720
|
+
* constructor that small in the lower JIT tiers. */
|
|
3721
|
+
table = 0;
|
|
3722
|
+
/** see `tableOwnKeys` / `tableDescriptor` */
|
|
3723
|
+
keys = undefined;
|
|
3724
|
+
descs = undefined;
|
|
3725
|
+
constructor(
|
|
3726
|
+
sources,
|
|
3727
|
+
/** `kinds[i]` is what `sources[i]` is (see `SourceKind`). */
|
|
3728
|
+
kinds
|
|
3729
|
+
) {
|
|
3730
|
+
this.sources = sources;
|
|
3731
|
+
this.kinds = kinds;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
/** @internal The `MergeView` behind a `merge()` proxy — its flattened
|
|
3735
|
+
* `sources` with their `kinds` — or undefined. */
|
|
3736
|
+
function mergeView(o) {
|
|
3737
|
+
return o != null && $PROXY in o && o[$TARGET] === undefined ? o[$VIEW] : undefined;
|
|
3738
|
+
}
|
|
3739
|
+
/** @internal The record behind a merge() or omit() proxy — a `MergeView`
|
|
3740
|
+
* (flattened `sources` with their `kinds`) or an `OmitView` — or undefined
|
|
3741
|
+
* for anything else (a plain object, a store, a foreign proxy). Two fast
|
|
3742
|
+
* traps on a store, none on a plain object. */
|
|
3743
|
+
function viewOf(o) {
|
|
3744
|
+
if (o == null || !($PROXY in o) || o[$TARGET] !== undefined) return undefined;
|
|
3745
|
+
const merged = o[$VIEW];
|
|
3746
|
+
return merged !== undefined ? merged : o[$OMIT];
|
|
3747
|
+
}
|
|
3748
|
+
/** @internal The resolved key table of a merge/omit view — every own key of
|
|
3749
|
+
* the view mapped to the plain object that owns it, in merged order (a key
|
|
3750
|
+
* at the position of the last source that carries it, see `tableSet`) — or
|
|
3751
|
+
* undefined when it has none: a leaf is a store or a memo source, whose
|
|
3752
|
+
* keys can change, or the object is not a view at all.
|
|
3753
|
+
*
|
|
3754
|
+
* This is the flat object the eager copy used to build, made lazily and
|
|
3755
|
+
* without copying: one pass over the leaves' own keys, then every
|
|
3756
|
+
* `get`/`has`/descriptor is one lookup plus one read of the owning leaf, and
|
|
3757
|
+
* a consumer (`spread` rerunning its effect) walks the table instead of
|
|
3758
|
+
* re-deriving shadowing from the leaves each time. Own keys only, as the
|
|
3759
|
+
* copy's were: a plain source's key set is fixed once merged (keys added to
|
|
3760
|
+
* it later are not seen — the copy didn't see them either).
|
|
3761
|
+
*
|
|
3762
|
+
* It is built by an ENUMERATION — the `ownKeys` trap, or a consumer asking
|
|
3763
|
+
* for it here — or once per-key reads have paid for it (`READS_FOR_TABLE`),
|
|
3764
|
+
* not on the first read. A per-key read has a direct answer (a walk of the
|
|
3765
|
+
* sources, last to first, one `in` each) whose cost is the source count,
|
|
3766
|
+
* while the table's is every key of every leaf, so the walk wins until a
|
|
3767
|
+
* view has been read about as many times as it has keys. On the server it
|
|
3768
|
+
* never is: a component reads its props a few times, the element enumerates
|
|
3769
|
+
* them once through its own source walk, and the view is gone — building on
|
|
3770
|
+
* first read there cost a component chain a table per layer (profiled on
|
|
3771
|
+
* the Kobalte-shaped chain: a third of SSR time in the table code and its
|
|
3772
|
+
* garbage). On the
|
|
3773
|
+
* client a view read on every reactive rerun crosses the threshold in its
|
|
3774
|
+
* first few updates and is one lookup per read from then on, as before.
|
|
3775
|
+
* Once built — by a `spread`, `Object.keys`, `{...props}`, or the count —
|
|
3776
|
+
* every trap uses it. */
|
|
3777
|
+
function resolvedTable(o) {
|
|
3778
|
+
if (o == null || !($PROXY in o) || o[$TARGET] !== undefined) return undefined;
|
|
3779
|
+
const view = o[$OMIT];
|
|
3780
|
+
if (view !== undefined) return omitTable(view);
|
|
3781
|
+
const merged = o[$VIEW];
|
|
3782
|
+
return merged === undefined ? undefined : mergeTable(merged);
|
|
3783
|
+
}
|
|
3784
|
+
function mergeTable(view) {
|
|
3785
|
+
let table = view.table;
|
|
3786
|
+
if (typeof table !== "object") {
|
|
3787
|
+
// a read count: not decided yet
|
|
3788
|
+
const f = view.sources,
|
|
3789
|
+
k = view.kinds;
|
|
3790
|
+
for (let i = 0; i < f.length; i++) {
|
|
3791
|
+
if (!entryHasStaticKeys(f[i], k[i])) {
|
|
3792
|
+
view.table = null;
|
|
3793
|
+
return undefined;
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
table = new Map();
|
|
3797
|
+
collectTable(table, view, undefined);
|
|
3798
|
+
view.table = table;
|
|
3799
|
+
}
|
|
3800
|
+
return table === null ? undefined : table;
|
|
3801
|
+
}
|
|
3802
|
+
// One pass over a merge record's leaves into `table`, through the nested
|
|
3803
|
+
// omit-over-merge entries — the filters enclosing the current leaf are the
|
|
3804
|
+
// `filters` stack — so a component chain builds ONE table at the view that
|
|
3805
|
+
// asked, not one per layer. A key an entry hides that an EARLIER entry owned
|
|
3806
|
+
// must stay: a filter applies to its own entry's contribution, not to the
|
|
3807
|
+
// merge, which is exactly what the stack expresses. Every entry has static
|
|
3808
|
+
// keys (the caller checked), so a leaf's own keys are the truth.
|
|
3809
|
+
function collectTable(table, view, filters) {
|
|
3810
|
+
const f = view.sources,
|
|
3811
|
+
k = view.kinds;
|
|
3812
|
+
for (let i = 0; i < f.length; i++) {
|
|
3813
|
+
const leaf = f[i];
|
|
3814
|
+
if (k[i] === SOURCE_OMIT) {
|
|
3815
|
+
if (leaf.kind === SOURCE_MERGE) {
|
|
3816
|
+
if (filters === undefined) filters = [leaf];
|
|
3817
|
+
else filters.push(leaf);
|
|
3818
|
+
collectTable(table, leaf.source, filters);
|
|
3819
|
+
filters.pop();
|
|
3820
|
+
continue;
|
|
3821
|
+
}
|
|
3822
|
+
const src = leaf.source;
|
|
3823
|
+
const keys = Reflect.ownKeys(src);
|
|
3824
|
+
for (let j = 0; j < keys.length; j++) {
|
|
3825
|
+
const key = keys[j];
|
|
3826
|
+
if (!isHidden(leaf, key) && !hiddenByAny(filters, key)) tableSet(table, key, src);
|
|
3827
|
+
}
|
|
3828
|
+
} else {
|
|
3829
|
+
const keys = Reflect.ownKeys(leaf);
|
|
3830
|
+
for (let j = 0; j < keys.length; j++) {
|
|
3831
|
+
const key = keys[j];
|
|
3832
|
+
if (!hiddenByAny(filters, key)) tableSet(table, key, leaf);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
function hiddenByAny(filters, key) {
|
|
3838
|
+
if (filters !== undefined)
|
|
3839
|
+
for (let i = filters.length - 1; i >= 0; i--) if (isHidden(filters[i], key)) return true;
|
|
3840
|
+
return false;
|
|
3841
|
+
}
|
|
3842
|
+
// Key order is the merged one — every key at the position of the LAST source
|
|
3843
|
+
// that carries it — the order `ssrElement`'s array form serializes in and the
|
|
3844
|
+
// eager copy enumerated in, so a spread through a view and a spread over the
|
|
3845
|
+
// sources emit the same attribute order.
|
|
3846
|
+
function tableSet(table, key, leaf) {
|
|
3847
|
+
if (table.has(key)) table.delete(key);
|
|
3848
|
+
table.set(key, leaf);
|
|
3849
|
+
}
|
|
3850
|
+
// An omit view's table: its source's (a merge's table, or a plain object's
|
|
3851
|
+
// own keys) minus the hidden keys. Cached on the record.
|
|
3852
|
+
function omitTable(view) {
|
|
3853
|
+
let table = view.table;
|
|
3854
|
+
if (typeof table !== "object") {
|
|
3855
|
+
const src = view.source;
|
|
3856
|
+
if (view.kind === SOURCE_MERGE) {
|
|
3857
|
+
// One pass over the merge's leaves with this filter on the stack —
|
|
3858
|
+
// the merge record builds no table of its own for it.
|
|
3859
|
+
if (!mergeHasStaticKeys(src)) {
|
|
3860
|
+
view.table = null;
|
|
3861
|
+
return undefined;
|
|
3862
|
+
}
|
|
3863
|
+
table = new Map();
|
|
3864
|
+
collectTable(table, src, [view]);
|
|
3865
|
+
} else if (view.kind === SOURCE_PLAIN) {
|
|
3866
|
+
table = new Map();
|
|
3867
|
+
const keys = Reflect.ownKeys(src);
|
|
3868
|
+
for (let j = 0; j < keys.length; j++) {
|
|
3869
|
+
const key = keys[j];
|
|
3870
|
+
if (!isHidden(view, key)) table.set(key, src);
|
|
3871
|
+
}
|
|
3872
|
+
} else {
|
|
3873
|
+
// a store or a memo: keys can change, no table
|
|
3874
|
+
view.table = null;
|
|
3875
|
+
return undefined;
|
|
3876
|
+
}
|
|
3877
|
+
view.table = table;
|
|
3878
|
+
}
|
|
3879
|
+
return table === null ? undefined : table;
|
|
3880
|
+
}
|
|
3881
|
+
// The user-facing key set of a resolved table: its keys that are enumerable
|
|
3882
|
+
// on the leaf that owns them (`Object.keys(merged)`, #2769). Fixed, like the
|
|
3883
|
+
// table, so it is built once per view: an `ownKeys` trap may hand back the
|
|
3884
|
+
// same array every time (the engine copies it).
|
|
3885
|
+
const propertyIsEnumerable$1 = Object.prototype.propertyIsEnumerable;
|
|
3886
|
+
function tableOwnKeys(view, table) {
|
|
3887
|
+
let keys = view.keys;
|
|
3888
|
+
if (keys === undefined) {
|
|
3889
|
+
keys = view.keys = [];
|
|
3890
|
+
for (const [key, leaf] of table) if (propertyIsEnumerable$1.call(leaf, key)) keys.push(key);
|
|
3891
|
+
}
|
|
3892
|
+
return keys;
|
|
3893
|
+
}
|
|
3894
|
+
// The descriptor for a table key — its owning leaf's, truthful (see
|
|
3895
|
+
// `sourceDescriptor`) — with the shape cached per key so an enumeration
|
|
3896
|
+
// (`for…in`, `Object.keys`, `{...props}`: a descriptor trap per key, on
|
|
3897
|
+
// every pass) does not re-read the leaf's descriptor and re-allocate a
|
|
3898
|
+
// getter each time. An accessor reads live, so its descriptor is reused as
|
|
3899
|
+
// is; a data descriptor is rebuilt with the current value.
|
|
3900
|
+
function tableDescriptor(view, table, key) {
|
|
3901
|
+
const leaf = table.get(key);
|
|
3902
|
+
if (leaf === undefined) return undefined;
|
|
3903
|
+
let descs = view.descs;
|
|
3904
|
+
if (descs === undefined) descs = view.descs = new Map();
|
|
3905
|
+
let cached = descs.get(key);
|
|
3906
|
+
if (cached === undefined) {
|
|
3907
|
+
cached = sourceDescriptor(leaf, SOURCE_PLAIN, key);
|
|
3908
|
+
if (cached === undefined) return undefined;
|
|
3909
|
+
descs.set(key, cached);
|
|
3910
|
+
return cached;
|
|
3911
|
+
}
|
|
3912
|
+
if (cached.get !== undefined) return cached;
|
|
3913
|
+
return {
|
|
3914
|
+
configurable: true,
|
|
3915
|
+
enumerable: cached.enumerable,
|
|
3916
|
+
writable: cached.writable,
|
|
3917
|
+
value: leaf[key]
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
// Per-key trap reads a view takes before building its table, from the
|
|
3921
|
+
// break-even: a build is ~60 ns per key (an `ownKeys` share, a `has`, a
|
|
3922
|
+
// `set`), a walk ~20 ns per source (an `in`, a hidden-list check), and a
|
|
3923
|
+
// leaf carries about five keys — so the table has paid for itself after
|
|
3924
|
+
// ~15 reads. Measured on the Kobalte-shaped chain: a walk is 2× a lookup at
|
|
3925
|
+
// depth 1 and up to 10× for a first-source key at depth 7 (a walk through
|
|
3926
|
+
// seven nested layers, a hidden-list check at each), so a view read on
|
|
3927
|
+
// every update wants the table; a view read a handful of times (every
|
|
3928
|
+
// server-side view) never wants it.
|
|
3929
|
+
const READS_FOR_TABLE = 16;
|
|
3930
|
+
// The table for a per-key trap: the one a view HAS (built by an enumeration
|
|
3931
|
+
// or an earlier read, see `resolvedTable`), or the one this read pays for,
|
|
3932
|
+
// or none — a walk answers. One per view type, so a trap pays no type check.
|
|
3933
|
+
// A settled slot is an object (the Map, or `null`); a number is the count.
|
|
3934
|
+
function mergeReadTable(view) {
|
|
3935
|
+
const table = view.table;
|
|
3936
|
+
if (typeof table === "object") return table === null ? undefined : table;
|
|
3937
|
+
if (table + 1 < READS_FOR_TABLE) {
|
|
3938
|
+
view.table = table + 1;
|
|
3939
|
+
return undefined;
|
|
3940
|
+
}
|
|
3941
|
+
return mergeTable(view);
|
|
3942
|
+
}
|
|
3943
|
+
// Only for an omit over a merge (`kind === SOURCE_MERGE`; the caller checks,
|
|
3944
|
+
// inline — a call is not free in every tier): an omit over one object reads
|
|
3945
|
+
// it directly — a list check and a property read, nothing a table would
|
|
3946
|
+
// shorten.
|
|
3947
|
+
function omitReadTable(view) {
|
|
3948
|
+
const table = view.table;
|
|
3949
|
+
if (typeof table === "object") return table === null ? undefined : table;
|
|
3950
|
+
if (table + 1 < READS_FOR_TABLE) {
|
|
3951
|
+
view.table = table + 1;
|
|
3952
|
+
return undefined;
|
|
3953
|
+
}
|
|
3954
|
+
return omitTable(view);
|
|
3955
|
+
}
|
|
3956
|
+
// The table a record HAS — built already by an enumeration or a trap's read
|
|
3957
|
+
// count — or undefined. What a nested walk asks: a record reached through an
|
|
3958
|
+
// outer view's entry counts no reads of its own (the outer view decides for
|
|
3959
|
+
// the whole tree, and its table build then builds the inner ones), so the
|
|
3960
|
+
// inner merges of a component chain build nothing on the server where the
|
|
3961
|
+
// leaves are read a few times each.
|
|
3962
|
+
function tableOf(view) {
|
|
3963
|
+
const table = view.table;
|
|
3964
|
+
return typeof table === "object" && table !== null ? table : undefined;
|
|
3965
|
+
}
|
|
3966
|
+
// "no entry has the key" — distinct from an entry that holds `undefined`.
|
|
3967
|
+
const MISSING = Symbol();
|
|
3968
|
+
// The read: the value of the last entry that has the key, or MISSING. ONE
|
|
3969
|
+
// walk — a nested omit-over-merge entry answers presence and value together,
|
|
3970
|
+
// so a chain of layers is walked once per read, not once per layer per
|
|
3971
|
+
// level.
|
|
3972
|
+
function mergeLookup(view, property) {
|
|
3973
|
+
const table = tableOf(view);
|
|
3974
|
+
if (table !== undefined) {
|
|
3975
|
+
const leaf = table.get(property);
|
|
3976
|
+
return leaf === undefined ? MISSING : leaf[property];
|
|
3977
|
+
}
|
|
3978
|
+
const f = view.sources,
|
|
3979
|
+
k = view.kinds;
|
|
3980
|
+
for (let i = f.length - 1; i >= 0; i--) {
|
|
3981
|
+
const kind = k[i];
|
|
3982
|
+
if (kind === SOURCE_OMIT) {
|
|
3983
|
+
const v = f[i];
|
|
3984
|
+
if (isHidden(v, property)) continue;
|
|
3985
|
+
if (v.kind === SOURCE_MERGE) {
|
|
3986
|
+
const value = mergeLookup(v.source, property);
|
|
3987
|
+
if (value !== MISSING) return value;
|
|
3988
|
+
continue;
|
|
3989
|
+
}
|
|
3990
|
+
const s = viewSource(v);
|
|
3991
|
+
if (property in s) return s[property];
|
|
3992
|
+
} else {
|
|
3993
|
+
const s = leafOf(f[i], kind);
|
|
3994
|
+
if (property in s) return s[property];
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
return MISSING;
|
|
3998
|
+
}
|
|
3999
|
+
function mergeGet(view, property) {
|
|
4000
|
+
const value = mergeLookup(view, property);
|
|
4001
|
+
return value === MISSING ? undefined : value;
|
|
4002
|
+
}
|
|
4003
|
+
// `key in merge`, on the record.
|
|
4004
|
+
function mergeHas(view, property) {
|
|
4005
|
+
const table = tableOf(view);
|
|
4006
|
+
if (table !== undefined) return table.has(property);
|
|
4007
|
+
const f = view.sources,
|
|
4008
|
+
k = view.kinds;
|
|
4009
|
+
for (let i = f.length - 1; i >= 0; i--) if (sourceHas(f[i], k[i], property)) return true;
|
|
4010
|
+
return false;
|
|
4011
|
+
}
|
|
4012
|
+
// The proxy's `getOwnPropertyDescriptor`, on the record.
|
|
4013
|
+
function mergeDescriptor(view, property) {
|
|
4014
|
+
const table = tableOf(view);
|
|
4015
|
+
if (table !== undefined) return tableDescriptor(view, table, property);
|
|
4016
|
+
const f = view.sources,
|
|
4017
|
+
k = view.kinds;
|
|
4018
|
+
for (let i = f.length - 1; i >= 0; i--) {
|
|
4019
|
+
if (!sourceHas(f[i], k[i], property)) continue;
|
|
4020
|
+
// `in` also answers for inherited keys, which have no own descriptor.
|
|
4021
|
+
return (
|
|
4022
|
+
sourceDescriptor(f[i], k[i], property, true) ??
|
|
4023
|
+
accessorDescriptor(() => mergeGet(view, property))
|
|
4024
|
+
);
|
|
4025
|
+
}
|
|
4026
|
+
return undefined;
|
|
4027
|
+
}
|
|
4028
|
+
// Own keys of a merge record in merged order — every key at the position
|
|
4029
|
+
// of the LAST entry that carries it, the order the table keeps and
|
|
4030
|
+
// `ssrElement` serializes in. `enumerable` selects the user-facing set
|
|
4031
|
+
// (`Object.keys`, #2769) over every own string key (a consumer's walk);
|
|
4032
|
+
// `filter` is the omit this record is read through, applied as the keys
|
|
4033
|
+
// are gathered so an omit over a merge builds ONE list per layer. A list
|
|
4034
|
+
// with `indexOf` rather than a Set: a props object has a dozen keys, and a
|
|
4035
|
+
// Set's hash store was 2 KB per row on the Kobalte-shaped chain.
|
|
4036
|
+
function mergeKeysOf(view, enumerable, filter) {
|
|
4037
|
+
const out = [];
|
|
4038
|
+
collectKeys(view, filter === undefined ? undefined : [filter], enumerable, out, null);
|
|
4039
|
+
return out;
|
|
4040
|
+
}
|
|
4041
|
+
// One pass over a merge record's leaves — through its nested omit-over-merge
|
|
4042
|
+
// entries, `filters` the omits enclosing the current leaf (see
|
|
4043
|
+
// `collectTable`) — appending each leaf's keys to `keys` in merged order
|
|
4044
|
+
// (a key already listed moves to the end: later wins) and, when `owners` is
|
|
4045
|
+
// given, the object that owns the key at the same index. A consumer that
|
|
4046
|
+
// reads every key once (`ssrElement`) then reads `owners[i][keys[i]]`: no
|
|
4047
|
+
// `in` walk per key, no table. A memo leaf is resolved once here.
|
|
4048
|
+
function collectKeys(view, filters, enumerable, keys, owners) {
|
|
4049
|
+
const f = view.sources,
|
|
4050
|
+
k = view.kinds;
|
|
4051
|
+
for (let i = 0; i < f.length; i++) {
|
|
4052
|
+
let leaf = f[i],
|
|
4053
|
+
kind = k[i];
|
|
4054
|
+
let filter;
|
|
4055
|
+
if (kind === SOURCE_OMIT) {
|
|
4056
|
+
if (leaf.kind === SOURCE_MERGE) {
|
|
4057
|
+
if (filters === undefined) filters = [leaf];
|
|
4058
|
+
else filters.push(leaf);
|
|
4059
|
+
collectKeys(leaf.source, filters, enumerable, keys, owners);
|
|
4060
|
+
filters.pop();
|
|
4061
|
+
continue;
|
|
4062
|
+
}
|
|
4063
|
+
filter = leaf;
|
|
4064
|
+
kind = leaf.kind;
|
|
4065
|
+
leaf = leaf.source;
|
|
4066
|
+
}
|
|
4067
|
+
leaf = leafOf(leaf, kind);
|
|
4068
|
+
const ks = enumerable ? ownEnumerableKeys(leaf) : leafKeys(leaf, kind);
|
|
4069
|
+
for (let j = 0; j < ks.length; j++) {
|
|
4070
|
+
const key = ks[j];
|
|
4071
|
+
if (filter !== undefined && isHidden(filter, key)) continue;
|
|
4072
|
+
if (hiddenByAny(filters, key)) continue;
|
|
4073
|
+
addKey(keys, owners, key, leaf);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
// Append `key` owned by `owner`, moving an earlier listing to the end: later
|
|
4078
|
+
// wins, and the position is the last owner's (the merged order).
|
|
4079
|
+
function addKey(keys, owners, key, owner) {
|
|
4080
|
+
const at = keys.indexOf(key);
|
|
4081
|
+
if (at !== -1) {
|
|
4082
|
+
keys.splice(at, 1);
|
|
4083
|
+
if (owners !== null) owners.splice(at, 1);
|
|
4084
|
+
}
|
|
4085
|
+
keys.push(key);
|
|
4086
|
+
if (owners !== null) owners.push(owner);
|
|
4087
|
+
}
|
|
4088
|
+
/** @internal Every own string key of a props SOURCE — a plain object, a
|
|
4089
|
+
* store or foreign proxy, or a merge/omit view — appended to `keys` in
|
|
4090
|
+
* merged order with the object that owns each at the same index of
|
|
4091
|
+
* `owners`: a key already listed (by this source or an earlier one) moves
|
|
4092
|
+
* to the end, so several sources collected in turn give the order and the
|
|
4093
|
+
* winners a merge of them would. A consumer that reads each key once
|
|
4094
|
+
* (`ssrElement`) then reads `owners[i][keys[i]]` — the owner's getter runs
|
|
4095
|
+
* there, once — and asks nothing else of a view: no table, no `in` walk per
|
|
4096
|
+
* key through the merge/omit layers, no key list per leaf. One pass,
|
|
4097
|
+
* however deep the layers nest. Symbols are listed; the consumer skips
|
|
4098
|
+
* them. */
|
|
4099
|
+
function sourceOwners(s, keys, owners) {
|
|
4100
|
+
if ($PROXY in s) {
|
|
4101
|
+
const view = viewOf(s);
|
|
4102
|
+
if (view === undefined) {
|
|
4103
|
+
// a store: one `ownKeys` trap, reads through `[]`
|
|
4104
|
+
const ks = Reflect.ownKeys(s);
|
|
4105
|
+
for (let i = 0; i < ks.length; i++) addKey(keys, owners, ks[i], s);
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
if (view instanceof OmitView) {
|
|
4109
|
+
if (view.kind === SOURCE_MERGE) return collectKeys(view.source, [view], false, keys, owners);
|
|
4110
|
+
const leaf = viewSource(view);
|
|
4111
|
+
const ks = leafKeys(leaf, view.kind);
|
|
4112
|
+
for (let i = 0; i < ks.length; i++)
|
|
4113
|
+
if (!isHidden(view, ks[i])) addKey(keys, owners, ks[i], leaf);
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
return collectKeys(view, undefined, false, keys, owners);
|
|
4117
|
+
}
|
|
4118
|
+
const ks = Object.keys(s);
|
|
4119
|
+
for (let i = 0; i < ks.length; i++) addKey(keys, owners, ks[i], s);
|
|
4120
|
+
}
|
|
4121
|
+
// The user-facing key set of a merge record, read through `filter` if given.
|
|
4122
|
+
function mergeEnumerableKeys(view, filter) {
|
|
4123
|
+
const table = mergeTable(view);
|
|
4124
|
+
if (table === undefined) return mergeKeysOf(view, true, filter);
|
|
4125
|
+
const keys = tableOwnKeys(view, table);
|
|
4126
|
+
if (filter === undefined) return keys;
|
|
4127
|
+
const out = [];
|
|
4128
|
+
for (let i = 0; i < keys.length; i++) if (!isHidden(filter, keys[i])) out.push(keys[i]);
|
|
4129
|
+
return out;
|
|
4130
|
+
}
|
|
4131
|
+
const mergeTraps = {
|
|
4132
|
+
get(view, property, receiver) {
|
|
3016
4133
|
if (property === $PROXY) return receiver;
|
|
3017
|
-
|
|
4134
|
+
if (property === $TARGET || property === $OMIT) return undefined;
|
|
4135
|
+
if (property === $SOURCES) return view.sources;
|
|
4136
|
+
if (property === $VIEW) return view;
|
|
4137
|
+
// A trap read counts toward the table (see `mergeReadTable`); the walk
|
|
4138
|
+
// itself is the record's.
|
|
4139
|
+
const table = mergeReadTable(view);
|
|
4140
|
+
if (table !== undefined) {
|
|
4141
|
+
const leaf = table.get(property);
|
|
4142
|
+
return leaf === undefined ? undefined : leaf[property];
|
|
4143
|
+
}
|
|
4144
|
+
return mergeGet(view, property);
|
|
3018
4145
|
},
|
|
3019
|
-
has(
|
|
4146
|
+
has(view, property) {
|
|
3020
4147
|
if (property === $PROXY) return true;
|
|
3021
|
-
|
|
4148
|
+
if (property === $TARGET || property === $OMIT || property === $SOURCES || property === $VIEW)
|
|
4149
|
+
return false;
|
|
4150
|
+
const table = mergeReadTable(view);
|
|
4151
|
+
if (table !== undefined) return table.has(property);
|
|
4152
|
+
return mergeHas(view, property);
|
|
3022
4153
|
},
|
|
3023
4154
|
set: trueFn,
|
|
3024
4155
|
deleteProperty: trueFn,
|
|
3025
|
-
getOwnPropertyDescriptor(
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
4156
|
+
getOwnPropertyDescriptor(view, property) {
|
|
4157
|
+
if (
|
|
4158
|
+
property === $PROXY ||
|
|
4159
|
+
property === $TARGET ||
|
|
4160
|
+
property === $OMIT ||
|
|
4161
|
+
property === $SOURCES ||
|
|
4162
|
+
property === $VIEW
|
|
4163
|
+
)
|
|
4164
|
+
return undefined;
|
|
4165
|
+
const table = mergeReadTable(view);
|
|
4166
|
+
if (table !== undefined) return tableDescriptor(view, table, property);
|
|
4167
|
+
return mergeDescriptor(view, property);
|
|
3035
4168
|
},
|
|
3036
|
-
ownKeys(
|
|
3037
|
-
return
|
|
4169
|
+
ownKeys(view) {
|
|
4170
|
+
return mergeEnumerableKeys(view);
|
|
3038
4171
|
}
|
|
3039
4172
|
};
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
4173
|
+
// An omit view reads its source directly — a hidden-key check and one
|
|
4174
|
+
// property read; over a merge record, the merge's own walk by function call,
|
|
4175
|
+
// never a trap. Once an enumeration or the read count has built its table
|
|
4176
|
+
// (over a merge with plain leaves only: the merge's, filtered) every trap
|
|
4177
|
+
// answers from that instead.
|
|
4178
|
+
const omitTraps = {
|
|
4179
|
+
get(view, property, receiver) {
|
|
4180
|
+
if (property === $PROXY) return receiver;
|
|
4181
|
+
// $VIEW is the underlying merge's record, UNFILTERED: never forwarded,
|
|
4182
|
+
// and $SOURCES never answers the merge's own sources, which would hand
|
|
4183
|
+
// a re-merge the unfiltered objects and leak the omitted keys (#3014).
|
|
4184
|
+
// A consumer reaches the record through $OMIT and walks it as ONE
|
|
4185
|
+
// filtered entry.
|
|
4186
|
+
if (property === $TARGET || property === $VIEW || property === $SOURCES) return undefined;
|
|
4187
|
+
if (property === $OMIT) return view;
|
|
4188
|
+
if (view.kind === SOURCE_MERGE) {
|
|
4189
|
+
const table = omitReadTable(view);
|
|
4190
|
+
if (table !== undefined) {
|
|
4191
|
+
const leaf = table.get(property);
|
|
4192
|
+
return leaf === undefined ? undefined : leaf[property];
|
|
4193
|
+
}
|
|
4194
|
+
if (isHidden(view, property)) return undefined;
|
|
4195
|
+
return mergeGet(view.source, property);
|
|
4196
|
+
}
|
|
4197
|
+
if (isHidden(view, property)) return undefined;
|
|
4198
|
+
return viewSource(view)[property];
|
|
4199
|
+
},
|
|
4200
|
+
has(view, property) {
|
|
4201
|
+
if (property === $PROXY) return true;
|
|
4202
|
+
if (property === $TARGET || property === $VIEW || property === $SOURCES || property === $OMIT)
|
|
4203
|
+
return false;
|
|
4204
|
+
if (view.kind === SOURCE_MERGE) {
|
|
4205
|
+
const table = omitReadTable(view);
|
|
4206
|
+
if (table !== undefined) return table.has(property);
|
|
4207
|
+
if (isHidden(view, property)) return false;
|
|
4208
|
+
return mergeHas(view.source, property);
|
|
4209
|
+
}
|
|
4210
|
+
if (isHidden(view, property)) return false;
|
|
4211
|
+
return property in viewSource(view);
|
|
4212
|
+
},
|
|
4213
|
+
set: trueFn,
|
|
4214
|
+
deleteProperty: trueFn,
|
|
4215
|
+
getOwnPropertyDescriptor(view, property) {
|
|
4216
|
+
if (
|
|
4217
|
+
property === $PROXY ||
|
|
4218
|
+
property === $TARGET ||
|
|
4219
|
+
property === $VIEW ||
|
|
4220
|
+
property === $OMIT ||
|
|
4221
|
+
property === $SOURCES
|
|
4222
|
+
)
|
|
4223
|
+
return undefined;
|
|
4224
|
+
if (view.kind === SOURCE_MERGE) {
|
|
4225
|
+
const table = omitReadTable(view);
|
|
4226
|
+
if (table !== undefined) return tableDescriptor(view, table, property);
|
|
4227
|
+
}
|
|
4228
|
+
return sourceDescriptor(view, SOURCE_OMIT, property);
|
|
4229
|
+
},
|
|
4230
|
+
ownKeys(view) {
|
|
4231
|
+
if (view.kind === SOURCE_MERGE) {
|
|
4232
|
+
const table = omitTable(view);
|
|
4233
|
+
if (table !== undefined) return tableOwnKeys(view, table);
|
|
4234
|
+
// No table (a store or memo leaf): the merge's own key set, filtered.
|
|
4235
|
+
return sourceEnumerableKeys(view);
|
|
4236
|
+
}
|
|
4237
|
+
const keys = Reflect.ownKeys(viewSource(view));
|
|
4238
|
+
const out = [];
|
|
4239
|
+
for (let i = 0; i < keys.length; i++) if (!isHidden(view, keys[i])) out.push(keys[i]);
|
|
4240
|
+
return out;
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
/** @internal The flattened sources behind a `merge()` proxy, or undefined.
|
|
4244
|
+
* A merge's writes are no-ops, so its sources are the whole truth. A COPY of
|
|
4245
|
+
* a merge (`{...merged}`, a descriptor copy) is a plain object that carries
|
|
4246
|
+
* no sources — `ownKeys` never answers $SOURCES — so what is on the copy is
|
|
4247
|
+
* the truth there and every consumer reads it directly (#3384). */
|
|
3050
4248
|
function mergeSources(o) {
|
|
3051
|
-
return o != null && o[$
|
|
4249
|
+
return o != null && $PROXY in o && o[$TARGET] === undefined ? o[$SOURCES] : undefined;
|
|
3052
4250
|
}
|
|
3053
4251
|
/**
|
|
3054
4252
|
* Merges multiple props-like objects into a single proxy that *preserves
|
|
@@ -3058,6 +4256,12 @@ function mergeSources(o) {
|
|
|
3058
4256
|
* Function arguments are treated as memo-backed sources — useful for passing
|
|
3059
4257
|
* derived defaults whose computation should track reactively.
|
|
3060
4258
|
*
|
|
4259
|
+
* The result is a live VIEW of its sources, never a copy: creating it costs
|
|
4260
|
+
* nothing per key, every read goes to the source that owns the key (a getter
|
|
4261
|
+
* runs there, a data property is read live), and writing to it is a no-op.
|
|
4262
|
+
* A single non-function source is returned as is. To own a mutable object,
|
|
4263
|
+
* copy it: `{ ...merged }` snapshots the current values.
|
|
4264
|
+
*
|
|
3061
4265
|
* Use this in component bodies to merge defaults / overrides without losing
|
|
3062
4266
|
* Solid's per-property tracking.
|
|
3063
4267
|
*
|
|
@@ -3072,44 +4276,69 @@ function mergeSources(o) {
|
|
|
3072
4276
|
*/
|
|
3073
4277
|
function merge(...sources) {
|
|
3074
4278
|
if (sources.length === 1 && typeof sources[0] !== "function") return sources[0];
|
|
3075
|
-
let proxy = false;
|
|
3076
4279
|
const flattened = [];
|
|
4280
|
+
const kinds = [];
|
|
4281
|
+
// The one non-falsy source, if there is exactly one: it IS the merge.
|
|
4282
|
+
let only = undefined;
|
|
4283
|
+
let count = 0;
|
|
3077
4284
|
for (let i = 0; i < sources.length; i++) {
|
|
3078
4285
|
const s = sources[i];
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
},
|
|
3101
|
-
keys() {
|
|
3102
|
-
const keys = new Set();
|
|
3103
|
-
for (let i = 0; i < flattened.length; i++) {
|
|
3104
|
-
const sourceKeys = ownEnumerableKeys(resolveSource(flattened[i]));
|
|
3105
|
-
for (let j = 0; j < sourceKeys.length; j++) keys.add(sourceKeys[j]);
|
|
4286
|
+
if (!s) continue;
|
|
4287
|
+
count++;
|
|
4288
|
+
only = s;
|
|
4289
|
+
if (typeof s === "function") {
|
|
4290
|
+
flattened.push(createMemo(s));
|
|
4291
|
+
kinds.push(SOURCE_MEMO);
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
if ($PROXY in s) {
|
|
4295
|
+
// A store (`$TARGET`) is a leaf as it is. A merge() proxy is flattened
|
|
4296
|
+
// through: its writes are no-ops, so its sources are exactly what it
|
|
4297
|
+
// reads. An omit() proxy joins as its view record — ONE entry, its
|
|
4298
|
+
// filter travelling with it, whether it is over a plain object or a
|
|
4299
|
+
// whole merge (never the merge's own sources, which would leak the
|
|
4300
|
+
// omitted keys, #3014). A consumer's walk recurses into the record.
|
|
4301
|
+
if (s[$TARGET] === undefined) {
|
|
4302
|
+
const child = s[$VIEW];
|
|
4303
|
+
if (child !== undefined) {
|
|
4304
|
+
for (let j = 0; j < child.sources.length; j++) {
|
|
4305
|
+
flattened.push(child.sources[j]);
|
|
4306
|
+
kinds.push(child.kinds[j]);
|
|
3106
4307
|
}
|
|
3107
|
-
|
|
4308
|
+
continue;
|
|
3108
4309
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
4310
|
+
const view = s[$OMIT];
|
|
4311
|
+
if (view !== undefined) {
|
|
4312
|
+
flattened.push(view);
|
|
4313
|
+
kinds.push(SOURCE_OMIT);
|
|
4314
|
+
continue;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
flattened.push(s);
|
|
4318
|
+
kinds.push(SOURCE_PROXY);
|
|
4319
|
+
continue;
|
|
4320
|
+
}
|
|
4321
|
+
flattened.push(s);
|
|
4322
|
+
kinds.push(SOURCE_PLAIN);
|
|
4323
|
+
}
|
|
4324
|
+
if (SUPPORTS_PROXY) {
|
|
4325
|
+
if (count === 1 && typeof only !== "function") return only;
|
|
4326
|
+
// Always a view, never a copy. Building a plain object here costs a
|
|
4327
|
+
// descriptor read, a bound getter and a defineProperty per key per
|
|
4328
|
+
// layer, and component libraries stack several layers per element
|
|
4329
|
+
// (defaults → omit → call-site statics → …), so the copies dominated
|
|
4330
|
+
// their render cost while every consumer that matters — `spread`,
|
|
4331
|
+
// `ssrElement`, a nested merge — reads the flattened sources directly
|
|
4332
|
+
// anyway (#3448). The view is O(1) to create and reads through to the
|
|
4333
|
+
// sources, so a data property on a source is read live, like a getter.
|
|
4334
|
+
// Writes to the result are no-ops (a consumer that needs its own object
|
|
4335
|
+
// copies: `{...merged}`, which the traps answer truthfully). Copies of
|
|
4336
|
+
// the result never carry $SOURCES (#3384): `ownKeys` answers only the
|
|
4337
|
+
// sources' keys.
|
|
4338
|
+
return new Proxy(new MergeView(flattened, kinds), mergeTraps);
|
|
4339
|
+
}
|
|
4340
|
+
// No Proxy: an eager descriptor copy, semantics as close to the view as a
|
|
4341
|
+
// plain object allows (getters stay live; data properties are snapshots).
|
|
3113
4342
|
const defined = Object.create(null);
|
|
3114
4343
|
let nonTargetKey = false;
|
|
3115
4344
|
let lastIndex = flattened.length - 1;
|
|
@@ -3145,62 +4374,50 @@ function merge(...sources) {
|
|
|
3145
4374
|
if (desc.get) Object.defineProperty(target, key, desc);
|
|
3146
4375
|
else target[key] = desc.value;
|
|
3147
4376
|
}
|
|
3148
|
-
target[$SOURCES] = flattened;
|
|
3149
4377
|
return target;
|
|
3150
4378
|
}
|
|
3151
|
-
/**
|
|
3152
|
-
* Returns a reactive proxy of `props` with the listed keys hidden. Tracking
|
|
3153
|
-
* on the remaining keys is preserved.
|
|
3154
|
-
*
|
|
3155
|
-
* Use it to forward "rest" props to a child element while pulling out the
|
|
3156
|
-
* keys your component handles itself — the equivalent of `splitProps(p, ["a","b"])[1]`.
|
|
3157
|
-
*
|
|
3158
|
-
* @example
|
|
3159
|
-
* ```tsx
|
|
3160
|
-
* function Input(props: { label: string; value: string; onInput: (v: string) => void } & JSX.HTMLAttributes<HTMLInputElement>) {
|
|
3161
|
-
* const rest = omit(props, "label", "value", "onInput");
|
|
3162
|
-
*
|
|
3163
|
-
* return (
|
|
3164
|
-
* <label>
|
|
3165
|
-
* {props.label}
|
|
3166
|
-
* <input
|
|
3167
|
-
* {...rest}
|
|
3168
|
-
* value={props.value}
|
|
3169
|
-
* onInput={e => props.onInput(e.currentTarget.value)}
|
|
3170
|
-
* />
|
|
3171
|
-
* </label>
|
|
3172
|
-
* );
|
|
3173
|
-
* }
|
|
3174
|
-
* ```
|
|
3175
|
-
*/
|
|
3176
4379
|
function omit(props, ...keys) {
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
4380
|
+
let hidden = keys.length === 1 && typeof keys[0] === "function" ? keys[0] : keys;
|
|
4381
|
+
if (SUPPORTS_PROXY) {
|
|
4382
|
+
// A view over a view folds: one record, both filters, the original
|
|
4383
|
+
// source — so a consumer walks the real object however deep the omits go.
|
|
4384
|
+
// Over a merge() proxy the source is the merge's RECORD (see OmitView):
|
|
4385
|
+
// one record whatever the leaf count, read by function call.
|
|
4386
|
+
let source = props;
|
|
4387
|
+
let kind = SOURCE_PLAIN;
|
|
4388
|
+
if (typeof props === "function") kind = SOURCE_MEMO;
|
|
4389
|
+
else if ($PROXY in props) {
|
|
4390
|
+
kind = SOURCE_PROXY;
|
|
4391
|
+
if (props[$TARGET] === undefined) {
|
|
4392
|
+
const inner = props[$OMIT];
|
|
4393
|
+
if (inner !== undefined) {
|
|
4394
|
+
source = inner.source;
|
|
4395
|
+
kind = inner.kind;
|
|
4396
|
+
hidden = combineHidden(inner.hidden, hidden);
|
|
4397
|
+
} else {
|
|
4398
|
+
const merged = props[$VIEW];
|
|
4399
|
+
if (merged !== undefined) {
|
|
4400
|
+
source = merged;
|
|
4401
|
+
kind = SOURCE_MERGE;
|
|
4402
|
+
}
|
|
3194
4403
|
}
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
return new Proxy(new OmitView(source, kind, hidden), omitTraps);
|
|
3198
4407
|
}
|
|
3199
4408
|
const result = {};
|
|
3200
4409
|
const propNames = Object.getOwnPropertyNames(props);
|
|
3201
|
-
const
|
|
4410
|
+
const isHiddenKey =
|
|
4411
|
+
typeof hidden === "function"
|
|
4412
|
+
? hidden
|
|
4413
|
+
: hidden.length > 4 && propNames.length > hidden.length
|
|
4414
|
+
? (
|
|
4415
|
+
blocked => key =>
|
|
4416
|
+
blocked.has(key)
|
|
4417
|
+
)(new Set(hidden))
|
|
4418
|
+
: key => hidden.includes(key);
|
|
3202
4419
|
for (const propName of propNames) {
|
|
3203
|
-
if (
|
|
4420
|
+
if (!isHiddenKey(propName)) {
|
|
3204
4421
|
const desc = Object.getOwnPropertyDescriptor(props, propName);
|
|
3205
4422
|
!desc.get && !desc.set && desc.enumerable && desc.writable && desc.configurable
|
|
3206
4423
|
? (result[propName] = desc.value)
|
|
@@ -3355,6 +4572,11 @@ const slotNodeEquals = function (a, b) {
|
|
|
3355
4572
|
setSlotUnobserved(node => {
|
|
3356
4573
|
// A live affects() mark keeps the node addressable (sweep parity).
|
|
3357
4574
|
if (node._x?._affectsCount) return;
|
|
4575
|
+
// An active override or a staged write is state only the node holds (an
|
|
4576
|
+
// optimistic signal keeps its override whether or not anything reads it —
|
|
4577
|
+
// store parity, posture-store-parity S7): defer the release to the flush
|
|
4578
|
+
// that resolves it (the scheduler's transient-node sweep).
|
|
4579
|
+
if (hasActiveOverride(node) || node._pendingValue !== NOT_PENDING) return deferSlotRelease(node);
|
|
3358
4580
|
const t = node._host;
|
|
3359
4581
|
const key = node._key;
|
|
3360
4582
|
if (t.n && t.n[key] === node) {
|
|
@@ -3387,11 +4609,16 @@ function getNode(
|
|
|
3387
4609
|
// (stageHeldKey). Its held-adoption notification (stageHeldAdoptions)
|
|
3388
4610
|
// ran before the node existed and the drain has nothing left to say.
|
|
3389
4611
|
// Without this the node was born from whichever view its first reader
|
|
3390
|
-
// saw and never learned the other.
|
|
3391
|
-
// write to an unobserved key
|
|
3392
|
-
//
|
|
3393
|
-
|
|
4612
|
+
// saw and never learned the other. Two kinds of hold, one rule (#3336):
|
|
4613
|
+
// - a held FOLD (pb): a setter's write to an unobserved key landed only
|
|
4614
|
+
// in the pending backing; committed is `v[key]`, staged is `pb[key]`
|
|
4615
|
+
// (undefined for a deleted key);
|
|
4616
|
+
// - a held ADOPTION (ht): the adopted value already swapped into `v`;
|
|
4617
|
+
// committed is the held view `hv[key]`, staged is `v[key]`.
|
|
4618
|
+
const fold = heldFoldTransition(target);
|
|
4619
|
+
let held = heldAdoptionTransition(target);
|
|
3394
4620
|
if (held !== null) current = target.hv[key];
|
|
4621
|
+
else if ((held = fold) !== null) current = target.v[key];
|
|
3395
4622
|
// Create-floor diet: slotSignal bakes the whole node into one literal —
|
|
3396
4623
|
// no options object, no equals/unobserved closures, no NodeExtension,
|
|
3397
4624
|
// no post-construction expandos (acc + the wrap cache px/pxv are
|
|
@@ -3418,7 +4645,10 @@ function getNode(
|
|
|
3418
4645
|
// "signal". Gated on the engine being installed — node creation is
|
|
3419
4646
|
// the hottest store path, and the disabled cost must stay one null
|
|
3420
4647
|
// check (nodes created before enable() stay generically named).
|
|
3421
|
-
if (attrHooks !== null)
|
|
4648
|
+
if (attrHooks !== null) {
|
|
4649
|
+
created._name = "store." + String(key);
|
|
4650
|
+
stampNodeOwner(created, target);
|
|
4651
|
+
}
|
|
3422
4652
|
// Optimistic families: arm the override slot — setSignal routes armed
|
|
3423
4653
|
// nodes through the core engine (lanes, ownership, reverts all native).
|
|
3424
4654
|
if (target.fam?.opt) {
|
|
@@ -3428,7 +4658,16 @@ function getNode(
|
|
|
3428
4658
|
// A node born inside a live mark's identity scope inherits the mark
|
|
3429
4659
|
// (the declaration walk could only cover nodes existing then).
|
|
3430
4660
|
if (key !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(created, target.v, key);
|
|
3431
|
-
if (held !== null)
|
|
4661
|
+
if (held !== null)
|
|
4662
|
+
stageHeldKey(
|
|
4663
|
+
created,
|
|
4664
|
+
fold !== null
|
|
4665
|
+
? target.del !== null && target.del.has(key)
|
|
4666
|
+
? undefined
|
|
4667
|
+
: target.pb[key]
|
|
4668
|
+
: target.v[key],
|
|
4669
|
+
held
|
|
4670
|
+
);
|
|
3432
4671
|
nodes[key] = node;
|
|
3433
4672
|
target.nc++;
|
|
3434
4673
|
markDescendants(target);
|
|
@@ -3456,6 +4695,48 @@ function heldAdoptionTransition(target) {
|
|
|
3456
4695
|
* it walks no subscriber (the node has none yet). Transition-stamped now, as
|
|
3457
4696
|
* `runFolded` does — no parked-flush pass will stamp it.
|
|
3458
4697
|
*/
|
|
4698
|
+
/** The live transaction holding `target`'s pending backing (the #3089
|
|
4699
|
+
* write-time stamp, resolved through merges), else null. */
|
|
4700
|
+
function liveFoldTransition(target) {
|
|
4701
|
+
if (target.pb === null) return null;
|
|
4702
|
+
const fb = foldBatches.get(target);
|
|
4703
|
+
if (fb === undefined) return null;
|
|
4704
|
+
const txn = currentTransition(fb);
|
|
4705
|
+
return txn._done === false ? txn : null;
|
|
4706
|
+
}
|
|
4707
|
+
/** liveFoldTransition for node materialization (stageHeldKey). Inside the
|
|
4708
|
+
* draft the backing is the setter's working copy, not a flushed hold — its
|
|
4709
|
+
* nodes take their writes at setter exit (notifyWrites). Optimistic families
|
|
4710
|
+
* hold at the backing: tentative writes are node overrides over a discarded
|
|
4711
|
+
* clone, and a truth-staged landing is masked from ordinary readers by
|
|
4712
|
+
* heldTruthMasked — neither is a plain staged write to mirror. Chained
|
|
4713
|
+
* backings serve the inner store's live value, never a node value. */
|
|
4714
|
+
function heldFoldTransition(target) {
|
|
4715
|
+
if (target.ch || target.fam?.opt === true || inDraft(target)) return null;
|
|
4716
|
+
return liveFoldTransition(target);
|
|
4717
|
+
}
|
|
4718
|
+
/**
|
|
4719
|
+
* Rule 1 at the backing, the hold half (core serve()'s stale-of-foreign and
|
|
4720
|
+
* A29 arms, for a container instead of a node): given the transaction
|
|
4721
|
+
* holding what a reader `c` (a pass; callers serve context-free readers the
|
|
4722
|
+
* committed container themselves) is about to be served, is the STAGED
|
|
4723
|
+
* container its to see? Its own hold, or none — yes. A foreign hold — a
|
|
4724
|
+
* stale pass (render effect) keeps the committed frame and is recorded for
|
|
4725
|
+
* replay at the hold's commit (recordStaleReplay, A15 / A26); a deriving
|
|
4726
|
+
* pass takes the staged world and enters the transaction (enterStagedRead,
|
|
4727
|
+
* A29). Shared by both hold kinds: a setter's fold (committed `v`, staged
|
|
4728
|
+
* `pb`, the write-time stamp) and an adoption under a transaction
|
|
4729
|
+
* (committed = the held view `hv`, staged = the adopted `v`, #3074).
|
|
4730
|
+
*/
|
|
4731
|
+
function holdVisible(txn, c) {
|
|
4732
|
+
if (txn === null || ownsHold(txn)) return true;
|
|
4733
|
+
if (stale) {
|
|
4734
|
+
recordStaleReplay(txn, c);
|
|
4735
|
+
return false;
|
|
4736
|
+
}
|
|
4737
|
+
enterStagedRead(null, txn);
|
|
4738
|
+
return true;
|
|
4739
|
+
}
|
|
3459
4740
|
function stageHeldKey(node, nv, txn) {
|
|
3460
4741
|
if (slotNodeEquals.call(node, node._value, nv)) return;
|
|
3461
4742
|
node._pendingValue = nv;
|
|
@@ -3467,6 +4748,27 @@ function sameLogicalSlot(target, a, b) {
|
|
|
3467
4748
|
const at = lookupTarget$1(a, target.fam);
|
|
3468
4749
|
return at !== undefined && at === lookupTarget$1(b, target.fam);
|
|
3469
4750
|
}
|
|
4751
|
+
/**
|
|
4752
|
+
* Observe-tier: the owner each store root was created under. The proxy
|
|
4753
|
+
* cannot carry `_owner` itself (`registerGraph`'s stamp is swallowed by the
|
|
4754
|
+
* set trap outside a draft), so the root target keys it here and the store's
|
|
4755
|
+
* nodes copy it into `_owner` as they are created — an `OBSERVE.exclude`d
|
|
4756
|
+
* panel's store nodes are then excluded subjects, exactly like its signals.
|
|
4757
|
+
* Both ends are gated with the naming on the engine being installed: node
|
|
4758
|
+
* creation is the hottest store path, store creation is next, and the
|
|
4759
|
+
* disabled cost of each stays one null check. A store created before
|
|
4760
|
+
* `enable()` therefore has no recorded owner and its nodes are never excluded
|
|
4761
|
+
* subjects — the same boundary the naming draws; a panel enables first.
|
|
4762
|
+
*/
|
|
4763
|
+
const storeOwners = new WeakMap();
|
|
4764
|
+
function storeRootOwner(target) {
|
|
4765
|
+
let root = target;
|
|
4766
|
+
while (root.u !== null) root = root.u;
|
|
4767
|
+
return storeOwners.get(root);
|
|
4768
|
+
}
|
|
4769
|
+
function stampNodeOwner(created, target) {
|
|
4770
|
+
created._owner = storeRootOwner(target) ?? null;
|
|
4771
|
+
}
|
|
3470
4772
|
function getHasNode(target, key, present) {
|
|
3471
4773
|
const nodes = (target.h ??= Object.create(null));
|
|
3472
4774
|
let node = nodes[key];
|
|
@@ -3477,6 +4779,13 @@ function getHasNode(target, key, present) {
|
|
|
3477
4779
|
equals: isEqual,
|
|
3478
4780
|
unobserved() {
|
|
3479
4781
|
if (created._x?._affectsCount) return;
|
|
4782
|
+
// The structural twin of the value slot's rule (setSlotUnobserved,
|
|
4783
|
+
// S7): an optimistic add/delete lives on this presence node as its
|
|
4784
|
+
// override — releasing it with the override on would let `in`,
|
|
4785
|
+
// `Object.keys` and descriptors fall back to committed structure
|
|
4786
|
+
// while the action is live. Defer to the flush that resolves it.
|
|
4787
|
+
if (hasActiveOverride(created) || created._pendingValue !== NOT_PENDING)
|
|
4788
|
+
return deferSlotRelease(created);
|
|
3480
4789
|
if (target.h && target.h[key] === created) {
|
|
3481
4790
|
delete target.h[key];
|
|
3482
4791
|
unlinkFirewallChild(created);
|
|
@@ -3486,6 +4795,7 @@ function getHasNode(target, key, present) {
|
|
|
3486
4795
|
target.fam?.node ?? undefined
|
|
3487
4796
|
));
|
|
3488
4797
|
created._config |= CONFIG_OWNED_WRITE;
|
|
4798
|
+
if (attrHooks !== null) stampNodeOwner(created, target);
|
|
3489
4799
|
if (target.fam?.opt) {
|
|
3490
4800
|
ext(created)._overrideValue = NOT_PENDING;
|
|
3491
4801
|
created._config |= CONFIG_OPTIMISTIC;
|
|
@@ -3513,6 +4823,7 @@ function getKeySetNode(target) {
|
|
|
3513
4823
|
target.fam?.node ?? undefined
|
|
3514
4824
|
));
|
|
3515
4825
|
created._config |= CONFIG_OWNED_WRITE;
|
|
4826
|
+
if (attrHooks !== null) stampNodeOwner(created, target);
|
|
3516
4827
|
if (target.fam?.opt) {
|
|
3517
4828
|
ext(created)._overrideValue = NOT_PENDING;
|
|
3518
4829
|
created._config |= CONFIG_OPTIMISTIC;
|
|
@@ -3539,6 +4850,7 @@ function getDeepNode(target) {
|
|
|
3539
4850
|
target.fam?.node ?? undefined
|
|
3540
4851
|
));
|
|
3541
4852
|
created._config |= CONFIG_OWNED_WRITE;
|
|
4853
|
+
if (attrHooks !== null) stampNodeOwner(created, target);
|
|
3542
4854
|
if (target.fam?.opt) {
|
|
3543
4855
|
ext(created)._overrideValue = NOT_PENDING;
|
|
3544
4856
|
created._config |= CONFIG_OPTIMISTIC;
|
|
@@ -4078,6 +5390,7 @@ function storePath$1(t) {
|
|
|
4078
5390
|
function reportReplacedContainers(t, old, pb, writtenKeys) {
|
|
4079
5391
|
const keys = writtenKeys ?? Reflect.ownKeys(pb);
|
|
4080
5392
|
const isArray = Array.isArray(pb);
|
|
5393
|
+
const owner = storeRootOwner(t);
|
|
4081
5394
|
for (const key of keys) {
|
|
4082
5395
|
if ((isArray && key === "length") || key === $OWNER) continue;
|
|
4083
5396
|
if (t.del !== null && t.del.has(key)) continue;
|
|
@@ -4115,7 +5428,8 @@ function reportReplacedContainers(t, old, pb, writtenKeys) {
|
|
|
4115
5428
|
isArr,
|
|
4116
5429
|
total,
|
|
4117
5430
|
unchanged,
|
|
4118
|
-
isArr ? ov.length : Object.keys(ov).length
|
|
5431
|
+
isArr ? ov.length : Object.keys(ov).length,
|
|
5432
|
+
owner
|
|
4119
5433
|
);
|
|
4120
5434
|
}
|
|
4121
5435
|
}
|
|
@@ -4528,22 +5842,12 @@ const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
4528
5842
|
* computed (#2687 — untracked reads inside mapArray Roots see in-flight
|
|
4529
5843
|
* values mid-flush). CHILDREN_FORBIDDEN execution scopes (createTrackedEffect
|
|
4530
5844
|
* / onSettled callbacks) get COMMITTED visibility (#3006), same as core. */
|
|
4531
|
-
|
|
5845
|
+
/** Core read()'s reader: the current computation, a root reading as its
|
|
5846
|
+
* parent computed (`context` persists under untrack — an untracked read
|
|
5847
|
+
* inside an effect is still that effect's read). */
|
|
5848
|
+
function readerContext() {
|
|
4532
5849
|
const c = getOwner();
|
|
4533
|
-
|
|
4534
|
-
const eff = c._root ? c._parentComputed : c;
|
|
4535
|
-
return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
4536
|
-
}
|
|
4537
|
-
/** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
|
|
4538
|
-
* callbacks). Distinct from context-free: these scopes get committed
|
|
4539
|
-
* visibility even against a projection's authoritative-elect pending
|
|
4540
|
-
* backing (#3082) — parity with signals, where core read() serves
|
|
4541
|
-
* committed to them regardless of staged writes. */
|
|
4542
|
-
function inForbiddenScope() {
|
|
4543
|
-
const c = getOwner();
|
|
4544
|
-
if (c === null) return false;
|
|
4545
|
-
const eff = c._root ? c._parentComputed : c;
|
|
4546
|
-
return eff != null && !!(eff._config & CONFIG_CHILDREN_FORBIDDEN);
|
|
5850
|
+
return c === null ? null : c._root ? (c._parentComputed ?? null) : c;
|
|
4547
5851
|
}
|
|
4548
5852
|
/** A pending fold is transition-held when any written node's parked value is
|
|
4549
5853
|
* stamped by a live transition (a plain batch parking — the lazy-recompute
|
|
@@ -4563,19 +5867,23 @@ function foldHeld(target) {
|
|
|
4563
5867
|
return false;
|
|
4564
5868
|
}
|
|
4565
5869
|
function readSource(target) {
|
|
4566
|
-
//
|
|
4567
|
-
// pre-hold committed
|
|
4568
|
-
// readers —
|
|
4569
|
-
//
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
!inDraft(target) &&
|
|
4574
|
-
!getWriteOverride() &&
|
|
4575
|
-
!inOwnerContext()
|
|
4576
|
-
) {
|
|
5870
|
+
// Adoption hold first (#3074): an adoption staged under a live transaction
|
|
5871
|
+
// (or a latest()-pull, PLAIN_HOLD) serves the pre-hold committed view to
|
|
5872
|
+
// committed-visibility readers — context-free and children-forbidden ones,
|
|
5873
|
+
// and stale passes off a foreign hold. Drafts, write-override and latest()
|
|
5874
|
+
// see the adopted backing.
|
|
5875
|
+
const ht = target.ht;
|
|
5876
|
+
if (ht !== null && !latestReadActive && !inDraft(target) && !getWriteOverride()) {
|
|
4577
5877
|
const hv = heldMaskView(target);
|
|
4578
|
-
if (hv !== null)
|
|
5878
|
+
if (hv !== null) {
|
|
5879
|
+
const c = readerContext();
|
|
5880
|
+
if (
|
|
5881
|
+
c === null ||
|
|
5882
|
+
c._config & CONFIG_CHILDREN_FORBIDDEN ||
|
|
5883
|
+
!holdVisible(ht === PLAIN_HOLD ? null : currentTransition(ht), c)
|
|
5884
|
+
)
|
|
5885
|
+
return hv;
|
|
5886
|
+
}
|
|
4579
5887
|
}
|
|
4580
5888
|
return pendingBackingVisible(target, false) ? target.pb : target.v;
|
|
4581
5889
|
}
|
|
@@ -4595,25 +5903,28 @@ function readSource(target) {
|
|
|
4595
5903
|
* divergence from context-free per-key reads) — but never through a hold:
|
|
4596
5904
|
* held truth stays masked exactly as it is for per-key readers. */
|
|
4597
5905
|
function pendingBackingVisible(target, speculative) {
|
|
4598
|
-
return
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
5906
|
+
if (target.pb === null) return false;
|
|
5907
|
+
// The writer's own channels compose on the pending backing regardless.
|
|
5908
|
+
if (inDraft(target) || getWriteOverride()) return true;
|
|
5909
|
+
// HELD truth on an optimistic family (#3164 fold) is masked from ordinary
|
|
5910
|
+
// readers until the transaction's reveal (the backing-level twin of core
|
|
5911
|
+
// serve()'s CONFIG_HELD_TRUTH arm; authoritative postures and latest()
|
|
5912
|
+
// tunnel through inside heldTruthMasked).
|
|
5913
|
+
if (heldTruthMasked(target)) return false;
|
|
5914
|
+
const c = readerContext();
|
|
5915
|
+
if (c === null || c._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
5916
|
+
// No pass, or a children-forbidden one: the committed frame (A32) —
|
|
5917
|
+
// except the speculative peek (deep()/snapshot()), which sees ordinary
|
|
5918
|
+
// pending staging but never through a live foreign hold, and a
|
|
5919
|
+
// projection's pending backing, authoritative-elect for context-free
|
|
5920
|
+
// readers UNLESS a transition holds the node commits (downstream async
|
|
5921
|
+
// hold — stale committed is the contract; the write-time stamp covers
|
|
5922
|
+
// keys with no node, #3336) or the scope is children-forbidden (#3082).
|
|
5923
|
+
const txn = liveFoldTransition(target);
|
|
5924
|
+
if (speculative) return txn === null || ownsHold(txn);
|
|
5925
|
+
return target.fam !== null && c === null && !foldHeld(target) && txn === null;
|
|
5926
|
+
}
|
|
5927
|
+
return holdVisible(liveFoldTransition(target), c);
|
|
4617
5928
|
}
|
|
4618
5929
|
/** #3164 fold: HELD truth on an optimistic family — a pending backing
|
|
4619
5930
|
* stamped by a live transition that retains optimism — is masked from
|
|
@@ -4655,11 +5966,6 @@ function runAuthoritative(fn) {
|
|
|
4655
5966
|
setProjectionWriteActive(was);
|
|
4656
5967
|
}
|
|
4657
5968
|
}
|
|
4658
|
-
/** Active optimistic override on an armed node (armed slot idles at
|
|
4659
|
-
* NOT_PENDING; undefined = unarmed plain node). */
|
|
4660
|
-
function hasActiveOverride(node) {
|
|
4661
|
-
return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
|
|
4662
|
-
}
|
|
4663
5969
|
/** The reading computation is until()'s authoritative-view predicate — same
|
|
4664
5970
|
* source of truth as core read()'s A17 carve-out (`context`, which persists
|
|
4665
5971
|
* under untrack). optimisticView()'s composition gate consults exactly this:
|
|
@@ -4688,26 +5994,23 @@ function authoritativeServe() {
|
|
|
4688
5994
|
* FORCE sentinels never surface (they only bump subscribers of accessor
|
|
4689
5995
|
* keys, which are served by the trap, not the node). */
|
|
4690
5996
|
function nodeValue(node, backing) {
|
|
4691
|
-
//
|
|
4692
|
-
//
|
|
4693
|
-
//
|
|
4694
|
-
//
|
|
4695
|
-
//
|
|
4696
|
-
|
|
4697
|
-
|
|
5997
|
+
// Store-only tunnels, ahead of Rule 1: truth authors (authoritativeServe —
|
|
5998
|
+
// the projection derive's draft, the write-override continuation) see
|
|
5999
|
+
// staged truth and never an override; latest() reaching this untracked
|
|
6000
|
+
// path for a store key sees the in-flight parked value like an
|
|
6001
|
+
// owner-context reader does (#3075), the visible override first.
|
|
6002
|
+
let v;
|
|
6003
|
+
if (authoritativeServe()) v = node._pendingValue !== NOT_PENDING ? node._pendingValue : backing;
|
|
6004
|
+
else if (latestReadActive)
|
|
6005
|
+
v = visibleOverride(node)
|
|
4698
6006
|
? unwrapOverride(node._x?._overrideValue)
|
|
4699
|
-
: node._pendingValue !== NOT_PENDING
|
|
4700
|
-
(latestReadActive ||
|
|
4701
|
-
// Owner-context pending visibility — except HELD truth (#3164,
|
|
4702
|
-
// see CONFIG_HELD_TRUTH: fold-staged or entangle-stolen
|
|
4703
|
-
// confirming truth), which only authoritative/latest readers
|
|
4704
|
-
// see (core read()'s A17-for-held-truth twin; ordinary readers
|
|
4705
|
-
// keep committed until the transaction's reveal — latest() is
|
|
4706
|
-
// exempted by the leading arm above).
|
|
4707
|
-
((inOwnerContext() || authoritativeServe()) &&
|
|
4708
|
-
!(node._config & CONFIG_HELD_TRUTH && !authoritativeServe())))
|
|
6007
|
+
: node._pendingValue !== NOT_PENDING
|
|
4709
6008
|
? node._pendingValue
|
|
4710
6009
|
: backing;
|
|
6010
|
+
// Otherwise the one slow selection core read() uses (serve): override,
|
|
6011
|
+
// lane gate, A28, readerSeesCommitted / A29 — with the BACKING as the
|
|
6012
|
+
// committed value (single-home rule, O6).
|
|
6013
|
+
else v = serve(node, readerContext(), node._firewall || node, backing);
|
|
4711
6014
|
return v === FORCE ? backing : v;
|
|
4712
6015
|
}
|
|
4713
6016
|
/** §7b: a chained target's child found as a RAW — from its pending backing
|
|
@@ -4759,7 +6062,8 @@ function serveDataKey(target, key, backingValue, src, node, accKnown = -1) {
|
|
|
4759
6062
|
// Truth authors read the backing's own length — an optimistic row from
|
|
4760
6063
|
// the caller's transaction must not shift where the author's next write
|
|
4761
6064
|
// lands (#3108).
|
|
4762
|
-
return (authoritativeServe() ? src : optHooks.optimisticView(target, src))
|
|
6065
|
+
return (authoritativeServe() ? src : optHooks.optimisticView(target, src, inDraft(target)))
|
|
6066
|
+
.length;
|
|
4763
6067
|
}
|
|
4764
6068
|
if (inDraft(target)) {
|
|
4765
6069
|
// Optimistic drafts before their first write have no pending backing yet;
|
|
@@ -4773,23 +6077,23 @@ function serveDataKey(target, key, backingValue, src, node, accKnown = -1) {
|
|
|
4773
6077
|
v = unwrapOverride(node._x?._overrideValue);
|
|
4774
6078
|
}
|
|
4775
6079
|
} else {
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
6080
|
+
// §7b: a lane value on the outer node SHADOWS read-through — an active
|
|
6081
|
+
// override pierces the chained gate; otherwise chained backings always
|
|
6082
|
+
// serve the live inner value.
|
|
6083
|
+
if (getObserver() !== null) {
|
|
6084
|
+
// First tracked read: create + link (the wrap-cache branch below
|
|
6085
|
+
// populates px/pxv so read #2 skips wrapNext, slice 2). The value is
|
|
6086
|
+
// served THROUGH the node from this read on — a node born under a held
|
|
6087
|
+
// fold carries the hold (getNode, #3336), and the backing it was read
|
|
6088
|
+
// from does not.
|
|
6089
|
+
if (node === undefined) node = getNode(target, key, backingValue, accKnown);
|
|
6090
|
+
// read()'s plain-signal fast path hoisted over the call (legacy trap
|
|
6091
|
+
// parity): READ_SLOW = a global read window or non-plain node.
|
|
6092
|
+
let nv = readNodeFast(node);
|
|
6093
|
+
if (nv === READ_SLOW) nv = read(node);
|
|
6094
|
+
if (!chained || hasActiveOverride(node)) v = nv === FORCE ? backingValue : nv;
|
|
6095
|
+
} else if (node !== undefined && (!chained || hasActiveOverride(node))) {
|
|
6096
|
+
v = nodeValue(node, backingValue);
|
|
4793
6097
|
}
|
|
4794
6098
|
}
|
|
4795
6099
|
// Shallow stores serve data raw; store-proxy slots get boundary wrappers.
|
|
@@ -5012,7 +6316,7 @@ const traps = {
|
|
|
5012
6316
|
!authoritativeServe()
|
|
5013
6317
|
) {
|
|
5014
6318
|
const node = target.n?.[key];
|
|
5015
|
-
if (node !== undefined &&
|
|
6319
|
+
if (node !== undefined && visibleOverride(node))
|
|
5016
6320
|
v = unwrapOverride(node._x?._overrideValue);
|
|
5017
6321
|
}
|
|
5018
6322
|
if (target.s) return serveShallow(target, key, v);
|
|
@@ -5044,7 +6348,7 @@ const traps = {
|
|
|
5044
6348
|
if (hasActiveOverride(node)) present = !!nv;
|
|
5045
6349
|
} else if (!authoritativeServe()) {
|
|
5046
6350
|
const node = target.h?.[key];
|
|
5047
|
-
if (node !== undefined &&
|
|
6351
|
+
if (node !== undefined && visibleOverride(node))
|
|
5048
6352
|
present = !!unwrapOverride(node._x?._overrideValue);
|
|
5049
6353
|
}
|
|
5050
6354
|
} else if (target.fam?.opt && draftSeesOverrides(target) && !authoritativeServe()) {
|
|
@@ -5062,7 +6366,25 @@ const traps = {
|
|
|
5062
6366
|
},
|
|
5063
6367
|
getOwnPropertyDescriptor(target, key) {
|
|
5064
6368
|
if (key === $OWNER) return undefined;
|
|
5065
|
-
|
|
6369
|
+
// A descriptor read is a PRESENCE read: it subscribes to the key's
|
|
6370
|
+
// presence node and witnesses affects()/isPending() exactly as `in` does
|
|
6371
|
+
// (structural oracle, 2026-09-17 — the trap read no node before, so a
|
|
6372
|
+
// render effect inspecting a key through getOwnPropertyDescriptor never
|
|
6373
|
+
// re-ran for an optimistic add or delete, and an isPending() probe over
|
|
6374
|
+
// it witnessed nothing). The value it reports rides the value node's
|
|
6375
|
+
// view through visibleDescriptor.
|
|
6376
|
+
if (pendingCheckActive) witnessAffectsMark(target, key);
|
|
6377
|
+
if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
|
|
6378
|
+
const src = readSource(target);
|
|
6379
|
+
const desc = visibleDescriptor(target, src, key);
|
|
6380
|
+
if (!inDraft(target) && getObserver() !== null) {
|
|
6381
|
+
// The node is born from the source's presence (as `has` births it),
|
|
6382
|
+
// not the override-adjusted answer.
|
|
6383
|
+
let present = key in src;
|
|
6384
|
+
if (present && target.del !== null && src === target.pb && target.del.has(key))
|
|
6385
|
+
present = false;
|
|
6386
|
+
read(getHasNode(target, key, present));
|
|
6387
|
+
}
|
|
5066
6388
|
if (desc === undefined) return undefined;
|
|
5067
6389
|
// Array targets carry a real non-configurable `length` the proxy
|
|
5068
6390
|
// invariant forces us to report faithfully; everything else reports
|
|
@@ -5176,8 +6498,10 @@ const traps = {
|
|
|
5176
6498
|
};
|
|
5177
6499
|
/** Low-level setter primitive: opens write mode on a next proxy, runs `fn`,
|
|
5178
6500
|
* emits write-time notifications at outermost exit, applies returned
|
|
5179
|
-
* replacements as adoptions. `guard=false` skips the owned-scope
|
|
5180
|
-
* projection recomputes legitimately write from
|
|
6501
|
+
* replacements as adoptions. `guard=false` skips the dev guards (owned-scope
|
|
6502
|
+
* write, thenable result) — projection recomputes legitimately write from
|
|
6503
|
+
* inside their computed, and their async derive is handled by the recompute,
|
|
6504
|
+
* not returned through here. */
|
|
5181
6505
|
function storeSetterNext(proxy, fn, guard = true) {
|
|
5182
6506
|
if (guard) devGuardStoreSetterWrite();
|
|
5183
6507
|
const target = proxy[$TARGET];
|
|
@@ -5203,6 +6527,9 @@ function storeSetterNext(proxy, fn, guard = true) {
|
|
|
5203
6527
|
for (const t of touched) notifyWrites(t);
|
|
5204
6528
|
}
|
|
5205
6529
|
}
|
|
6530
|
+
// After the sync writes have notified (they were real, like an effect's
|
|
6531
|
+
// side effects before its invalid-cleanup throw) and before adoption.
|
|
6532
|
+
if (guard) devGuardStoreSetterResult(result);
|
|
5206
6533
|
if (result !== undefined && result !== proxy && isWrappable(result)) {
|
|
5207
6534
|
// Returned replacement: on an optimistic family (outside authoritative
|
|
5208
6535
|
// writes) the replacement is itself an optimistic edit — diff it against
|
|
@@ -5236,7 +6563,18 @@ function createStoreNext(initialValue, shallow = false) {
|
|
|
5236
6563
|
proxy[$TARGET].s = true;
|
|
5237
6564
|
markRawIngest(initialValue);
|
|
5238
6565
|
}
|
|
5239
|
-
|
|
6566
|
+
{
|
|
6567
|
+
const owner = getOwner();
|
|
6568
|
+
// Dev-tier graph registration (owner signal lists, onGraph); the
|
|
6569
|
+
// `_owner` write itself never reaches the proxy, see storeOwners.
|
|
6570
|
+
registerGraph(proxy, owner);
|
|
6571
|
+
// Only once the engine is installed, like the node stamping it feeds: a
|
|
6572
|
+
// WeakMap.set per fresh store is a growing ephemeron table (~+35% on the
|
|
6573
|
+
// 2000-store create+commit shape, CodSpeed −11.7% on #3380's first cut)
|
|
6574
|
+
// and, disabled, buys nothing — a store created before enable() has no
|
|
6575
|
+
// excluded owner to inherit either way.
|
|
6576
|
+
if (attrHooks !== null) storeOwners.set(proxy[$TARGET], owner);
|
|
6577
|
+
}
|
|
5240
6578
|
const setter = fn => storeSetterNext(proxy, fn);
|
|
5241
6579
|
return [proxy, setter];
|
|
5242
6580
|
}
|
|
@@ -5313,9 +6651,10 @@ function visibleKeys(target, src) {
|
|
|
5313
6651
|
(!inDraft(target) || draftSeesOverrides(target))
|
|
5314
6652
|
) {
|
|
5315
6653
|
let set = null;
|
|
6654
|
+
const draft = inDraft(target);
|
|
5316
6655
|
for (const key of Reflect.ownKeys(target.h)) {
|
|
5317
6656
|
const node = target.h[key];
|
|
5318
|
-
if (!hasActiveOverride(node)) continue;
|
|
6657
|
+
if (!(draft ? hasActiveOverride(node) : visibleOverride(node))) continue;
|
|
5319
6658
|
set ??= new Set(keys);
|
|
5320
6659
|
if (unwrapOverride(node._x?._overrideValue)) set.add(key);
|
|
5321
6660
|
else set.delete(key);
|
|
@@ -5337,7 +6676,7 @@ function visibleDescriptor(target, src, key) {
|
|
|
5337
6676
|
}
|
|
5338
6677
|
if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
|
|
5339
6678
|
const node = target.h?.[key];
|
|
5340
|
-
if (node !== undefined &&
|
|
6679
|
+
if (node !== undefined && visibleOverride(node)) {
|
|
5341
6680
|
if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
|
|
5342
6681
|
if (desc === undefined) {
|
|
5343
6682
|
const vn = target.n?.[key];
|
|
@@ -5978,7 +7317,7 @@ function descend(pv, nv, keyFn, fam, proj = false) {
|
|
|
5978
7317
|
* driven from inside an enclosing authoritative-write scope (next-store
|
|
5979
7318
|
* optimistic derives), and a hard `false` would clobber it mid-derive.
|
|
5980
7319
|
*/
|
|
5981
|
-
function wrapDraft(inner, isActive, aroundWrite) {
|
|
7320
|
+
function wrapDraft(inner, isActive, aroundWrite, shallow) {
|
|
5982
7321
|
const write = op => (aroundWrite ? aroundWrite(op) : op());
|
|
5983
7322
|
const traps = {
|
|
5984
7323
|
get(_, prop) {
|
|
@@ -5992,8 +7331,9 @@ function wrapDraft(inner, isActive, aroundWrite) {
|
|
|
5992
7331
|
setWriteOverride(false);
|
|
5993
7332
|
setProjectionWriteActive(was);
|
|
5994
7333
|
}
|
|
5995
|
-
|
|
5996
|
-
|
|
7334
|
+
// A shallow store's leaves are raw by contract (#3498): no draft proxy
|
|
7335
|
+
// over them, so identity holds and a frozen leaf is never trapped.
|
|
7336
|
+
return !shallow && typeof value === "object" && value !== null && prop !== $TARGET
|
|
5997
7337
|
? wrapDraft(value, isActive, aroundWrite)
|
|
5998
7338
|
: value;
|
|
5999
7339
|
},
|
|
@@ -6128,21 +7468,27 @@ function createStoreDerivedNext(fn, seed, options) {
|
|
|
6128
7468
|
}
|
|
6129
7469
|
];
|
|
6130
7470
|
}
|
|
7471
|
+
// A detached copy of projection state: the root container alone for a shallow
|
|
7472
|
+
// store — its leaves are raw references by contract and stay so (#3498) — the
|
|
7473
|
+
// whole tree otherwise.
|
|
7474
|
+
function cloneState(v, shallow) {
|
|
7475
|
+
return shallow ? (Array.isArray(v) ? v.slice() : { ...v }) : JSON.parse(JSON.stringify(v));
|
|
7476
|
+
}
|
|
6131
7477
|
function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
|
|
6132
7478
|
const owner = getOwner();
|
|
7479
|
+
const target = wrappedStore[$TARGET];
|
|
6133
7480
|
let settled = false;
|
|
6134
7481
|
let result;
|
|
6135
7482
|
// Open loading window (seedLoadingValue): the observable store IS commit #0
|
|
6136
7483
|
// for the whole first flight — the derive works a detached shadow of the
|
|
6137
7484
|
// seed so draft writes cannot tear through to readers (#2988). Every commit
|
|
6138
7485
|
// point reconciles the shadow through the normal commit path.
|
|
6139
|
-
const shadow = owner._loading
|
|
6140
|
-
? JSON.parse(JSON.stringify(wrappedStore[$TARGET][STORE_VALUE]))
|
|
6141
|
-
: null;
|
|
7486
|
+
const shadow = owner._loading ? cloneState(target[STORE_VALUE], target.s) : null;
|
|
6142
7487
|
const draft = wrapDraft(
|
|
6143
7488
|
wrappedStore,
|
|
6144
7489
|
() => !settled || owner._x?._inFlight === result,
|
|
6145
|
-
aroundDraftWrite
|
|
7490
|
+
aroundDraftWrite,
|
|
7491
|
+
target.s
|
|
6146
7492
|
);
|
|
6147
7493
|
storeSetterNext(
|
|
6148
7494
|
draft,
|
|
@@ -6153,7 +7499,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraf
|
|
|
6153
7499
|
// Shadow run: commit a detached snapshot, never the shadow itself
|
|
6154
7500
|
// (adoption takes the value by identity — handing it the live shadow
|
|
6155
7501
|
// would fuse the draft to the observable store).
|
|
6156
|
-
if (shadow && (v === undefined || v === shadow)) v =
|
|
7502
|
+
if (shadow && (v === undefined || v === shadow)) v = cloneState(shadow, target.s);
|
|
6157
7503
|
if (v === s || v === undefined) return;
|
|
6158
7504
|
const write = () =>
|
|
6159
7505
|
storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
|
|
@@ -6214,7 +7560,10 @@ function installNextBlockedHalf() {
|
|
|
6214
7560
|
applyTentative,
|
|
6215
7561
|
retainsOptimism: transitionHoldsOptimism
|
|
6216
7562
|
});
|
|
6217
|
-
|
|
7563
|
+
// The affects() declaration walk is a WRITER channel (A28 (5)): tagging a
|
|
7564
|
+
// parent covers the record as the writer sees it, this tick's optimistic
|
|
7565
|
+
// writes included — the draft view, not the reader view.
|
|
7566
|
+
setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw, true));
|
|
6218
7567
|
// Scheduler flush tails call _clearOptimisticStores whenever tracked
|
|
6219
7568
|
// stores exist; next has no layer to clear — reverts are engine-native —
|
|
6220
7569
|
// so the hook only empties the batch set.
|
|
@@ -6707,7 +8056,7 @@ function notifyOptimisticWrites(t, pb) {
|
|
|
6707
8056
|
* Authoritative-view reads (until()'s predicate) skip composition entirely:
|
|
6708
8057
|
* the predicate observes authoritative truth, never the caller's tentative
|
|
6709
8058
|
* overlay. (Write-side emission callers never run under such a compute.) */
|
|
6710
|
-
function optimisticView(t, src) {
|
|
8059
|
+
function optimisticView(t, src, draft = false) {
|
|
6711
8060
|
if (t.fam?.opt !== true || authoritativeRead()) return src;
|
|
6712
8061
|
let out = null;
|
|
6713
8062
|
const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
|
|
@@ -6715,7 +8064,9 @@ function optimisticView(t, src) {
|
|
|
6715
8064
|
if (nodes !== null) {
|
|
6716
8065
|
for (const key of Reflect.ownKeys(nodes)) {
|
|
6717
8066
|
const node = nodes[key];
|
|
6718
|
-
|
|
8067
|
+
// A28 (5): readers see an optimistic write once a flush carried it;
|
|
8068
|
+
// the draft (writer channel) composes on it now.
|
|
8069
|
+
if (!(draft ? hasActiveOverride(node) : visibleOverride(node))) continue;
|
|
6719
8070
|
const ov = unwrapOverride(node._x?._overrideValue);
|
|
6720
8071
|
if (key === "length" && Array.isArray(src)) {
|
|
6721
8072
|
if (src.length !== ov) ensure().length = ov;
|
|
@@ -6726,7 +8077,9 @@ function optimisticView(t, src) {
|
|
|
6726
8077
|
if (has !== null) {
|
|
6727
8078
|
for (const key of Reflect.ownKeys(has)) {
|
|
6728
8079
|
const node = has[key];
|
|
6729
|
-
|
|
8080
|
+
// A28 (5): readers see an optimistic write once a flush carried it;
|
|
8081
|
+
// the draft (writer channel) composes on it now.
|
|
8082
|
+
if (!(draft ? hasActiveOverride(node) : visibleOverride(node))) continue;
|
|
6730
8083
|
const present = !!unwrapOverride(node._x?._overrideValue);
|
|
6731
8084
|
if (!present && key in (out ?? src)) delete ensure()[key];
|
|
6732
8085
|
}
|
|
@@ -7680,6 +9033,8 @@ class CollectionQueue extends Queue {
|
|
|
7680
9033
|
_initialized = false;
|
|
7681
9034
|
_onFn;
|
|
7682
9035
|
_prevOn = ON_INIT;
|
|
9036
|
+
/** The boundary's owner — where a `caught` report locates itself, set before the children are built (a creation-time throw arrives before `_tree`). */
|
|
9037
|
+
_owner;
|
|
7683
9038
|
constructor(type) {
|
|
7684
9039
|
super();
|
|
7685
9040
|
this._collectionType = type;
|
|
@@ -7688,20 +9043,47 @@ class CollectionQueue extends Queue {
|
|
|
7688
9043
|
if (!type || (read(this._disabled) && (!_revealUsed || read(this._collapsed)))) return;
|
|
7689
9044
|
return super.run(type);
|
|
7690
9045
|
}
|
|
9046
|
+
/** The `on` key, or ON_INIT when it throws. Evaluated mid-propagation,
|
|
9047
|
+
* inside the pending node's own pass: read as a spectator so the key's
|
|
9048
|
+
* sources never become that node's dependencies (#3528). */
|
|
9049
|
+
_readOn() {
|
|
9050
|
+
return spectate(() => {
|
|
9051
|
+
try {
|
|
9052
|
+
return this._onFn();
|
|
9053
|
+
} catch {
|
|
9054
|
+
return ON_INIT;
|
|
9055
|
+
}
|
|
9056
|
+
});
|
|
9057
|
+
}
|
|
7691
9058
|
notify(node, type, flags, error) {
|
|
7692
9059
|
if (!(type & this._collectionType)) return super.notify(node, type, flags, error);
|
|
7693
9060
|
if (this._initialized && this._onFn) {
|
|
7694
|
-
const currentOn =
|
|
7695
|
-
try {
|
|
7696
|
-
return this._onFn();
|
|
7697
|
-
} catch {
|
|
7698
|
-
return ON_INIT;
|
|
7699
|
-
}
|
|
7700
|
-
});
|
|
9061
|
+
const currentOn = this._readOn();
|
|
7701
9062
|
if (currentOn !== this._prevOn) {
|
|
7702
9063
|
this._prevOn = currentOn;
|
|
7703
9064
|
this._initialized = false;
|
|
7704
9065
|
this._sources.clear();
|
|
9066
|
+
// Readers forwarded while this boundary showed content are behind the
|
|
9067
|
+
// fallback now: they stop blocking (`reporterBlocksSource`), and the
|
|
9068
|
+
// transactions they were holding must be re-judged for it (A33, #3375).
|
|
9069
|
+
// What those readers still wait on is this boundary's to wait on now:
|
|
9070
|
+
// they never re-notify (status propagation dedupes on the reader's
|
|
9071
|
+
// `_pendingSources`), so the reset collects it from their registrations
|
|
9072
|
+
// — the one place a forwarded reader is recorded (INV-3) — or a sibling
|
|
9073
|
+
// reader's flight that lands first reveals them stale (A33, #3459).
|
|
9074
|
+
for (const t of transitions)
|
|
9075
|
+
for (const [source, reporters] of t._asyncReporters)
|
|
9076
|
+
for (const reporter of reporters)
|
|
9077
|
+
if (this._holds(reporter)) {
|
|
9078
|
+
this._sources.add(source);
|
|
9079
|
+
reporter._x?._pendingSources?.forEach(s => this._sources.add(s));
|
|
9080
|
+
}
|
|
9081
|
+
if (this._sources.size) {
|
|
9082
|
+
setSignal(this._disabled, true);
|
|
9083
|
+
if (attrHooks !== null && this._collectionType & STATUS_PENDING)
|
|
9084
|
+
attrHooks.boundaryFallback(this, this._tree, true);
|
|
9085
|
+
}
|
|
9086
|
+
wakeParked();
|
|
7705
9087
|
}
|
|
7706
9088
|
}
|
|
7707
9089
|
// Routing is dimension-independent: each boundary consumes only its own
|
|
@@ -7722,19 +9104,47 @@ class CollectionQueue extends Queue {
|
|
|
7722
9104
|
if (source) {
|
|
7723
9105
|
const wasEmpty = this._sources.size === 0;
|
|
7724
9106
|
this._sources.add(source);
|
|
9107
|
+
// A collecting boundary waits on everything the effect is pending on,
|
|
9108
|
+
// not only the source this notification carries. Status propagation
|
|
9109
|
+
// dedupes on the effect's `_pendingSources`: a source it already
|
|
9110
|
+
// carries (a flight that started before an `on` reset cleared the
|
|
9111
|
+
// set) is never re-reported, and that source's later re-flight
|
|
9112
|
+
// stays invisible — the boundary revealed when its one collected
|
|
9113
|
+
// source settled while the effect was still pending (#3375).
|
|
9114
|
+
if (this._collectionType & STATUS_PENDING)
|
|
9115
|
+
node._x?._pendingSources?.forEach(s => this._sources.add(s));
|
|
7725
9116
|
if (wasEmpty) {
|
|
7726
9117
|
setSignal(this._disabled, true);
|
|
7727
9118
|
if (attrHooks !== null && this._collectionType & STATUS_PENDING)
|
|
7728
9119
|
attrHooks.boundaryFallback(this, this._tree, true);
|
|
7729
9120
|
}
|
|
7730
9121
|
if (this._collectionType & STATUS_ERROR) {
|
|
7731
|
-
|
|
9122
|
+
const caught = unwrapStatusError(source._x?._error);
|
|
9123
|
+
setSignal(this._error, caught);
|
|
9124
|
+
// The client error hook: this boundary renders its fallback for
|
|
9125
|
+
// it — the one road a rendered failure took that no global handler
|
|
9126
|
+
// ever saw. `source` is the computation that threw (the status
|
|
9127
|
+
// wrapper's, made at the first landing and kept downstream), so the
|
|
9128
|
+
// hook hears where it broke as well as where it was met. Once per
|
|
9129
|
+
// error object; a `reset()` re-collecting the same failure says
|
|
9130
|
+
// nothing new.
|
|
9131
|
+
reportClientError(caught, this._owner, source);
|
|
7732
9132
|
}
|
|
7733
9133
|
}
|
|
7734
9134
|
}
|
|
7735
9135
|
type &= ~this._collectionType;
|
|
7736
9136
|
return type ? super.notify(node, type, flags, error) : true;
|
|
7737
9137
|
}
|
|
9138
|
+
/** Is `reporter` live and routed to this boundary — under it, with no
|
|
9139
|
+
* collecting pending-type boundary in between (`reporterBlocksSource`'s test)? */
|
|
9140
|
+
_holds(reporter) {
|
|
9141
|
+
if (reporter._flags & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
|
|
9142
|
+
for (let q = reporter._queue; q; q = q._parent) {
|
|
9143
|
+
if (q === this) return true;
|
|
9144
|
+
if (q._collectionType & STATUS_PENDING && !q._initialized) return false;
|
|
9145
|
+
}
|
|
9146
|
+
return false;
|
|
9147
|
+
}
|
|
7738
9148
|
_checkSources() {
|
|
7739
9149
|
for (const source of this._sources) {
|
|
7740
9150
|
// A source with a live affects() mark holds display state for the
|
|
@@ -7765,11 +9175,10 @@ class CollectionQueue extends Queue {
|
|
|
7765
9175
|
if (attrHooks !== null && this._collectionType & STATUS_PENDING)
|
|
7766
9176
|
attrHooks.boundaryFallback(this, this._tree, false);
|
|
7767
9177
|
if (this._onFn) {
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
}
|
|
9178
|
+
// A throw (value not yet committed) leaves _prevOn stale; the next
|
|
9179
|
+
// notify then resets.
|
|
9180
|
+
const on = this._readOn();
|
|
9181
|
+
if (on !== ON_INIT) this._prevOn = on;
|
|
7773
9182
|
}
|
|
7774
9183
|
}
|
|
7775
9184
|
}
|
|
@@ -7793,6 +9202,7 @@ function createCollectionBoundary(type, fn, fallback, onFn) {
|
|
|
7793
9202
|
const owner = createOwner();
|
|
7794
9203
|
if (_revealUsed) setContext(RevealControllerContext, null, owner);
|
|
7795
9204
|
const queue = new CollectionQueue(type);
|
|
9205
|
+
queue._owner = owner;
|
|
7796
9206
|
if (type === STATUS_ERROR)
|
|
7797
9207
|
queue._error = signal(undefined, { ownedWrite: true, _noSnapshot: true });
|
|
7798
9208
|
if (onFn) queue._onFn = onFn;
|
|
@@ -8063,13 +9473,22 @@ export {
|
|
|
8063
9473
|
$TRACK,
|
|
8064
9474
|
ContextNotFoundError,
|
|
8065
9475
|
DEV,
|
|
9476
|
+
MergeView,
|
|
8066
9477
|
NoOwnerError,
|
|
8067
9478
|
NotReadyError,
|
|
8068
9479
|
OBSERVE,
|
|
9480
|
+
OmitView,
|
|
9481
|
+
ROOT_ERROR_HOOK,
|
|
9482
|
+
SOURCE_MEMO,
|
|
9483
|
+
SOURCE_MERGE,
|
|
9484
|
+
SOURCE_OMIT,
|
|
9485
|
+
SOURCE_PLAIN,
|
|
9486
|
+
SOURCE_PROXY,
|
|
8069
9487
|
SUPPORTS_PROXY,
|
|
8070
9488
|
TimeoutError,
|
|
8071
9489
|
action,
|
|
8072
9490
|
affects,
|
|
9491
|
+
configureClientErrors,
|
|
8073
9492
|
createContext,
|
|
8074
9493
|
createEffect,
|
|
8075
9494
|
createErrorBoundary,
|
|
@@ -8093,27 +9512,37 @@ export {
|
|
|
8093
9512
|
getContext,
|
|
8094
9513
|
getObserver,
|
|
8095
9514
|
getOwner,
|
|
9515
|
+
hasStaticKeys,
|
|
8096
9516
|
isEqual,
|
|
8097
9517
|
isPending,
|
|
9518
|
+
isStatic,
|
|
8098
9519
|
isWrappable,
|
|
8099
9520
|
latest,
|
|
8100
9521
|
mapArray,
|
|
8101
9522
|
merge,
|
|
8102
9523
|
mergeSources,
|
|
9524
|
+
mergeView,
|
|
8103
9525
|
omit,
|
|
9526
|
+
omitView,
|
|
8104
9527
|
onCleanup,
|
|
8105
9528
|
onSettled,
|
|
8106
9529
|
reconcile,
|
|
8107
9530
|
refresh,
|
|
8108
9531
|
repeat,
|
|
8109
9532
|
resolve,
|
|
9533
|
+
resolvedTable,
|
|
8110
9534
|
runWithOwner,
|
|
8111
9535
|
setContext,
|
|
8112
9536
|
snapshot,
|
|
9537
|
+
sourceGet,
|
|
9538
|
+
sourceHas,
|
|
9539
|
+
sourceKeys,
|
|
9540
|
+
sourceOwners,
|
|
8113
9541
|
storeHasFamily,
|
|
8114
9542
|
storeHasOptimisticFamily,
|
|
8115
9543
|
storeIsShallow,
|
|
8116
9544
|
storePath,
|
|
8117
9545
|
until,
|
|
8118
|
-
untrack
|
|
9546
|
+
untrack,
|
|
9547
|
+
viewOf
|
|
8119
9548
|
};
|