@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
@@ -8,6 +8,8 @@ import "../../core/verdict.js";
8
8
 
9
9
  import "../../core/effect.js";
10
10
 
11
+ import { patchHooks, rowHooks } from "./patch-hooks.js";
12
+
11
13
  import { $TARGET, markRawIngest, isWrappable, rawValuesUsed, isRawValue, getWriteOverride } from "../store.js";
12
14
 
13
15
  import { materializePB, adoptPB, unwrapValue, notifyFold, targetsEqual, bumpDeep, notifyKeyValue, notifyKeyDiff, notifyFoldTail, hasAccessorFlag } from "./store.js";
@@ -33,29 +35,29 @@ import { storeNextLookup, optHooks, ownedRaw } from "./target.js";
33
35
  * key mismatch detaches (fresh proxy on next read, recon-snap R18);
34
36
  * keyless items fall back positional; null/primitive slots are legal
35
37
  * members (R11). Kind changes replace wholesale (R10).
36
- */ function reconcileNextState(e, t, n, o = false) {
37
- if (t == null) throw new Error("");
38
- const l = t?.[$TARGET];
39
- if (l === undefined || l.px !== t) throw new Error("");
38
+ */ function reconcileNextState(e, n, t, o = false) {
39
+ if (n == null) throw new Error("");
40
+ const l = n?.[$TARGET];
41
+ if (l === undefined || l.px !== n) throw new Error("");
40
42
  // Reconcile's diff walks need a REAL pending container — a prototype
41
43
  // overlay (#3044) materializes to the clone path first (edge: reconcile
42
44
  // inside a setter that already wrote this target).
43
45
  if (l.ovl) materializePB(l);
44
- let f = n === null ? null : typeof n === "string" ? e => e?.[n] : n;
46
+ let i = t === null ? null : typeof t === "string" ? e => e?.[t] : t;
45
47
  // §7b chained backing: a projection derive returning a LIVE store proxy
46
48
  // adopts the proxy itself as the backing — reads flow through the inner
47
49
  // store's traps, so consumers subscribe to the inner graph and updates
48
50
  // flow with no re-derive (#2941). The adoption diff still notifies THIS
49
51
  // store's existing subscribers of the swap.
50
- if (o && e !== t && e?.[$TARGET] !== undefined) {
51
- const t = l.pb ?? l.v;
52
- if (t === e) return;
52
+ if (o && e !== n && e?.[$TARGET] !== undefined) {
53
+ const n = l.pb ?? l.v;
54
+ if (n === e) return;
53
55
  // already chained to this store
54
56
  adoptPB(l, e);
55
57
  return;
56
58
  }
57
- const i = unwrapValue(e);
58
- if (f) {
59
+ const u = unwrapValue(e);
60
+ if (i) {
59
61
  // Root identity precondition — checked before ANY mutation, so a throwing
60
62
  // reconcile is atomic by construction (RUL-12 ruling). Projections
61
63
  // (replace=true) relax it: a root entity change merges in place — the
@@ -63,8 +65,8 @@ import { storeNextLookup, optHooks, ownedRaw } from "./target.js";
63
65
  // key-matched across the entity change (proj R7: keyFn drops to
64
66
  // positional so old-entity subtrees never merge into the new entity's).
65
67
  const e = l.pb ?? l.v;
66
- const t = f(e);
67
- if (t !== undefined && f(i) !== t) {
68
+ const n = i(e);
69
+ if (n !== undefined && !sameKey(i(u), n)) {
68
70
  if (!o) throw new Error("");
69
71
  // Entity change: wholesale swap. The root proxy is stable for life
70
72
  // (proj R5) but NOTHING below survives — children are never matched
@@ -72,7 +74,7 @@ import { storeNextLookup, optHooks, ownedRaw } from "./target.js";
72
74
  // Displaced-raw unregistration (proj R10): the outgoing raw stops
73
75
  // resolving to this proxy; re-handed later it wraps fresh.
74
76
  (l.fam?.map ?? storeNextLookup).delete(l.pb ?? l.v);
75
- adoptPB(l, i);
77
+ adoptPB(l, u);
76
78
  return;
77
79
  }
78
80
  }
@@ -82,141 +84,225 @@ import { storeNextLookup, optHooks, ownedRaw } from "./target.js";
82
84
  // touched. Key-matched rows keep proxy identity by descending into the
83
85
  // existing child targets instead of overriding their parent slots.
84
86
  if (l.fam?.opt === true && !projectionWriteActive && !getWriteOverride()) {
85
- optHooks.applyTentative(l, i, f);
87
+ optHooks.applyTentative(l, u, i);
86
88
  return;
87
89
  }
88
- applyAdopt(l, i, f, o);
90
+ applyAdopt(l, u, i, o);
89
91
  }
90
92
 
91
- function applyAdopt(e, t, n, o = false) {
93
+ function applyAdopt(e, n, t, o = false) {
92
94
  const l = e.pb ?? e.v;
93
95
  // The sound identity skip (O7): same reference AND we never diverged it.
94
- if (t === l && !ownedRaw.has(l)) return;
95
- const f = e.fam;
96
+ if (n === l && !ownedRaw.has(l)) return;
97
+ const i = e.fam;
96
98
  // §6b (R28): the diff's previous-arrangement baseline is the LANE VIEW —
97
99
  // optimistic rows must be visible to key matching so a landing carrying the
98
100
  // same key recycles their proxies. Raw `prev` keeps the identity/ownership
99
101
  // roles above; only matching reads the view.
100
- const i = f?.opt === true ? optHooks.optimisticView(e, l) : l;
101
- const r = Array.isArray(t);
102
+ const u = i?.opt === true ? optHooks.optimisticView(e, l) : l;
103
+ const f = Array.isArray(n);
102
104
  // Plain stores notify inline AFTER the descent (child registrations feed
103
105
  // the fold diff's identity-preservation check); projections keep deferred
104
106
  // folds (downstream holds can form later in the flush).
105
- const u = f === null;
106
- const s = e.s === true;
107
- const a = e.v;
108
- adoptPB(e, t, u);
107
+ const s = i === null;
108
+ const r = e.s === true;
109
+ const c = e.v;
110
+ adoptPB(e, n, s);
111
+ // Patch channel (adoption site): this record transitioned — queue its
112
+ // patches with the pre-adopt prev. No bubbling walk: the adoption walk
113
+ // visits parents before children, so ancestors emitted already. EAGER
114
+ // only — family targets' visibility moment is their fold commit
115
+ // (drainFolds emits there; emitting here too would double-fire).
116
+ if (patchHooks !== null && s && e.pc !== null && e.pc.p !== null) {
117
+ // Accessor demotion at the ADOPTION seam is DEV-ONLY (prod principle:
118
+ // explicitly-odd input must not cost correct-input prod — the
119
+ // per-adoption scan was ~12% of dbmon's tick since adoptPB resets the
120
+ // verdict every adoption). Dev demotes AND warns; prod emits directly,
121
+ // so a getter adoptee's OUTSIDE deps (signals) won't re-apply in prod —
122
+ // caught loudly during development instead. Registration-time admission
123
+ // (patchableRaw) keeps its full one-time scan in both modes.
124
+ {
125
+ patchHooks.emitPatchLocal(e, n, c);
126
+ }
127
+ }
109
128
  // Shallow adoption: records are slot values — sticky raw-mark the incoming
110
129
  // set (R41) and never descend; slot notification is the positional diff.
111
- if (s) markRawIngest(t);
112
- if (Array.isArray(i) !== r) {
113
- if (u) notifyFold(e, a, t);
130
+ if (r) markRawIngest(n);
131
+ if (Array.isArray(u) !== f) {
132
+ if (s) notifyFold(e, c, n);
114
133
  return;
115
134
  }
116
- if (r) {
117
- const l = i;
118
- const r = t;
135
+ if (f) {
136
+ const l = u;
137
+ const f = n;
119
138
  // Fused array walk (eager mode): per-index notification rides the same
120
139
  // loop as the descent (descend first — targetsEqual needs the child's
121
140
  // re-registration, R9). Length, trailing removed indexes, and any other
122
141
  // unvisited node keys land in the counted sweep below.
123
- const c = u ? e.n : null;
142
+ const a = s ? e.n : null;
124
143
  let p = 0;
125
- if (n && !s) {
144
+ if (t && !r) {
126
145
  // Positional-prefix fast path (legacy keyedMatch-walk parity): while
127
146
  // rows key-match in place — the steady-state polling shape — descend
128
147
  // directly with zero staging. The prevByKey map is built only for the
129
148
  // misaligned remainder, and never at all on aligned ticks.
130
- const i = l.length;
131
- const u = r.length;
132
- let s = false;
149
+ const u = l.length;
150
+ const s = f.length;
151
+ let r = false;
133
152
  let d = 0;
134
- for (const y = Math.min(i, u); d < y; d++) {
135
- const i = r[d];
136
- const u = l[d];
153
+ for (const y = Math.min(u, s); d < y; d++) {
154
+ const u = f[d];
155
+ const s = l[d];
137
156
  // Routing heuristic only (aligned vs keyed remainder) — both routes
138
157
  // notify identically and descend() is the one authoritative
139
158
  // validator, so bare typeof gates suffice here; full isWrappable
140
159
  // per row was the walk's dominant residual cost.
141
- if (u !== i && !(u !== null && typeof u === "object" && i !== null && typeof i === "object" && n(u) === n(i))) break;
160
+ if (s !== u && !(s !== null && typeof s === "object" && u !== null && typeof u === "object" && sameKey(t(s), t(u)))) break;
142
161
  // misaligned: fall to the keyed remainder below
143
162
  // Identity skip inline (FINDING-1 guard), then descend the pair.
144
- if ((u !== i || i !== null && typeof i === "object" && ownedRaw.has(i)) && i !== null && typeof i === "object") descend(unwrapValue(u), i, n, f, o);
145
- if (e.dk !== null && !s && !(i !== null && typeof i === "object" ? targetsEqual(u, i) : isEqual(u, i))) {
163
+ if ((s !== u || u !== null && typeof u === "object" && ownedRaw.has(u)) && u !== null && typeof u === "object") descend(unwrapValue(s), u, t, i, o);
164
+ if (e.dk !== null && !r && !(u !== null && typeof u === "object" ? targetsEqual(s, u) : isEqual(s, u))) {
146
165
  bumpDeep(e);
147
- s = true;
166
+ r = true;
148
167
  }
149
- if (c !== null) {
150
- const e = c[d];
168
+ if (a !== null) {
169
+ const e = a[d];
151
170
  if (e !== undefined) {
152
171
  p++;
153
- notifyKeyValue(e, d, a[d], i, a, t);
172
+ notifyKeyValue(e, d, c[d], u, c, n);
154
173
  }
155
174
  }
156
175
  }
157
- if (e.dk !== null && !s && d < r.length) bumpDeep(e);
158
- let y = null;
159
- for (;d < r.length; d++) {
160
- const e = r[d];
176
+ if (e.dk !== null && !r && d < f.length) bumpDeep(e);
177
+ const y = d;
178
+ // misalignment point (== nlen on aligned ticks)
179
+ let w = null;
180
+ for (;d < f.length; d++) {
181
+ const e = f[d];
161
182
  // typeof gates route; descend validates (same contract as the prefix).
162
183
  if (e !== null && typeof e === "object") {
163
- const t = n(e);
164
- let i;
165
- if (t !== undefined) {
166
- if (y === null) {
167
- y = new Map;
168
- for (let e = 0; e < l.length; e++) {
169
- const t = unwrapValue(l[e]);
170
- if (t !== null && typeof t === "object") {
171
- const e = n(t);
172
- if (e !== undefined && !y.has(e)) y.set(e, t);
184
+ const n = t(e);
185
+ let u;
186
+ if (n !== undefined) {
187
+ if (w === null) {
188
+ // Occurrence-aware (re-audit 2, P1-5): duplicate keys queue
189
+ // their prev INDICES (rows can themselves be arrays, so index
190
+ // queues are the unambiguous encoding — same as buildRowOps)
191
+ // and each is consumed ONCE. First-wins would adopt two next
192
+ // rows into the SAME prev target while row ops retain two
193
+ // separate DOM rows (the second one stale).
194
+ w = new Map;
195
+ // From structStart, not 0 (re-audit 3, P1-2): prefix-aligned
196
+ // rows already adopted their incoming counterparts — re-offering
197
+ // them here let a duplicate key adopt a prefix row AGAIN while
198
+ // row ops (which correctly window from structStart) retained
199
+ // the later occurrence's DOM row against a never-adopted target.
200
+ for (let e = y; e < l.length; e++) {
201
+ const n = unwrapValue(l[e]);
202
+ if (n !== null && typeof n === "object") {
203
+ const o = t(n);
204
+ if (o === undefined) continue;
205
+ const l = w.get(o);
206
+ if (l === undefined) w.set(o, e); else if (Array.isArray(l)) l.push(e); else w.set(o, [ l, e ]);
173
207
  }
174
208
  }
175
209
  }
176
- i = y.get(t);
210
+ const e = w.get(n);
211
+ if (e === undefined) u = undefined; else if (Array.isArray(e)) {
212
+ u = unwrapValue(l[e.shift()]);
213
+ if (e.length === 1) w.set(n, e[0]);
214
+ } else {
215
+ u = unwrapValue(l[e]);
216
+ w.delete(n);
217
+ }
177
218
  } else {
178
- i = unwrapValue(l[d]);
219
+ u = unwrapValue(l[d]);
179
220
  // keyless item: positional fallback
180
221
  }
181
- descend(i, e, n, f, o);
222
+ descend(u, e, t, i, o);
182
223
  }
183
- if (c !== null) {
184
- const e = c[d];
224
+ if (a !== null) {
225
+ const e = a[d];
185
226
  if (e !== undefined) {
186
227
  p++;
187
- notifyKeyDiff(e, d, a, t, false);
228
+ notifyKeyDiff(e, d, c, n, false);
188
229
  }
189
230
  }
190
231
  }
232
+ // Row ops (PR-B): emit structural ops ONLY when structure changed —
233
+ // aligned value ticks pay nothing. Built after the walk so retained
234
+ // rows' value patches queue first (adds bind at op-apply).
235
+ if (rowHooks !== null && e.pc !== null && e.pc.ro !== null && (y < s || u !== s)) buildAndEmitRowOps(e, l, f, y, t);
191
236
  } else {
192
- const i = Math.min(l.length, r.length);
193
- const u = r.length;
237
+ const u = Math.min(l.length, f.length);
238
+ const s = f.length;
194
239
  let d = false;
195
- for (let y = 0; y < u; y++) {
196
- const u = r[y];
197
- if (!s && y < i && u !== null && typeof u === "object") descend(unwrapValue(l[y]), u, n, f, o);
198
- if (e.dk !== null && !d && !(u !== null && typeof u === "object" ? targetsEqual(l[y], u) : isEqual(l[y], u))) {
240
+ const y = rowHooks !== null && e.pc !== null ? e.pc.sp : null;
241
+ // Row ops for shallow/positional lists: track the key-aligned prefix
242
+ // (keyed) so aligned value ticks emit nothing; keyless lists emit only
243
+ // on length change (append/truncate). Slot-patch consumers need the
244
+ // alignment tracking too (aligned = value tick, misaligned = ops).
245
+ const w = rowHooks !== null && e.pc !== null ? e.pc.ro : null;
246
+ let b = t !== null && (w !== null || y !== null);
247
+ let m = 0;
248
+ for (let w = 0; w < s; w++) {
249
+ const s = f[w];
250
+ if (b && w < u) {
251
+ const e = l[w];
252
+ if (e !== null && typeof e === "object" && s !== null && typeof s === "object" &&
253
+ // SameValueZero (self-sweep): strict === here broke slot
254
+ // alignment on NaN keys while buildRowOps retained the row —
255
+ // retained DOM with suppressed value ticks (the round-1 NaN
256
+ // staleness, in the shallow branch).
257
+ sameKey(t(e), t(s))) m++; else b = false;
258
+ }
259
+ // Slot-patch dispatch (shallow): a KEY-ALIGNED slot whose value was
260
+ // replaced by reference is a value tick — emit through the queue.
261
+ // Misaligned/appended slots are STRUCTURE (row ops rebuild or move
262
+ // them; new rows initial-apply at bind), so they emit nothing here.
263
+ // Keyless positional lists treat same-index replacement as the value
264
+ // tick for indices below the common length.
265
+ // `i < dlen` is load-bearing for BOTH modes: an appended position
266
+ // past a fully-aligned prefix (vacuously aligned when prev is empty)
267
+ // has no previous slot — emitting a slot tick for it races the row
268
+ // ops that CREATE the row (the slot queue applies first, indexing a
269
+ // row that does not exist yet). Equivalence-matrix finding:
270
+ // clear-then-refill and pure appends crashed the driver.
271
+ if (y !== null && w < u && (t === null || b)) {
272
+ const n = l[w];
273
+ if (n !== s) rowHooks.emitSlotPatch(e, w, s, n);
274
+ }
275
+ if (!r && w < u && s !== null && typeof s === "object") descend(unwrapValue(l[w]), s, t, i, o);
276
+ if (e.dk !== null && !d && !(s !== null && typeof s === "object" ? targetsEqual(l[w], s) : isEqual(l[w], s))) {
199
277
  bumpDeep(e);
200
278
  d = true;
201
279
  }
202
- if (c !== null) {
203
- const e = c[y];
280
+ if (a !== null) {
281
+ const e = a[w];
204
282
  if (e !== undefined) {
205
283
  p++;
206
- notifyKeyDiff(e, y, a, t, false);
284
+ notifyKeyDiff(e, w, c, n, false);
207
285
  }
208
286
  }
209
287
  }
288
+ if (w !== null) {
289
+ const n = l.length;
290
+ if (t !== null) {
291
+ if (m < s || n !== s) buildAndEmitRowOps(e, l, f, m, t);
292
+ } else if (n !== s) {
293
+ buildAndEmitRowOps(e, l, f, u, null);
294
+ }
295
+ }
210
296
  }
211
- if (u) {
212
- if (c !== null && p < e.nc) {
213
- for (const e of Reflect.ownKeys(c)) {
297
+ if (s) {
298
+ if (a !== null && p < e.nc) {
299
+ for (const e of Reflect.ownKeys(a)) {
214
300
  // visited indexes are < nextRows.length; everything else sweeps
215
- const n = typeof e === "string" ? +e : NaN;
216
- if (!(n >= 0 && n < r.length)) notifyKeyDiff(c[e], e, a, t, false);
301
+ const t = typeof e === "string" ? +e : NaN;
302
+ if (!(t >= 0 && t < f.length)) notifyKeyDiff(a[e], e, c, n, false);
217
303
  }
218
304
  }
219
- notifyFoldTail(e, a, t);
305
+ notifyFoldTail(e, c, n);
220
306
  }
221
307
  return;
222
308
  } else {
@@ -226,61 +312,68 @@ function applyAdopt(e, t, n, o = false) {
226
312
  // slots must not notify, R9). This replaces the notifyFold re-walk that
227
313
  // doubled dbmon's diff cost. for-in covers own enumerable string keys
228
314
  // with no key-array allocation; symbols get a pass only when present.
229
- const l = u ? e.n : null;
230
- let r = 0;
231
- let c = false;
315
+ // PROTOTYPE compiled-patch fast path: a pure-patch record (no nodes,
316
+ // no presence/key-set/deep subscribers, no family) adopts and hands the
317
+ // (next, prev) pair to its compiled patch — no per-key walk at all.
318
+ if (e.pc !== null && e.pc.p !== null && s && e.n === null && e.h === null && e.k === null && e.dk === null && i === null) {
319
+ // Adoption already ran at applyAdopt entry; emission was queued there.
320
+ return;
321
+ }
322
+ const l = s ? e.n : null;
323
+ let f = 0;
324
+ let a = false;
232
325
  // The per-key body is inlined on purpose (legacy applyStateFast parity:
233
326
  // an extracted helper costs a call per key on the hottest object-diff
234
327
  // site). Reference-identical values early-continue BEFORE any other
235
328
  // work — sound only with the ownership guard (FINDING-1: an owned
236
329
  // backing is setter-diverged and must still diff).
237
- for (const u in t) {
238
- const p = t[u];
239
- const d = a[u];
330
+ for (const s in n) {
331
+ const p = n[s];
332
+ const d = c[s];
240
333
  const y = p !== null && typeof p === "object";
241
- if (d === p && (!y || !ownedRaw.has(p)) && (l === null || l[u] === undefined || !hasAccessorFlag(l[u]))) {
242
- if (l !== null && l[u] !== undefined) r++;
334
+ if (d === p && (!y || !ownedRaw.has(p)) && (l === null || l[s] === undefined || !hasAccessorFlag(l[s]))) {
335
+ if (l !== null && l[s] !== undefined) f++;
243
336
  continue;
244
337
  }
245
- if (y && !s) descend(unwrapValue(i[u]), p, n, f, o);
338
+ if (y && !r) descend(unwrapValue(u[s]), p, t, i, o);
246
339
  // Deep-witness (dk): value changes must notify even with NO per-key
247
340
  // node — deep() subscribes one node per record. Checked after descend
248
341
  // so in-place adoptions (same logical slot) don't bump; child records
249
342
  // carry their own witness. One flag + null check when unused.
250
- if (e.dk !== null && !c && !(y ? targetsEqual(d, p) : isEqual(d, p))) {
343
+ if (e.dk !== null && !a && !(y ? targetsEqual(d, p) : isEqual(d, p))) {
251
344
  bumpDeep(e);
252
- c = true;
345
+ a = true;
253
346
  }
254
347
  if (l !== null) {
255
- const e = l[u];
348
+ const e = l[s];
256
349
  if (e !== undefined) {
257
- r++;
258
- notifyKeyValue(e, u, d, p, a, t);
350
+ f++;
351
+ notifyKeyValue(e, s, d, p, c, n);
259
352
  }
260
353
  }
261
354
  }
262
- const p = Object.getOwnPropertySymbols(t);
355
+ const p = Object.getOwnPropertySymbols(n);
263
356
  for (let e = 0; e < p.length; e++) {
264
- const u = p[e];
265
- const c = t[u];
266
- if (!s && c !== null && typeof c === "object") descend(unwrapValue(i[u]), c, n, f, o);
357
+ const s = p[e];
358
+ const a = n[s];
359
+ if (!r && a !== null && typeof a === "object") descend(unwrapValue(u[s]), a, t, i, o);
267
360
  if (l !== null) {
268
- const e = l[u];
361
+ const e = l[s];
269
362
  if (e !== undefined) {
270
- r++;
271
- notifyKeyValue(e, u, a[u], c, a, t);
363
+ f++;
364
+ notifyKeyValue(e, s, c[s], a, c, n);
272
365
  }
273
366
  }
274
367
  }
275
- if (u) {
368
+ if (s) {
276
369
  // Deleted-key nodes (in the map but absent from incoming) — counted
277
370
  // fast-out: when every node was visited, skip the sweep entirely.
278
- if (l !== null && r < e.nc) {
371
+ if (l !== null && f < e.nc) {
279
372
  for (const e of Reflect.ownKeys(l)) {
280
- if (!hasOwnP.call(t, e)) notifyKeyDiff(l[e], e, a, t, false);
373
+ if (!hasOwnP.call(n, e)) notifyKeyDiff(l[e], e, c, n, false);
281
374
  }
282
375
  }
283
- notifyFoldTail(e, a, t);
376
+ notifyFoldTail(e, c, n);
284
377
  }
285
378
  return;
286
379
  }
@@ -288,31 +381,125 @@ function applyAdopt(e, t, n, o = false) {
288
381
 
289
382
  const hasOwnP = Object.prototype.hasOwnProperty;
290
383
 
291
- function descend(e, t, n, o, l = false) {
292
- if (e === null || typeof e !== "object" || t === null || typeof t !== "object") return;
384
+ /** Setter-channel row ops (the fold site calls this for array targets with
385
+ * ops consumers): structural mutation through the setter push/splice/index
386
+ * assignment/permutation — is a visibility transition for the list container
387
+ * just like a reconcile walk, and drivers consuming registerRowOps must see
388
+ * it. Setter mutations move the SAME row objects around, so RAW IDENTITY is
389
+ * the key. Aligned arrays (value-only folds) emit nothing. */ const identityKey = e => unwrapValue(e);
390
+
391
+ /** Key equality for EVERY key comparison in this module (re-audit 2, P1-5):
392
+ * SameValueZero, matching the Map-based matchers (buildRowOps, the adoption
393
+ * window) — NaN keys are equal to themselves, so aligned NaN rows stay
394
+ * aligned in the prefix walk instead of forever misaligning. Adoption and
395
+ * row ops MUST agree on key equality or retained DOM rows go stale. */ function sameKey(e, n) {
396
+ return e === n || e !== e && n !== n;
397
+ }
398
+
399
+ function emitSetterRowOps(e, n, t) {
400
+ const o = buildIdentityRowOps(n, t);
401
+ if (o !== null) rowHooks.emitRowOps(e, t, o);
402
+ }
403
+
404
+ /** Identity-keyed structural diff, returned rather than emitted: shared by
405
+ * the setter channel (regular queue) and the OPTIMISTIC write channel (lane
406
+ * queue) — same retention semantics, different dispatch timing. Returns
407
+ * null when the lists are identity-aligned (no structure changed). */ function buildIdentityRowOps(e, n) {
408
+ let t = 0;
409
+ const o = e.length < n.length ? e.length : n.length;
410
+ while (t < o && unwrapValue(e[t]) === unwrapValue(n[t])) t++;
411
+ if (t === e.length && t === n.length) return null;
412
+ return buildRowOps(e, n, t, identityKey);
413
+ }
414
+
415
+ /** Shared row-ops builder (keyed deep branch + shallow/positional branch):
416
+ * key-matches the misaligned window into { prefix, sources, removed }.
417
+ * `keyFn === null` degrades to positional ops (append/truncate only). */ function buildAndEmitRowOps(e, n, t, o, l) {
418
+ rowHooks.emitRowOps(e, t, buildRowOps(n, t, o, l));
419
+ }
420
+
421
+ function buildRowOps(e, n, t, o) {
422
+ const l = e.length;
423
+ const i = n.length;
424
+ const u = new Array(i - t);
425
+ // Occurrence-aware matching (re-audit): duplicate keys queue their old
426
+ // indices and each is consumed ONCE — first-wins reuse would hand the same
427
+ // source (and its one DOM row) to multiple next positions. The no-dup fast
428
+ // shape stays a bare number; collisions upgrade to a queue.
429
+ let f = null;
430
+ if (o !== null && t < l) {
431
+ f = new Map;
432
+ for (let n = t; n < l; n++) {
433
+ const t = unwrapValue(e[n]);
434
+ if (t !== null && typeof t === "object") {
435
+ const e = o(t);
436
+ if (e === undefined) continue;
437
+ const l = f.get(e);
438
+ if (l === undefined) f.set(e, n); else if (Array.isArray(l)) l.push(n); else f.set(e, [ l, n ]);
439
+ }
440
+ }
441
+ }
442
+ const s = f !== null ? new Set : null;
443
+ for (let e = t; e < i; e++) {
444
+ const l = n[e];
445
+ let i = -1;
446
+ if (l !== null && typeof l === "object" && f !== null) {
447
+ const e = o(l);
448
+ if (e !== undefined) {
449
+ const n = f.get(e);
450
+ if (n !== undefined) {
451
+ if (Array.isArray(n)) {
452
+ i = n.shift();
453
+ if (n.length === 1) f.set(e, n[0]);
454
+ } else {
455
+ i = n;
456
+ f.delete(e);
457
+ }
458
+ s.add(i);
459
+ }
460
+ }
461
+ }
462
+ u[e - t] = i;
463
+ }
464
+ const r = [];
465
+ for (let n = t; n < l; n++) {
466
+ if (s === null || !s.has(n)) r.push(unwrapValue(e[n]));
467
+ }
468
+ return {
469
+ prefix: t,
470
+ sources: u,
471
+ removed: r
472
+ };
473
+ }
474
+
475
+ function descend(e, n, t, o, l = false) {
476
+ if (e === null || typeof e !== "object" || n === null || typeof n !== "object") return;
293
477
  // Lookup FIRST: a hit implies pv was wrappable and never raw-marked (only
294
478
  // wrappables acquire targets; rawValues never wrap) — one WeakMap get
295
479
  // replaces isWrappable(pv) + isRawValue(pv), and a miss prunes untracked
296
480
  // subtrees before any further checks.
297
- const f = (o?.map ?? storeNextLookup).get(e);
298
- if (f === undefined) return;
481
+ const i = (o?.map ?? storeNextLookup).get(e);
482
+ if (i === undefined) return;
299
483
  // nothing proxied below this pair
300
484
  // The NEW side still validates fully: a frozen/platform/markRaw'd incoming
301
485
  // value is a leaf for reconcile — replaced by reference, never recursed
302
486
  // into (R42); the parent's slot notification covers the change.
303
- if (!isWrappable(t)) return;
304
- if (rawValuesUsed && isRawValue(t)) return;
305
- t = unwrapValue(t);
487
+ if (!isWrappable(n)) return;
488
+ if (rawValuesUsed && isRawValue(n)) return;
489
+ n = unwrapValue(n);
306
490
  // Kind change replaces wholesale, never merges (R10): a target's carrier
307
491
  // class (array vs object) is fixed at creation, so the slot detaches and a
308
492
  // fresh proxy of the right kind wraps the incoming value on next read.
309
- if (Array.isArray(e) !== Array.isArray(t)) return;
310
- if (n) {
311
- const o = n(e);
312
- const l = n(t);
493
+ if (Array.isArray(e) !== Array.isArray(n)) return;
494
+ if (t) {
495
+ const o = t(e);
496
+ const l = t(n);
313
497
  // Key mismatch detaches: the slot takes the new entity; the old proxy
314
498
  // keeps its (old) backing and a fresh proxy wraps the new value on read.
315
- if (o !== undefined && l !== undefined && o !== l) return;
499
+ // SameValueZero (re-audit 2, P1-5): NaN keys are self-equal strict
500
+ // inequality detached every NaN-keyed slot on every tick while the
501
+ // Map-based row-ops matcher retained its DOM row (stale forever).
502
+ if (o !== undefined && l !== undefined && !sameKey(o, l)) return;
316
503
  }
317
504
  // Reachability pruning (§6d) is MODE-dependent, both pinned:
318
505
  // - keyed matching descends only where subscriptions exist at/below (`d`) —
@@ -324,8 +511,8 @@ function descend(e, t, n, o, l = false) {
324
511
  // UNCONDITIONALLY (proj R6: the slot keeps its proxy without needing a
325
512
  // subscriber below); plain keyed reconcile detaches unobserved captures
326
513
  // (recon-snap R18 — staleness is the pinned pruning contract).
327
- if (!l && n !== null && !f.d) return;
328
- applyAdopt(f, t, n, l);
514
+ if (!l && t !== null && !i.d) return;
515
+ applyAdopt(i, n, t, l);
329
516
  }
330
517
 
331
- export { reconcileNextState };
518
+ export { buildIdentityRowOps, emitSetterRowOps, reconcileNextState, sameKey };