@solidjs/signals 2.0.0-rc.1 → 2.0.0-rc.2

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 (50) hide show
  1. package/dist/dev.js +1661 -361
  2. package/dist/node.cjs +1939 -1331
  3. package/dist/prod/affects.js +16 -16
  4. package/dist/prod/boundaries.js +90 -90
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +74 -72
  7. package/dist/prod/core/constants.js +39 -1
  8. package/dist/prod/core/core.js +332 -224
  9. package/dist/prod/core/effect.js +56 -56
  10. package/dist/prod/core/external.js +4 -4
  11. package/dist/prod/core/graph.js +65 -54
  12. package/dist/prod/core/heap.js +52 -44
  13. package/dist/prod/core/invariants.js +3 -2
  14. package/dist/prod/core/lanes.js +41 -34
  15. package/dist/prod/core/optimistic.js +63 -60
  16. package/dist/prod/core/owner.js +97 -96
  17. package/dist/prod/core/scheduler.js +243 -194
  18. package/dist/prod/core/verdict.js +184 -77
  19. package/dist/prod/map.js +104 -104
  20. package/dist/prod/signals.js +1 -1
  21. package/dist/prod/store/next/optimistic.js +31 -23
  22. package/dist/prod/store/next/projection.js +3 -3
  23. package/dist/prod/store/next/reconcile.js +78 -74
  24. package/dist/prod/store/next/store.js +357 -90
  25. package/dist/prod/store/store.js +7 -7
  26. package/dist/types/core/attribution-hooks.d.ts +52 -0
  27. package/dist/types/core/attribution.d.ts +186 -0
  28. package/dist/types/core/constants.d.ts +26 -0
  29. package/dist/types/core/core.d.ts +8 -1
  30. package/dist/types/core/dev.d.ts +20 -3
  31. package/dist/types/core/graph.d.ts +1 -0
  32. package/dist/types/core/lanes.d.ts +4 -16
  33. package/dist/types/core/scheduler.d.ts +8 -0
  34. package/dist/types/core/types.d.ts +85 -41
  35. package/dist/types/store/next/projection.d.ts +0 -16
  36. package/dist/types/store/next/store.d.ts +6 -0
  37. package/dist/types/store/next/target.d.ts +18 -0
  38. package/dist/types-cjs/core/attribution-hooks.d.cts +52 -0
  39. package/dist/types-cjs/core/attribution.d.cts +186 -0
  40. package/dist/types-cjs/core/constants.d.cts +26 -0
  41. package/dist/types-cjs/core/core.d.cts +8 -1
  42. package/dist/types-cjs/core/dev.d.cts +20 -3
  43. package/dist/types-cjs/core/graph.d.cts +1 -0
  44. package/dist/types-cjs/core/lanes.d.cts +4 -16
  45. package/dist/types-cjs/core/scheduler.d.cts +8 -0
  46. package/dist/types-cjs/core/types.d.cts +85 -41
  47. package/dist/types-cjs/store/next/projection.d.cts +0 -16
  48. package/dist/types-cjs/store/next/store.d.cts +6 -0
  49. package/dist/types-cjs/store/next/target.d.cts +18 -0
  50. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { unwrapOverride, $REFRESH, CONFIG_OWNED_WRITE, NOT_PENDING, STATUS_UNINITIALIZED, STATUS_ERROR, CONFIG_CHILDREN_FORBIDDEN } from "../../core/constants.js";
1
+ import { unwrapOverride, $REFRESH, CONFIG_OWNED_WRITE, NOT_PENDING, CONFIG_OPTIMISTIC, STATUS_UNINITIALIZED, STATUS_ERROR, CONFIG_CHILDREN_FORBIDDEN } from "../../core/constants.js";
2
2
 
3
- import { setSignal, isEqual, read, pendingCheckActive, readNodeFast, READ_SLOW, signal } from "../../core/core.js";
3
+ import { setSignal, isEqual, read, pendingCheckActive, readNodeFast, READ_SLOW, signal, ext } from "../../core/core.js";
4
4
 
5
5
  import { projectionWriteActive, setProjectionWriteActive, schedule, setStoreCommitHook } from "../../core/scheduler.js";
6
6
 
@@ -30,13 +30,48 @@ import { storeNextLookup, optHooks, ownedRaw, devAssertNeverUserMutation } from
30
30
  */
31
31
  // ---------------------------------------------------------------------------
32
32
  // wrap / dedupe
33
+ /** Pre-shaped constructor for OBJECT proxy targets: V8 tips a bare `{}` into
34
+ * dictionary mode once ~19 named properties are assigned onto it (the #3044
35
+ * `ovl`/`del` fields crossed that line — every trap's field read became a
36
+ * hash lookup, a 15% deep-dbmon tick regression). Declaring every field in a
37
+ * constructor pre-allocates in-object slots so the map stays fast, with
38
+ * headroom for future fields. The prototype is reset to `Object.prototype`
39
+ * so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
40
+ * plain object's. Array targets keep the bare-`[]` path — they must carry
41
+ * the array exotic class for `Array.isArray(proxy)`, and arrays store named
42
+ * fields off-object where this cliff does not apply. */
43
+ function TargetShape() {
44
+ this.v = undefined;
45
+ this.ch = undefined;
46
+ this.pb = undefined;
47
+ this.n = undefined;
48
+ this.h = undefined;
49
+ this.k = undefined;
50
+ this.dk = undefined;
51
+ this.u = undefined;
52
+ this.pk = undefined;
53
+ this.px = undefined;
54
+ this.d = undefined;
55
+ this.a = undefined;
56
+ this.sc = undefined;
57
+ this.nc = undefined;
58
+ this.adopted = undefined;
59
+ this.fam = undefined;
60
+ this.s = undefined;
61
+ this.ovl = undefined;
62
+ this.del = undefined;
63
+ this.wk = undefined;
64
+ }
65
+
66
+ TargetShape.prototype = Object.prototype;
67
+
33
68
  function createTarget(e, t, n, r = t?.fam ?? null) {
34
69
  // The proxy target carries the array exotic class when the value is an
35
70
  // array, so Array.isArray(proxy) is true; the fields live on it directly.
36
71
  // Direct field assignment in one fixed order (no Object.assign literal
37
72
  // copy): every target shares a hidden-class transition chain — createTarget
38
73
  // was the #2 store cost in the uibench creation profile.
39
- const i = Array.isArray(e) ? [] : {};
74
+ const i = Array.isArray(e) ? [] : new TargetShape;
40
75
  i.v = e;
41
76
  // Chained-backing flag (backing IS another store's proxy, §7b) — cached so
42
77
  // the hot read path never does a per-read symbol lookup on the backing.
@@ -56,6 +91,9 @@ function createTarget(e, t, n, r = t?.fam ?? null) {
56
91
  i.adopted = false;
57
92
  i.fam = r;
58
93
  i.s = false;
94
+ i.ovl = false;
95
+ i.del = null;
96
+ i.wk = null;
59
97
  i.px = new Proxy(i, traps);
60
98
  // Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
61
99
  // proxy off looked-up targets as a field.
@@ -83,7 +121,13 @@ function wrapNext(e, t = null, n = null, r = t?.fam ?? null) {
83
121
  /** Unwrap our own proxies to their current backing; leave everything else. */ function unwrapValue(e) {
84
122
  if (e == null || typeof e !== "object") return e;
85
123
  const t = e[$TARGET];
86
- if (t !== undefined && t.px === e && t.v !== undefined) return t.pb ?? t.v;
124
+ if (t !== undefined && t.px === e && t.v !== undefined) {
125
+ // A draft escaping into other storage must be a REAL container that
126
+ // becomes this target's committed backing at fold (the shared-raw
127
+ // contract) — a prototype overlay is neither.
128
+ if (t.ovl) materializePB(t);
129
+ return t.pb ?? t.v;
130
+ }
87
131
  return e;
88
132
  }
89
133
 
@@ -100,7 +144,7 @@ function getNode(e, t, n) {
100
144
  equals: (t, n) => isEqual(t, n) || sameLogicalSlot(e, t, n),
101
145
  unobserved() {
102
146
  // A live affects() mark keeps the node addressable (sweep parity).
103
- if (o.t) return;
147
+ if (o.o?.t) return;
104
148
  if (e.n && e.n[t] === o) {
105
149
  delete e.n[t];
106
150
  e.nc--;
@@ -126,7 +170,10 @@ function getNode(e, t, n) {
126
170
  o.pxv = undefined;
127
171
  // Optimistic families: arm the override slot — setSignal routes armed
128
172
  // nodes through the core engine (lanes, ownership, reverts all native).
129
- if (e.fam?.opt) o.De = NOT_PENDING;
173
+ if (e.fam?.opt) {
174
+ ext(o).De = NOT_PENDING;
175
+ o.T |= CONFIG_OPTIMISTIC;
176
+ }
130
177
  // A node born inside a live mark's identity scope inherits the mark
131
178
  // (the declaration walk could only cover nodes existing then).
132
179
  if (t !== $AFFECTS && affectsScopesLive()) inheritAffectsMarks(o, e.v, t);
@@ -151,12 +198,15 @@ function getHasNode(e, t, n) {
151
198
  const o = i = signal(n, {
152
199
  equals: isEqual,
153
200
  unobserved() {
154
- if (o.t) return;
201
+ if (o.o?.t) return;
155
202
  if (e.h && e.h[t] === o) delete e.h[t];
156
203
  }
157
204
  }, e.fam?.node ?? undefined);
158
205
  o.T |= CONFIG_OWNED_WRITE;
159
- if (e.fam?.opt) o.De = NOT_PENDING;
206
+ if (e.fam?.opt) {
207
+ ext(o).De = NOT_PENDING;
208
+ o.T |= CONFIG_OPTIMISTIC;
209
+ }
160
210
  if (affectsScopesLive()) inheritAffectsMarks(o, e.v, t);
161
211
  r[t] = i;
162
212
  markDescendants(e);
@@ -174,7 +224,10 @@ function getKeySetNode(e) {
174
224
  }
175
225
  }, e.fam?.node ?? undefined);
176
226
  n.T |= CONFIG_OWNED_WRITE;
177
- if (e.fam?.opt) n.De = NOT_PENDING;
227
+ if (e.fam?.opt) {
228
+ ext(n).De = NOT_PENDING;
229
+ n.T |= CONFIG_OPTIMISTIC;
230
+ }
178
231
  e.k = t;
179
232
  markDescendants(e);
180
233
  }
@@ -191,7 +244,10 @@ function getDeepNode(e) {
191
244
  }
192
245
  }, e.fam?.node ?? undefined);
193
246
  n.T |= CONFIG_OWNED_WRITE;
194
- if (e.fam?.opt) n.De = NOT_PENDING;
247
+ if (e.fam?.opt) {
248
+ ext(n).De = NOT_PENDING;
249
+ n.T |= CONFIG_OPTIMISTIC;
250
+ }
195
251
  if (affectsScopesLive()) inheritAffectsMarks(n, e.v, $TRACK);
196
252
  e.dk = t;
197
253
  markDescendants(e);
@@ -235,10 +291,56 @@ function cloneRaw(e, t) {
235
291
  return Array.isArray(e) ? Object.defineProperties([], n) : Object.create(Object.getPrototypeOf(e), n);
236
292
  }
237
293
 
294
+ /** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
295
+ * returns true when the container is plain data (overlay-safe). */ function scanAccessorsOnce(e) {
296
+ const t = e.v;
297
+ for (const n of Reflect.ownKeys(t)) {
298
+ // Own keys shadow prototype accessors, so the lookups are exact here.
299
+ if (lookupGetter.call(t, n) !== undefined || lookupSetter.call(t, n) !== undefined) {
300
+ e.a = true;
301
+ break;
302
+ }
303
+ }
304
+ e.sc = true;
305
+ return !e.a;
306
+ }
307
+
308
+ /** Downgrade a prototype-overlay pending backing to the clone path: builds
309
+ * the real container (committed + overlay writes − deletes) that fold will
310
+ * SWAP in as the committed backing, exactly as if the draft had started on
311
+ * the clone path. Consumers that need a complete container (reconcile's
312
+ * diff walks, drafts escaping into other storage) call this. */ function materializePB(e) {
313
+ if (!e.ovl) return;
314
+ const t = e.pb;
315
+ const n = cloneRaw(e.v, e);
316
+ for (const e of Reflect.ownKeys(t)) {
317
+ const r = Object.getOwnPropertyDescriptor(t, e);
318
+ if (r.get || r.set || !r.enumerable || !r.writable || !r.configurable) Object.defineProperty(n, e, r); else n[e] = r.value;
319
+ }
320
+ if (e.del !== null) {
321
+ for (const t of e.del) delete n[t];
322
+ e.del = null;
323
+ }
324
+ const r = e.fam?.map ?? storeNextLookup;
325
+ r.delete(t);
326
+ ownedRaw.add(n);
327
+ r.set(n, e);
328
+ e.pb = n;
329
+ e.ovl = false;
330
+ }
331
+
238
332
  function ensurePB(e) {
239
333
  let t = e.pb;
240
334
  if (t === null) {
241
- t = e.pb = cloneRaw(e.v, e);
335
+ // Prototype-chain overlay (#3044): plain-data non-array containers
336
+ // outside projection/optimistic families open drafts in O(1) — own keys
337
+ // are the writes, reads fall through to committed. Everything else
338
+ // (arrays: splice/length semantics; families: seeding/revert machinery;
339
+ // accessor containers: live getters) keeps the descriptor clone.
340
+ if (e.fam === null && !Array.isArray(e.v) && (e.sc ? !e.a : scanAccessorsOnce(e))) {
341
+ t = e.pb = Object.create(e.v);
342
+ e.ovl = true;
343
+ } else t = e.pb = cloneRaw(e.v, e);
242
344
  // Optimistic families: seed USER drafts from the OPTIMISTIC VIEW
243
345
  // (committed + active node overrides), so follow-up writes compose on
244
346
  // optimism instead of clobbering from base (#2951's compose half).
@@ -250,14 +352,14 @@ function ensurePB(e) {
250
352
  if (n !== null) {
251
353
  for (const e of Reflect.ownKeys(n)) {
252
354
  const r = n[e];
253
- if (hasActiveOverride(r)) t[e] = unwrapOverride(r.De);
355
+ if (hasActiveOverride(r)) t[e] = unwrapOverride(r.o?.De);
254
356
  }
255
357
  }
256
358
  const r = e.h;
257
359
  if (r !== null) {
258
360
  for (const e of Reflect.ownKeys(r)) {
259
361
  const n = r[e];
260
- if (hasActiveOverride(n) && !unwrapOverride(n.De)) delete t[e];
362
+ if (hasActiveOverride(n) && !unwrapOverride(n.o?.De)) delete t[e];
261
363
  }
262
364
  }
263
365
  }
@@ -285,6 +387,19 @@ function ensurePB(e) {
285
387
  e.adopted = true;
286
388
  }
287
389
  e.pb = null;
390
+ // Overlay and accessor-scan state describe the OUTGOING backing — a
391
+ // swapped container must not inherit them: a stale `ovl` beside a nulled
392
+ // pb crashes materializePB (unwrapValue consults ovl before the
393
+ // null-coalesce), a stale `del` would read the adoptee's keys as deleted
394
+ // in the next draft, and a stale plain-data verdict (`sc`/`a`) could
395
+ // admit an accessor-bearing adoptee to the overlay path. Reset; the next
396
+ // draft rescans once (#3044 audit follow-up).
397
+ e.ovl = false;
398
+ e.del = null;
399
+ e.wk = null;
400
+ // adoption supersedes any staged trap writes
401
+ e.sc = false;
402
+ e.a = false;
288
403
  e.v = t;
289
404
  e.ch = t[$TARGET] !== undefined;
290
405
  (e.fam?.map ?? storeNextLookup).set(t, e);
@@ -331,9 +446,14 @@ function drainFolds() {
331
446
  const r = t.pb;
332
447
  const i = t.n;
333
448
  if (i !== null) {
334
- for (const t of Reflect.ownKeys(i)) {
449
+ // Only written keys can hold (their nodes took the setSignal); the
450
+ // wk bound keeps this O(written) — see notifyWrites. Same fallback
451
+ // rules as the notify (WK_ALL / accessors / non-plain prototypes).
452
+ const n = t.wk;
453
+ const o = n === null || n === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : r) ? Reflect.ownKeys(i) : n;
454
+ for (const t of o) {
335
455
  const n = i[t];
336
- if (n._e !== NOT_PENDING) {
456
+ if (n !== undefined && n.Pe !== NOT_PENDING) {
337
457
  e = true;
338
458
  break;
339
459
  }
@@ -344,10 +464,37 @@ function drainFolds() {
344
464
  // re-queue: commit happens when the hold settles
345
465
  continue;
346
466
  }
347
- t.v = r;
348
- t.ch = false;
467
+ if (t.ovl) {
468
+ // Overlay flatten (#3044): apply this batch's writes onto an OWNED
469
+ // committed backing in place — O(written), not O(container). The
470
+ // backing keeps its identity, so the `t.v === old` gate below skips
471
+ // path copying (the parent slot already points here) and the
472
+ // adopted-notify (setter notifications happened at write time).
473
+ // Unowned backings privatize first (clone once, parents re-slotted)
474
+ // — the never-mutate-user-data contract holds.
475
+ privatizeCommitted(t);
476
+ const e = t.v;
477
+ for (const t of Reflect.ownKeys(r)) {
478
+ const n = Object.getOwnPropertyDescriptor(r, t);
479
+ if (n.get || n.set || !n.enumerable || !n.writable || !n.configurable) Object.defineProperty(e, t, n); else e[t] = n.value;
480
+ }
481
+ if (t.del !== null) {
482
+ for (const n of t.del) delete e[n];
483
+ t.del = null;
484
+ }
485
+ (t.fam?.map ?? storeNextLookup).delete(r);
486
+ t.pb = null;
487
+ t.ovl = false;
488
+ t.wk = null;
489
+ // written-keys window closes with the fold commit
490
+ } else {
491
+ t.v = r;
492
+ t.ch = false;
349
493
  // pb is always a plain clone
350
- t.pb = null;
494
+ t.pb = null;
495
+ t.wk = null;
496
+ // written-keys window closes with the fold commit
497
+ }
351
498
  }
352
499
  if (t.v === n) continue;
353
500
  // adopted then re-adopted back, or no-op
@@ -371,8 +518,20 @@ function drainFolds() {
371
518
  * isPending, affects, and lane machinery ride the core natively (§3's
372
519
  * "pending home = the node when a node exists"). Unobserved keys stay in the
373
520
  * pending backing and fold directly at commit.
374
- */ function notifyWrites(e) {
375
- const t = e.pb;
521
+ */
522
+ /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
523
+ * array length write implicitly deleted indices) — consumers full-scan. */ const WK_ALL = new Set;
524
+
525
+ /** Plain-prototype check for the written-keys bound: prototype getters on
526
+ * class instances can derive from ANY field, so only plain-data containers
527
+ * may bound the notify to written keys. Overlay pbs chain to the COMMITTED
528
+ * object (#3044), so overlay plainness is judged on the committed proto. */ const plainProto = e => {
529
+ const t = Object.getPrototypeOf(e);
530
+ return t === Object.prototype || t === Array.prototype || t === null;
531
+ };
532
+
533
+ function notifyWrites(e) {
534
+ let t = e.pb;
376
535
  if (t === null) return;
377
536
  // Optimistic channel: user writes on an optimistic family become node-level
378
537
  // engine writes (armed nodes route setSignal through optimisticWrite) — the
@@ -402,40 +561,51 @@ function drainFolds() {
402
561
  }
403
562
  const n = e.v;
404
563
  const r = e.n;
564
+ // Written-keys bound: trap writes record their keys, so the notify visits
565
+ // O(written) nodes instead of every subscription on the record (a selection
566
+ // map with thousands of per-key subscribers pays two visits per select,
567
+ // not a full scan). Falls back to the full node scan when the bound can't
568
+ // hold: no trap granularity (wk null), an array length write (WK_ALL —
569
+ // implicit index deletes), accessors on the record (t.a — a getter node's
570
+ // value can change when ANY key is written), or a non-plain prototype.
571
+ const i = e.wk;
572
+ const o = i === WK_ALL || e.a === true || !plainProto(e.ovl ? e.v : t) ? null : i;
405
573
  if (r !== null) {
406
- for (const e of Reflect.ownKeys(r)) {
407
- const i = r[e];
574
+ const i = o ?? Reflect.ownKeys(r);
575
+ for (const o of i) {
576
+ const i = r[o];
577
+ if (i === undefined) continue;
408
578
  // Per-key accessor handling: the node's cached flag plus ONE getter
409
579
  // probe on the incoming side (getters arriving via merge/adoption).
410
580
  // Setter-only props read as data (value undefined) so lookupSetter is
411
581
  // not consulted on this hot path; prototype getters never own nodes.
412
- if (i.acc === true || hasOwn.call(t, e) && lookupGetter.call(t, e) !== undefined) {
413
- i.acc = isOwnAccessor(t, e);
414
- const r = Object.getOwnPropertyDescriptor(n, e);
415
- const o = Object.getOwnPropertyDescriptor(t, e);
416
- if (r && (r.get || r.set) || o && (o.get || o.set)) {
417
- if (r?.get !== o?.get || r?.set !== o?.set || r?.value !== o?.value) setSignal(i, () => FORCE);
582
+ if (i.acc === true || hasOwn.call(t, o) && lookupGetter.call(t, o) !== undefined) {
583
+ i.acc = isOwnAccessor(t, o);
584
+ const e = Object.getOwnPropertyDescriptor(n, o);
585
+ const r = Object.getOwnPropertyDescriptor(t, o);
586
+ if (e && (e.get || e.set) || r && (r.get || r.set)) {
587
+ if (e?.get !== r?.get || e?.set !== r?.set || e?.value !== r?.value) setSignal(i, () => FORCE);
418
588
  continue;
419
589
  }
420
- if (!isEqual(r?.value, o?.value)) setSignal(i, () => o?.value);
590
+ if (!isEqual(e?.value, r?.value)) setSignal(i, () => r?.value);
421
591
  continue;
422
592
  }
423
593
  // No old-side pre-compare: t.v lags across multi-batch windows (a
424
594
  // projection recompute can run before the prior fold commits) — the
425
595
  // node's OWN current value is the true old side, and setSignal's
426
596
  // internal equality already checks exactly that.
427
- const o = t[e];
428
- setSignal(i, () => o);
597
+ const f = e.del !== null && e.del.has(o) ? undefined : t[o];
598
+ setSignal(i, () => f);
429
599
  }
430
600
  }
431
- const i = e.h;
432
- if (i !== null) {
433
- for (const e of Reflect.ownKeys(i)) setSignal(i[e], e in t);
601
+ const f = e.h;
602
+ if (f !== null) {
603
+ for (const n of Reflect.ownKeys(f)) setSignal(f[n], n in t && !(e.del !== null && e.del.has(n)));
434
604
  }
435
605
  // Deep-witness (dk): setter writes must notify a deep() subscriber even on
436
606
  // keys with no node. O(pb keys) equality only when a witness exists.
437
607
  if (e.dk !== null) {
438
- for (const r of Reflect.ownKeys(t)) {
608
+ if (e.del !== null && e.del.size !== 0) bumpDeep(e); else for (const r of Reflect.ownKeys(t)) {
439
609
  const i = t[r];
440
610
  const o = n[r];
441
611
  if (i !== null && typeof i === "object" ? !targetsEqual(o, i) : !isEqual(o, i)) {
@@ -445,7 +615,21 @@ function drainFolds() {
445
615
  }
446
616
  }
447
617
  if (e.k !== null) {
448
- const r = Array.isArray(t) && Array.isArray(n) ? arrayStructureChanged(n, t) : membershipChanged(n, t);
618
+ let r;
619
+ if (e.ovl) {
620
+ // Overlay membership: only NEW own keys or deletes can change it.
621
+ r = e.del !== null && e.del.size !== 0;
622
+ if (!r) {
623
+ for (const e of Reflect.ownKeys(t)) {
624
+ if (!hasOwn.call(n, e)) {
625
+ r = true;
626
+ break;
627
+ }
628
+ }
629
+ }
630
+ } else {
631
+ r = Array.isArray(t) && Array.isArray(n) ? arrayStructureChanged(n, t) : membershipChanged(n, t);
632
+ }
449
633
  if (r) setSignal(e.k, e => e + 1);
450
634
  }
451
635
  // Projection backing folds split by channel (two pinned contracts):
@@ -461,7 +645,9 @@ function drainFolds() {
461
645
  if (e.fam !== null && e.pb !== null && getWriteOverride()) {
462
646
  const n = e.v;
463
647
  e.pb = null;
464
- e.v = t;
648
+ e.wk = null;
649
+ // written-keys window closes with the eager fold
650
+ e.v = t;
465
651
  e.ch = false;
466
652
  if (e.u && e.u.v[e.pk] === n) {
467
653
  privatizeCommitted(e.u);
@@ -650,7 +836,7 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
650
836
  * / onSettled callbacks) get COMMITTED visibility (#3006), same as core. */ function inOwnerContext() {
651
837
  const e = getOwner();
652
838
  if (e === null) return false;
653
- const t = e.Tt ? e.It : e;
839
+ const t = e.Ct ? e.Ot : e;
654
840
  return t != null && !(t.T & CONFIG_CHILDREN_FORBIDDEN);
655
841
  }
656
842
 
@@ -661,7 +847,7 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
661
847
  if (t === null) return false;
662
848
  for (const e of Reflect.ownKeys(t)) {
663
849
  const n = t[e];
664
- if (n._e !== NOT_PENDING && n.Ne != null && n.Ne.fn !== true) return true;
850
+ if (n.Pe !== NOT_PENDING && n._e != null && n._e.an !== true) return true;
665
851
  }
666
852
  return false;
667
853
  }
@@ -710,7 +896,7 @@ function isOwnAccessor(e, t) {
710
896
 
711
897
  /** Active optimistic override on an armed node (armed slot idles at
712
898
  * NOT_PENDING; undefined = unarmed plain node). */ function hasActiveOverride(e) {
713
- return e.De !== undefined && e.De !== NOT_PENDING;
899
+ return e.o?.De !== undefined && e.o?.De !== NOT_PENDING;
714
900
  }
715
901
 
716
902
  /** Context-aware node view for reads outside tracking: active override >
@@ -720,7 +906,7 @@ function isOwnAccessor(e, t) {
720
906
  * reader that forced it (backing commits eagerly; node values fold at flush).
721
907
  * FORCE sentinels never surface (they only bump subscribers of accessor
722
908
  * keys, which are served by the trap, not the node). */ function nodeValue(e, t) {
723
- const n = hasActiveOverride(e) ? unwrapOverride(e.De) : e._e !== NOT_PENDING && inOwnerContext() ? e._e : t;
909
+ const n = hasActiveOverride(e) ? unwrapOverride(e.o?.De) : e.Pe !== NOT_PENDING && inOwnerContext() ? e.Pe : t;
724
910
  return n === FORCE ? t : n;
725
911
  }
726
912
 
@@ -753,7 +939,7 @@ function isOwnAccessor(e, t) {
753
939
  // #2951). Once ensurePB runs, the seeded clone carries the view.
754
940
  if (e.fam?.opt && e.pb === null) {
755
941
  const n = e.n?.[t];
756
- if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.De);
942
+ if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.o?.De);
757
943
  }
758
944
  } else {
759
945
  if (i !== undefined) {
@@ -796,6 +982,15 @@ function isOwnAccessor(e, t) {
796
982
  * read() links the node and throws the firewall's error itself (the node
797
983
  * link is what wakes async-memo readers when the landing writes values;
798
984
  * the firewall link rides the same read). */ function firewallGate(e) {
985
+ // Own-draft ops are exempt: an async derive's continuation (generator body
986
+ // after an `await`/`yield`) runs OUTSIDE the sync write scope (inDraft is
987
+ // already false), but its draft-proxy traps mark every op with the write
988
+ // override. Those reads are the derive working its own draft (state.push
989
+ // reading .length) — gating them throws NotReadyError back into the derive
990
+ // itself, which the post-await read diagnostic (#2987) then escalates to a
991
+ // reactivity halt. The gate exists for EXTERNAL readers (seed invisibility,
992
+ // proj R23); the derive is the author.
993
+ if (projectionWriteActive || getWriteOverride()) return;
799
994
  const t = e.fam?.node;
800
995
  if (t != null && t.S & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(t);
801
996
  }
@@ -828,6 +1023,12 @@ const traps = {
828
1023
  if (pendingCheckActive) witnessAffectsMark(e, t);
829
1024
  if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
830
1025
  const r = readSource(e);
1026
+ // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
1027
+ // deleted keys are tracked aside and read as absent in the pending view.
1028
+ if (e.del !== null && r === e.pb && e.del.has(t)) {
1029
+ if (!inDraft(e) && getObserver() !== null) read(getNode(e, t, undefined));
1030
+ return undefined;
1031
+ }
831
1032
  // Hot inline case: existing PLAIN node (non-accessor), unchained backing,
832
1033
  // tracked read of a present data key — the dbmon/uibench effect re-read
833
1034
  // shape. Skips serveDataKey's frame, the FORCE compare (only accessor
@@ -855,11 +1056,17 @@ const traps = {
855
1056
  // the node's cached flag; the first TRACKED read (which creates the
856
1057
  // node) probes once — untracked node-less reads take the plain path,
857
1058
  // where a raw-receiver getter still returns correct committed values.
1059
+ // Tracking suppression is PER-TARGET (inDraft), never global: `writing`
1060
+ // counts every open setter anywhere, and a projection derive runs its
1061
+ // whole body inside one — a global gate silently swallowed EXTERNAL
1062
+ // absent-key/accessor subscriptions for every store read during any
1063
+ // derive, leaving nested projections permanently dependency-less when
1064
+ // their sources hadn't materialized yet (#3037).
858
1065
  const i = e.n?.[t];
859
1066
  {
860
- const o = i !== undefined ? i.acc === true : !writing && getObserver() !== null && isOwnAccessor(r, t);
1067
+ const o = i !== undefined ? i.acc === true : !inDraft(e) && getObserver() !== null && isOwnAccessor(r, t);
861
1068
  if (o) {
862
- if (!writing && getObserver() !== null) read(i ?? getNode(e, t, undefined));
1069
+ if (!inDraft(e) && getObserver() !== null) read(i ?? getNode(e, t, undefined));
863
1070
  const o = Reflect.get(r, t, n);
864
1071
  if (e.s) return serveShallow(e, t, o);
865
1072
  return isWrappable(o) ? draftServe(e, wrapNext(o, e, t)) : o;
@@ -868,16 +1075,21 @@ const traps = {
868
1075
  // Plain-data fast path: no descriptor allocation per read.
869
1076
  // Inherited pollution keys are never served (core R30) — checked before
870
1077
  // the proto-function branch can leak `constructor`. Interned-string
871
- // compares beat a Set hash on this per-read path.
872
- if ((t === "constructor" || t === "__proto__" || t === "prototype") && !hasOwn.call(r, t)) return undefined;
873
- let o = r[t];
874
- if (o === undefined ? !hasOwn.call(r, t) : false) {
1078
+ // compares beat a Set hash on this per-read path. Overlay pending
1079
+ // backings chain to the committed backing, so "own in the view" means
1080
+ // own on either layer (ownInView) — a genuine prototype method is one
1081
+ // that is own on NEITHER.
1082
+ const o = e.ovl && r === e.pb;
1083
+ if ((t === "constructor" || t === "__proto__" || t === "prototype") && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return undefined;
1084
+ let f = r[t];
1085
+ if (f === undefined ? !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t)) : false) {
875
1086
  // Inherited: prototype getters/methods run with the proxy receiver.
876
- o = Reflect.get(r, t, n);
877
- if (typeof o === "function") return o;
1087
+ f = Reflect.get(r, t, n);
1088
+ if (typeof f === "function") return f;
878
1089
  // proto methods untracked
879
- // Reading a currently-absent own key subscribes to it (R12).
880
- if (o === undefined && !writing) {
1090
+ // Reading a currently-absent own key subscribes to it (R12) — for any
1091
+ // target OUTSIDE its own draft scope, even mid-setter (#3037, above).
1092
+ if (f === undefined && !inDraft(e)) {
881
1093
  if (getObserver() !== null) read(getNode(e, t, undefined));
882
1094
  const n = e.n?.[t];
883
1095
  if (n) {
@@ -885,16 +1097,16 @@ const traps = {
885
1097
  if (e.s) return serveShallow(e, t, r);
886
1098
  return isWrappable(r) ? draftServe(e, wrapNext(r, e, t)) : r;
887
1099
  }
888
- } else if (o === undefined && inDraft(e) && e.fam?.opt && e.pb === null) {
1100
+ } else if (f === undefined && inDraft(e) && e.fam?.opt && e.pb === null) {
889
1101
  const n = e.n?.[t];
890
- if (n !== undefined && hasActiveOverride(n)) o = unwrapOverride(n.De);
1102
+ if (n !== undefined && hasActiveOverride(n)) f = unwrapOverride(n.o?.De);
891
1103
  }
892
- if (e.s) return serveShallow(e, t, o);
893
- return isWrappable(o) ? draftServe(e, wrapNext(o, e, t)) : o;
1104
+ if (e.s) return serveShallow(e, t, f);
1105
+ return isWrappable(f) ? draftServe(e, wrapNext(f, e, t)) : f;
894
1106
  }
895
- if (typeof o === "function" && !hasOwn.call(r, t)) return o;
1107
+ if (typeof f === "function" && !hasOwn.call(r, t) && !(o && hasOwn.call(e.v, t))) return f;
896
1108
  // proto method
897
- return serveDataKey(e, t, o, r, i);
1109
+ return serveDataKey(e, t, f, r, i);
898
1110
  },
899
1111
  has(e, t) {
900
1112
  if (t === $TARGET || t === $PROXY || t === $TRACK) return true;
@@ -902,6 +1114,8 @@ const traps = {
902
1114
  if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
903
1115
  const n = readSource(e);
904
1116
  let r = t in n;
1117
+ // Overlay deletes read as absent in the pending view (#3044).
1118
+ if (r && e.del !== null && n === e.pb && e.del.has(t)) r = false;
905
1119
  if (!inDraft(e)) {
906
1120
  if (getObserver() !== null) {
907
1121
  const n = getHasNode(e, t, r);
@@ -909,11 +1123,11 @@ const traps = {
909
1123
  if (hasActiveOverride(n)) r = !!i;
910
1124
  } else {
911
1125
  const n = e.h?.[t];
912
- if (n !== undefined && hasActiveOverride(n)) r = !!unwrapOverride(n.De);
1126
+ if (n !== undefined && hasActiveOverride(n)) r = !!unwrapOverride(n.o?.De);
913
1127
  }
914
1128
  } else if (e.fam?.opt && e.pb === null) {
915
1129
  const n = e.h?.[t];
916
- if (n !== undefined && hasActiveOverride(n)) r = !!unwrapOverride(n.De);
1130
+ if (n !== undefined && hasActiveOverride(n)) r = !!unwrapOverride(n.o?.De);
917
1131
  }
918
1132
  return r;
919
1133
  },
@@ -921,31 +1135,49 @@ const traps = {
921
1135
  if (pendingCheckActive) witnessAffectsMark(e);
922
1136
  if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
923
1137
  if (!inDraft(e) && getObserver() !== null) read(getKeySetNode(e));
924
- const t = Reflect.ownKeys(readSource(e));
1138
+ const t = readSource(e);
1139
+ let n;
1140
+ if (e.ovl && t === e.pb) {
1141
+ // Overlay merge (#3044): committed keys in their order, then this
1142
+ // batch's NEW keys, minus deletes.
1143
+ n = Reflect.ownKeys(e.v);
1144
+ const r = e.del;
1145
+ if (r !== null && r.size !== 0) n = n.filter(e => !r.has(e));
1146
+ for (const r of Reflect.ownKeys(t)) {
1147
+ if (!hasOwn.call(e.v, r)) n.push(r);
1148
+ }
1149
+ } else n = Reflect.ownKeys(t);
925
1150
  // Optimistic membership overlay: presence-node overrides add/remove keys
926
1151
  // (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
927
1152
  // Draft reads before the first write overlay too (pb, once created, is
928
1153
  // seeded with the view).
929
1154
  if (e.fam?.opt && e.h !== null && (!inDraft(e) || e.pb === null)) {
930
- let n = null;
1155
+ let t = null;
931
1156
  for (const r of Reflect.ownKeys(e.h)) {
932
1157
  const i = e.h[r];
933
1158
  if (!hasActiveOverride(i)) continue;
934
- n ??= new Set(t);
935
- if (unwrapOverride(i.De)) n.add(r); else n.delete(r);
1159
+ t ??= new Set(n);
1160
+ if (unwrapOverride(i.o?.De)) t.add(r); else t.delete(r);
936
1161
  }
937
- if (n !== null) return [ ...n ];
1162
+ if (t !== null) return [ ...t ];
938
1163
  }
939
- return t;
1164
+ return n;
940
1165
  },
941
1166
  getOwnPropertyDescriptor(e, t) {
942
- const n = Object.getOwnPropertyDescriptor(readSource(e), t);
1167
+ const n = readSource(e);
1168
+ let r = Object.getOwnPropertyDescriptor(n, t);
1169
+ // Overlay (#3044): unwritten keys live on the committed backing;
1170
+ // deleted keys are absent from the pending view.
1171
+ if (e.ovl && n === e.pb) {
1172
+ if (e.del !== null && e.del.has(t)) return undefined;
1173
+ if (r === undefined) r = Object.getOwnPropertyDescriptor(e.v, t);
1174
+ }
943
1175
  if (e.fam?.opt && !inDraft(e)) {
944
- const r = e.h?.[t];
945
- if (r !== undefined && hasActiveOverride(r)) {
946
- if (!unwrapOverride(r.De)) return undefined;
1176
+ const n = e.h?.[t];
1177
+ if (n !== undefined && hasActiveOverride(n)) {
1178
+ if (!unwrapOverride(n.o?.De)) return undefined;
947
1179
  // opt delete
948
- if (n === undefined) {
1180
+ if (r === undefined) {
949
1181
  const n = e.n?.[t];
950
1182
  return {
951
1183
  value: n !== undefined ? nodeValue(n, undefined) : undefined,
@@ -956,12 +1188,12 @@ const traps = {
956
1188
  }
957
1189
  }
958
1190
  }
959
- if (n === undefined) return undefined;
1191
+ if (r === undefined) return undefined;
960
1192
  // Array targets carry a real non-configurable `length` the proxy
961
1193
  // invariant forces us to report faithfully; everything else reports
962
1194
  // configurable via target indirection (core R51).
963
- if (!(t === "length" && Array.isArray(e))) n.configurable = true;
964
- return n;
1195
+ if (!(t === "length" && Array.isArray(e))) r.configurable = true;
1196
+ return r;
965
1197
  },
966
1198
  set(e, t, n) {
967
1199
  // Writes require the target's draft scope OR the projection write
@@ -972,27 +1204,52 @@ const traps = {
972
1204
  if (!r && !i) return true;
973
1205
  if (t === "__proto__") return true;
974
1206
  // pollution guard (core R30)
975
- const o = ensurePB(e);
1207
+ // Unwrap BEFORE ensurePB: unwrapValue materializes a self-referencing
1208
+ // draft's overlay (replacing target.pb), so a pb local captured earlier
1209
+ // would be the abandoned overlay and the write would vanish.
1210
+ // Shallow slots store what was written VERBATIM — another store's proxy
1211
+ // passes through by reference (#2932; markRawOne skips proxies), while
1212
+ // deep stores unwrap to raw backings.
1213
+ const o = e.s ? n : unwrapValue(n);
1214
+ const f = ensurePB(e);
976
1215
  pendingNotify.add(e);
1216
+ // Array length writes implicitly delete indices — the written-keys bound
1217
+ // can't see them, so poison to the full scan for this batch. Index
1218
+ // writes implicitly GROW length, so arrays always record it alongside.
1219
+ if (Array.isArray(f)) {
1220
+ if (t === "length") e.wk = WK_ALL; else if (e.wk !== WK_ALL) {
1221
+ const n = e.wk ??= new Set;
1222
+ n.add(t);
1223
+ n.add("length");
1224
+ }
1225
+ } else if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
977
1226
  // Own data keys literally named "prototype"/"constructor" land as data —
978
1227
  // defineProperty sidesteps a proto-chain setter named the same.
979
1228
  if (UNSAFE_KEYS.has(t)) {
980
- Object.defineProperty(o, t, {
981
- value: unwrapValue(n),
1229
+ Object.defineProperty(f, t, {
1230
+ value: o,
982
1231
  writable: true,
983
1232
  enumerable: true,
984
1233
  configurable: true
985
1234
  });
1235
+ if (e.del !== null) e.del.delete(t);
986
1236
  return true;
987
1237
  }
988
- // Shallow slots store what was written VERBATIM another store's proxy
989
- // passes through by reference (#2932; markRawOne skips proxies), while
990
- // deep stores unwrap to raw backings.
991
- const f = e.s ? n : unwrapValue(n);
992
- o[t] = f;
1238
+ // Overlay first-write DEFINES the own key: assignment through the proto
1239
+ // chain would reject on a non-writable committed property (the clone
1240
+ // path normalized descriptors for exactly this — R51 parity).
1241
+ if (e.ovl && !hasOwn.call(f, t)) {
1242
+ Object.defineProperty(f, t, {
1243
+ value: o,
1244
+ writable: true,
1245
+ enumerable: true,
1246
+ configurable: true
1247
+ });
1248
+ } else f[t] = o;
1249
+ if (e.del !== null) e.del.delete(t);
993
1250
  // Shallow ingest: written records are sticky raw-marked (one entity is
994
1251
  // never both deep-wrapped and raw — R41/#2932, shared invariant).
995
- if (e.s && f !== null && typeof f === "object") markRawOne(f);
1252
+ if (e.s && o !== null && typeof o === "object") markRawOne(o);
996
1253
  // Override-mode (post-await draft) writes have no setter exit — notify
997
1254
  // per-op (setSignal equality-gates repeats).
998
1255
  if (i) notifyWrites(e);
@@ -1004,13 +1261,16 @@ const traps = {
1004
1261
  if (!r && !i) return true;
1005
1262
  if (t === "__proto__") return true;
1006
1263
  if (n.get || n.set) e.a = true;
1007
- const o = ensurePB(e);
1008
- pendingNotify.add(e);
1009
- if ("value" in n) n = {
1264
+ // Unwrap before ensurePB (see the set trap: self-reference materializes).
1265
+ if ("value" in n) n = {
1010
1266
  ...n,
1011
1267
  value: unwrapValue(n.value)
1012
1268
  };
1269
+ const o = ensurePB(e);
1270
+ pendingNotify.add(e);
1271
+ if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
1013
1272
  Object.defineProperty(o, t, n);
1273
+ if (e.del !== null) e.del.delete(t);
1014
1274
  if (i) notifyWrites(e);
1015
1275
  return true;
1016
1276
  },
@@ -1020,7 +1280,11 @@ const traps = {
1020
1280
  if (!n && !r) return true;
1021
1281
  const i = ensurePB(e);
1022
1282
  pendingNotify.add(e);
1283
+ if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
1023
1284
  delete i[t];
1285
+ // A prototype overlay cannot shadow a delete of a committed key —
1286
+ // record it aside (#3044); reads/has/ownKeys/commit consult the set.
1287
+ if (e.ovl && hasOwn.call(e.v, t)) (e.del ??= new Set).add(t);
1024
1288
  if (r) notifyWrites(e);
1025
1289
  return true;
1026
1290
  }
@@ -1155,6 +1419,9 @@ function snapshotWalk(e, t, n) {
1155
1419
  if (e === undefined) break;
1156
1420
  if (e.fam !== null) n = e.fam;
1157
1421
  if (e.fam?.opt === true) (i ??= []).push(e);
1422
+ // Snapshot runs mid-flush (tracked memos execute before commit), so a
1423
+ // pending prototype overlay must present as a REAL merged container.
1424
+ if (e.ovl) materializePB(e);
1158
1425
  const t = e.pb ?? e.v;
1159
1426
  if (t === r) break;
1160
1427
  r = t;
@@ -1199,10 +1466,10 @@ function snapshotWalk(e, t, n) {
1199
1466
  continue;
1200
1467
  }
1201
1468
  const u = f.value;
1202
- const s = u !== null && typeof u === "object" ? snapshotWalk(u, t, n) : u;
1203
- if (f.enumerable && f.writable && f.configurable) i[o] = s; else Object.defineProperty(i, o, {
1469
+ const l = u !== null && typeof u === "object" ? snapshotWalk(u, t, n) : u;
1470
+ if (f.enumerable && f.writable && f.configurable) i[o] = l; else Object.defineProperty(i, o, {
1204
1471
  ...f,
1205
- value: s
1472
+ value: l
1206
1473
  });
1207
1474
  }
1208
1475
  if (e && i.length !== r.length) i.length = r.length;
@@ -1229,4 +1496,4 @@ function snapshotWalk(e, t, n) {
1229
1496
  return f ?? r;
1230
1497
  }
1231
1498
 
1232
- export { adoptPB, bumpDeep, createStoreNext, deepNext, getHasNode, getKeySetNode, getNode, hasAccessorFlag, hasActiveOverride, notifyFold, notifyFoldTail, notifyKeyDiff, notifyKeyValue, runAuthoritative, snapshotNext, storeSetterNext, targetsEqual, unwrapValue, wrapNext };
1499
+ export { adoptPB, bumpDeep, createStoreNext, deepNext, getHasNode, getKeySetNode, getNode, hasAccessorFlag, hasActiveOverride, materializePB, notifyFold, notifyFoldTail, notifyKeyDiff, notifyKeyValue, runAuthoritative, snapshotNext, storeSetterNext, targetsEqual, unwrapValue, wrapNext };