@solidjs/signals 2.0.0-rc.18 → 2.0.0-rc.19

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 (65) hide show
  1. package/dist/dev-shared.js +168 -88
  2. package/dist/dev.attribution.js +16 -13
  3. package/dist/dev.js +272 -129
  4. package/dist/observe/boundaries.js +88 -67
  5. package/dist/observe/core/action.js +8 -0
  6. package/dist/observe/core/async.js +46 -34
  7. package/dist/observe/core/attribution.js +30 -28
  8. package/dist/observe/core/constants.js +21 -1
  9. package/dist/observe/core/core.js +164 -136
  10. package/dist/observe/core/effect.js +6 -6
  11. package/dist/observe/core/graph.js +3 -3
  12. package/dist/observe/core/heap.js +5 -5
  13. package/dist/observe/core/lanes.js +104 -84
  14. package/dist/observe/core/owner.js +10 -9
  15. package/dist/observe/core/scheduler.js +38 -35
  16. package/dist/observe/core/truth.js +6 -6
  17. package/dist/observe/core/verdict.js +13 -13
  18. package/dist/observe/deferred.js +39 -40
  19. package/dist/observe/index.js +2 -2
  20. package/dist/observe/map.js +22 -22
  21. package/dist/observe/reveal.js +5 -5
  22. package/dist/observe/signals.js +57 -15
  23. package/dist/observe/store/affects.js +1 -1
  24. package/dist/observe/store/hooks.js +3 -0
  25. package/dist/observe/store/optimistic.js +11 -11
  26. package/dist/observe/store/projection.js +34 -21
  27. package/dist/observe/store/reconcile.js +2 -2
  28. package/dist/observe/store/store.js +63 -53
  29. package/dist/observe/store/utils.js +7 -2
  30. package/dist/prod/boundaries.js +153 -132
  31. package/dist/prod/core/action.js +8 -0
  32. package/dist/prod/core/async.js +55 -43
  33. package/dist/prod/core/constants.js +14 -1
  34. package/dist/prod/core/core.js +162 -134
  35. package/dist/prod/core/effect.js +9 -9
  36. package/dist/prod/core/graph.js +3 -3
  37. package/dist/prod/core/heap.js +12 -12
  38. package/dist/prod/core/lanes.js +98 -78
  39. package/dist/prod/core/owner.js +15 -14
  40. package/dist/prod/core/scheduler.js +43 -40
  41. package/dist/prod/core/truth.js +6 -6
  42. package/dist/prod/core/verdict.js +17 -17
  43. package/dist/prod/deferred.js +39 -40
  44. package/dist/prod/index.js +2 -2
  45. package/dist/prod/map.js +14 -14
  46. package/dist/prod/reveal.js +8 -8
  47. package/dist/prod/signals.js +46 -4
  48. package/dist/prod/store/affects.js +1 -1
  49. package/dist/prod/store/hooks.js +3 -0
  50. package/dist/prod/store/optimistic.js +16 -16
  51. package/dist/prod/store/projection.js +22 -9
  52. package/dist/prod/store/reconcile.js +2 -2
  53. package/dist/prod/store/store.js +262 -262
  54. package/dist/prod/store/utils.js +2 -0
  55. package/dist/types/core/action.d.ts +8 -0
  56. package/dist/types/core/constants.d.ts +20 -0
  57. package/dist/types/core/core.d.ts +14 -1
  58. package/dist/types/core/dev.d.ts +1 -1
  59. package/dist/types/core/index.d.ts +1 -1
  60. package/dist/types/core/lanes.d.ts +5 -1
  61. package/dist/types/core/scheduler.d.ts +2 -0
  62. package/dist/types/index.d.ts +3 -1
  63. package/dist/types/signals.d.ts +37 -0
  64. package/dist/types/store/target.d.ts +2 -0
  65. package/package.json +1 -1
package/dist/dev.js CHANGED
@@ -33,8 +33,8 @@ import {
33
33
  A as tracking,
34
34
  B as CONFIG_CHILDREN_FORBIDDEN,
35
35
  D as passLane,
36
- E as STATUS_PENDING,
37
- F as EFFECT_RENDER,
36
+ E as EFFECT_RENDER,
37
+ F as setPassLane,
38
38
  H as sameLane,
39
39
  I as flushTransaction,
40
40
  J as REACTIVE_SCREEN_READ,
@@ -44,15 +44,15 @@ import {
44
44
  O as REACTIVE_IN_HEAP,
45
45
  P as REACTIVE_DIRTY,
46
46
  Q as REACTIVE_CHECK,
47
- T as CONFIG_INPUTS_PUBLISHED,
48
- U as releaseQueues,
49
- V as CONFIG_SLOT_NODE,
50
- W as holdNode,
51
- X as joinFuture,
52
- Y as reruns,
53
- Z as CONFIG_STAGED,
54
- _ as merge$1,
55
- $ as setPassLane,
47
+ T as STATUS_PENDING,
48
+ U as CONFIG_INPUTS_PUBLISHED,
49
+ V as releaseQueues,
50
+ W as CONFIG_SLOT_NODE,
51
+ X as holdNode,
52
+ Y as joinFuture,
53
+ Z as reruns,
54
+ _ as CONFIG_STAGED,
55
+ $ as merge$1,
56
56
  a0 as REACTIVE_JOINED,
57
57
  a1 as REACTIVE_STAGED_READ,
58
58
  a2 as stagedRead,
@@ -161,31 +161,33 @@ import {
161
161
  bF as joinPassAt,
162
162
  bG as notifyEpoch,
163
163
  bH as SUPPORTS_PROXY,
164
- bI as releaseMarksHook,
165
- bJ as installMarkHooks,
166
- bK as CONFIG_LAGS,
167
- bL as REACTIVE_REASK,
168
- bM as onSupersede,
169
- bN as haltReactivity,
170
- bO as heldUnder,
171
- bP as unhold,
172
- bQ as laneStageHook,
173
- bR as REACTIVE_ZOMBIE,
174
- bS as installBoundaryHooks,
175
- bT as CAUGHT_REARMED,
176
- bU as OBSERVE$1
164
+ bI as CONFIG_KEY_BAG,
165
+ bJ as releaseMarksHook,
166
+ bK as installMarkHooks,
167
+ bL as CONFIG_LAGS,
168
+ bM as REACTIVE_REASK,
169
+ bN as onSupersede,
170
+ bO as haltReactivity,
171
+ bP as CONFIG_FRAME,
172
+ bQ as heldUnder,
173
+ bR as unhold,
174
+ bS as laneStageHook,
175
+ bT as REACTIVE_ZOMBIE,
176
+ bU as installBoundaryHooks,
177
+ bV as CAUGHT_REARMED,
178
+ bW as OBSERVE$1
177
179
  } from "./dev-shared.js";
178
180
  export {
179
- bV as clearSnapshots,
180
- bW as enforceLoadingBoundary,
181
- bX as getNextChildId,
182
- bY as markSnapshotScope,
183
- bZ as peekNextChildId,
184
- b_ as releaseSnapshotScope,
185
- b$ as resetErrorHalt,
186
- c0 as setConsoleFooter,
187
- c1 as setSnapshotCapture,
188
- c2 as setWriteHook
181
+ bX as clearSnapshots,
182
+ bY as enforceLoadingBoundary,
183
+ bZ as getNextChildId,
184
+ b_ as markSnapshotScope,
185
+ b$ as peekNextChildId,
186
+ c0 as releaseSnapshotScope,
187
+ c1 as resetErrorHalt,
188
+ c2 as setConsoleFooter,
189
+ c3 as setSnapshotCapture,
190
+ c4 as setWriteHook
189
191
  } from "./dev-shared.js";
190
192
 
191
193
  // Truth reads (`until`, `refresh`; CONFIG_AUTHORITATIVE) over lane nodes.
@@ -700,7 +702,11 @@ function dissolveLane(l, into, except) {
700
702
  * has shown, or one the seam has not judged yet (its run waits on the
701
703
  * verdict: released if the lane shows, held if it blocks) — and sees the
702
704
  * lane's latest. NOT_PENDING falls through: lane work's read of a pending
703
- * member throws like any. */
705
+ * member throws like any. A verdict reader entering a lane the seam has
706
+ * not judged reads ahead of it like a staging read: if the seam holds the
707
+ * lane, the pass re-derives on the screen (`entered`, #4284) — `latest()`
708
+ * beside the lane's nodes tears ahead of the hold however the reads are
709
+ * grouped into effects. */
704
710
  function laneRead(c, el) {
705
711
  const guess = el._config & CONFIG_GUESS;
706
712
  const l = txOf(el);
@@ -718,6 +724,7 @@ function laneRead(c, el) {
718
724
  // An errored member's lane value predates its error; the error is the answer (#4106).
719
725
  if (!guess && status & STATUS_ERROR) return NOT_PENDING;
720
726
  if (c === null) return laneValueOf(el);
727
+ const pending = !guess && status & STATUS_PENDING;
721
728
  if (!tracking || c._config & CONFIG_CHILDREN_FORBIDDEN) {
722
729
  // An untracked read inside lane work derives from the lane like a
723
730
  // tracked one, minus the subscription — `untrack` is about
@@ -726,40 +733,47 @@ function laneRead(c, el) {
726
733
  // lanes read by one pass link, as for tracked reads). Outside lane
727
734
  // work, the screen.
728
735
  if (passLane === null || c._config & CONFIG_CHILDREN_FORBIDDEN) return display(el);
729
- enterLane(l, c);
730
- if (!guess && status & STATUS_PENDING) return NOT_PENDING;
731
- return laneValueOf(el);
732
- }
733
- if (c._type === EFFECT_RENDER && (passLane === null || !sameLane(passLane, l))) {
734
- if (!guess) {
735
- // A flight whose value has never shown: nothing to show — the leaf
736
- // waits on it, and its own frame holds (A15, #3334), not the lane's
737
- // parent. The landing lands the frame: its own pass (the flush
738
- // joined its transaction) reads the answer it waited on — the lane's
739
- // staging, revealed at this seam.
740
- if (status & STATUS_PENDING && el._x._lane === NOT_PENDING) return NOT_PENDING;
736
+ } else if (c._type === EFFECT_RENDER) {
737
+ // A flight whose value has never shown: nothing to show — the leaf
738
+ // waits on it, and its own frame holds (A15, #3334), not the lane's
739
+ // parent. The landing lands the frame: its own pass (the flush joined
740
+ // its transaction) reads the answer it waited on — the lane's staging,
741
+ // revealed at this seam.
742
+ const never = pending && el._x._lane === NOT_PENDING;
743
+ // D-28: a leaf a verdict read moved into the lane still throws on such
744
+ // a flight — the pass leaves the lane, and its frame holds as if the
745
+ // read had come first (S-39, #4275). Nor does a verdict lane's seat carry
746
+ // the leaf into another lane (no link): it reads that lane as from the
747
+ // frame's seat (#4284).
748
+ if ((never || passLane !== l) && passLane?._parent._verdict === passLane) setPassLane(null);
749
+ if (passLane === null || !sameLane(passLane, l)) {
750
+ // (A lane that has never shown: the screen is the committed value.)
751
+ if (never && l._shown) return NOT_PENDING;
741
752
  if (
753
+ !guess &&
742
754
  el._pendingValue !== NOT_PENDING &&
743
755
  c._config & CONFIG_HELD &&
744
756
  flushTransaction !== null &&
745
757
  resolveTx(flushTransaction) === txOf(c)
746
758
  )
747
759
  return el._pendingValue;
748
- }
749
- if (l._held || (!guess && status & STATUS_PENDING)) {
750
- if (!guess && !l._shown && status & STATUS_UNINITIALIZED) throw new NotReadyError(null);
751
- // Re-derived at the reveal; once per pass (the pass may also be a stale
752
- // reader of a transaction, REACTIVE_FRAME_READ — both reruns apply).
753
- if (!(c._flags & REACTIVE_SCREEN_READ)) {
754
- c._flags |= REACTIVE_FRAME_READ | REACTIVE_SCREEN_READ;
755
- l._reruns.push(c);
760
+ if (l._held || pending) {
761
+ if (!guess && !l._shown && status & STATUS_UNINITIALIZED) throw new NotReadyError(null);
762
+ // Re-derived at the reveal; once per pass (the pass may also be a stale
763
+ // reader of a transaction, REACTIVE_FRAME_READ — both reruns apply).
764
+ if (!(c._flags & REACTIVE_SCREEN_READ)) {
765
+ c._flags |= REACTIVE_FRAME_READ | REACTIVE_SCREEN_READ;
766
+ l._reruns.push(c);
767
+ }
768
+ return display(el);
756
769
  }
757
- return display(el);
770
+ // A verdict reader reads ahead of the seam's verdict: re-derived on the
771
+ // screen if the lane holds (#4284).
772
+ if (c._config & CONFIG_VERDICT && l._parent._verdict !== l) entered.push(c);
758
773
  }
759
774
  }
760
775
  enterLane(l, c);
761
- if (!guess && status & STATUS_PENDING) return NOT_PENDING;
762
- return laneValueOf(el);
776
+ return pending ? NOT_PENDING : laneValueOf(el);
763
777
  }
764
778
  /** The value a guess covered: the node's committed value — a store slot's
765
779
  * asked of the store (a chained link follows the inner store's commits). */
@@ -992,7 +1006,17 @@ const laneSeams = leaks => {
992
1006
  }
993
1007
  for (let k = Math.min(judged, lanes.length) - 1; k >= 0; k--)
994
1008
  laneSeam(lanes[k], leaked.indexOf(lanes[k]) !== -1);
1009
+ // A verdict reader that entered a lane this seam held: its run waits with
1010
+ // the lane, and the verdict beside it would freeze — it re-derives next
1011
+ // round from the frame's seat, a stale reader of the lane (#4284).
1012
+ for (let r; (r = entered.pop()); )
1013
+ if (r._x._transaction?._held) {
1014
+ r._flags &= ~REACTIVE_LANE_DIRTY;
1015
+ enqueueSub(r);
1016
+ }
995
1017
  };
1018
+ /** Render effects that read a verdict and entered a lane before its seam (#4284). */
1019
+ const entered = [];
996
1020
  installLaneHooks(
997
1021
  laneRead,
998
1022
  laneStage,
@@ -1510,6 +1534,14 @@ function inTransaction(t, q, fn) {
1510
1534
  * yield until(() => todos.some(t => t.id === saved.id));
1511
1535
  * ```
1512
1536
  *
1537
+ * The same holds inside an async helper the action yields
1538
+ * (`yield (async () => { ... })()`): everything after its first `await` runs
1539
+ * outside the transaction. An `await refresh(x)` or `await until(...)` there
1540
+ * waits for this action's commit, which waits on the helper, so it hangs
1541
+ * whenever `x` re-ran during the action (#4263) and works otherwise. Call
1542
+ * `refresh(x)` in the generator once the helper returns; `yield` it only when
1543
+ * the action needs the fresh value.
1544
+ *
1513
1545
  * (For the same reason, don't call `flush()` inside an action body — it
1514
1546
  * cannot reveal the held writes and is refused.)
1515
1547
  *
@@ -3093,6 +3125,65 @@ function onSettled(callback) {
3093
3125
  }
3094
3126
  });
3095
3127
  }
3128
+ /**
3129
+ * Schedules `callback` to run **once**, right after the pending flush
3130
+ * finishes, in the same microtask checkpoint (before the browser paints).
3131
+ * Unlike `onSettled`, it never waits on async work: if that flush is held by
3132
+ * a pending transition, the callback still runs.
3133
+ *
3134
+ * Its main use is keeping a write out of another update's hold. Writes made
3135
+ * in the same synchronous frame are applied together, so when one of them
3136
+ * starts async work (a route change, a fetch), the others are held with it
3137
+ * until it resolves. A write made in `onFlush` happens after that flush, so
3138
+ * it is its own update: it commits on its own unless something it changes
3139
+ * also derives from the held work (a memo or `createEffect` reading both),
3140
+ * in which case it joins that hold as any later write would.
3141
+ *
3142
+ * Call it from event handlers and effect callbacks (including
3143
+ * `createTrackedEffect` and `onSettled` bodies). In an owned scope such as a
3144
+ * component body or a computation it warns in dev: use `onSettled` there.
3145
+ *
3146
+ * - A write in the callback lands after any write made inline in the same
3147
+ * frame. Don't write the same signal both inside and outside it; use a
3148
+ * functional setter if the order matters.
3149
+ * - Several `onFlush` callbacks run in call order and batch into one flush.
3150
+ * - `queueMicrotask` is not a substitute: called before the frame's first
3151
+ * write, it runs before that flush and its write joins it.
3152
+ *
3153
+ * @example
3154
+ * ```tsx
3155
+ * // A menu closes now even when the consumer's onSelect starts async work.
3156
+ * const select = (id: string) => {
3157
+ * onFlush(() => setOpen(false));
3158
+ * props.onSelect?.(id);
3159
+ * };
3160
+ * ```
3161
+ *
3162
+ * @param callback Function to run once after the pending flush
3163
+ */
3164
+ function onFlush(callback) {
3165
+ {
3166
+ const owner = getOwner();
3167
+ if (owner && !(owner._config & CONFIG_CHILDREN_FORBIDDEN)) {
3168
+ const message =
3169
+ "[ON_FLUSH_IN_OWNED_SCOPE] onFlush called in an owned scope (a component body or a computation). It is meant for event handlers and effect callbacks; to run work after this scope's first stable render, use onSettled.";
3170
+ reportDiagnostic(
3171
+ emitDiagnostic({
3172
+ code: "ON_FLUSH_IN_OWNED_SCOPE",
3173
+ kind: "lifecycle",
3174
+ severity: "warn",
3175
+ message,
3176
+ ownerId: owner.id,
3177
+ ownerName: owner._name
3178
+ })
3179
+ );
3180
+ }
3181
+ }
3182
+ // The frame's flush is queued (or already draining) ahead of this
3183
+ // microtask, so the callback's writes start a flush of their own.
3184
+ schedule();
3185
+ queueMicrotask(callback);
3186
+ }
3096
3187
 
3097
3188
  function mapArray(list, map, options) {
3098
3189
  const keyFn = typeof options?.keyed === "function" ? options.keyed : undefined;
@@ -3721,6 +3812,9 @@ function compare(key, a, b) {
3721
3812
  return key ? key(a) === key(b) : true;
3722
3813
  }
3723
3814
 
3815
+ // Set before any family is optimistic (`fam.opt`): a call site checks the
3816
+ // hooks first, so a build that does not import optimistic.ts folds the arm
3817
+ // away.
3724
3818
  let optHooks = null;
3725
3819
  function installOptHooks(hooks) {
3726
3820
  optHooks = hooks;
@@ -4272,7 +4366,7 @@ function ensurePB(target) {
4272
4366
  // An optimistic edit composes on the view the user saw and becomes
4273
4367
  // guesses at the setter's exit: nothing is staged, nothing folds
4274
4368
  // (optimistic.ts).
4275
- if (userWrite && target.fam?.opt === true && !pendingNotify.has(target))
4369
+ if (optHooks && userWrite && target.fam?.opt === true && !pendingNotify.has(target))
4276
4370
  return (target.pb = optHooks.draft(target));
4277
4371
  if (pb !== null && !isOwned(pb)) {
4278
4372
  // An adopted object awaiting its fold is the user's: the draft writes a
@@ -4552,9 +4646,9 @@ function foldTarget(t, old) {
4552
4646
  else delete v[key];
4553
4647
  }
4554
4648
  } else {
4555
- for (const key of Reflect.ownKeys(pb)) copyOwn(v, pb, key);
4649
+ for (const key of Reflect.ownKeys(pb)) if (key !== $OWNER) copyOwn(v, pb, key);
4556
4650
  for (const key of Reflect.ownKeys(old)) {
4557
- if (!hasOwn$1.call(pb, key)) delete v[key];
4651
+ if (key !== $OWNER && !hasOwn$1.call(pb, key)) delete v[key];
4558
4652
  }
4559
4653
  }
4560
4654
  } else {
@@ -4604,6 +4698,9 @@ function reportReplacedContainers(t, old, pb, writtenKeys) {
4604
4698
  const keys = writtenKeys ?? Reflect.ownKeys(pb);
4605
4699
  const isArray = Array.isArray(pb);
4606
4700
  const owner = storeRootOwner(t);
4701
+ // Items the array held before the write: an index that now holds one of
4702
+ // them was moved into place (an in-place splice / shift), not replaced.
4703
+ let oldArrayItems = null;
4607
4704
  for (const key of keys) {
4608
4705
  if ((isArray && key === "length") || key === $OWNER) continue;
4609
4706
  if (t.del?.has(key)) continue;
@@ -4618,6 +4715,13 @@ function reportReplacedContainers(t, old, pb, writtenKeys) {
4618
4715
  targetsEqual(ov, nv)
4619
4716
  )
4620
4717
  continue;
4718
+ if (isArray) {
4719
+ if (oldArrayItems === null) {
4720
+ oldArrayItems = new Set();
4721
+ for (const item of old) oldArrayItems.add(unwrapValue(item));
4722
+ }
4723
+ if (oldArrayItems.has(nv)) continue;
4724
+ }
4621
4725
  const isArr = Array.isArray(nv);
4622
4726
  if (isArr !== Array.isArray(ov)) continue;
4623
4727
  let total;
@@ -4679,7 +4783,7 @@ function notifyWrites(t) {
4679
4783
  // Optimistic channel: a user's writes on an optimistic family become
4680
4784
  // guesses on the nodes (lanes); the draft is discarded, the committed
4681
4785
  // backing never touched.
4682
- if (userWrite && t.fam?.opt === true) {
4786
+ if (optHooks && userWrite && t.fam?.opt === true) {
4683
4787
  // The draft is discarded first: the diff reads the target (its nodes
4684
4788
  // are born from the committed frame, not the draft).
4685
4789
  t.pb = null;
@@ -5353,10 +5457,10 @@ function enumerationSource(target) {
5353
5457
  // A container's arrangement guess is not a backing: the view composes
5354
5458
  // over the base it is shown over.
5355
5459
  const base = asBacking(read(getContainerNode(target)));
5356
- return target.fam?.opt === true ? optHooks.view(target, base) : base;
5460
+ return optHooks && target.fam?.opt === true ? optHooks.view(target, base) : base;
5357
5461
  }
5358
5462
  const src = readSource(target);
5359
- return optRead(target) ? optHooks.view(target, src) : src;
5463
+ return optHooks && optRead(target) ? optHooks.view(target, src) : src;
5360
5464
  }
5361
5465
  /** A read on an optimistic family that composes the lanes' values: not the
5362
5466
  * draft's (it composed already), not a derive's write-override read (the
@@ -5616,7 +5720,7 @@ const traps = {
5616
5720
  node._value = present; // the link follows the inner store (S4)
5617
5721
  if (node._config & CONFIG_OVERRIDE) return !!nv;
5618
5722
  return present;
5619
- } else if (optRead(target)) {
5723
+ } else if (optHooks && optRead(target)) {
5620
5724
  const guessed = optHooks.has(target, key);
5621
5725
  if (guessed !== undefined) return guessed;
5622
5726
  }
@@ -5656,7 +5760,7 @@ const traps = {
5656
5760
  return undefined;
5657
5761
  } else src = readSource(target, key, true);
5658
5762
  let desc;
5659
- if (!enumerating && optRead(target)) {
5763
+ if (optHooks && !enumerating && optRead(target)) {
5660
5764
  const guessed = optHooks.descriptor(target, key);
5661
5765
  if (guessed === null) return undefined;
5662
5766
  desc = guessed;
@@ -5850,7 +5954,7 @@ function storeSetter(proxy, fn, guard = true) {
5850
5954
  // A returned replacement: on an optimistic family a user's is itself an
5851
5955
  // optimistic edit — diffed against the view as guesses (`next` parity);
5852
5956
  // otherwise it is adopted as the incoming truth.
5853
- if (guard && target.fam?.opt === true && !writeOverride)
5957
+ if (optHooks && guard && target.fam?.opt === true && !writeOverride)
5854
5958
  optHooks.writes(target, unwrapValue(result));
5855
5959
  else adoptPB(target, unwrapValue(result));
5856
5960
  }
@@ -6030,7 +6134,7 @@ function snapshotWalk(value, seen, fam) {
6030
6134
  if (t.ovl) materializePB(t);
6031
6135
  backing = t.pb;
6032
6136
  }
6033
- if (optRead(t)) backing = optHooks.view(t, backing);
6137
+ if (optHooks && optRead(t)) backing = optHooks.view(t, backing);
6034
6138
  if (backing === src) break;
6035
6139
  src = backing;
6036
6140
  }
@@ -6206,7 +6310,7 @@ function reconcileState(value, state, key, replace = false) {
6206
6310
  // truth but an optimistic edit — the keyed diff is written into the draft
6207
6311
  // (a matched row keeps its proxy, its changed leaves become its guesses),
6208
6312
  // and the setter's exit turns the draft into guesses like any.
6209
- if (userWriting() && t.fam?.opt === true) {
6313
+ if (optHooks && userWriting() && t.fam?.opt === true) {
6210
6314
  optHooks.reconcile(state, unwrapValue(value), keyFn);
6211
6315
  return;
6212
6316
  }
@@ -6247,7 +6351,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
6247
6351
  // Q-D: a landing on an optimistic family is reconciled against the VIEW
6248
6352
  // — key matching from the arrangement the guesses made (R28), so a row
6249
6353
  // the guess added keeps its proxy when the server returns it.
6250
- const prevView = t.fam?.opt === true ? optHooks.view(t, old) : old;
6354
+ const prevView = optHooks && t.fam?.opt === true ? optHooks.view(t, old) : old;
6251
6355
  const nextArr = Array.isArray(incoming);
6252
6356
  const shallow = t.s === true;
6253
6357
  adoptPB(t, incoming, false);
@@ -6563,8 +6667,14 @@ function cloneState(v, shallow) {
6563
6667
  * pending derivatively on the derive (its readers hold; it re-runs at the
6564
6668
  * landing). The leaves do not subscribe to the derive, so the family does
6565
6669
  * the walk. */
6566
- function wakeFamily(fam, error) {
6670
+ function wakeFamily(fam) {
6567
6671
  const fw = fam.node;
6672
+ // Pending on the derive itself, whatever source took it there (an upstream
6673
+ // memo's flight reaches it as a NotReady of that memo, #4271): the leaves
6674
+ // are not that source's dependents, so only the family's settle — which
6675
+ // retires the derive (`settleFamily`) — reaches the readers parked here. A
6676
+ // reader the landing leaves unchanged does not re-run to clear it (#4305).
6677
+ const error = new NotReadyError(fw);
6568
6678
  fam.woke = true;
6569
6679
  forEachFamilyNode(fam, n => {
6570
6680
  if (n._subs === null) return;
@@ -6602,6 +6712,13 @@ function settleFamily(fam) {
6602
6712
  if (n._subs !== null) settlePendingSource(n, fw);
6603
6713
  });
6604
6714
  }
6715
+ /** A source's status reached the derive by propagation (core
6716
+ * `propagateStatus`), its pass not re-run: the family learns it as from the
6717
+ * pass's own throw. */
6718
+ function familyStatus(status) {
6719
+ if (status & STATUS_PENDING) wakeFamily(this);
6720
+ else if (status & STATUS_ERROR) errorFamily(this);
6721
+ }
6605
6722
  function forEachFamilyNode(fam, fn) {
6606
6723
  for (const t of fam.live) {
6607
6724
  const nodes = t.n;
@@ -6688,7 +6805,7 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
6688
6805
  // #2897) — and the flush that runs them advances the clock, so a
6689
6806
  // later tracked re-read may retry (core `read`).
6690
6807
  if (first && owner._statusFlags & STATUS_UNINITIALIZED) return;
6691
- if (pending) wakeFamily(fam, error);
6808
+ if (pending) wakeFamily(fam);
6692
6809
  else errorFamily(fam);
6693
6810
  });
6694
6811
  if (!owner._loading) commit(sync);
@@ -6703,7 +6820,7 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
6703
6820
  // they do not subscribe to the derive.
6704
6821
  // A synchronous error the same way (#3956: a rerun that finds the
6705
6822
  // entry failed throws): every live reader re-derives and meets it.
6706
- if (!first) e instanceof NotReadyError ? wakeFamily(fam, e) : errorFamily(fam);
6823
+ if (!first) e instanceof NotReadyError ? wakeFamily(fam) : errorFamily(fam);
6707
6824
  throw e;
6708
6825
  }
6709
6826
  // A pass that returned with a flight up (an iterator's sync first yield):
@@ -6716,7 +6833,7 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, aroundDraftWri
6716
6833
  !owner._loading &&
6717
6834
  !first
6718
6835
  )
6719
- wakeFamily(fam, owner._x?._error);
6836
+ wakeFamily(fam);
6720
6837
  return owner;
6721
6838
  }
6722
6839
  function createProjectionInternal(fn, seed, options) {
@@ -6728,6 +6845,7 @@ function createProjectionInternal(fn, seed, options) {
6728
6845
  map: new WeakMap(),
6729
6846
  node: null,
6730
6847
  live: new Set(),
6848
+ st: familyStatus,
6731
6849
  shallow: !!options?.shallow,
6732
6850
  // The row key, for a child paired after the adoption (store.ts
6733
6851
  // `adoptChild`, #3712) as the adoption pairs them.
@@ -8267,7 +8385,9 @@ function merge(...sources) {
8267
8385
  count++;
8268
8386
  only = s;
8269
8387
  if (typeof s === "function") {
8270
- flattened[n] = createMemo(s);
8388
+ // Observe tiers name it and exempt it from UNSTABLE_MEMO_OUTPUT (see
8389
+ // CONFIG_KEY_BAG); prod passes no options.
8390
+ flattened[n] = createMemo(s, { name: "merge", _extraConfig: CONFIG_KEY_BAG });
8271
8391
  kinds[n++] = SOURCE_MEMO;
8272
8392
  continue;
8273
8393
  }
@@ -8574,7 +8694,7 @@ function walk(value, entry, found, fam, visited) {
8574
8694
  if (t !== undefined) {
8575
8695
  if (t.pb !== null) entry._scope.add(t.v);
8576
8696
  // The writer's view: the tick's own unflushed guesses are in motion too.
8577
- if (t.fam?.opt === true) raw = optHooks.view(t, raw, true);
8697
+ if (optHooks && t.fam?.opt === true) raw = optHooks.view(t, raw, true);
8578
8698
  const nodes = t.n;
8579
8699
  if (nodes !== null)
8580
8700
  for (const k of Reflect.ownKeys(nodes))
@@ -8882,9 +9002,41 @@ function createDeferred(fn, options) {
8882
9002
  // Sticky, and read before this wrapper ever opens the window itself.
8883
9003
  const open = (answered ||= !(node._statusFlags & STATUS_UNINITIALIZED) && !node._loading);
8884
9004
  if (open) ((node._loading = true), (node._config |= CONFIG_LAGS));
8885
- let r;
8886
9005
  try {
8887
- r = fn(prev);
9006
+ const r = fn(prev);
9007
+ if (open && typeof r === "object" && r !== null) {
9008
+ if (isThenable(r)) {
9009
+ ask(r);
9010
+ const done = () => land(r, true);
9011
+ r.then(done, done);
9012
+ return serve(r);
9013
+ }
9014
+ const iterate = r[Symbol.asyncIterator];
9015
+ if (iterate) {
9016
+ // A stream's flight ends at its first yield (or its end).
9017
+ const it = iterate.call(r);
9018
+ let first = true;
9019
+ const done = () => land(s, true);
9020
+ const s = {
9021
+ // Registered ahead of the engine's own reaction to the same
9022
+ // promise, as for a plain flight: verdict and value in one flush.
9023
+ next: v => {
9024
+ const p = it.next(v);
9025
+ if (first) {
9026
+ first = false;
9027
+ if (isThenable(p)) p.then(done, done);
9028
+ else done();
9029
+ }
9030
+ return p;
9031
+ },
9032
+ return: v => it.return?.(v) ?? Promise.resolve({ done: true, value: v })
9033
+ };
9034
+ ask(s);
9035
+ return serve({ [Symbol.asyncIterator]: () => s });
9036
+ }
9037
+ }
9038
+ land(flight);
9039
+ return r;
8888
9040
  } catch (e) {
8889
9041
  // The wrapping form: an unready source parks the node in the window,
8890
9042
  // and the wait is its flight until the source's settle re-runs it.
@@ -8892,39 +9044,6 @@ function createDeferred(fn, options) {
8892
9044
  else land(flight);
8893
9045
  throw e;
8894
9046
  }
8895
- if (open && typeof r === "object" && r !== null) {
8896
- if (isThenable(r)) {
8897
- ask(r);
8898
- const done = () => land(r, true);
8899
- r.then(done, done);
8900
- return serve(r);
8901
- }
8902
- const iterate = r[Symbol.asyncIterator];
8903
- if (iterate) {
8904
- // A stream's flight ends at its first yield (or its end).
8905
- const it = iterate.call(r);
8906
- let first = true;
8907
- const done = () => land(s, true);
8908
- const s = {
8909
- // Registered ahead of the engine's own reaction to the same
8910
- // promise, as for a plain flight: verdict and value in one flush.
8911
- next: v => {
8912
- const p = it.next(v);
8913
- if (first) {
8914
- first = false;
8915
- if (isThenable(p)) p.then(done, done);
8916
- else done();
8917
- }
8918
- return p;
8919
- },
8920
- return: v => it.return?.(v) ?? Promise.resolve({ done: true, value: v })
8921
- };
8922
- ask(s);
8923
- return serve({ [Symbol.asyncIterator]: () => s });
8924
- }
8925
- }
8926
- land(flight);
8927
- return r;
8928
9047
  }, options)
8929
9048
  );
8930
9049
  }
@@ -9277,6 +9396,9 @@ const BOUNDARY = Symbol("boundary");
9277
9396
  * slot of (reveal.ts); a boundary clears it for its content — only direct
9278
9397
  * children are slots. */
9279
9398
  const REVEAL = Symbol("reveal");
9399
+ // Every call site checks the hooks themselves first (not only the slot or
9400
+ // controller, which are never set without them): a build that does not
9401
+ // import `createRevealOrder` folds the reveal-order arms away.
9280
9402
  let revealHooks = null;
9281
9403
  function setRevealHooks(h) {
9282
9404
  revealHooks = h;
@@ -9314,7 +9436,7 @@ function caught(b, node, error) {
9314
9436
  // nothing on screen. The fallback is instantiated once per show — a later
9315
9437
  // error reaches it through the `error` accessor, which is why it is one.
9316
9438
  if (!b._fallback) redraw(b);
9317
- if (b._reveal !== null) revealHooks._changed(b._reveal);
9439
+ if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
9318
9440
  }
9319
9441
  /** A re-armed boundary found something pending: fresh again, and the swap
9320
9442
  * the arming pass asked for is that pass's lane's (if any): the output is
@@ -9326,7 +9448,8 @@ function flip(b) {
9326
9448
  b._lane = b._armLane;
9327
9449
  const o = b._output;
9328
9450
  if (!b._lane && o._config & CONFIG_HELD && txOf(o) !== flushTransaction) unhold(o);
9329
- if (b._lane !== null && b._output !== null) laneStageHook(b._output, b._lane, true, false);
9451
+ if (laneStageHook && b._lane !== null && b._output !== null)
9452
+ laneStageHook(b._output, b._lane, true, false);
9330
9453
  }
9331
9454
  function redraw(b) {
9332
9455
  if (b._output !== null && !(b._output._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
@@ -9354,7 +9477,7 @@ function catchStatus(node, flags, error) {
9354
9477
  b._readers.size === 0
9355
9478
  ) {
9356
9479
  if (b._fallback) redraw(b);
9357
- if (b._reveal !== null) revealHooks._changed(b._reveal);
9480
+ if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
9358
9481
  }
9359
9482
  return false;
9360
9483
  }
@@ -9488,7 +9611,7 @@ function boundarySeam() {
9488
9611
  if (b._readers.size === 0) {
9489
9612
  collecting.delete(b);
9490
9613
  if (b._fallback) redraw(b);
9491
- if (b._reveal !== null) revealHooks._changed(b._reveal);
9614
+ if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
9492
9615
  }
9493
9616
  }
9494
9617
  }
@@ -9567,6 +9690,8 @@ function arm(b) {
9567
9690
  return;
9568
9691
  }
9569
9692
  b._armed = true;
9693
+ // The swap is the change's work: the output joins it (D-13, #3540).
9694
+ b._output._config &= ~CONFIG_FRAME;
9570
9695
  // A verdict read in `on` (`isPending`, or a memo of one — #3528's
9571
9696
  // `createMemo(() => isPending(m2))`: the mark carries, core.ts `read`)
9572
9697
  // whose frame has no transaction yet: the swap is still display-ahead — the frame gets one now (it lands
@@ -9646,7 +9771,7 @@ function createBoundary(type, fn, show, onFn) {
9646
9771
  if (getOwner())
9647
9772
  cleanup(() => {
9648
9773
  collecting.delete(b);
9649
- if (controller) revealHooks._unregister(controller, b);
9774
+ if (revealHooks && controller) revealHooks._unregister(controller, b);
9650
9775
  });
9651
9776
  // The `on` dependencies live OUTSIDE the boundary, as the condition of a
9652
9777
  // `<Show>` wrapping it would: created before the owner's context names it.
@@ -9685,9 +9810,16 @@ function createBoundary(type, fn, show, onFn) {
9685
9810
  b._tree = tree;
9686
9811
  // A slot of the reveal order in its context (reveal.ts): gated until the
9687
9812
  // controller releases it — before the output's first pass.
9688
- if (controller) revealHooks._register(controller, b);
9813
+ if (revealHooks && controller) revealHooks._register(controller, b);
9689
9814
  const output = computed(
9690
9815
  () => {
9816
+ // Structure, not a derivation of its content (CONFIG_FRAME, #4283): it
9817
+ // reads a hold it is not part of as the screen, as the render effect
9818
+ // flattening the same children without a boundary would. Not while a
9819
+ // re-arm's swap is the change's work (`arm`), until that swap commits
9820
+ // (D-13, #3540).
9821
+ const self = getOwner();
9822
+ if (!b._armed && !(self._config & CONFIG_HELD)) self._config |= CONFIG_FRAME;
9691
9823
  // Reveal order (reveal.ts): a gated slot shows its fallback — or, in a
9692
9824
  // collapsed tail, nothing — whatever its content's state; the
9693
9825
  // controller is told the pass ran (its readiness may have changed) and
@@ -9698,8 +9830,8 @@ function createBoundary(type, fn, show, onFn) {
9698
9830
  // It may release this very slot (the frontier whose content just
9699
9831
  // settled): the pass goes on to show it — the redraw it asks for is
9700
9832
  // refused mid-pass and not needed.
9701
- link(tree, getOwner());
9702
- if (b._reveal !== null) revealHooks._changed(b._reveal);
9833
+ link(tree, self);
9834
+ if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
9703
9835
  if (b._gated) {
9704
9836
  if (b._collapsed) {
9705
9837
  b._fallback = true;
@@ -9717,7 +9849,7 @@ function createBoundary(type, fn, show, onFn) {
9717
9849
  // once clean.
9718
9850
  const status = tree._statusFlags & (STATUS_PENDING | STATUS_ERROR);
9719
9851
  if (status !== 0) {
9720
- link(tree, getOwner());
9852
+ link(tree, self);
9721
9853
  if (status & type) {
9722
9854
  if (isCollecting(b)) {
9723
9855
  caught(b, tree, undefined);
@@ -9743,7 +9875,6 @@ function createBoundary(type, fn, show, onFn) {
9743
9875
  // enters: the outside sees its committed value until the landing,
9744
9876
  // which reveals the content — a fallback staged earlier is replaced
9745
9877
  // ahead of the commit and never shown.
9746
- const self = getOwner();
9747
9878
  if (
9748
9879
  b._readers.size !== 0 &&
9749
9880
  self._statusFlags & STATUS_UNINITIALIZED &&
@@ -9784,19 +9915,27 @@ function createBoundary(type, fn, show, onFn) {
9784
9915
  // landing when the frame parks (the run is held with it, as the old
9785
9916
  // children of a held pass are) — not now: a held swap keeps the
9786
9917
  // fallback on screen, live, until it commits. A pass back to the
9787
- // fallback before then finds it still built and keeps it.
9918
+ // fallback before then finds it still built and keeps it. A fallback
9919
+ // the screen does not show (a swap staged with a held frame, replaced
9920
+ // before it commits) dies now: what it reads holds nothing (S-22 row
9921
+ // 3′, #4268).
9788
9922
  const fb = b._fb;
9923
+ const drop = () => {
9924
+ if (!b._fallback && b._fb === fb) {
9925
+ // A fallback that threw (a rethrow the boundary does not
9926
+ // handle) is what an enclosing boundary waits on: it died, and
9927
+ // the seam of another round drops it there (#4270).
9928
+ if (fb._statusFlags & STATUS_ERROR) schedule();
9929
+ dispose(fb);
9930
+ b._fb = null;
9931
+ }
9932
+ };
9789
9933
  if (fb !== null)
9790
- globalQueue.enqueue(EFFECT_RENDER, () => {
9791
- if (!b._fallback && b._fb === fb) {
9792
- dispose(fb);
9793
- b._fb = null;
9794
- }
9795
- });
9934
+ b._output._value !== fb._value ? drop() : globalQueue.enqueue(EFFECT_RENDER, drop);
9796
9935
  release(b);
9797
9936
  // Shown: the slot leaves its reveal order (a later re-arm gates nobody
9798
9937
  // else; the frontier moves on).
9799
- if (b._reveal !== null) {
9938
+ if (revealHooks && b._reveal !== null) {
9800
9939
  const controller = b._reveal;
9801
9940
  b._reveal = null;
9802
9941
  revealHooks._changed(controller);
@@ -9809,6 +9948,8 @@ function createBoundary(type, fn, show, onFn) {
9809
9948
  { name: "value", _noSnapshot: true }
9810
9949
  );
9811
9950
  output._config |= CONFIG_REDERIVE;
9951
+ // The tree is structure too (the output's pass marks the output).
9952
+ tree._config |= CONFIG_FRAME;
9812
9953
  b._output = output;
9813
9954
  return accessor(output);
9814
9955
  }
@@ -10177,6 +10318,7 @@ export {
10177
10318
  TimeoutError,
10178
10319
  action,
10179
10320
  affects,
10321
+ clock,
10180
10322
  configureClientErrors,
10181
10323
  createContext,
10182
10324
  createDeferred,
@@ -10218,6 +10360,7 @@ export {
10218
10360
  omit,
10219
10361
  omitView,
10220
10362
  onCleanup,
10363
+ onFlush,
10221
10364
  onSettled,
10222
10365
  reconcile,
10223
10366
  refresh,