@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.
- package/dist/dev-shared.js +100 -49
- package/dist/dev.attribution.js +27 -12
- package/dist/dev.js +163 -22
- package/dist/observe/core/attribution.js +36 -21
- package/dist/observe/core/constants.js +21 -12
- package/dist/observe/core/core.js +44 -34
- package/dist/observe/core/dev.js +38 -15
- package/dist/observe/core/optimistic.js +5 -5
- package/dist/observe/core/owner.js +11 -11
- package/dist/observe/core/scheduler.js +6 -6
- package/dist/observe/core/verdict.js +2 -2
- package/dist/observe/store/next/store.js +159 -31
- package/dist/prod/core/constants.js +21 -12
- package/dist/prod/core/core.js +27 -17
- package/dist/prod/store/next/store.js +201 -73
- package/dist/types/core/attribution-hooks.d.ts +28 -0
- package/dist/types/core/attribution.d.ts +9 -0
- package/dist/types/core/constants.d.ts +21 -12
- package/dist/types/core/dev.d.ts +59 -6
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/signals.d.ts +18 -1
- package/dist/types/store/next/store.d.ts +4 -2
- package/package.json +1 -1
package/dist/dev-shared.js
CHANGED
|
@@ -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
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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.
|
|
516
|
-
//
|
|
517
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4759
|
-
//
|
|
4760
|
-
//
|
|
4761
|
-
//
|
|
4762
|
-
//
|
|
4763
|
-
//
|
|
4764
|
-
//
|
|
4765
|
-
//
|
|
4766
|
-
//
|
|
4767
|
-
// the
|
|
4768
|
-
//
|
|
4769
|
-
//
|
|
4770
|
-
//
|
|
4771
|
-
//
|
|
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 (
|
|
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 &&
|
package/dist/dev.attribution.js
CHANGED
|
@@ -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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* exists, before that a flag
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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.
|