@solidjs/signals 2.0.0-rc.11 → 2.0.0-rc.13

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.
@@ -226,20 +226,29 @@ const CONFIG_OVERRIDE_SUPERSEDED = 1 << 19;
226
226
  * transaction. A parked node (status propagation stamps `_transition`
227
227
  * without recomputing) recomputed when its source lands otherwise disposed
228
228
  * its committed children mid-hold, running their cleanups before the
229
- * transaction's atomic reveal. Cleared by `commitPendingNode`. */
229
+ * transaction's atomic reveal. Cleared by `commitPendingNode`. Transaction
230
+ * work only (A15 lane work and transaction work, #3698): zombies are parked
231
+ * by a pass under a held transaction; a pass over a lane parks a LANE frame
232
+ * instead (`CONFIG_LANE_FRAME`), whatever the node's kind. */
230
233
  const CONFIG_HELD_CHILDREN = 1 << 20;
231
234
  /** The frame parked in `_pendingFirstChild` / `_pendingDisposal` is a LANE
232
- * frame (#3662, A15 lanes corollary): an effect's lane pass direct-commits,
233
- * so the frame it replaces leaves the screen when the lane's queue applies
234
- * the run (A30) — not at the action's commit like #3404's transaction
235
- * zombies, and not at the pass (a held lane defers the run). Drained by the
236
- * lane's render entry the parking site pushed ahead of the new frame's
237
- * effects (cleanups before side effects), by `commitPendingNode` if a hold
238
- * commits the node first, or with the owner's death. While set the parked
239
- * frame is not a hold (the node is not queued or stamped for it), a
240
- * superseding pass disposes the never-shown live children on the spot, and a
241
- * lane-channel dirty on a member is cancelled (`laneZombie`). Effects only: a
242
- * memo's lane pass publishes an override (A17, #3479). */
235
+ * frame (#3662, #3698; A15 lane work and transaction work): a lane pass —
236
+ * on an effect or a memo alike — publishes into the lane's frame (an
237
+ * effect's run; a memo's derived override, A17), so the frame it replaces
238
+ * leaves the screen when the lane applies (A30) — not at the action's
239
+ * commit like #3404's transaction zombies, and not at the pass (a held lane
240
+ * defers the apply with the frame still displayed). Drained by the lane's
241
+ * render entry the parking site pushed ahead of the new frame's effects
242
+ * (cleanups before side effects), by `commitPendingNode` if a hold commits
243
+ * the node first, or with the owner's death. While set the parked frame is
244
+ * not a hold (the node is not queued or stamped for it — lane work never
245
+ * makes its node transaction work), a superseding pass disposes the
246
+ * never-shown live children on the spot, and a lane-channel dirty on a
247
+ * member is cancelled (`laneZombie`). Ruled 2026-09-28 (#3698): a pass is
248
+ * lane work or transaction work by its owner, never by node kind. #3662
249
+ * flagged effects only, and a memo's lane pass parked a transaction zombie
250
+ * that queued and stamped the memo as the action's pending node, so its
251
+ * next mainline recompute re-entered the hold. */
243
252
  const CONFIG_LANE_FRAME = 1 << 26;
244
253
  /** In-flight async node whose inputs were PUBLISHED while it was pending: a
245
254
  * batch or transaction committed with the node still `STATUS_PENDING` (an
@@ -461,22 +470,33 @@ function recordsChannel() {
461
470
  const g = globalThis;
462
471
  if (g[RECORDS]) return g[RECORDS];
463
472
  const listeners = new Map();
473
+ const bodyListeners = new Map();
464
474
  return (g[RECORDS] = {
465
- subscribe(type, listener) {
475
+ subscribe(type, listener, options) {
466
476
  const current = listeners.get(type);
467
- // Set semantics: one entry per function, however often it is passed.
468
- if (current === undefined) listeners.set(type, [listener]);
469
- else if (!current.includes(listener)) listeners.set(type, [...current, listener]);
477
+ // Set semantics: one entry per function, however often it is passed —
478
+ // the first subscription's options stand for it.
479
+ if (current === undefined || !current.includes(listener)) {
480
+ listeners.set(type, current === undefined ? [listener] : [...current, listener]);
481
+ if (options !== undefined && options.bodies) {
482
+ let wanting = bodyListeners.get(type);
483
+ if (wanting === undefined) bodyListeners.set(type, (wanting = new Set()));
484
+ wanting.add(listener);
485
+ }
486
+ }
470
487
  return () => {
471
488
  const list = listeners.get(type);
472
- if (list === undefined) return;
489
+ if (list === undefined || !list.includes(listener)) return;
473
490
  const next = list.filter(l => l !== listener);
474
491
  if (next.length > 0) listeners.set(type, next);
475
492
  else listeners.delete(type);
493
+ const wanting = bodyListeners.get(type);
494
+ if (wanting !== undefined && wanting.delete(listener) && wanting.size === 0)
495
+ bodyListeners.delete(type);
476
496
  };
477
497
  },
478
- observed(type) {
479
- return listeners.has(type);
498
+ observed(type, facet) {
499
+ return facet === "bodies" ? bodyListeners.has(type) : listeners.has(type);
480
500
  },
481
501
  emit(type, event, live) {
482
502
  const list = listeners.get(type);
@@ -502,9 +522,14 @@ const OBSERVE = {
502
522
  // empty HERE and gains its members by augmentation downstream.
503
523
  server: {},
504
524
  exclude(owner) {
505
- excludedOwners.add(owner);
525
+ markedOwners.set(owner, true);
506
526
  hasExclusions = true;
507
527
  },
528
+ include(owner) {
529
+ // No flag flip: with nothing excluded there is nothing to re-admit,
530
+ // and the walk stays short-circuited.
531
+ markedOwners.set(owner, false);
532
+ },
508
533
  isExcluded,
509
534
  ownerPath
510
535
  };
@@ -512,16 +537,23 @@ const OBSERVE = {
512
537
  //
513
538
  // An observer that lives inside the observed app (an adapter's panel,
514
539
  // devtools) marks its root; both channels check the subject's owner chain —
515
- // the same walk `ownerPath` already makes — and stay silent under it. The
516
- // flag short-circuits the walk for the common case of no exclusions.
517
- const excludedOwners = new WeakSet();
540
+ // the same walk `ownerPath` already makes — and stay silent under it. An
541
+ // observer that WRAPS the app (a toolbar around it) excludes its own root
542
+ // and includes the app's back: one map, `true` excluded / `false` included,
543
+ // and the nearest marker up the chain decides. The flag short-circuits the
544
+ // walk for the common case of no exclusions (an include with nothing
545
+ // excluded changes no verdict, so it does not flip it).
546
+ const markedOwners = new WeakMap();
518
547
  let hasExclusions = false;
519
548
  /** Events built for an excluded subject: never delivered, never reported. */
520
549
  const suppressedEvents = new WeakSet();
521
550
  function isExcluded(subject) {
522
551
  if (!hasExclusions || !subject) return false;
523
552
  let owner = "_parent" in subject ? subject : (subject._owner ?? null);
524
- for (; owner !== null; owner = owner._parent) if (excludedOwners.has(owner)) return true;
553
+ for (; owner !== null; owner = owner._parent) {
554
+ const marked = markedOwners.get(owner);
555
+ if (marked !== undefined) return marked;
556
+ }
525
557
  return false;
526
558
  }
527
559
  /** For engines that cache the verdict per node: is anything excluded at all? */
@@ -688,8 +720,14 @@ function throwPendingUntrackedRead(strictReadLabel, fields) {
688
720
  throw new Error(message);
689
721
  }
690
722
  function warnStrictReadUntracked(strictReadLabel, fields) {
723
+ // Name the value when the name says something — a `name` option or a store
724
+ // key (#3675). The constructors' defaults ("signal", "computed") would only
725
+ // restate what "reactive value" already says.
726
+ const nodeName = fields?.nodeName;
727
+ const named = nodeName !== undefined && nodeName !== "signal" && nodeName !== "computed";
691
728
  const message =
692
- `[STRICT_READ_UNTRACKED] Reactive value read directly in ${strictReadLabel} will not update. ` +
729
+ `[STRICT_READ_UNTRACKED] Reactive value${named ? ` "${nodeName}"` : ""} read directly in ` +
730
+ `${strictReadLabel} will not update. ` +
693
731
  `Move it into a tracking scope (JSX, a memo, or an effect's compute function).`;
694
732
  reportDiagnostic(
695
733
  emitDiagnostic({
@@ -1696,7 +1734,7 @@ function haltReactivity(cause) {
1696
1734
  let message = "[REACTIVITY_HALTED]";
1697
1735
  {
1698
1736
  message +=
1699
- " An uncaught error halted the reactive system. No further updates will be processed. Handle errors with createErrorBoundary/<Errored> or treat this as a crash.";
1737
+ " An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
1700
1738
  emitDiagnostic({
1701
1739
  code: "REACTIVITY_HALTED",
1702
1740
  kind: "error",
@@ -4705,8 +4743,8 @@ function recompute(el, create = false) {
4705
4743
  let devChanged = false;
4706
4744
  if (attrHooks !== null) attrHooks.recomputeStart(el, create);
4707
4745
  // Lane posture is resolved BEFORE the previous frame is parked below: a
4708
- // lane pass on an effect direct-commits (#3662, see the parking site), so
4709
- // the decision must be known there. `lane` is applied to
4746
+ // lane pass parks a lane frame, not a transaction zombie (#3662, #3698;
4747
+ // see the parking site), so the decision must be known there. `lane` is applied to
4710
4748
  // `currentOptimisticLane` further down, once the previous posture is saved.
4711
4749
  let isOptimisticDirty = !!(el._flags & REACTIVE_OPTIMISTIC_DIRTY);
4712
4750
  let lane = null;
@@ -4755,20 +4793,30 @@ function recompute(el, create = false) {
4755
4793
  // Tracked effects run after finalizePureQueue, so dispose immediately
4756
4794
  // instead of deferring. Children built by an uncommitted recompute
4757
4795
  // (CONFIG_HELD_CHILDREN) die immediately too: no frame ever showed them.
4758
- // Everything else is the committed frame's and is deferred as zombies
4759
- // until this node's commit — a transaction-owned node included (#3404):
4760
- // a parked node's children predate the hold, and tearing them down when
4761
- // the source lands ran cleanups before the transaction's atomic reveal.
4762
- // A lane pass on an effect parks a LANE frame instead (CONFIG_LANE_FRAME,
4763
- // #3662; A15 lanes corollary): the frame it replaces leaves the screen
4764
- // when the lane's queue applies this run (A30) — not at the action's
4765
- // commit — and a held lane defers that with the frame still displayed.
4766
- // The drain is the lane's first render entry for this pass, pushed before
4767
- // the pass builds the new frame: cleanups before side effects — the
4768
- // retired frame's `onCleanup`s run ahead of every effect callback of the
4769
- // new frame. While the frame waits, the live children were never shown:
4770
- // a superseding pass disposes them here like held children, and the
4771
- // parked frame stays.
4796
+ // Everything else is the committed frame's. Which frame retires it is
4797
+ // decided by who owns the pass, never by the node's kind (A15 lane work
4798
+ // and transaction work, ruled 2026-09-28, #3698). Transaction work — a
4799
+ // pass under a held transaction — defers them as zombies until this
4800
+ // node's commit, a transaction-owned node included (#3404): a parked
4801
+ // node's children predate the hold, and tearing them down when the
4802
+ // source lands ran cleanups before the transaction's atomic reveal.
4803
+ // Lane work — a pass over a lane, effect or memo alike — parks a LANE
4804
+ // frame instead (CONFIG_LANE_FRAME, #3662, #3698): the frame it replaces
4805
+ // leaves the screen when the lane's queue applies this pass (A30) — not
4806
+ // at the action's commit — and a held lane defers that with the frame
4807
+ // still displayed. The drain is the lane's first render entry for this
4808
+ // pass, pushed before the pass builds the new frame: cleanups before
4809
+ // side effects — the retired frame's `onCleanup`s run ahead of every
4810
+ // effect callback of the new frame. While the frame waits, the live
4811
+ // children were never shown: a superseding pass disposes them here like
4812
+ // held children, and the parked frame stays. Zombies exist for
4813
+ // transaction work only: #3662 gated the lane frame on `isEffect`, and a
4814
+ // memo's lane pass (its value a derived override, A17) parked a
4815
+ // transaction zombie that made the memo a pending node of the action,
4816
+ // stamped it, and sent its next mainline recompute through the
4817
+ // stamped-memo arm above — a `Show` whose `when` getter owns a
4818
+ // compiler-emitted memo held an unrelated sync write for the action's
4819
+ // lifetime, against the #3460 ruling that a lane never holds a sync write.
4772
4820
  if (isEffect === EFFECT_TRACKED || el._config & (CONFIG_HELD_CHILDREN | CONFIG_LANE_FRAME))
4773
4821
  disposeChildren(el);
4774
4822
  else if (el._firstChild !== null || el._disposal !== null) {
@@ -4779,7 +4827,7 @@ function recompute(el, create = false) {
4779
4827
  el._disposal = null;
4780
4828
  el._firstChild = null;
4781
4829
  el._childCount = 0;
4782
- if (isEffect && lane) {
4830
+ if (lane) {
4783
4831
  el._config |= CONFIG_LANE_FRAME;
4784
4832
  findLane(lane)._effectQueues[0].push(() => {
4785
4833
  el._config &= ~CONFIG_LANE_FRAME;
@@ -6458,6 +6506,15 @@ function read(el) {
6458
6506
  return read(el);
6459
6507
  } else throw owner._x?._error;
6460
6508
  }
6509
+ // Ahead of the snapshot serve below: a component body's direct read is
6510
+ // wrong in the same way whether the pass is hydrating or not, and the
6511
+ // hydration pass is the console nobody is watching (#3675).
6512
+ if (strictRead)
6513
+ warnStrictReadUntracked(strictRead, {
6514
+ ownerId: c?.id,
6515
+ ownerName: c?._name,
6516
+ nodeName: owner?._name
6517
+ });
6461
6518
  if (snapshotCaptureActive && c && c._config & CONFIG_IN_SNAPSHOT_SCOPE) {
6462
6519
  const sv = el._x?._snapshotValue;
6463
6520
  if (sv !== undefined) {
@@ -6467,12 +6524,6 @@ function read(el) {
6467
6524
  return snapshot;
6468
6525
  }
6469
6526
  }
6470
- if (strictRead)
6471
- warnStrictReadUntracked(strictRead, {
6472
- ownerId: c?.id,
6473
- ownerName: c?._name,
6474
- nodeName: owner?._name
6475
- });
6476
6527
  const value = serve(el, c, owner, el._value);
6477
6528
  if (
6478
6529
  !c &&
@@ -19,12 +19,13 @@ import {
19
19
  /** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */
20
20
  const FALLBACK_FLASH_MS = 150;
21
21
  /**
22
- * Under an owner the observer marked as its own (`OBSERVE.exclude`), or
23
- * framework plumbing itself (`CONFIG_PLUMBING` — the HMR memo between a
24
- * component's root and its body, which is nobody's node): the engine records
25
- * nothing about the node. Plumbing is a bit read and excludes the node alone,
26
- * not what it owns; the observer exclusion is cached per node once any
27
- * exists, before that a flag read.
22
+ * Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
23
+ * re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
24
+ * (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
25
+ * body, which is nobody's node): the engine records nothing about the node.
26
+ * Plumbing is a bit read and excludes the node alone, not what it owns; the
27
+ * observer exclusion is cached per node once any exists, before that a flag
28
+ * read.
28
29
  */
29
30
  function excludedNode(el) {
30
31
  if ((el._config & CONFIG_PLUMBING) !== 0) return true;
@@ -333,9 +334,16 @@ function originStart(ref) {
333
334
  return;
334
335
  }
335
336
  }
336
- const frame = { kind: ref.kind, at: ref.at ?? now() };
337
- if (ref.from !== undefined) frame.from = ref.from;
338
- const under = enclosingInteraction();
337
+ // The initial navigation is the document's: its request is the time origin
338
+ // unless the router says otherwise, and it left nowhere.
339
+ const initial = ref.initial === true;
340
+ const frame = { kind: ref.kind, at: ref.at ?? (initial ? 0 : now()) };
341
+ if (!initial && ref.from !== undefined) frame.from = ref.from;
342
+ // Declared beats ambient: a router that awaited before writing hands back
343
+ // the origin it captured in the request (`undefined` when the request ran
344
+ // under none — still a declaration); what is on the stack now is whatever
345
+ // happened to be running.
346
+ const under = "interaction" in ref ? interactionOf(ref.interaction) : enclosingInteraction();
339
347
  if (under !== undefined) frame.interaction = under;
340
348
  originFrames.push(frame);
341
349
  openNavigation(frame, ref);
@@ -369,7 +377,8 @@ function formatOrigin(origin) {
369
377
  notes.push(
370
378
  `redirected from ${redirects.map(hop => hop.to ?? hop.name ?? "?").join(" → ")}`
371
379
  );
372
- return `navigation to ${name}${notes.length > 0 ? ` (${notes.join(", ")})` : ""}`;
380
+ const initial = navStates.get(origin)?.event.initial === true;
381
+ return `${initial ? "initial " : ""}navigation to ${name}${notes.length > 0 ? ` (${notes.join(", ")})` : ""}`;
373
382
  }
374
383
  default:
375
384
  return "outside the reactive system";
@@ -1844,8 +1853,10 @@ function checkOptimisticRevert(el, shown, truth, how) {
1844
1853
  // derived override promotes rather than reverts. Same predicate the hold
1845
1854
  // census uses to skip them.
1846
1855
  if (!options.optimisticReverts || isCompanion(el)) return;
1856
+ // Method call, as every commit path does: store slot nodes share one
1857
+ // comparator that reads `this._host` (#3687).
1847
1858
  const equals = el._equals;
1848
- if (equals && equals(shown, truth)) return;
1859
+ if (equals && equals.call(el, shown, truth)) return;
1849
1860
  const source = nodeName(el);
1850
1861
  // The two values are user data: quoted only under `values: "full"`; the
1851
1862
  // other levels say what happened without saying what was shown.
@@ -2142,6 +2153,7 @@ function syncNavigation(state) {
2142
2153
  }
2143
2154
  function openNavigation(frame, ref) {
2144
2155
  const event = { at: frame.at, writes: 0, origin: frame };
2156
+ if (ref.initial === true) event.initial = true;
2145
2157
  if (frame.from !== undefined) event.from = frame.from;
2146
2158
  if (frame.interaction !== undefined) event.interaction = frame.interaction;
2147
2159
  const state = {
@@ -2354,7 +2366,10 @@ function settleNavigation(state, outcome, hold) {
2354
2366
  event.settledMs = now() - event.at;
2355
2367
  event.outcome = outcome;
2356
2368
  if (hold !== undefined) event.hold = hold;
2357
- for (const f of folds) f.navigation?.(event);
2369
+ // The initial declaration is not a navigation the person waited on: its
2370
+ // `settledMs` is document start → router built, which would read as the
2371
+ // route's responsiveness in the feedback tables. Consumers get the record.
2372
+ if (event.initial !== true) for (const f of folds) f.navigation?.(event);
2358
2373
  records.emit("navigation", event, undefined);
2359
2374
  trackGraph(event);
2360
2375
  // The interaction that performed it may have been waiting only on this.