@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.
Files changed (57) hide show
  1. package/dist/dev.js +1454 -118
  2. package/dist/node.cjs +2709 -1396
  3. package/dist/prod/affects.js +13 -12
  4. package/dist/prod/boundaries.js +39 -34
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +48 -46
  7. package/dist/prod/core/core.js +99 -67
  8. package/dist/prod/core/effect.js +25 -28
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +85 -49
  11. package/dist/prod/core/heap.js +10 -10
  12. package/dist/prod/core/lanes.js +19 -19
  13. package/dist/prod/core/optimistic.js +66 -41
  14. package/dist/prod/core/owner.js +13 -13
  15. package/dist/prod/core/scheduler.js +131 -85
  16. package/dist/prod/core/verdict.js +36 -15
  17. package/dist/prod/index.js +4 -0
  18. package/dist/prod/map.js +101 -101
  19. package/dist/prod/signals.js +1 -1
  20. package/dist/prod/store/index.js +2 -0
  21. package/dist/prod/store/next/optimistic.js +65 -11
  22. package/dist/prod/store/next/patch-hooks.js +13 -0
  23. package/dist/prod/store/next/patch.js +614 -0
  24. package/dist/prod/store/next/projection.js +107 -45
  25. package/dist/prod/store/next/reconcile.js +307 -120
  26. package/dist/prod/store/next/store.js +321 -92
  27. package/dist/prod/store/next/target.js +13 -4
  28. package/dist/prod/store/store.js +5 -5
  29. package/dist/types/core/core.d.ts +15 -1
  30. package/dist/types/core/dev.d.ts +8 -0
  31. package/dist/types/core/graph.d.ts +22 -0
  32. package/dist/types/core/invariants.d.ts +1 -1
  33. package/dist/types/core/scheduler.d.ts +12 -0
  34. package/dist/types/store/index.d.ts +2 -0
  35. package/dist/types/store/next/patch-hooks.d.ts +41 -0
  36. package/dist/types/store/next/patch.d.ts +91 -0
  37. package/dist/types/store/next/reconcile.d.ts +14 -0
  38. package/dist/types/store/next/store.d.ts +30 -2
  39. package/dist/types/store/next/target.d.ts +58 -8
  40. package/dist/types-cjs/core/core.d.cts +15 -1
  41. package/dist/types-cjs/core/dev.d.cts +8 -0
  42. package/dist/types-cjs/core/graph.d.cts +22 -0
  43. package/dist/types-cjs/core/invariants.d.cts +1 -1
  44. package/dist/types-cjs/core/scheduler.d.cts +12 -0
  45. package/dist/types-cjs/store/index.d.cts +2 -0
  46. package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
  47. package/dist/types-cjs/store/next/patch.d.cts +91 -0
  48. package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
  49. package/dist/types-cjs/store/next/store.d.cts +30 -2
  50. package/dist/types-cjs/store/next/target.d.cts +58 -8
  51. package/package.json +14 -14
  52. package/dist/types/store/optimistic.d.ts +0 -45
  53. package/dist/types/store/projection.d.ts +0 -70
  54. package/dist/types/store/reconcile.d.ts +0 -46
  55. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  56. package/dist/types-cjs/store/projection.d.cts +0 -70
  57. 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 };