@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
@@ -1,14 +1,16 @@
1
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, ext } from "../../core/core.js";
3
+ import { setSignal, isEqual, read, pendingCheckActive, latestReadActive, readNodeFast, READ_SLOW, signal, ext, setLatestReadActive, prepareComputed } from "../../core/core.js";
4
4
 
5
- import { projectionWriteActive, setProjectionWriteActive, schedule, setStoreCommitHook } from "../../core/scheduler.js";
5
+ import { projectionWriteActive, setProjectionWriteActive, activeTransition, schedule, currentTransition, setStoreCommitHook } from "../../core/scheduler.js";
6
6
 
7
7
  import { getObserver, getOwner } from "../../core/owner.js";
8
8
 
9
9
  import { $TARGET, isWrappable, markRawIngest, $PROXY, getWriteOverride, markRawOne, witnessAffectsMark, $TRACK, affectsScopesLive, inheritAffectsMarks, $AFFECTS, rawValuesUsed, isRawValue, setNextAffectsNodeResolver } from "../store.js";
10
10
 
11
- import { storeNextLookup, optHooks, ownedRaw, devAssertNeverUserMutation } from "./target.js";
11
+ import { storeNextLookup, optHooks, ownedRaw, markDescendants, devAssertNeverUserMutation } from "./target.js";
12
+
13
+ import { patchHooks, rowHooks } from "./patch-hooks.js";
12
14
 
13
15
  /**
14
16
  * Store rewrite — increment 2: plain deep stores with pending-backing writes.
@@ -38,8 +40,13 @@ import { storeNextLookup, optHooks, ownedRaw, devAssertNeverUserMutation } from
38
40
  * headroom for future fields. The prototype is reset to `Object.prototype`
39
41
  * so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
40
42
  * 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
+ * the array exotic class for `Array.isArray(proxy)`.
44
+ *
45
+ * ARRAY SHAPE RULE: arrays normalize their named properties to dictionary
46
+ * mode as the count grows (V8 13.x: counts ≡ 0 mod 3 from 18 up), so the
47
+ * target's named field count is capped at 20 — write-side patch-channel
48
+ * state lives inside the single `pc` extension (see target.ts), never as
49
+ * new named fields here. */
43
50
  function TargetShape() {
44
51
  this.v = undefined;
45
52
  this.ch = undefined;
@@ -60,11 +67,24 @@ function TargetShape() {
60
67
  this.s = undefined;
61
68
  this.ovl = undefined;
62
69
  this.del = undefined;
63
- this.wk = undefined;
70
+ this.pc = undefined;
71
+ this.hv = undefined;
72
+ this.ht = undefined;
64
73
  }
65
74
 
66
75
  TargetShape.prototype = Object.prototype;
67
76
 
77
+ /** Lazily allocate the patch-channel extension (one literal shape). */ function pcOf(e) {
78
+ return e.pc ?? (e.pc = {
79
+ sp: null,
80
+ p: null,
81
+ ro: null,
82
+ wk: null,
83
+ qa: null,
84
+ qe: null
85
+ });
86
+ }
87
+
68
88
  function createTarget(e, t, n, r = t?.fam ?? null) {
69
89
  // The proxy target carries the array exotic class when the value is an
70
90
  // array, so Array.isArray(proxy) is true; the fields live on it directly.
@@ -81,6 +101,7 @@ function createTarget(e, t, n, r = t?.fam ?? null) {
81
101
  i.h = null;
82
102
  i.k = null;
83
103
  i.dk = null;
104
+ i.pc = null;
84
105
  i.u = t;
85
106
  i.pk = n;
86
107
  i.px = null;
@@ -93,7 +114,8 @@ function createTarget(e, t, n, r = t?.fam ?? null) {
93
114
  i.s = false;
94
115
  i.ovl = false;
95
116
  i.del = null;
96
- i.wk = null;
117
+ i.hv = null;
118
+ i.ht = null;
97
119
  i.px = new Proxy(i, traps);
98
120
  // Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
99
121
  // proxy off looked-up targets as a field.
@@ -260,14 +282,6 @@ function getDeepNode(e) {
260
282
  if (e.dk !== null) setSignal(e.dk, 1);
261
283
  }
262
284
 
263
- function markDescendants(e) {
264
- let t = e;
265
- while (t && !t.d) {
266
- t.d = true;
267
- t = t.u;
268
- }
269
- }
270
-
271
285
  // ---------------------------------------------------------------------------
272
286
  // pending backing + fold (the single mutation point)
273
287
  /** target → committed backing at batch start (the fold diff's old side). */ const foldOlds = new Map;
@@ -291,6 +305,12 @@ function cloneRaw(e, t) {
291
305
  return Array.isArray(e) ? Object.defineProperties([], n) : Object.create(Object.getPrototypeOf(e), n);
292
306
  }
293
307
 
308
+ /** Scanned plainness for patch admission (patchableRaw): runs the one-time
309
+ * accessor scan if it hasn't happened yet — the sticky `a` flag alone is not
310
+ * trustworthy before a scan (it starts false and is discovered lazily). */ function targetIsPlain(e) {
311
+ return e.sc ? !e.a : scanAccessorsOnce(e);
312
+ }
313
+
294
314
  /** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
295
315
  * returns true when the container is plain data (overlay-safe). */ function scanAccessorsOnce(e) {
296
316
  const t = e.v;
@@ -330,6 +350,7 @@ function cloneRaw(e, t) {
330
350
  }
331
351
 
332
352
  function ensurePB(e) {
353
+ if (activeTransition !== null) foldBatches.set(e, activeTransition);
333
354
  let t = e.pb;
334
355
  if (t === null) {
335
356
  // Prototype-chain overlay (#3044): plain-data non-array containers
@@ -370,6 +391,25 @@ function ensurePB(e) {
370
391
  return t;
371
392
  }
372
393
 
394
+ /** Sentinel holder for `t.ht`: a latest()-pull staged this adoption outside
395
+ * any transition — the hold lasts until the fold commit (drainFolds). */ const PLAIN_HOLD = Symbol("plainHold");
396
+
397
+ /** True while a latest() read is pulling the projection computed up to date
398
+ * (see the get trap): adoptions landing during the pull are speculative
399
+ * against the un-flushed batch and stage a held view. (Not injectable — the
400
+ * derived createStore overload retains projection machinery in every store
401
+ * bundle, see treeshake.test.ts.) */ let latestPullActive = false;
402
+
403
+ /** Resolve the held committed view (#3074): answers the masked old backing
404
+ * while the hold is live, and lazily clears a hold whose transition has
405
+ * committed (transitions merge — resolve through currentTransition, same as
406
+ * foldHeld's node stamps). */ function heldMaskView(e) {
407
+ const t = e.ht;
408
+ if (t === null) return null;
409
+ if (t !== PLAIN_HOLD && currentTransition(t)?.fn === true) return e.ht = e.hv = null;
410
+ return e.hv;
411
+ }
412
+
373
413
  /**
374
414
  * Adoption (2026-08-16c): the incoming object becomes the committed backing
375
415
  * IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
@@ -385,6 +425,20 @@ function ensurePB(e) {
385
425
  queueFold(e);
386
426
  // records the pre-batch old before we swap
387
427
  e.adopted = true;
428
+ // #3074/#3075: a projection recompute deriving from uncommitted inputs
429
+ // swaps the backing SPECULATIVELY — committed-visibility readers must
430
+ // keep the pre-hold view until the hold resolves (a source held by a
431
+ // live transition, or a latest()-pull ahead of the flush). Post-await
432
+ // landings (write-override) stay immediately visible — landed truth —
433
+ // and clear any hold; optimistic families ride the lane machinery.
434
+ if (e.fam?.opt !== true) {
435
+ if (getWriteOverride()) {
436
+ e.ht = e.hv = null;
437
+ } else if (activeTransition !== null || latestPullActive) {
438
+ if (heldMaskView(e) === null) e.hv = e.v;
439
+ e.ht = activeTransition ?? PLAIN_HOLD;
440
+ }
441
+ }
388
442
  }
389
443
  e.pb = null;
390
444
  // Overlay and accessor-scan state describe the OUTGOING backing — a
@@ -396,28 +450,47 @@ function ensurePB(e) {
396
450
  // draft rescans once (#3044 audit follow-up).
397
451
  e.ovl = false;
398
452
  e.del = null;
399
- e.wk = null;
400
- // adoption supersedes any staged trap writes
401
- e.sc = false;
453
+ e.sc = false;
402
454
  e.a = false;
403
- e.v = t;
455
+ if (e.pc !== null) e.pc.wk = null;
456
+ // adoption supersedes staged trap writes
457
+ e.v = t;
404
458
  e.ch = t[$TARGET] !== undefined;
405
459
  (e.fam?.map ?? storeNextLookup).set(t, e);
406
460
  }
407
461
 
462
+ /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
463
+ * array length write implicitly deleted indices) — consumers full-scan. */ const WK_ALL = new Set;
464
+
465
+ const plainProto = e => {
466
+ const t = Object.getPrototypeOf(e);
467
+ return t === Object.prototype || t === Array.prototype || t === null;
468
+ };
469
+
408
470
  function queueFold(e) {
409
471
  if (foldOlds.has(e)) return;
410
- if (foldOlds.size === 0) {
411
- if (!hookInstalled) {
412
- hookInstalled = true;
413
- setStoreCommitHook(drainFolds);
414
- }
472
+ if (!hookInstalled) {
473
+ hookInstalled = true;
474
+ setStoreCommitHook(drainFolds);
475
+ }
476
+ // Always arm — "map non-empty ⇒ drain scheduled" is NOT an invariant: a
477
+ // held re-queue, or an incomplete-transition flush (which skips
478
+ // commitPendingNodes entirely), leaves entries behind after `scheduled`
479
+ // was consumed. A size-gated arm then strands every LATER fold — queued
480
+ // silently, never drained, committed base frozen at stale state while its
481
+ // nodes commit (#3089). schedule() early-returns when already armed.
415
482
  schedule();
416
- // once per batch — drain clears the map
417
- }
418
483
  foldOlds.set(e, e.v);
419
484
  }
420
485
 
486
+ /** Fold write-attribution (#3089): a draft written while a transition is
487
+ * active belongs to that transition — its fold must not commit before the
488
+ * transition settles. Observed keys already defer through the held check in
489
+ * drainFolds (their nodes carry _pendingValue); this write-time stamp is the
490
+ * equivalent hold for UNOBSERVED keys, which have no node to consult.
491
+ * Refreshed on every write; resolved through currentTransition at drain
492
+ * (transitions merge — same rule as heldMaskView). */ const foldBatches = new WeakMap;
493
+
421
494
  /** Committed-time privatization for parent-chain slot updates (path copying). */ function privatizeCommitted(e) {
422
495
  if (ownedRaw.has(e.v)) return;
423
496
  const t = cloneRaw(e.v, e);
@@ -437,29 +510,52 @@ function drainFolds() {
437
510
  const e = [ ...foldOlds ];
438
511
  foldOlds.clear();
439
512
  for (const [t, n] of e) {
513
+ // A latest()-pull staging holds only until the fold commit: this flush
514
+ // is committing the batch the pull ran ahead of. Transition holds stay —
515
+ // they clear when their transition is done (heldMaskView).
516
+ if (t.ht === PLAIN_HOLD) t.ht = t.hv = null;
517
+ // Eager (write-override) family folds swap pb -> v at notifyWrites'
518
+ // tail: by the time this drain runs they carry no pb, and their
519
+ // structural ops must emit at the fold-commit site below (the clone
520
+ // branch never sees them). Re-audit blocker 4.
521
+ const e = t.pb === null;
440
522
  if (t.pb !== null) {
523
+ // #3089: a fold written under a still-running transition defers to
524
+ // that transition's settle (the write-time stamp covers unobserved
525
+ // keys; observed keys also hit the pending-node held check below).
526
+ const e = foldBatches.get(t);
527
+ if (e !== undefined) {
528
+ if (currentTransition(e).fn === false) {
529
+ foldOlds.set(t, n);
530
+ continue;
531
+ }
532
+ foldBatches.delete(t);
533
+ }
441
534
  // Setter path: nodes were setSignal'd at setter exit (write-time
442
535
  // notification — transitions/holds ride core machinery). Commit the
443
536
  // backing only for keys whose nodes have committed; a still-pending
444
537
  // node (transition-held) re-queues the target for the settling flush.
445
- let e = false;
446
- const r = t.pb;
447
- const i = t.n;
448
- if (i !== null) {
538
+ let r = false;
539
+ const i = t.pb;
540
+ const o = t.n;
541
+ if (o !== null) {
449
542
  // Only written keys can hold (their nodes took the setSignal); the
450
543
  // wk bound keeps this O(written) — see notifyWrites. Same fallback
451
544
  // 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) {
455
- const n = i[t];
456
- if (n !== undefined && n.Pe !== NOT_PENDING) {
457
- e = true;
545
+ const e = t.pc !== null ? t.pc.wk : null;
546
+ const n = e === null || e === WK_ALL || t.a === true ||
547
+ // Overlay pbs chain to the COMMITTED object (#3044) — plainness is
548
+ // the committed container's prototype, not the overlay's.
549
+ !plainProto(t.ovl ? t.v : i) ? Reflect.ownKeys(o) : e;
550
+ for (const e of n) {
551
+ const t = o[e];
552
+ if (t !== undefined && t.Pe !== NOT_PENDING) {
553
+ r = true;
458
554
  break;
459
555
  }
460
556
  }
461
557
  }
462
- if (e) {
558
+ if (r) {
463
559
  foldOlds.set(t, n);
464
560
  // re-queue: commit happens when the hold settles
465
561
  continue;
@@ -474,30 +570,68 @@ function drainFolds() {
474
570
  // — the never-mutate-user-data contract holds.
475
571
  privatizeCommitted(t);
476
572
  const e = t.v;
477
- for (const t of Reflect.ownKeys(r)) {
478
- const n = Object.getOwnPropertyDescriptor(r, t);
573
+ for (const t of Reflect.ownKeys(i)) {
574
+ const n = Object.getOwnPropertyDescriptor(i, t);
479
575
  if (n.get || n.set || !n.enumerable || !n.writable || !n.configurable) Object.defineProperty(e, t, n); else e[t] = n.value;
480
576
  }
481
577
  if (t.del !== null) {
482
578
  for (const n of t.del) delete e[n];
483
579
  t.del = null;
484
580
  }
485
- (t.fam?.map ?? storeNextLookup).delete(r);
581
+ (t.fam?.map ?? storeNextLookup).delete(i);
486
582
  t.pb = null;
487
583
  t.ovl = false;
488
- t.wk = null;
584
+ if (t.pc !== null) t.pc.wk = null;
489
585
  // written-keys window closes with the fold commit
490
586
  } else {
491
- t.v = r;
587
+ // Setter-channel structural ops: a fold that changes an array's shape
588
+ // (push/splice/permutation through the setter — the reconcile walk
589
+ // never queues here) is a structural visibility transition for any
590
+ // registered list driver. Identity-keyed; aligned folds emit nothing.
591
+ // Family targets defer to their own adoption emission (fam reconcile).
592
+ // Arrays always fold on this clone branch (overlay is non-array only).
593
+ // Family setter drafts (writable projection push/splice through the
594
+ // masked setter) fold on this branch too and the fold IS their
595
+ // visibility moment — emit unless the structure already rode another
596
+ // channel: adoption folds (reconcile walk emitted ops) and
597
+ // optimistic families (lane-timed override channel). Re-audit
598
+ // blocker 4.
599
+ if (t.pc !== null && t.pc.ro !== null && !t.adopted && t.fam?.opt !== true && Array.isArray(i) && Array.isArray(t.v)) rowHooks.emitSetterRowOps(t, t.v, i);
600
+ t.v = i;
492
601
  t.ch = false;
493
602
  // pb is always a plain clone
494
603
  t.pb = null;
495
- t.wk = null;
604
+ if (t.pc !== null) t.pc.wk = null;
496
605
  // written-keys window closes with the fold commit
497
606
  }
498
607
  }
499
- if (t.v === n) continue;
500
- // adopted then re-adopted back, or no-op
608
+ if (t.v === n) {
609
+ // A no-op adoption (A -> B -> A before flush) still consumed its walk:
610
+ // clear the flag or every later setter row-op gate (!t.adopted) stays
611
+ // failed and a driven family list freezes (re-audit 5, P1-1).
612
+ t.adopted = false;
613
+ continue;
614
+ }
615
+ // Patch channel (fold-commit site): family targets emit HERE — the fold
616
+ // IS their visibility moment (held folds re-queued above emit when they
617
+ // actually commit) — and so do PLAIN fold-adopted targets (setter-
618
+ // returned root replacements, chained-store swaps: adoptions WITHOUT a
619
+ // reconcile walk, so no walk-site emission ever happened — re-audit 2,
620
+ // P1-2). Plain eager targets emitted at their walk/setter sites already.
621
+ if (t.pc !== null && (t.fam !== null || t.adopted)) {
622
+ // Structural ops for folds whose structure rode no other channel:
623
+ // eager-folded family SETTER drafts (write-override swaps pb -> v at
624
+ // notifyWrites' tail — the clone branch never sees them; adoption
625
+ // folds re-emitting would double the walk's ops) and PLAIN fold
626
+ // adoptions (no walk at all). Optimistic families ride the override
627
+ // channel (lane-timed ops + revert RESYNC) — never re-emit here.
628
+ if (t.pc.ro !== null && t.fam?.opt !== true && (t.fam !== null ? e && !t.adopted : t.adopted) && Array.isArray(t.v) && Array.isArray(n)) rowHooks.emitSetterRowOps(t, n, t.v);
629
+ if (t.pc.p !== null) {
630
+ // Accessor demotion at the fold-commit seam is DEV-ONLY (see the
631
+ // reconcile seam note: prod never pays per-adoption scans).
632
+ patchHooks.emitPatchLocal(t, t.v, n);
633
+ }
634
+ }
501
635
  // Path copying (CAS: see the eager-fold twin above).
502
636
  if (t.u && t.u.v[t.pk] === n) {
503
637
  privatizeCommitted(t.u);
@@ -518,19 +652,7 @@ function drainFolds() {
518
652
  * isPending, affects, and lane machinery ride the core natively (§3's
519
653
  * "pending home = the node when a node exists"). Unobserved keys stay in the
520
654
  * pending backing and fold directly at commit.
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) {
655
+ */ function notifyWrites(e) {
534
656
  let t = e.pb;
535
657
  if (t === null) return;
536
658
  // Optimistic channel: user writes on an optimistic family become node-level
@@ -567,9 +689,16 @@ function notifyWrites(e) {
567
689
  // not a full scan). Falls back to the full node scan when the bound can't
568
690
  // hold: no trap granularity (wk null), an array length write (WK_ALL —
569
691
  // 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;
692
+ // value can change when ANY key is written), or a non-plain prototype
693
+ // (class instances: prototype getters derive from arbitrary fields).
694
+ const i = e.pc !== null ? e.pc.wk : null;
695
+ // Overlay pbs chain to the COMMITTED object (#3044): a prototype-overlay
696
+ // draft is plain data on its own layer, but its getPrototypeOf is the
697
+ // committed container — judge plainness by the COMMITTED prototype or the
698
+ // bound never engages for overlay writes (every plain-object setter batch
699
+ // would full-scan: the exact selection-map workload wk exists for; jf
700
+ // `select` regressed 2x on this).
701
+ const o = i === WK_ALL || e.a === true || !plainProto(e.ovl ? e.v : t) ? null : i;
573
702
  if (r !== null) {
574
703
  const i = o ?? Reflect.ownKeys(r);
575
704
  for (const o of i) {
@@ -600,12 +729,16 @@ function notifyWrites(e) {
600
729
  }
601
730
  const f = e.h;
602
731
  if (f !== null) {
603
- for (const n of Reflect.ownKeys(f)) setSignal(f[n], n in t && !(e.del !== null && e.del.has(n)));
732
+ const n = o ?? Reflect.ownKeys(f);
733
+ for (const r of n) {
734
+ const n = f[r];
735
+ if (n !== undefined) setSignal(n, r in t && !(e.del !== null && e.del.has(r)));
736
+ }
604
737
  }
605
738
  // Deep-witness (dk): setter writes must notify a deep() subscriber even on
606
- // keys with no node. O(pb keys) equality only when a witness exists.
739
+ // keys with no node. O(written/pb keys) equality only when a witness exists.
607
740
  if (e.dk !== null) {
608
- if (e.del !== null && e.del.size !== 0) bumpDeep(e); else for (const r of Reflect.ownKeys(t)) {
741
+ if (e.del !== null && e.del.size !== 0) bumpDeep(e); else for (const r of o ?? Reflect.ownKeys(t)) {
609
742
  const i = t[r];
610
743
  const o = n[r];
611
744
  if (i !== null && typeof i === "object" ? !targetsEqual(o, i) : !isEqual(o, i)) {
@@ -632,6 +765,12 @@ function notifyWrites(e) {
632
765
  }
633
766
  if (r) setSignal(e.k, e => e + 1);
634
767
  }
768
+ // Patch channel (setter site): a committed write transitions this record —
769
+ // queue its patches and bubble to ancestors (targeted nested writes must
770
+ // reach the row patch, §4b). One number compare when no patches exist.
771
+ // Family targets skip this site: their visibility moment is the FOLD
772
+ // commit (drainFolds emits), not the recompute/draft write.
773
+ if (e.fam === null && patchHooks !== null && patchHooks.hasPatches()) patchHooks.emitPatch(e, t, n);
635
774
  // Projection backing folds split by channel (two pinned contracts):
636
775
  // - sync-derive drafts (recompute body): NEVER eager — a downstream async
637
776
  // hold can form LATER in the same flush and the leaf must stay at stale
@@ -643,11 +782,12 @@ function notifyWrites(e) {
643
782
  // downstream consumer's own async still holds the effect-level reveal
644
783
  // (spec-async "verdicts never inherit consumers' in-flight state").
645
784
  if (e.fam !== null && e.pb !== null && getWriteOverride()) {
785
+ // Landed truth (post-await write-override): immediately visible to every
786
+ // reader — any staged held view is superseded.
787
+ if (e.ht !== null) e.ht = e.hv = null;
646
788
  const n = e.v;
647
789
  e.pb = null;
648
- e.wk = null;
649
- // written-keys window closes with the eager fold
650
- e.v = t;
790
+ e.v = t;
651
791
  e.ch = false;
652
792
  if (e.u && e.u.v[e.pk] === n) {
653
793
  privatizeCommitted(e.u);
@@ -710,8 +850,8 @@ i = true) {
710
850
  return;
711
851
  }
712
852
  const f = i?.value;
713
- const u = o?.value;
714
- if (!isEqual(f, u) && !targetsEqual(f, u)) setSignal(e, typeof u === "function" ? () => u : u);
853
+ const l = o?.value;
854
+ if (!isEqual(f, l) && !targetsEqual(f, l)) setSignal(e, typeof l === "function" ? () => l : l);
715
855
  } else {
716
856
  const i = n[t];
717
857
  const o = r[t];
@@ -840,6 +980,17 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
840
980
  return t != null && !(t.T & CONFIG_CHILDREN_FORBIDDEN);
841
981
  }
842
982
 
983
+ /** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
984
+ * callbacks). Distinct from context-free: these scopes get committed
985
+ * visibility even against a projection's authoritative-elect pending
986
+ * backing (#3082) — parity with signals, where core read() serves
987
+ * committed to them regardless of staged writes. */ function inForbiddenScope() {
988
+ const e = getOwner();
989
+ if (e === null) return false;
990
+ const t = e.Ct ? e.Ot : e;
991
+ return t != null && !!(t.T & CONFIG_CHILDREN_FORBIDDEN);
992
+ }
993
+
843
994
  /** A pending fold is transition-held when any written node's parked value is
844
995
  * stamped by a live transition (a plain batch parking — the lazy-recompute
845
996
  * read case — has no transition stamp and serves fresh). */ function foldHeld(e) {
@@ -847,23 +998,33 @@ const UNSAFE_KEYS = new Set([ "__proto__", "prototype", "constructor" ]);
847
998
  if (t === null) return false;
848
999
  for (const e of Reflect.ownKeys(t)) {
849
1000
  const n = t[e];
850
- if (n.Pe !== NOT_PENDING && n._e != null && n._e.an !== true) return true;
1001
+ if (n.Pe !== NOT_PENDING && n._e != null && n._e.fn !== true) return true;
851
1002
  }
852
1003
  return false;
853
1004
  }
854
1005
 
855
1006
  function readSource(e) {
1007
+ // Held view first (#3074): an adoption staged under a live hold serves the
1008
+ // pre-hold committed backing to committed-visibility readers. Speculative
1009
+ // readers — drafts, write-override, owner-context computeds recomputing
1010
+ // inside the transaction, and latest() reads — see the adopted backing.
1011
+ if (e.ht !== null && !latestReadActive && !inDraft(e) && !getWriteOverride() && !inOwnerContext()) {
1012
+ const t = heldMaskView(e);
1013
+ if (t !== null) return t;
1014
+ }
856
1015
  // Signal-parity visibility (core read(): owner-context reads serve
857
1016
  // _pendingValue, context-free reads serve committed — effects recompute
858
1017
  // BEFORE commitPendingNodes in the flush, so the pending view must be
859
1018
  // servable). Drafts (setter window OR projection write-override) and
860
1019
  // owner-context reads see the pending backing; context-free reads see
861
1020
  // committed. Node reads apply the same rule, so both homes agree.
862
- if (e.pb !== null && (inDraft(e) || getWriteOverride() || inOwnerContext() ||
1021
+ if (e.pb !== null && (inDraft(e) || getWriteOverride() || inOwnerContext() ||
863
1022
  // A projection's pending backing is authoritative-elect: serve it to
864
1023
  // context-free readers too UNLESS a transition is holding the node
865
- // commits (downstream async hold — stale committed is the contract).
866
- e.fam !== null && !foldHeld(e))) return e.pb;
1024
+ // commits (downstream async hold — stale committed is the contract)
1025
+ // or the reader is a CHILDREN_FORBIDDEN scope, which never observes
1026
+ // its own unsettled write (#3082, signal parity per #3006).
1027
+ e.fam !== null && !foldHeld(e) && !inForbiddenScope())) return e.pb;
867
1028
  return e.v;
868
1029
  }
869
1030
 
@@ -906,7 +1067,9 @@ function isOwnAccessor(e, t) {
906
1067
  * reader that forced it (backing commits eagerly; node values fold at flush).
907
1068
  * FORCE sentinels never surface (they only bump subscribers of accessor
908
1069
  * keys, which are served by the trap, not the node). */ function nodeValue(e, t) {
909
- const n = hasActiveOverride(e) ? unwrapOverride(e.o?.De) : e.Pe !== NOT_PENDING && inOwnerContext() ? e.Pe : t;
1070
+ // latest() sees the in-flight parked value like an owner-context reader
1071
+ // does (#3075) — signal/memo parity for store-node-backed keys.
1072
+ const n = hasActiveOverride(e) ? unwrapOverride(e.o?.De) : e.Pe !== NOT_PENDING && (latestReadActive || inOwnerContext()) ? e.Pe : t;
910
1073
  return n === FORCE ? t : n;
911
1074
  }
912
1075
 
@@ -995,6 +1158,27 @@ function isOwnAccessor(e, t) {
995
1158
  if (t != null && t.S & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(t);
996
1159
  }
997
1160
 
1161
+ /** latest() pull (#3075): bring the projection computed up to date so the
1162
+ * read serves the IN-FLIGHT derivation — signal/memo parity, where core
1163
+ * read() routes latest() through a companion that recomputes speculatively.
1164
+ * The latest flag is suspended for the recompute (the derive's own reads
1165
+ * are normal reads), and latestPullActive marks any adoption it commits as
1166
+ * staged (see adoptPB) — the speculative swap must not leak to
1167
+ * committed-visibility readers before the flush. */ function pullProjectionForLatest(e) {
1168
+ const t = e.fam.node;
1169
+ if (t == null) return;
1170
+ const n = latestReadActive;
1171
+ setLatestReadActive(false);
1172
+ const r = latestPullActive;
1173
+ latestPullActive = true;
1174
+ try {
1175
+ prepareComputed(t, true);
1176
+ } finally {
1177
+ latestPullActive = r;
1178
+ setLatestReadActive(n);
1179
+ }
1180
+ }
1181
+
998
1182
  const traps = {
999
1183
  get(e, t, n) {
1000
1184
  // One typeof gates every brand-symbol compare off the hot string path
@@ -1022,6 +1206,10 @@ const traps = {
1022
1206
  }
1023
1207
  if (pendingCheckActive) witnessAffectsMark(e, t);
1024
1208
  if (e.fam !== null && getObserver() === null && !inDraft(e)) firewallGate(e);
1209
+ // latest() pull (#3075): store traps never reach core read() without an
1210
+ // observer, so bring the projection computed up to date here — signal/
1211
+ // memo parity for latest() reads through a projection.
1212
+ if (e.fam !== null && latestReadActive && !inDraft(e) && !getWriteOverride()) pullProjectionForLatest(e);
1025
1213
  const r = readSource(e);
1026
1214
  // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
1027
1215
  // deleted keys are tracked aside and read as absent in the pending view.
@@ -1216,13 +1404,14 @@ const traps = {
1216
1404
  // Array length writes implicitly delete indices — the written-keys bound
1217
1405
  // can't see them, so poison to the full scan for this batch. Index
1218
1406
  // 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");
1407
+ const l = pcOf(e);
1408
+ if (Array.isArray(f)) {
1409
+ if (t === "length") l.wk = WK_ALL; else if (l.wk !== WK_ALL) {
1410
+ const e = l.wk ??= new Set;
1411
+ e.add(t);
1412
+ e.add("length");
1224
1413
  }
1225
- } else if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
1414
+ } else if (l.wk !== WK_ALL) (l.wk ??= new Set).add(t);
1226
1415
  // Own data keys literally named "prototype"/"constructor" land as data —
1227
1416
  // defineProperty sidesteps a proto-chain setter named the same.
1228
1417
  if (UNSAFE_KEYS.has(t)) {
@@ -1260,7 +1449,14 @@ const traps = {
1260
1449
  const i = !r && getWriteOverride();
1261
1450
  if (!r && !i) return true;
1262
1451
  if (t === "__proto__") return true;
1263
- if (n.get || n.set) e.a = true;
1452
+ if (n.get || n.set) {
1453
+ e.a = true;
1454
+ // Accessor demotion (re-audit blocker 3): a record that acquires an
1455
+ // accessor after patch registration stops being patchable — pull its
1456
+ // patches and re-drive them as tracked effect fallbacks. Hooks are
1457
+ // installed whenever pc.p exists (registration installs them).
1458
+ if (e.pc !== null && e.pc.p !== null) patchHooks.demoteToEffects(e);
1459
+ }
1264
1460
  // Unwrap before ensurePB (see the set trap: self-reference materializes).
1265
1461
  if ("value" in n) n = {
1266
1462
  ...n,
@@ -1268,7 +1464,8 @@ const traps = {
1268
1464
  };
1269
1465
  const o = ensurePB(e);
1270
1466
  pendingNotify.add(e);
1271
- if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
1467
+ const f = pcOf(e);
1468
+ if (f.wk !== WK_ALL) (f.wk ??= new Set).add(t);
1272
1469
  Object.defineProperty(o, t, n);
1273
1470
  if (e.del !== null) e.del.delete(t);
1274
1471
  if (i) notifyWrites(e);
@@ -1280,7 +1477,8 @@ const traps = {
1280
1477
  if (!n && !r) return true;
1281
1478
  const i = ensurePB(e);
1282
1479
  pendingNotify.add(e);
1283
- if (e.wk !== WK_ALL) (e.wk ??= new Set).add(t);
1480
+ const o = pcOf(e);
1481
+ if (o.wk !== WK_ALL) (o.wk ??= new Set).add(t);
1284
1482
  delete i[t];
1285
1483
  // A prototype overlay cannot shadow a delete of a committed key —
1286
1484
  // record it aside (#3044); reads/has/ownKeys/commit consult the set.
@@ -1344,8 +1542,39 @@ function createStoreNext(e, t = false) {
1344
1542
  return [ n, setter ];
1345
1543
  }
1346
1544
 
1545
+ /** True when `proxy` is a SHALLOW store (children served verbatim, slots
1546
+ * replaced by reference — #2932). The list driver uses this to choose the
1547
+ * slot-patch channel (collected row bodies) over per-record registration. */ function storeIsShallow(e) {
1548
+ const t = e?.[$TARGET];
1549
+ return t !== undefined && t.s === true;
1550
+ }
1551
+
1552
+ /** True when `proxy` belongs to a projection/optimistic FAMILY. The list
1553
+ * driver must DECLINE family arrays (external audit finding): family
1554
+ * structural changes never emit row/slot ops (the setter channel is
1555
+ * fam-gated; optimistic writes ride node overrides), and the proxy identity
1556
+ * is stable so the each-watch cannot catch the change either — an engaged
1557
+ * list would freeze on optimistic/projection structural updates. Record-
1558
+ * level family patches are unaffected (they have their own emission). */ function storeHasFamily(e) {
1559
+ const t = e?.[$TARGET];
1560
+ return t !== undefined && t.fam !== null;
1561
+ }
1562
+
1563
+ /** True when `proxy` belongs to an OPTIMISTIC family specifically. The list
1564
+ * driver declines these (audit finding, narrowed): optimistic user writes
1565
+ * ride node-level overrides — they never enter the reconcile walk, so no
1566
+ * row/slot ops are emitted and an engaged list would freeze on optimistic
1567
+ * structural changes. PROJECTION (non-optimistic) families are drivable:
1568
+ * their recomputes go through the reconcile walk, whose emissions are
1569
+ * transition-stamped in the apply queue like any other (equivalence-matrix
1570
+ * gated). Re-admitting optimistic families requires a lane-timed structural
1571
+ * emission mirroring emitPatchOptimistic, plus revert resync. */ function storeHasOptimisticFamily(e) {
1572
+ const t = e?.[$TARGET];
1573
+ return t !== undefined && t.fam?.opt === true;
1574
+ }
1575
+
1347
1576
  /** Tracking deep snapshot (`deep()` for next targets): subscribes to the
1348
- * key-set and every property node at every reachable level, then returns the
1577
+ * key-set and deep-witness node at every reachable level, then returns the
1349
1578
  * plain view. Shared references and cycles handled via the visited set. */ function deepNext(e) {
1350
1579
  const t = e?.[$TARGET];
1351
1580
  if (t === undefined || t.px !== e) return e;
@@ -1465,11 +1694,11 @@ function snapshotWalk(e, t, n) {
1465
1694
  Object.defineProperty(i, o, f);
1466
1695
  continue;
1467
1696
  }
1468
- const u = f.value;
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, {
1697
+ const l = f.value;
1698
+ const u = l !== null && typeof l === "object" ? snapshotWalk(l, t, n) : l;
1699
+ if (f.enumerable && f.writable && f.configurable) i[o] = u; else Object.defineProperty(i, o, {
1471
1700
  ...f,
1472
- value: l
1701
+ value: u
1473
1702
  });
1474
1703
  }
1475
1704
  if (e && i.length !== r.length) i.length = r.length;
@@ -1484,16 +1713,16 @@ function snapshotWalk(e, t, n) {
1484
1713
  if (!i || i.get || i.set) continue;
1485
1714
  const o = i.value;
1486
1715
  if (o === null || typeof o !== "object") continue;
1487
- const u = snapshotWalk(o, t, n);
1488
- if (u !== o) {
1716
+ const l = snapshotWalk(o, t, n);
1717
+ if (l !== o) {
1489
1718
  if (f === null) {
1490
1719
  f = Array.isArray(r) ? [ ...r ] : Object.create(Object.getPrototypeOf(r), Object.getOwnPropertyDescriptors(r));
1491
1720
  t.set(r, f);
1492
1721
  }
1493
- f[e] = u;
1722
+ f[e] = l;
1494
1723
  }
1495
1724
  }
1496
1725
  return f ?? r;
1497
1726
  }
1498
1727
 
1499
- export { adoptPB, bumpDeep, createStoreNext, deepNext, getHasNode, getKeySetNode, getNode, hasAccessorFlag, hasActiveOverride, materializePB, notifyFold, notifyFoldTail, notifyKeyDiff, notifyKeyValue, runAuthoritative, snapshotNext, storeSetterNext, targetsEqual, unwrapValue, wrapNext };
1728
+ export { adoptPB, bumpDeep, createStoreNext, deepNext, getHasNode, getKeySetNode, getNode, hasAccessorFlag, hasActiveOverride, materializePB, notifyFold, notifyFoldTail, notifyKeyDiff, notifyKeyValue, pcOf, runAuthoritative, snapshotNext, storeHasFamily, storeHasOptimisticFamily, storeIsShallow, storeSetterNext, targetIsPlain, targetsEqual, unwrapValue, wrapNext };