@solidjs/signals 2.0.0-rc.2 → 2.0.0-rc.4
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.js +1454 -118
- package/dist/node.cjs +2709 -1396
- package/dist/prod/affects.js +13 -12
- package/dist/prod/boundaries.js +39 -34
- package/dist/prod/core/action.js +3 -3
- package/dist/prod/core/async.js +48 -46
- package/dist/prod/core/core.js +99 -67
- package/dist/prod/core/effect.js +25 -28
- package/dist/prod/core/external.js +2 -2
- package/dist/prod/core/graph.js +85 -49
- package/dist/prod/core/heap.js +10 -10
- package/dist/prod/core/lanes.js +19 -19
- package/dist/prod/core/optimistic.js +66 -41
- package/dist/prod/core/owner.js +13 -13
- package/dist/prod/core/scheduler.js +131 -85
- package/dist/prod/core/verdict.js +36 -15
- package/dist/prod/index.js +4 -0
- package/dist/prod/map.js +101 -101
- package/dist/prod/signals.js +1 -1
- package/dist/prod/store/index.js +2 -0
- package/dist/prod/store/next/optimistic.js +65 -11
- package/dist/prod/store/next/patch-hooks.js +13 -0
- package/dist/prod/store/next/patch.js +614 -0
- package/dist/prod/store/next/projection.js +107 -45
- package/dist/prod/store/next/reconcile.js +307 -120
- package/dist/prod/store/next/store.js +321 -92
- package/dist/prod/store/next/target.js +13 -4
- package/dist/prod/store/store.js +5 -5
- package/dist/types/core/core.d.ts +15 -1
- package/dist/types/core/dev.d.ts +8 -0
- package/dist/types/core/graph.d.ts +22 -0
- package/dist/types/core/invariants.d.ts +1 -1
- package/dist/types/core/scheduler.d.ts +12 -0
- package/dist/types/store/index.d.ts +2 -0
- package/dist/types/store/next/patch-hooks.d.ts +41 -0
- package/dist/types/store/next/patch.d.ts +91 -0
- package/dist/types/store/next/reconcile.d.ts +14 -0
- package/dist/types/store/next/store.d.ts +30 -2
- package/dist/types/store/next/target.d.ts +58 -8
- package/dist/types-cjs/core/core.d.cts +15 -1
- package/dist/types-cjs/core/dev.d.cts +8 -0
- package/dist/types-cjs/core/graph.d.cts +22 -0
- package/dist/types-cjs/core/invariants.d.cts +1 -1
- package/dist/types-cjs/core/scheduler.d.cts +12 -0
- package/dist/types-cjs/store/index.d.cts +2 -0
- package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
- package/dist/types-cjs/store/next/patch.d.cts +91 -0
- package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
- package/dist/types-cjs/store/next/store.d.cts +30 -2
- package/dist/types-cjs/store/next/target.d.cts +58 -8
- package/package.json +14 -14
- package/dist/types/store/optimistic.d.ts +0 -45
- package/dist/types/store/projection.d.ts +0 -70
- package/dist/types/store/reconcile.d.ts +0 -46
- package/dist/types-cjs/store/optimistic.d.cts +0 -45
- package/dist/types-cjs/store/projection.d.cts +0 -70
- package/dist/types-cjs/store/reconcile.d.cts +0 -46
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { STATUS_ERROR, EFFECT_RENDER } from "../../core/constants.js";
|
|
2
|
+
|
|
3
|
+
import { ext, runWithOwner, untrack } from "../../core/core.js";
|
|
4
|
+
|
|
5
|
+
import { StatusError } from "../../core/error.js";
|
|
6
|
+
|
|
7
|
+
import { GlobalQueue, haltReactivity, setPatchCommitHook, globalQueue, activeTransition } from "../../core/scheduler.js";
|
|
8
|
+
|
|
9
|
+
import { getOwner, isDisposed } from "../../core/owner.js";
|
|
10
|
+
|
|
11
|
+
import { $TARGET } from "../store.js";
|
|
12
|
+
|
|
13
|
+
import { markDescendants, ownedRaw } from "./target.js";
|
|
14
|
+
|
|
15
|
+
import { installPatchHooks, installRowHooks } from "./patch-hooks.js";
|
|
16
|
+
|
|
17
|
+
import { emitSetterRowOps } from "./reconcile.js";
|
|
18
|
+
|
|
19
|
+
import { targetIsPlain, pcOf } from "./store.js";
|
|
20
|
+
|
|
21
|
+
import { createRenderEffect } from "../../signals.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PR-A: the patch channel (DESIGN-PATCH-CHANNEL.md).
|
|
25
|
+
*
|
|
26
|
+
* Compiled patch functions — per-record compare-and-write consumers —
|
|
27
|
+
* dispatched by the store's visibility transitions instead of render
|
|
28
|
+
* effects. This module owns registration, the per-flush apply queue
|
|
29
|
+
* (effect-phase timing, §2b), the owned-prev rule (§2c), and dispatch
|
|
30
|
+
* bubbling (§4b). Emission calls live at the four visibility-transition
|
|
31
|
+
* sites (adoption walk, setter notify, fold commit, override lifecycle)
|
|
32
|
+
* and are gated on registration, so unpatched stores pay a null check.
|
|
33
|
+
*
|
|
34
|
+
* Bubbling contract: a targeted nested write reaches ancestor patches as a
|
|
35
|
+
* FORCED re-apply — the third `force` argument makes every compiled compare
|
|
36
|
+
* pass, so the ancestor rewrites its bound fields from its current backing
|
|
37
|
+
* (idempotent, and prev-free: an ancestor's pre-state is not reconstructible
|
|
38
|
+
* after in-place folds). Compiled bodies therefore have the signature
|
|
39
|
+
* `(next, prev, force?)`.
|
|
40
|
+
*
|
|
41
|
+
* Tree-shaking: core never imports this module; stores without patches
|
|
42
|
+
* never schedule the queue.
|
|
43
|
+
*/ let queue = null;
|
|
44
|
+
|
|
45
|
+
let scheduled = false;
|
|
46
|
+
|
|
47
|
+
function drainApplyQueue() {
|
|
48
|
+
// Settle-time fallback for optimistic emissions (a reverting flush may
|
|
49
|
+
// have no active lanes left to run the lane-slot drain).
|
|
50
|
+
drainOptimistic();
|
|
51
|
+
const e = queue;
|
|
52
|
+
queue = null;
|
|
53
|
+
scheduled = false;
|
|
54
|
+
if (e === null) return;
|
|
55
|
+
// Per-entry isolation: one throwing patch must not abort its siblings
|
|
56
|
+
// (effect parity — each effect isolates its failure). A throwing patch
|
|
57
|
+
// routes through its REGISTERING OWNER's queue chain exactly like a
|
|
58
|
+
// render-effect error (§2b): an Errored boundary above the row collects
|
|
59
|
+
// it (source = the owner, error read via owner._x?._error). Unhandled errors
|
|
60
|
+
// rethrow after the drain so they still surface.
|
|
61
|
+
let t = UNSET;
|
|
62
|
+
for (let n = 0; n < e.length; n++) {
|
|
63
|
+
clearStamp(e[n]);
|
|
64
|
+
const {list: l, prev: o, force: u, t: i} = e[n];
|
|
65
|
+
const r = i !== null ? i.pb ?? i.v : e[n].next;
|
|
66
|
+
t = applyEntries(l, r, o, u, t);
|
|
67
|
+
}
|
|
68
|
+
if (t !== UNSET) {
|
|
69
|
+
// Unhandled patch errors HALT like unhandled effect errors (re-audit 2,
|
|
70
|
+
// P1-4): app state is undefined past an unboundaried throw.
|
|
71
|
+
haltReactivity(t);
|
|
72
|
+
throw t;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const UNSET = Symbol();
|
|
77
|
+
|
|
78
|
+
/** ONE callback/error primitive for every drain (normal, transition-held,
|
|
79
|
+
* optimistic): per-entry isolation — a throwing patch must not abort its
|
|
80
|
+
* siblings (effect parity) — and failures route through the REGISTERING
|
|
81
|
+
* OWNER's queue chain exactly like a render-effect error (§2b): an Errored
|
|
82
|
+
* boundary above the row collects it. Unhandled errors are aggregated by the
|
|
83
|
+
* caller (first one rethrows after its drain completes). */ function applyEntries(e, t, n, l, o) {
|
|
84
|
+
// SNAPSHOT multi-consumer lists (re-audit 5, P1-3): a callback can dispose
|
|
85
|
+
// a sibling's owner, whose unbind SPLICES this same array mid-iteration —
|
|
86
|
+
// index-walking the live array skips the shifted consumer. The dominant
|
|
87
|
+
// single-consumer case pays nothing; unbound entries are marked so a
|
|
88
|
+
// snapshot never applies a consumer severed by an earlier callback.
|
|
89
|
+
const u = e.length > 1 ? e.slice() : e;
|
|
90
|
+
for (let e = 0; e < u.length; e++) {
|
|
91
|
+
const i = u[e];
|
|
92
|
+
if (i.u === true) continue;
|
|
93
|
+
// Disposed owners drop their patches (the row unmounted mid-flush).
|
|
94
|
+
if (i.owner !== null && isDisposed(i.owner)) continue;
|
|
95
|
+
try {
|
|
96
|
+
i.fn(t, n, l);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
let t = false;
|
|
99
|
+
const n = i.owner;
|
|
100
|
+
if (n !== null) {
|
|
101
|
+
// Route through the nearest COMPUTED ancestor (re-audit 2, P1-4):
|
|
102
|
+
// <Errored>.reset() recomputes its sources, and a plain owner (the
|
|
103
|
+
// list driver's listOwner) is not recomputable — the component/memo
|
|
104
|
+
// scope above it is, and recomputing it rebuilds the rows, exactly
|
|
105
|
+
// what reset means for a throwing render effect.
|
|
106
|
+
let l = n;
|
|
107
|
+
while (l !== null && l.oe === undefined) l = l.ke;
|
|
108
|
+
l ??= n;
|
|
109
|
+
const o = new StatusError(l, e);
|
|
110
|
+
ext(l)._ = o;
|
|
111
|
+
l.S = (l.S ?? 0) | STATUS_ERROR;
|
|
112
|
+
t = n.C.notify(l, STATUS_ERROR, STATUS_ERROR, o);
|
|
113
|
+
}
|
|
114
|
+
if (!t && o === UNSET) o = e;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return o;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Transition-stamped emissions (§2b, "the walk is not the visibility moment
|
|
121
|
+
// inside a transition"): entries stash DIRECTLY on their transition
|
|
122
|
+
// (`_heldPatches`) and release into the live queue when THAT batch commits
|
|
123
|
+
// (patchCommitHook). Reverted transitions never commit — their stash drops
|
|
124
|
+
// with the transition object, no revert bookkeeping. The field (rather than
|
|
125
|
+
// a WeakMap) keeps the every-flush commit-hook check to one property read;
|
|
126
|
+
// the ambient batch never stashes.
|
|
127
|
+
let commitHookInstalled = false;
|
|
128
|
+
|
|
129
|
+
function releaseBatch(e) {
|
|
130
|
+
const t = e.Mt;
|
|
131
|
+
if (t === undefined) return;
|
|
132
|
+
e.Mt = undefined;
|
|
133
|
+
for (let e = 0; e < t.length; e++) pushLive(t[e]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pushLive(e) {
|
|
137
|
+
if (queue === null) queue = [];
|
|
138
|
+
queue.push(e);
|
|
139
|
+
if (!scheduled) {
|
|
140
|
+
scheduled = true;
|
|
141
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function push(e) {
|
|
146
|
+
const t = activeTransition;
|
|
147
|
+
if (t !== null) {
|
|
148
|
+
let n = t.Mt;
|
|
149
|
+
if (n === undefined) t.Mt = n = [];
|
|
150
|
+
n.push(e);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
pushLive(e);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Self-entry push with SAME-BATCH COALESCING (re-audit 2/3): a record's
|
|
157
|
+
* later non-forced emission into the same container UPDATES the queued
|
|
158
|
+
* entry in place — `next` takes the newest capture (adoption swaps the
|
|
159
|
+
* backing object per emission; dropping the later one applied STALE state),
|
|
160
|
+
* `prev` keeps the batch's earliest (effect semantics: one application per
|
|
161
|
+
* batch spanning the whole window). The entry's consumer list is the live
|
|
162
|
+
* pc.p array, so mid-batch registrants ride the single application. Forced
|
|
163
|
+
* entries and row/slot ops never coalesce; the drain clears the stamps so a
|
|
164
|
+
* quiet record retains nothing from its last batch. */ function pushSelf(e, t) {
|
|
165
|
+
const n = activeTransition;
|
|
166
|
+
let l;
|
|
167
|
+
if (n !== null) {
|
|
168
|
+
let e = n.Mt;
|
|
169
|
+
if (e === undefined) n.Mt = e = [];
|
|
170
|
+
l = e;
|
|
171
|
+
} else {
|
|
172
|
+
if (queue === null) queue = [];
|
|
173
|
+
l = queue;
|
|
174
|
+
}
|
|
175
|
+
if (e.qa === l && e.qe !== null) {
|
|
176
|
+
const n = e.qe;
|
|
177
|
+
n.next = t.next;
|
|
178
|
+
n.list = t.list;
|
|
179
|
+
// pc.p can be re-created if emptied mid-batch
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
e.qa = l;
|
|
183
|
+
e.qe = t;
|
|
184
|
+
t.pc = e;
|
|
185
|
+
l.push(t);
|
|
186
|
+
if (l === queue && !scheduled) {
|
|
187
|
+
scheduled = true;
|
|
188
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Drain-side stamp clear (re-audit 3, P2-6): without it a quiet long-lived
|
|
193
|
+
* record's channel retains its last batch's container array, entry, and both
|
|
194
|
+
* captured backings for the record's lifetime. */ function clearStamp(e) {
|
|
195
|
+
const t = e.pc;
|
|
196
|
+
if (t !== undefined && t.qe === e) {
|
|
197
|
+
t.qa = null;
|
|
198
|
+
t.qe = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Shallow clone for the owned-prev rule (§2c): owned backings fold values
|
|
203
|
+
* INTO the same raw at commit, so a queued prev must be snapshotted. */ function clonePrev(e) {
|
|
204
|
+
return Array.isArray(e) ? e.slice() : {
|
|
205
|
+
...e
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Emit a record's visibility transition. Callers gate on `hasPatches()` and
|
|
211
|
+
* `t.d` cheaply; this function re-checks and walks ancestors (§4b).
|
|
212
|
+
*/ function emitPatch(e, t, n) {
|
|
213
|
+
const l = e.pc !== null ? e.pc.p : null;
|
|
214
|
+
if (l !== null) pushSelf(e.pc, {
|
|
215
|
+
list: l,
|
|
216
|
+
next: t,
|
|
217
|
+
prev: ownedRaw.has(n) ? clonePrev(n) : n,
|
|
218
|
+
force: false,
|
|
219
|
+
t: null
|
|
220
|
+
});
|
|
221
|
+
// Bubbling: ancestors force-re-apply from their LIVE backing, resolved at
|
|
222
|
+
// drain (privatization may clone it between now and then).
|
|
223
|
+
let o = e.u;
|
|
224
|
+
while (o !== null) {
|
|
225
|
+
const e = o.pc !== null ? o.pc.p : null;
|
|
226
|
+
if (e !== null) push({
|
|
227
|
+
list: e,
|
|
228
|
+
next: null,
|
|
229
|
+
prev: null,
|
|
230
|
+
force: true,
|
|
231
|
+
t: o
|
|
232
|
+
});
|
|
233
|
+
o = o.u;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Emission for sites that already stand at the record with both sides in
|
|
238
|
+
* hand and have already handled ancestors (the adoption walk descends —
|
|
239
|
+
* parents were visited first), so no bubbling walk. */ function emitPatchLocal(e, t, n) {
|
|
240
|
+
const l = e.pc !== null ? e.pc.p : null;
|
|
241
|
+
if (l !== null) pushSelf(e.pc, {
|
|
242
|
+
list: l,
|
|
243
|
+
next: t,
|
|
244
|
+
prev: ownedRaw.has(n) ? clonePrev(n) : n,
|
|
245
|
+
force: false,
|
|
246
|
+
t: null
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Optimistic-channel emission: overrides are visible THIS flush while the
|
|
251
|
+
* transaction is in flight — that is what optimism means. These ride a
|
|
252
|
+
* dedicated queue drained at LANE-EFFECT timing (the regular effect queues
|
|
253
|
+
* are stashed by an in-flight action), with the regular drain as the
|
|
254
|
+
* settle-time fallback. `next === null` = forced re-apply from the live
|
|
255
|
+
* target (the revert shape: committed truth back onto the DOM). */ let optQueue = null;
|
|
256
|
+
|
|
257
|
+
function drainOptimistic() {
|
|
258
|
+
const e = optQueue;
|
|
259
|
+
optQueue = null;
|
|
260
|
+
if (e === null) return;
|
|
261
|
+
// Same isolation/routing primitive as the normal drain (re-audit blocker
|
|
262
|
+
// 5): one throwing optimistic patch must not abort its siblings, and it
|
|
263
|
+
// must reach the registering owner's Errored boundary.
|
|
264
|
+
let t = UNSET;
|
|
265
|
+
for (let n = 0; n < e.length; n++) {
|
|
266
|
+
clearStamp(e[n]);
|
|
267
|
+
const {list: l, prev: o, force: u, t: i} = e[n];
|
|
268
|
+
const r = i !== null ? i.pb ?? i.v : e[n].next;
|
|
269
|
+
t = applyEntries(l, r, o, u, t);
|
|
270
|
+
}
|
|
271
|
+
if (t !== UNSET) {
|
|
272
|
+
haltReactivity(t);
|
|
273
|
+
throw t;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function emitPatchOptimistic(e, t, n) {
|
|
278
|
+
const l = e.pc !== null ? e.pc.p : null;
|
|
279
|
+
if (l === null) return;
|
|
280
|
+
if (optQueue === null) optQueue = [];
|
|
281
|
+
if (t === null) optQueue.push({
|
|
282
|
+
list: l,
|
|
283
|
+
next: null,
|
|
284
|
+
prev: null,
|
|
285
|
+
force: true,
|
|
286
|
+
t: e
|
|
287
|
+
}); else {
|
|
288
|
+
// Same-batch coalescing, optimistic container (re-audit 3): later
|
|
289
|
+
// non-forced emission updates the queued entry's next in place.
|
|
290
|
+
const o = e.pc;
|
|
291
|
+
if (o.qa === optQueue && o.qe !== null) {
|
|
292
|
+
const e = o.qe;
|
|
293
|
+
e.next = t;
|
|
294
|
+
e.list = l;
|
|
295
|
+
} else {
|
|
296
|
+
const e = {
|
|
297
|
+
list: l,
|
|
298
|
+
next: t,
|
|
299
|
+
prev: n,
|
|
300
|
+
force: false,
|
|
301
|
+
t: null
|
|
302
|
+
};
|
|
303
|
+
o.qa = optQueue;
|
|
304
|
+
o.qe = e;
|
|
305
|
+
e.pc = o;
|
|
306
|
+
optQueue.push(e);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Backup scheduling: the lane-slot drain covers in-flight application; a
|
|
310
|
+
// stashed regular drain guarantees settle-time application when no lane
|
|
311
|
+
// survives to the final flush (pure reverts).
|
|
312
|
+
if (!scheduled) {
|
|
313
|
+
scheduled = true;
|
|
314
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Row-ops emission at OPTIMISTIC (lane) timing: user drafts on an
|
|
319
|
+
* optimistic family must show structure IN FLIGHT — bypassing the
|
|
320
|
+
* transition stash exactly like emitPatchOptimistic. Two forms:
|
|
321
|
+
* - `ops` given (write site): `nextRows` is the draft's intended visible
|
|
322
|
+
* list, ops the identity diff against the pre-write optimistic view.
|
|
323
|
+
* - `ops === null` (revert site): RESYNC — the consumer rebuilds retention
|
|
324
|
+
* by row identity against the live post-revert view, resolved from the
|
|
325
|
+
* target at drain time (overrides are gone by then, so `pb ?? v` IS the
|
|
326
|
+
* committed truth). */ function emitRowOpsOptimistic(e, t, n) {
|
|
327
|
+
const l = e.pc !== null ? e.pc.ro : null;
|
|
328
|
+
if (l === null) return;
|
|
329
|
+
if (optQueue === null) optQueue = [];
|
|
330
|
+
optQueue.push({
|
|
331
|
+
list: l.map(e => ({
|
|
332
|
+
owner: e.owner,
|
|
333
|
+
fn: (t, l) => e.fn(t, n)
|
|
334
|
+
})),
|
|
335
|
+
next: t,
|
|
336
|
+
prev: null,
|
|
337
|
+
force: false,
|
|
338
|
+
t: t === null ? e : null
|
|
339
|
+
});
|
|
340
|
+
if (!scheduled) {
|
|
341
|
+
scheduled = true;
|
|
342
|
+
globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Register a compiled patch on a store record. Multi-consumer (two lists
|
|
348
|
+
* can render one record); owner-scoped for disposal. Returns unbind.
|
|
349
|
+
*/
|
|
350
|
+
// Global registration count: the cheap gate emission sites check before any
|
|
351
|
+
// per-record work (unpatched apps pay one number compare per transition).
|
|
352
|
+
let patchCount = 0;
|
|
353
|
+
|
|
354
|
+
function hasPatches() {
|
|
355
|
+
return patchCount > 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function registerPatch(e, t) {
|
|
359
|
+
let n = e?.[$TARGET];
|
|
360
|
+
if (n === undefined) throw new Error("registerPatch: not a store record");
|
|
361
|
+
// Chained backings (§7b): register on the ULTIMATE owner — that is where
|
|
362
|
+
// value transitions fold and dispatch; the wrapper's identity is stable
|
|
363
|
+
// and would never fire (see ultimateTarget).
|
|
364
|
+
n = ultimateTarget(n) ?? n;
|
|
365
|
+
if (!commitHookInstalled) {
|
|
366
|
+
commitHookInstalled = true;
|
|
367
|
+
armPatchHooks();
|
|
368
|
+
setPatchCommitHook(releaseBatch);
|
|
369
|
+
GlobalQueue.ln = drainOptimistic;
|
|
370
|
+
}
|
|
371
|
+
const l = {
|
|
372
|
+
fn: t,
|
|
373
|
+
owner: getOwner()
|
|
374
|
+
};
|
|
375
|
+
const o = pcOf(n);
|
|
376
|
+
const u = o.p ??= [];
|
|
377
|
+
u.push(l);
|
|
378
|
+
patchCount++;
|
|
379
|
+
// Bindings are subscriptions for reachability (§6d pruning must descend
|
|
380
|
+
// into bound records).
|
|
381
|
+
markDescendants(n);
|
|
382
|
+
let i = false;
|
|
383
|
+
return () => {
|
|
384
|
+
if (i) return;
|
|
385
|
+
i = true;
|
|
386
|
+
l.u = true;
|
|
387
|
+
// dispatch snapshots skip severed consumers
|
|
388
|
+
// Decrement ONLY on actual removal: a demotion (demoteToEffects) may
|
|
389
|
+
// have already pulled this entry and repaired the count — the splice
|
|
390
|
+
// miss is how this closure learns that.
|
|
391
|
+
const e = u.indexOf(l);
|
|
392
|
+
if (e >= 0) {
|
|
393
|
+
u.splice(e, 1);
|
|
394
|
+
patchCount--;
|
|
395
|
+
}
|
|
396
|
+
if (u.length === 0 && o.p === u) o.p = null;
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Resolve a target through CHAINED backings (§7b) to the ultimate owner.
|
|
401
|
+
* A projection family wrapper's backing IS another store's proxy: value
|
|
402
|
+
* transitions fold on the ULTIMATE target (the wrapper's identity never
|
|
403
|
+
* changes), so patch registration and raw resolution must land there or
|
|
404
|
+
* registered patches never fire (equivalence-matrix finding: projection
|
|
405
|
+
* value ticks froze driver rows while classic effects tracked through). */ function ultimateTarget(e) {
|
|
406
|
+
while (e.ch) {
|
|
407
|
+
const t = (e.pb ?? e.v)?.[$TARGET];
|
|
408
|
+
if (t === undefined) return undefined;
|
|
409
|
+
e = t;
|
|
410
|
+
}
|
|
411
|
+
return e;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Dual-driver bind probe (compiler runtime contract): when `record` is a
|
|
415
|
+
* patchable store record, returns its CURRENT raw backing (the driver's
|
|
416
|
+
* initial force-apply reads it directly — no proxy traffic, no tracking);
|
|
417
|
+
* returns undefined otherwise (driver falls back to the effect path).
|
|
418
|
+
* Not patchable: non-records, non-proxies, accessor-bearing records
|
|
419
|
+
* (patches read raw — getters need tracked evaluation), broken chains. */ function patchableRaw(e) {
|
|
420
|
+
let t = e?.[$TARGET];
|
|
421
|
+
if (t === undefined || t.px !== e || t.a === true) return undefined;
|
|
422
|
+
t = ultimateTarget(t);
|
|
423
|
+
// SCAN before trusting (re-audit blocker 3): `a` starts false and is only
|
|
424
|
+
// discovered lazily (first draft, deep walks) — admission must run the
|
|
425
|
+
// one-time own-accessor scan itself, or a getter-bearing record takes the
|
|
426
|
+
// patch path and its getter's OUTSIDE dependencies (signals, other
|
|
427
|
+
// records) never re-apply. Sticky `sc` makes this one probe pass per
|
|
428
|
+
// record lifetime.
|
|
429
|
+
if (t === undefined || !targetIsPlain(t)) return undefined;
|
|
430
|
+
return t.pb ?? t.v;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Accessor demotion (design §5): a record that acquires an accessor after
|
|
434
|
+
* registration stops being patchable — reads must go through tracked
|
|
435
|
+
* evaluation. Clears patches and repairs the global count; callers re-drive
|
|
436
|
+
* the pulled bodies (demoteToEffects). */ function demotePatches(e) {
|
|
437
|
+
if (e.pc === null) return null;
|
|
438
|
+
const t = e.pc.p;
|
|
439
|
+
e.pc.p = null;
|
|
440
|
+
if (t === null) return null;
|
|
441
|
+
patchCount -= t.length;
|
|
442
|
+
// Drain IN PLACE: unbind closures captured this array — a late unbind must
|
|
443
|
+
// miss its indexOf and not double-decrement the repaired count.
|
|
444
|
+
return t.splice(0, t.length);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The demotion re-drive (re-audit blocker 3): each pulled body becomes the
|
|
448
|
+
* SAME dual-driver effect fallback the web runtime would have chosen had the
|
|
449
|
+
* record carried the accessor at bind — a tracked compute pass (next === prev
|
|
450
|
+
* short-circuits every compare into a pure read THROUGH THE PROXY, so getter
|
|
451
|
+
* dependencies track) plus an untracked force-apply at effect timing.
|
|
452
|
+
*
|
|
453
|
+
* Creation is DEFERRED to the effect phase: the trap that discovers the
|
|
454
|
+
* accessor runs mid-draft, and an effect's initial pass must not read
|
|
455
|
+
* through the proxy inside the write window. The record's own transition
|
|
456
|
+
* for that draft is covered by the new effect's initial force-apply.
|
|
457
|
+
*
|
|
458
|
+
* Known edge (documented): a demoted LIST-ROW body re-drives under its
|
|
459
|
+
* registering owner (the list owner), so per-row severing on removal is
|
|
460
|
+
* lost for demoted rows — the effect lives until the LIST disposes. Rows
|
|
461
|
+
* only demote when user code defines an accessor on a row record at
|
|
462
|
+
* runtime. */ function demoteToEffects(e) {
|
|
463
|
+
const t = demotePatches(e);
|
|
464
|
+
if (t === null || t.length === 0) return;
|
|
465
|
+
const n = e.px;
|
|
466
|
+
globalQueue.enqueue(EFFECT_RENDER, () => {
|
|
467
|
+
for (let e = 0; e < t.length; e++) {
|
|
468
|
+
const l = t[e];
|
|
469
|
+
if (l.owner !== null && isDisposed(l.owner)) continue;
|
|
470
|
+
const o = l.fn;
|
|
471
|
+
runWithOwner(l.owner, () => createRenderEffect(() => {
|
|
472
|
+
o(n, n, false);
|
|
473
|
+
}, () => {
|
|
474
|
+
// Block body: a compiled patch body's return value must not be
|
|
475
|
+
// mistaken for an effect cleanup.
|
|
476
|
+
untrack(() => o(n, undefined, true));
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Register a structural-ops consumer on a keyed store array (the list
|
|
483
|
+
* container's channel — what `For` consumes through the seam). */ function registerRowOps(e, t) {
|
|
484
|
+
let n = e?.[$TARGET];
|
|
485
|
+
if (n === undefined) throw new Error("registerRowOps: not a store array");
|
|
486
|
+
// Chained backings resolve to the ULTIMATE owner, same as registerPatch
|
|
487
|
+
// (§7b) — the walk/fold emits there (re-audit blocker 4).
|
|
488
|
+
n = ultimateTarget(n) ?? n;
|
|
489
|
+
armRowHooks();
|
|
490
|
+
if (!commitHookInstalled) {
|
|
491
|
+
commitHookInstalled = true;
|
|
492
|
+
armPatchHooks();
|
|
493
|
+
setPatchCommitHook(releaseBatch);
|
|
494
|
+
GlobalQueue.ln = drainOptimistic;
|
|
495
|
+
}
|
|
496
|
+
const l = {
|
|
497
|
+
fn: t,
|
|
498
|
+
owner: getOwner()
|
|
499
|
+
};
|
|
500
|
+
const o = pcOf(n);
|
|
501
|
+
const u = o.ro ??= [];
|
|
502
|
+
u.push(l);
|
|
503
|
+
patchCount++;
|
|
504
|
+
markDescendants(n);
|
|
505
|
+
let i = false;
|
|
506
|
+
return () => {
|
|
507
|
+
if (i) return;
|
|
508
|
+
i = true;
|
|
509
|
+
patchCount--;
|
|
510
|
+
const e = u.indexOf(l);
|
|
511
|
+
if (e >= 0) u.splice(e, 1);
|
|
512
|
+
if (u.length === 0 && o.ro === u) o.ro = null;
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Slot patches (shallow arrays) ride the same apply queue: the walk emits
|
|
517
|
+
* per aligned value-replaced slot; application happens at effect phase under
|
|
518
|
+
* the registration owner's lifetime. */ function emitSlotPatch(e, t, n, l) {
|
|
519
|
+
const o = e.pc !== null ? e.pc.sp : null;
|
|
520
|
+
if (o === null) return;
|
|
521
|
+
push({
|
|
522
|
+
list: o.map(e => ({
|
|
523
|
+
owner: e.owner,
|
|
524
|
+
fn: () => e.fn(t, n, l)
|
|
525
|
+
})),
|
|
526
|
+
next: n,
|
|
527
|
+
prev: l,
|
|
528
|
+
force: false,
|
|
529
|
+
t: null
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Slot patch for shallow arrays: the reconcile walk emits (index, next,
|
|
534
|
+
* prev) for KEY-ALIGNED value-replaced slots (structure rides row ops), and
|
|
535
|
+
* the emission queues through the patch apply queue — effect-phase timing,
|
|
536
|
+
* transition stamping, disposed-owner drop — like every other channel. */ function registerSlotPatchNext(e, t) {
|
|
537
|
+
let n = e?.[$TARGET];
|
|
538
|
+
if (n === undefined) throw new Error("registerSlotPatchNext: not a store array");
|
|
539
|
+
// Chained backings resolve to the ULTIMATE owner, same as registerPatch
|
|
540
|
+
// (§7b) — the walk emits slot ticks there (re-audit blocker 4).
|
|
541
|
+
n = ultimateTarget(n) ?? n;
|
|
542
|
+
armRowHooks();
|
|
543
|
+
if (!commitHookInstalled) {
|
|
544
|
+
commitHookInstalled = true;
|
|
545
|
+
armPatchHooks();
|
|
546
|
+
setPatchCommitHook(releaseBatch);
|
|
547
|
+
GlobalQueue.ln = drainOptimistic;
|
|
548
|
+
}
|
|
549
|
+
// Multi-consumer (external audit): one shallow array can drive several
|
|
550
|
+
// lists — registrations are a list, unbinds splice their own entry.
|
|
551
|
+
const l = pcOf(n);
|
|
552
|
+
const o = {
|
|
553
|
+
fn: t,
|
|
554
|
+
owner: getOwner()
|
|
555
|
+
};
|
|
556
|
+
(l.sp ??= []).push(o);
|
|
557
|
+
markDescendants(n);
|
|
558
|
+
let u = false;
|
|
559
|
+
return () => {
|
|
560
|
+
if (u || l.sp === null) return;
|
|
561
|
+
u = true;
|
|
562
|
+
const e = l.sp.indexOf(o);
|
|
563
|
+
if (e >= 0) l.sp.splice(e, 1);
|
|
564
|
+
if (l.sp.length === 0) l.sp = null;
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Row-ops ride the SAME apply queue/timing as record patches: transition-
|
|
569
|
+
* stamped, applied at effect phase, in emission order (structure before the
|
|
570
|
+
* new rows' own patches can exist; retained rows' value patches commute). */ function emitRowOps(e, t, n) {
|
|
571
|
+
const l = e.pc !== null ? e.pc.ro : null;
|
|
572
|
+
if (l === null) return;
|
|
573
|
+
push({
|
|
574
|
+
list: l.map(e => ({
|
|
575
|
+
owner: e.owner,
|
|
576
|
+
fn: (t, l) => e.fn(t, n)
|
|
577
|
+
})),
|
|
578
|
+
next: t,
|
|
579
|
+
prev: null,
|
|
580
|
+
force: false,
|
|
581
|
+
t: null
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Pay-for-use seams: the write paths (store/reconcile/optimistic) emit
|
|
586
|
+
// through installed hooks instead of importing this module. Installation is
|
|
587
|
+
// LAZY (first registration) rather than a module-scope call — the dist is a
|
|
588
|
+
// flat bundle, and a top-level side effect would retain the whole channel in
|
|
589
|
+
// every consumer. TWO TIERS so a value-only registration (registerPatch —
|
|
590
|
+
// present in ~every bundle under patch-mode default) does not retain the
|
|
591
|
+
// list machinery (row-ops emitters + reconcile's diff builders): row hooks
|
|
592
|
+
// arm only from the list driver's registrations. Sound because every
|
|
593
|
+
// emission site is guarded by the matching pc channel, which only the
|
|
594
|
+
// corresponding registration creates. See patch-hooks.ts.
|
|
595
|
+
function armPatchHooks() {
|
|
596
|
+
installPatchHooks({
|
|
597
|
+
emitPatch: emitPatch,
|
|
598
|
+
emitPatchLocal: emitPatchLocal,
|
|
599
|
+
emitPatchOptimistic: emitPatchOptimistic,
|
|
600
|
+
hasPatches: hasPatches,
|
|
601
|
+
demoteToEffects: demoteToEffects
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function armRowHooks() {
|
|
606
|
+
installRowHooks({
|
|
607
|
+
emitRowOps: emitRowOps,
|
|
608
|
+
emitSlotPatch: emitSlotPatch,
|
|
609
|
+
emitSetterRowOps: emitSetterRowOps,
|
|
610
|
+
emitRowOpsOptimistic: emitRowOpsOptimistic
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export { demotePatches, demoteToEffects, emitPatch, emitPatchLocal, emitPatchOptimistic, emitRowOps, emitRowOpsOptimistic, emitSlotPatch, hasPatches, patchableRaw, registerPatch, registerRowOps, registerSlotPatchNext };
|