@solidjs/signals 2.0.0-rc.16 → 2.0.0-rc.17

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 (39) hide show
  1. package/dist/dev-shared.js +1719 -1603
  2. package/dist/dev.attribution.js +13 -13
  3. package/dist/dev.js +182 -45
  4. package/dist/observe/boundaries.js +2 -2
  5. package/dist/observe/core/async.js +3 -1
  6. package/dist/observe/core/context.js +2 -0
  7. package/dist/observe/core/core.js +26 -10
  8. package/dist/observe/core/effect.js +2 -0
  9. package/dist/observe/core/graph.js +10 -8
  10. package/dist/observe/core/lanes.js +25 -14
  11. package/dist/observe/core/owner.js +73 -47
  12. package/dist/observe/core/scheduler.js +189 -173
  13. package/dist/observe/core/verdict.js +3 -0
  14. package/dist/observe/map.js +2 -2
  15. package/dist/observe/signals.js +6 -1
  16. package/dist/observe/store/affects.js +13 -13
  17. package/dist/observe/store/optimistic.js +112 -71
  18. package/dist/observe/store/store.js +25 -16
  19. package/dist/prod/boundaries.js +2 -2
  20. package/dist/prod/core/async.js +3 -1
  21. package/dist/prod/core/context.js +2 -0
  22. package/dist/prod/core/core.js +31 -15
  23. package/dist/prod/core/effect.js +2 -0
  24. package/dist/prod/core/graph.js +10 -8
  25. package/dist/prod/core/heap.js +1 -1
  26. package/dist/prod/core/lanes.js +25 -14
  27. package/dist/prod/core/owner.js +70 -44
  28. package/dist/prod/core/scheduler.js +43 -27
  29. package/dist/prod/core/verdict.js +4 -1
  30. package/dist/prod/map.js +2 -2
  31. package/dist/prod/signals.js +18 -13
  32. package/dist/prod/store/optimistic.js +112 -71
  33. package/dist/prod/store/store.js +18 -7
  34. package/dist/types/core/core.d.ts +10 -3
  35. package/dist/types/core/dev.d.ts +9 -1
  36. package/dist/types/core/error.d.ts +9 -1
  37. package/dist/types/core/owner.d.ts +4 -0
  38. package/dist/types/core/scheduler.d.ts +23 -4
  39. package/package.json +1 -1
@@ -1,24 +1,24 @@
1
1
  import {
2
- bN as OBSERVE,
3
- bY as setAttributionHooks,
4
- bZ as liveRootOwners,
2
+ bP as OBSERVE,
3
+ b_ as setAttributionHooks,
4
+ b$ as liveRootOwners,
5
5
  N as NOT_PENDING,
6
6
  as as emitDiagnostic,
7
- b_ as ownerPath,
7
+ c0 as ownerPath,
8
8
  aJ as reportDiagnostic,
9
- b$ as isSuppressed,
10
- c0 as blockersOf,
11
- c1 as CONFIG_PLUMBING,
12
- c2 as anyExcluded,
13
- c3 as isExcluded,
14
- c4 as CONFIG_WIDE,
9
+ c1 as isSuppressed,
10
+ c2 as blockersOf,
11
+ c3 as CONFIG_PLUMBING,
12
+ c4 as anyExcluded,
13
+ c5 as isExcluded,
14
+ c6 as CONFIG_WIDE,
15
15
  c as CONFIG_OVERRIDE,
16
16
  C as CONFIG_GUESS,
17
17
  G as GlobalQueue,
18
18
  f as CONFIG_VERDICT,
19
- c5 as GRAPH_SIZE_WARN_AT,
20
- c6 as noteFanOut,
21
- c7 as CONFIG_SUPERSEDES,
19
+ c7 as GRAPH_SIZE_WARN_AT,
20
+ c8 as noteFanOut,
21
+ c9 as CONFIG_SUPERSEDES,
22
22
  b0 as $REFRESH
23
23
  } from "./dev-shared.js";
24
24
 
package/dist/dev.js CHANGED
@@ -105,7 +105,7 @@ import {
105
105
  aN as EFFECT_TRACKED,
106
106
  aO as heldRunHook,
107
107
  aP as unwrapStatusError,
108
- aQ as haltReactivity,
108
+ aQ as haltReactivityAt,
109
109
  aR as setStrictRead,
110
110
  aS as StatusError,
111
111
  aT as trimStaleDeps,
@@ -154,31 +154,33 @@ import {
154
154
  by as propagateStatus,
155
155
  bz as forEachDependent,
156
156
  bA as settlePendingSource,
157
- bB as notifyEpoch,
158
- bC as SUPPORTS_PROXY,
159
- bD as releaseMarksHook,
160
- bE as installMarkHooks,
161
- bF as CONFIG_LAGS,
162
- bG as REACTIVE_REASK,
163
- bH as heldUnder,
164
- bI as unhold,
165
- bJ as laneStageHook,
166
- bK as REACTIVE_ZOMBIE,
167
- bL as installBoundaryHooks,
168
- bM as CAUGHT_REARMED,
169
- bN as OBSERVE$1
157
+ bB as joinPassAt,
158
+ bC as notifyEpoch,
159
+ bD as SUPPORTS_PROXY,
160
+ bE as releaseMarksHook,
161
+ bF as installMarkHooks,
162
+ bG as CONFIG_LAGS,
163
+ bH as REACTIVE_REASK,
164
+ bI as haltReactivity,
165
+ bJ as heldUnder,
166
+ bK as unhold,
167
+ bL as laneStageHook,
168
+ bM as REACTIVE_ZOMBIE,
169
+ bN as installBoundaryHooks,
170
+ bO as CAUGHT_REARMED,
171
+ bP as OBSERVE$1
170
172
  } from "./dev-shared.js";
171
173
  export {
172
- bO as clearSnapshots,
173
- bP as enforceLoadingBoundary,
174
- bQ as getNextChildId,
175
- bR as markSnapshotScope,
176
- bS as peekNextChildId,
177
- bT as releaseSnapshotScope,
178
- bU as resetErrorHalt,
179
- bV as setConsoleFooter,
180
- bW as setSnapshotCapture,
181
- bX as setWriteHook
174
+ bQ as clearSnapshots,
175
+ bR as enforceLoadingBoundary,
176
+ bS as getNextChildId,
177
+ bT as markSnapshotScope,
178
+ bU as peekNextChildId,
179
+ bV as releaseSnapshotScope,
180
+ bW as resetErrorHalt,
181
+ bX as setConsoleFooter,
182
+ bY as setSnapshotCapture,
183
+ bZ as setWriteHook
182
184
  } from "./dev-shared.js";
183
185
 
184
186
  // Truth reads (`until`, `refresh`; CONFIG_AUTHORITATIVE) over lane nodes.
@@ -696,12 +698,21 @@ function dissolveLane(l, into, except) {
696
698
  * member throws like any. */
697
699
  function laneRead(c, el) {
698
700
  const guess = el._config & CONFIG_GUESS;
701
+ const l = txOf(el);
702
+ // A lane a correction folded under (D-34): store/optimistic.ts reads it.
703
+ // Before the authoritative arm: an `until` outside the lane reads the
704
+ // same truth there, and joins the hold it folded into — its posture over
705
+ // a foreign hold (D-31), which the truth read alone would not take.
706
+ const folded = c !== null && l.folded;
707
+ if (folded) {
708
+ const v = folded(c, el);
709
+ if (v !== NOT_PENDING) return v;
710
+ }
699
711
  if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth(el);
700
712
  const status = el._statusFlags;
701
713
  // An errored member's lane value predates its error; the error is the answer (#4106).
702
714
  if (!guess && status & STATUS_ERROR) return NOT_PENDING;
703
715
  if (c === null) return laneValueOf(el);
704
- const l = txOf(el);
705
716
  if (!tracking || c._config & CONFIG_CHILDREN_FORBIDDEN) {
706
717
  // An untracked read inside lane work derives from the lane like a
707
718
  // tracked one, minus the subscription — `untrack` is about
@@ -869,10 +880,12 @@ function laneWrite(el, v) {
869
880
  // question is the one being asked (`question`: a slice's, a derive's
870
881
  // continuation's — the flight's); unstamped writes and mainline are
871
882
  // current.
872
- if (stale(el, question)) {
883
+ // A store re-bases a keyed arrangement guess over an older truth, and
884
+ // folds a current one beneath it (D-34): true.
885
+ const older = stale(el, question);
886
+ if (GlobalQueue._laneRebase?.(el, v, older) || older) {
873
887
  el._pendingValue = v;
874
888
  el._config |= CONFIG_HELD;
875
- GlobalQueue._laneRebase?.(el, v);
876
889
  schedule();
877
890
  return v;
878
891
  }
@@ -1325,6 +1338,9 @@ function verdictValue(el, c) {
1325
1338
  (!el._equals || !el._equals(el._x._lane, el._pendingValue))))
1326
1339
  )
1327
1340
  probeFound = true;
1341
+ // A lane a correction folded under (D-34): `latest()` shows the hold's
1342
+ // world whole — the truth beneath each guess, else what it covered.
1343
+ if (latestActive && l.folded) return l.folded(null, el);
1328
1344
  return el._x._lane;
1329
1345
  }
1330
1346
  if (probing && flying) probeFound = true;
@@ -1776,6 +1792,10 @@ function setReadWatch(watch) {
1776
1792
  * via a `setContext` call.
1777
1793
  */
1778
1794
  function createContext(defaultValue, description) {
1795
+ // Dev: an explicit `undefined` default is still no default (`undefined`
1796
+ // means "not provided"); remembered so a failed read can say so (#4181).
1797
+ if (arguments.length && defaultValue === undefined)
1798
+ return { id: Symbol(description), defaultValue, _undefinedDefault: true };
1779
1799
  return { id: Symbol(description), defaultValue };
1780
1800
  }
1781
1801
  /**
@@ -1796,7 +1816,9 @@ function getContext(context, owner = getOwner()) {
1796
1816
  let value = owner._context[context.id];
1797
1817
  if (value === undefined) value = context.defaultValue;
1798
1818
  if (value === undefined) {
1799
- throw new ContextNotFoundError();
1819
+ // Dev names the context and why it has no value: no provider, or a
1820
+ // provider that passed `undefined` (the key is set, to `undefined`).
1821
+ throw new ContextNotFoundError(context, context.id in owner._context);
1800
1822
  }
1801
1823
  return value;
1802
1824
  }
@@ -1886,7 +1908,12 @@ function notifyEffectStatus(status, error) {
1886
1908
  return;
1887
1909
  }
1888
1910
  if (!globalQueue.notify(this, STATUS_ERROR, STATUS_ERROR)) {
1889
- haltReactivity(unwrapStatusError(actualError));
1911
+ // Dev locates the halt at the node that threw: the StatusError's
1912
+ // source, upstream of this effect when a computed failed.
1913
+ haltReactivityAt(
1914
+ unwrapStatusError(actualError),
1915
+ actualError instanceof StatusError ? actualError.source : this
1916
+ );
1890
1917
  throw actualError;
1891
1918
  }
1892
1919
  } else if (this._type === EFFECT_RENDER) {
@@ -1952,7 +1979,7 @@ function runEffect(node, type) {
1952
1979
  : console.error(err);
1953
1980
  } catch (error) {
1954
1981
  if (!globalQueue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
1955
- haltReactivity(error);
1982
+ haltReactivityAt(error, node);
1956
1983
  throw error;
1957
1984
  }
1958
1985
  }
@@ -1988,7 +2015,7 @@ function runEffect(node, type) {
1988
2015
  ext(node)._error = new StatusError(node, error);
1989
2016
  node._statusFlags |= STATUS_ERROR;
1990
2017
  if (!globalQueue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
1991
- haltReactivity(error);
2018
+ haltReactivityAt(error, node);
1992
2019
  throw error;
1993
2020
  }
1994
2021
  } finally {
@@ -2617,7 +2644,12 @@ function createReaction(effectFn, options) {
2617
2644
  }
2618
2645
  runWithOwner(owner, () => {
2619
2646
  effect(
2620
- () => (tracking(), (arm = getOwner())),
2647
+ () => {
2648
+ const node = getOwner();
2649
+ arm = node;
2650
+ tracking();
2651
+ return node;
2652
+ },
2621
2653
  node => {
2622
2654
  arm = undefined;
2623
2655
  cl?.();
@@ -3355,7 +3387,7 @@ function updateKeyedMap() {
3355
3387
  this._rows && (this._rows = []);
3356
3388
  this._indexes && (this._indexes = []);
3357
3389
  }
3358
- if (this._fallback && !this._mappings[0]) {
3390
+ if (this._fallback && !this._mappings.length) {
3359
3391
  // an aborted fallback attempt leaves an owner without a mapping;
3360
3392
  // dispose it before re-creating
3361
3393
  this._nodes[0]?.dispose();
@@ -3636,7 +3668,7 @@ function updateRepeat() {
3636
3668
  // Reset offset to match the cleared data (#2767, repro 2).
3637
3669
  this._offset = 0;
3638
3670
  }
3639
- if (this._fallback && !this._mappings[0]) {
3671
+ if (this._fallback && !this._mappings.length) {
3640
3672
  // an aborted fallback attempt leaves an owner without a mapping;
3641
3673
  // dispose it before re-creating
3642
3674
  this._nodes[0]?.dispose();
@@ -3941,9 +3973,13 @@ function bornStaged(target, node, staged, key) {
3941
3973
  if (k === null) return; // a node-less staging holds nothing (`stageOn`)
3942
3974
  // Held by the container's transaction only for a key the HELD staging
3943
3975
  // changed — a mainline layer's change above it stages mainline. (A
3944
- // container carrying a guess is a lane's: the flush's park decides.)
3945
- if ((k._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && heldKeyChanged(target, key))
3946
- holdNode(node, txOf(k));
3976
+ // container carrying a guess is a lane's: the flush's park decides —
3977
+ // unless a correction folded under the guess, D-34: the key is that
3978
+ // correction's hold's.)
3979
+ if (k._config & CONFIG_HELD && heldKeyChanged(target, key)) {
3980
+ const l = txOf(k);
3981
+ if (!(k._config & CONFIG_OVERRIDE) || l.folded) holdNode(node, resolveTx(l._parent ?? l));
3982
+ }
3947
3983
  // A28 (4): a batch written inside a creation-time pass is promoted — its
3948
3984
  // late-materialized leaf with it.
3949
3985
  if (k._config & CONFIG_PROMOTED) notePromotedWrite(node);
@@ -5371,6 +5407,47 @@ function touch(target, key) {
5371
5407
  }
5372
5408
  if (wk !== WK_ALL) (target.wk ??= new Set()).add(key);
5373
5409
  }
5410
+ /** Dev: targets (and their keys) already warned about for a write outside
5411
+ * the setter — #4179's once-per-target+key memory. */
5412
+ let warnedOutsideWrites;
5413
+ /** Dev: set while the runtime itself writes through a proxy it knows the
5414
+ * trap ignores (`registerGraph`'s `_owner` stamp). */
5415
+ let internalOutsideWrite = false;
5416
+ /** Dev: a write, defineProperty or delete on a store proxy outside its
5417
+ * setter's draft is ignored (the trap returns `true` so strict-mode code
5418
+ * does not throw) — say so once per target+key instead of silently dropping
5419
+ * it (#4179). */
5420
+ function warnWriteOutsideSetter(target, key, operation) {
5421
+ if (internalOutsideWrite) return;
5422
+ warnedOutsideWrites ??= new WeakMap();
5423
+ let keys = warnedOutsideWrites.get(target);
5424
+ if (keys === undefined) warnedOutsideWrites.set(target, (keys = new Set()));
5425
+ if (keys.has(key)) return;
5426
+ keys.add(key);
5427
+ const name = String(key);
5428
+ const access =
5429
+ typeof key === "string" && /^[A-Za-z_$][\w$]*$/.test(key)
5430
+ ? `.${key}`
5431
+ : typeof key === "string" && /^\d+$/.test(key)
5432
+ ? `[${key}]`
5433
+ : `[${typeof key === "string" ? JSON.stringify(key) : name}]`;
5434
+ const hint =
5435
+ operation === "deleteProperty"
5436
+ ? `setStore(s => { delete s${access} })`
5437
+ : `setStore(s => { s${access} = value })`;
5438
+ reportDiagnostic(
5439
+ emitDiagnostic({
5440
+ code: "STORE_WRITE_OUTSIDE_SETTER",
5441
+ kind: "write",
5442
+ severity: "warn",
5443
+ message:
5444
+ `[STORE_WRITE_OUTSIDE_SETTER] ${operation === "deleteProperty" ? "Deleting" : "Writing"} store property "${name}" outside its setter was ignored. ` +
5445
+ `Stores are read-only outside the setter's draft: use \`${hint}\`.`,
5446
+ nodeName: name,
5447
+ data: { operation, property: name }
5448
+ })
5449
+ );
5450
+ }
5374
5451
  const traps = {
5375
5452
  get(target, key, receiver) {
5376
5453
  // One typeof gates every brand-symbol compare off the hot string path.
@@ -5614,7 +5691,10 @@ const traps = {
5614
5691
  set(target, key, value) {
5615
5692
  const draft = inDraft(target);
5616
5693
  const override = !draft && writeOverride;
5617
- if (!draft && !override) return true;
5694
+ if (!draft && !override) {
5695
+ warnWriteOutsideSetter(target, key, "set");
5696
+ return true;
5697
+ }
5618
5698
  if (key === "__proto__") return true; // pollution guard (core R30)
5619
5699
  const uv = target.s ? value : unwrapValue(value);
5620
5700
  const pb = ensurePB(target);
@@ -5663,7 +5743,10 @@ const traps = {
5663
5743
  defineProperty(target, key, desc) {
5664
5744
  const draft = inDraft(target);
5665
5745
  const override = !draft && writeOverride;
5666
- if (!draft && !override) return true;
5746
+ if (!draft && !override) {
5747
+ warnWriteOutsideSetter(target, key, "defineProperty");
5748
+ return true;
5749
+ }
5667
5750
  if (key === "__proto__") return true;
5668
5751
  if (desc.get || desc.set) target.a = true;
5669
5752
  if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
@@ -5678,7 +5761,10 @@ const traps = {
5678
5761
  deleteProperty(target, key) {
5679
5762
  const draft = inDraft(target);
5680
5763
  const override = !draft && writeOverride;
5681
- if (!draft && !override) return true;
5764
+ if (!draft && !override) {
5765
+ warnWriteOutsideSetter(target, key, "deleteProperty");
5766
+ return true;
5767
+ }
5682
5768
  const pb = ensurePB(target);
5683
5769
  touch(target, key);
5684
5770
  // The key count (`kc`, the overlay gate and the rebuild cap) follows a
@@ -5836,8 +5922,11 @@ function createStore$1(initialValue, options) {
5836
5922
  {
5837
5923
  const owner = getOwner();
5838
5924
  // Dev-tier graph registration (owner signal lists, onGraph); the
5839
- // `_owner` write itself never reaches the proxy, see storeOwners.
5925
+ // `_owner` write itself never reaches the proxy, see storeOwners — an
5926
+ // internal ignored write, not the user's (#4179's warning skips it).
5927
+ internalOutsideWrite = true;
5840
5928
  registerGraph(proxy, owner);
5929
+ internalOutsideWrite = false;
5841
5930
  // Only once the engine is installed, like the node stamping it feeds: a
5842
5931
  // WeakMap.set per fresh store is a growing ephemeron table and,
5843
5932
  // disabled, buys nothing.
@@ -6809,10 +6898,12 @@ function notifyOptimisticWrites(t, pb) {
6809
6898
  if (structural) {
6810
6899
  const rows = isArr ? pb.map(unwrapValue) : shallowKeys(pb);
6811
6900
  // The rows the setter removed from the view it saw, by key: a later
6812
- // re-base over another action's truth must not bring them back.
6901
+ // re-base over another action's truth must not bring them back. A set
6902
+ // (empty or not) marks a keyed arrangement guess (`fold`).
6813
6903
  let removed = null;
6814
6904
  const keyFn = t.fam.key;
6815
6905
  if (isArr && keyFn !== null && keyFn !== undefined) {
6906
+ removed = new Set();
6816
6907
  const kept = new Set();
6817
6908
  for (let i = 0; i < rows.length; i++) {
6818
6909
  const k = isWrappable(rows[i]) ? keyFn(rows[i]) : undefined;
@@ -6821,7 +6912,7 @@ function notifyOptimisticWrites(t, pb) {
6821
6912
  for (let i = 0; i < old.length; i++) {
6822
6913
  const r = unwrapValue(old[i]);
6823
6914
  const k = isWrappable(r) ? keyFn(r) : undefined;
6824
- if (k !== undefined && !kept.has(k)) (removed ??= new Set()).add(k);
6915
+ if (k !== undefined && !kept.has(k)) removed.add(k);
6825
6916
  }
6826
6917
  }
6827
6918
  optimisticWrite(getContainerNode(t), new LaneView(rows, base, removed));
@@ -6957,7 +7048,8 @@ function reconcileDraft(draft, incoming, keyFn) {
6957
7048
  * committed length differs, and as the `LaneView`'s base for structural
6958
7049
  * reads. Unkeyed families compose
6959
7050
  * positionally — the guess stands as written (the documented residue). */
6960
- GlobalQueue._laneRebase = (el, truth) => {
7051
+ GlobalQueue._laneRebase = (el, truth, older) => {
7052
+ if (!older) return fold(el, truth);
6961
7053
  const t = el._host;
6962
7054
  if (t === undefined || el._key !== $TRACK) return;
6963
7055
  const view = el._x._lane;
@@ -7018,6 +7110,51 @@ GlobalQueue._laneRebase = (el, truth) => {
7018
7110
  laneSet(len, rows.length, lane);
7019
7111
  }
7020
7112
  };
7113
+ /** D-34 for a keyed arrangement guess: rows by key, not positions. A
7114
+ * current truth that differs folds into the hold beneath it instead of
7115
+ * voiding the lane: the guess stays the lane's value and the screen's until
7116
+ * the hold commits, where rows match by key, and the graph outside the lane
7117
+ * re-derives from the truth now (`foldedRead`). A confirmation is silent
7118
+ * (A17): only truth readers are told. True: the lane stages the truth
7119
+ * beneath (`laneWrite`). */
7120
+ function fold(el, truth) {
7121
+ const k = el._host?.k;
7122
+ const l = txOf(el);
7123
+ // A guess the screen never showed is void at the correction (D-34); a
7124
+ // keyed arrangement guess's view carries its removed-keys set.
7125
+ if (!l._shown || !(k?._config & CONFIG_OVERRIDE) || !k._x._lane?.removed) return false;
7126
+ const guess = el._x._lane;
7127
+ if ((!el._equals || !el._equals(guess, truth)) && !GlobalQueue._echoes(guess, truth)) {
7128
+ l.folded = foldedRead;
7129
+ insertSubs(el);
7130
+ } else GlobalQueue._confirmed?.(l);
7131
+ joinFuture(resolveTx(l._parent ?? l));
7132
+ return true;
7133
+ }
7134
+ /** A folded lane's guess read by a deriving pass outside the lane, tracked
7135
+ * or not (`untrack` is about dependencies, not about which world a pass
7136
+ * derives from), or by `latest()` (`c` null): the hold's world — the truth
7137
+ * staged beneath the guess, else the value it covered — and the pass joins
7138
+ * the hold. Lane passes and the screen keep the guess: held truth is masked
7139
+ * from lane passes only (#3164). The store's own derive reconciles its
7140
+ * landings against the arrangement the guesses made: it is the author, not
7141
+ * a reader. */
7142
+ function foldedRead(c, el) {
7143
+ if (c !== null) {
7144
+ if (
7145
+ !(el._config & CONFIG_GUESS) ||
7146
+ passLane !== null ||
7147
+ c._type === EFFECT_RENDER ||
7148
+ c._config & CONFIG_CHILDREN_FORBIDDEN ||
7149
+ el._host?.fam?.node === c
7150
+ )
7151
+ return NOT_PENDING;
7152
+ const l = txOf(el);
7153
+ joinPassAt(c, resolveTx(l._parent ?? l));
7154
+ }
7155
+ if (el._pendingValue !== NOT_PENDING) return el._pendingValue;
7156
+ return el._config & CONFIG_SLOT_NODE ? GlobalQueue._slotCovered(el) : el._value;
7157
+ }
7021
7158
  function sameRow(a, b, keyFn) {
7022
7159
  if (a === b) return true;
7023
7160
  if (!isWrappable(a) || !isWrappable(b)) return false;
@@ -9310,7 +9447,7 @@ function createBoundary(type, fn, show, onFn) {
9310
9447
  } catch (e) {
9311
9448
  if (!(e instanceof NotReadyError)) {
9312
9449
  if (!globalQueue.notify(getOwner(), STATUS_ERROR, STATUS_ERROR, e)) {
9313
- haltReactivity(e);
9450
+ if (true) haltReactivityAt(e, getOwner());
9314
9451
  throw e;
9315
9452
  }
9316
9453
  return;
@@ -14,7 +14,7 @@ import { enqueueSub } from "./core/heap.js";
14
14
 
15
15
  import { createOwner, getOwner, cleanup, dispose } from "./core/owner.js";
16
16
 
17
- import { globalQueue, haltReactivity, passLane, joinFuture, GlobalQueue, flushTransaction, heldUnder, setPassLane, passTx, resolveTx, schedule, txOf, unhold, laneStageHook, installBoundaryHooks } from "./core/scheduler.js";
17
+ import { globalQueue, haltReactivityAt, haltReactivity, passLane, joinFuture, GlobalQueue, flushTransaction, heldUnder, setPassLane, passTx, resolveTx, schedule, txOf, unhold, laneStageHook, installBoundaryHooks } from "./core/scheduler.js";
18
18
 
19
19
  import { attrHooks } from "./core/attribution-hooks.js";
20
20
 
@@ -353,7 +353,7 @@ function createBoundary(e, r, n, t) {
353
353
  } catch (e) {
354
354
  if (!(e instanceof NotReadyError)) {
355
355
  if (!globalQueue.notify(getOwner(), STATUS_ERROR, STATUS_ERROR, e)) {
356
- haltReactivity(e);
356
+ if (false) ; else haltReactivity(e);
357
357
  throw e;
358
358
  }
359
359
  return;
@@ -148,7 +148,9 @@ function releaseSettledDependents(e) {
148
148
  // Staged whether or not the flush has joined a hold yet: a pass that runs
149
149
  // before its async sibling joins is the seam's to decide, like a value
150
150
  // (#4109). A flush that parks nothing publishes it at its commit (S-38).
151
- if (!(e.l & CONFIG_HELD || n && e.R & REACTIVE_STAGED_READ && globalQueue.q) || passLane) return false;
151
+ // Not a projection's derive: its readers learn its error through the
152
+ // family, which the commit's publish never reaches (#4189).
153
+ if (!(e.l & CONFIG_HELD || n && e.R & REACTIVE_STAGED_READ && globalQueue.q && !e.fam) || passLane) return false;
152
154
  // The node answered (an error ends the wait — no longer pending, nor
153
155
  // blocked); a shown error stays its display until the commit.
154
156
  clearPendingSources(e);
@@ -33,6 +33,8 @@ import { getOwner } from "./owner.js";
33
33
  let r = t.A[e.id];
34
34
  if (r === undefined) r = e.defaultValue;
35
35
  if (r === undefined) {
36
+ // Dev names the context and why it has no value: no provider, or a
37
+ // provider that passed `undefined` (the key is set, to `undefined`).
36
38
  throw new ContextNotFoundError;
37
39
  }
38
40
  return r;
@@ -14,7 +14,7 @@ import { attrHooks } from "./attribution-hooks.js";
14
14
 
15
15
  import { inheritId, linkChild, disposeChildren, markDisposal } from "./owner.js";
16
16
 
17
- import { schedule, externalUntrackHook, clock, txOf, flushTransaction, laneWriteHook, joinFuture, queuePendingNode, globalQueue, notifyEpoch, insertSubs, dirtyQueue, observeFlightHook, laneReadHook, laneStageHook, passLane, wireExternalSourceHook, bumpNotifyEpoch, setPassLane, joinPassTx, freshHook, GlobalQueue, laneOutcomeHook, passTx, holdNode, laneDirty, heldTrim, staleReader, laneSeamsHook, stagedReaders, deferZombie } from "./scheduler.js";
17
+ import { schedule, externalUntrackHook, clock, txOf, flushTransaction, laneWriteHook, joinFuture, queuePendingNode, globalQueue, notifyEpoch, insertSubs, dirtyQueue, observeFlightHook, laneStageHook, passLane, laneReadHook, wireExternalSourceHook, bumpNotifyEpoch, setPassLane, joinPassTx, freshHook, GlobalQueue, laneOutcomeHook, passTx, holdNode, laneDirty, heldTrim, staleReader, laneSeamsHook, stagedReaders, deferZombie } from "./scheduler.js";
18
18
 
19
19
  // The heap's per-node step. A tracked effect's heap visit is its compute
20
20
  // phase — empty, like a user effect whose compute reads nothing — and hands
@@ -222,7 +222,9 @@ function recompute(e, t = false) {
222
222
  // and own nothing to park.
223
223
  if (n === EFFECT_TRACKED || e.l & CONFIG_STAGED) {
224
224
  // A lazy node only those children read is released when this pass
225
- // ends, if the pass doesn't read it again (#4142).
225
+ // ends, if the pass doesn't read it again (#4142). A cleanup or
226
+ // `unobserved` hook that throws here neither stops the pass nor skips
227
+ // that drain: its error is rethrown when the flush ends (owner.ts).
226
228
  const t = deferredReleases;
227
229
  deferReleases(o = []);
228
230
  disposeChildren(e);
@@ -240,8 +242,8 @@ function recompute(e, t = false) {
240
242
  // A conditional can drop its pending source and recover to an unchanged
241
243
  // value, leaving blocked dependents outside that source’s settle walk.
242
244
  const u = e.m & STATUS_ERROR ? e.T?.F : undefined;
243
- const _ = (e.m & STATUS_PENDING) !== 0;
244
- const a = _ ? e.T?.fe : undefined;
245
+ const a = (e.m & STATUS_PENDING) !== 0;
246
+ const _ = a ? e.T?.fe : undefined;
245
247
  // Pending SOURCE-hood, captured before the compute clears status: a node
246
248
  // whose own flight parked dependents self-registers in _pendingSources
247
249
  // (notifyStatus, isSource). If this recompute supersedes that flight and
@@ -332,7 +334,7 @@ function recompute(e, t = false) {
332
334
  // re-park twin of the unchanged-value recovery sweep below; dependents
333
335
  // with another path keep the source (retryReaches). A staged error
334
336
  // answered what the pass carried: the same.
335
- if ((n || N) && a) for (const t of a) if (t !== e && !e.T?.fe?.has(t)) settlePendingSource(e, t);
337
+ if ((n || N) && _) for (const t of _) if (t !== e && !e.T?.fe?.has(t)) settlePendingSource(e, t);
336
338
  }
337
339
  } finally {
338
340
  tracking = d;
@@ -343,7 +345,10 @@ function recompute(e, t = false) {
343
345
  // (markNode(c) in read()) marks the running node as part of ordinary
344
346
  // bookkeeping, and those marks are correctly discarded here.
345
347
  A = (e.R & REACTIVE_MISSED_WAKE) !== 0;
346
- R = !!(e.R & (l ? REACTIVE_JOINED | REACTIVE_STAGED_READ : REACTIVE_JOINED));
348
+ // A mount's staged read joins it — except a nested derivation left
349
+ // pending: no render effect reaches it yet, so it holds nothing and
350
+ // reaches no boundary (Reader, #4203). A reader that does is caught.
351
+ R = !!(e.R & (l && (s || n || !(e.m & STATUS_PENDING)) ? REACTIVE_JOINED | REACTIVE_STAGED_READ : REACTIVE_JOINED));
347
352
  // A verdict reader is one for as long as it probes, or reads a memo
348
353
  // that does (`read`): a pass that did neither (REACTIVE_PROBED, the same
349
354
  // bit) is an ordinary derivation again.
@@ -526,7 +531,7 @@ function recompute(e, t = false) {
526
531
  if (u !== undefined) settleErroredDependents(e, u);
527
532
  // Self-registration (this node's own superseded flight) is the #3181
528
533
  // sweep's business below — retiring it here too would walk twice.
529
- if (a) for (const t of a) if (t !== e) settlePendingSource(e, t);
534
+ if (_) for (const t of _) if (t !== e) settlePendingSource(e, t);
530
535
  }
531
536
  // #3181: a synchronous settle supersedes the old landing callback, so
532
537
  // recompute owns its pending-source sweep. An uninitialized node without
@@ -726,6 +731,12 @@ function computed(e, t) {
726
731
  if (t.m & STATUS_UNINITIALIZED || e.i !== EFFECT_RENDER) joinPassTx(txOf(t), !e.i && e.m & STATUS_UNINITIALIZED || freshHook?.(e));
727
732
  }
728
733
 
734
+ /** `joinPass` into the hold `t` (a folded guess's: store/optimistic.ts
735
+ * `foldedRead`, D-34) — a reader is never a render effect there. */ function joinPassAt(e, t) {
736
+ e.R |= REACTIVE_JOINED;
737
+ joinPassTx(t, !e.i && e.m & STATUS_UNINITIALIZED || freshHook?.(e));
738
+ }
739
+
729
740
  /** A15's stale reader (shared-hole and reveal corollaries): a render effect
730
741
  * reading a node held by a transaction that is not the flush's. It is served
731
742
  * the committed value — a pending one's too — publishes mainline, and is
@@ -1197,7 +1208,9 @@ function read(e) {
1197
1208
  // Committed visibility for untracked and children-forbidden readers.
1198
1209
  if (!t || e.ue === NOT_PENDING || t.l & CONFIG_CHILDREN_FORBIDDEN) return e.Ie;
1199
1210
  // (A plain signal is never uninitialized — the carve-out is `serve`'s.)
1200
- if (observeFlightHook && t.l & CONFIG_VERDICT && stagedScreen(t)) return e.Ie;
1211
+ // Lane work over a folded lane (D-34), once the frame has a
1212
+ // transaction, reads the screen too.
1213
+ if ((observeFlightHook && t.l & CONFIG_VERDICT || laneStageHook && passLane?.folded) && stagedScreen(t)) return e.Ie;
1201
1214
  stagedRead(t);
1202
1215
  return e.ue;
1203
1216
  }
@@ -1338,7 +1351,10 @@ function read(e) {
1338
1351
 
1339
1352
  /** A pass read a staging of this flush: it derives from what the flush may
1340
1353
  * yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a
1341
- * lane sees the screen plus its own guesses): the staging is the screen if
1354
+ * lane sees the screen plus its own guesses) — a folded lane's only before
1355
+ * the frame has a transaction (after, it reads the screen like a verdict
1356
+ * reader, `stagedScreen` — a list pass never pairs a held length with the
1357
+ * guess's rows, D-34): the staging is the screen if
1342
1358
  * the frame commits — one pass, the common case — and a held write if it
1343
1359
  * parks, which the seam repairs (`stagedReaders`): the pass re-derives on
1344
1360
  * the committed world and its lane's runs wait that round, so the held
@@ -1580,4 +1596,4 @@ function staleValues(e, t = true) {
1580
1596
  }
1581
1597
  }
1582
1598
 
1583
- export { bornInto, captureWriteSnapshot, clearSnapshots, computed, context, createEffectNode, effectStatusNotify, ext, isEqual, isSame, joinPass, markLateLinker, markRefresh, markSnapshotScope, markUnflushedStaged, notePromotedWrite, prepareComputed, pullComputed, read, readerSeesCommitted, recompute, releaseSnapshotScope, resyncUnflushedCompanions, runWithOwner, serve, setEffectStatusNotify, setMemo, setSignal, setSnapshotCapture, setVerdict, setWriteHook, signal, slotSignal, snapshotCaptureActive, snapshotSources, spectating, stagedRead, stagedScreen, staleValues, stashFlushed, statusNotifierOf, tracking, unflushedStaged, unflushedValue, untrack, verdict, writeHook };
1599
+ export { bornInto, captureWriteSnapshot, clearSnapshots, computed, context, createEffectNode, effectStatusNotify, ext, isEqual, isSame, joinPass, joinPassAt, markLateLinker, markRefresh, markSnapshotScope, markUnflushedStaged, notePromotedWrite, prepareComputed, pullComputed, read, readerSeesCommitted, recompute, releaseSnapshotScope, resyncUnflushedCompanions, runWithOwner, serve, setEffectStatusNotify, setMemo, setSignal, setSnapshotCapture, setVerdict, setWriteHook, signal, slotSignal, snapshotCaptureActive, snapshotSources, spectating, stagedRead, stagedScreen, staleValues, stashFlushed, statusNotifierOf, tracking, unflushedStaged, unflushedValue, untrack, verdict, writeHook };
@@ -50,6 +50,8 @@ function notifyEffectStatus(t, e) {
50
50
  return;
51
51
  }
52
52
  if (!globalQueue.notify(this, STATUS_ERROR, STATUS_ERROR)) {
53
+ // Dev locates the halt at the node that threw: the StatusError's
54
+ // source, upstream of this effect when a computed failed.
53
55
  haltReactivity(unwrapStatusError(u));
54
56
  throw u;
55
57
  }
@@ -2,7 +2,7 @@ import { REACTIVE_RECOMPUTING_DEPS, CONFIG_AUTO_DISPOSE, STATUS_PENDING, REACTIV
2
2
 
3
3
  import { deleteFromHeap } from "./heap.js";
4
4
 
5
- import { disposeChildren } from "./owner.js";
5
+ import { disposeChildren, runCleanup } from "./owner.js";
6
6
 
7
7
  import { bumpNotifyEpoch, dirtyQueue } from "./scheduler.js";
8
8
 
@@ -27,7 +27,9 @@ function unlinkSubs(e) {
27
27
  if (o === null) {
28
28
  // Store slot nodes dispatch to the ONE shared hook — no per-node
29
29
  // unobserved closure, no NodeExtension to hold it.
30
- if (n.l & CONFIG_SLOT_NODE) slotUnobservedHook(n); else n.T?.Dt?.();
30
+ // A user hook that throws is collected like a cleanup (owner.ts): the
31
+ // unlink and the teardown around it carry on.
32
+ if (n.l & CONFIG_SLOT_NODE) slotUnobservedHook(n); else if (n.T?.Dt) runCleanup(n.T.Dt);
31
33
  // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
32
34
  // A pending node is exempt: its in-flight async work is an observer —
33
35
  // tearing down would orphan the work and re-execute it on the next
@@ -141,18 +143,18 @@ function link(e, n) {
141
143
  // [deps.._depsTail] prefix — the O(1) equivalent of scanning the dep list
142
144
  // (the old alien-signals `isValidLink` walk, O(n²) when a computation
143
145
  // re-reads earlier deps non-consecutively, e.g. store leaf reads).
144
- const t = e.dt;
145
- if (t !== null && t.u === n && (!s || t.Te === n.Ne)) return;
146
- const r = n._ = e.dt = {
146
+ const r = e.dt;
147
+ if (r !== null && r.u === n && (!s || r.Te === n.Ne)) return;
148
+ const t = n._ = e.dt = {
147
149
  S: e,
148
150
  u: n,
149
151
  G: o,
150
- en: t,
152
+ en: r,
151
153
  t: null,
152
154
  Te: n.Ne
153
155
  };
154
- if (l !== null) l.G = r; else n.C = r;
155
- if (t !== null) t.t = r; else e.o = r;
156
+ if (l !== null) l.G = t; else n.C = t;
157
+ if (r !== null) r.t = t; else e.o = t;
156
158
  // New subscriber edge: staged-rewrite skips (§12d) must not miss it.
157
159
  bumpNotifyEpoch();
158
160
  }