Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/trim-batch-bytes-1026.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/signals": patch
"solid-js": patch
---

Smaller bundles: builds without the optimistic-store, reveal-order or lane hook modules drop their dead branches, and the #4273/#4275 fixes share existing paths.
22 changes: 13 additions & 9 deletions packages/signals/src/boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export interface RevealHooks {
_unregister(controller: object, b: Boundary): void;
_changed(controller: object): void;
}
// Every call site checks the hooks themselves first (not only the slot or
// controller, which are never set without them): a build that does not
// import `createRevealOrder` folds the reveal-order arms away.
export let revealHooks: RevealHooks | null = null;
export function setRevealHooks(h: RevealHooks): void {
revealHooks = h;
Expand Down Expand Up @@ -193,7 +196,7 @@ function caught(b: Boundary, node: Computed<any>, error: unknown): void {
// nothing on screen. The fallback is instantiated once per show — a later
// error reaches it through the `error` accessor, which is why it is one.
if (!b._fallback) redraw(b);
if (b._reveal !== null) revealHooks!._changed(b._reveal);
if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
}

/** A re-armed boundary found something pending: fresh again, and the swap
Expand All @@ -206,7 +209,8 @@ function flip(b: Boundary): void {
b._lane = b._armLane;
const o = b._output!;
if (!b._lane && o._config & CONFIG_HELD && txOf(o) !== flushTransaction) unhold(o);
if (b._lane !== null && b._output !== null) laneStageHook!(b._output, b._lane, true, false);
if (laneStageHook && b._lane !== null && b._output !== null)
laneStageHook(b._output, b._lane, true, false);
}

export function redraw(b: Boundary): void {
Expand Down Expand Up @@ -240,7 +244,7 @@ function catchStatus(
b._readers.size === 0
) {
if (b._fallback) redraw(b);
if (b._reveal !== null) revealHooks!._changed(b._reveal);
if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
}
return false;
}
Expand Down Expand Up @@ -384,7 +388,7 @@ function boundarySeam(): void {
if (b._readers.size === 0) {
collecting.delete(b);
if (b._fallback) redraw(b);
if (b._reveal !== null) revealHooks!._changed(b._reveal);
if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
}
}
}
Expand Down Expand Up @@ -558,7 +562,7 @@ function createBoundary<T>(
if (getOwner())
cleanup(() => {
collecting.delete(b);
if (controller) revealHooks!._unregister(controller, b);
if (revealHooks && controller) revealHooks._unregister(controller, b);
});
// The `on` dependencies live OUTSIDE the boundary, as the condition of a
// `<Show>` wrapping it would: created before the owner's context names it.
Expand Down Expand Up @@ -598,7 +602,7 @@ function createBoundary<T>(
b._tree = tree;
// A slot of the reveal order in its context (reveal.ts): gated until the
// controller releases it — before the output's first pass.
if (controller) revealHooks!._register(controller, b);
if (revealHooks && controller) revealHooks._register(controller, b);
const output = computed<T>(
(): T => {
// Reveal order (reveal.ts): a gated slot shows its fallback — or, in a
Expand All @@ -612,7 +616,7 @@ function createBoundary<T>(
// settled): the pass goes on to show it — the redraw it asks for is
// refused mid-pass and not needed.
link(tree, getOwner() as Computed<any>);
if (b._reveal !== null) revealHooks!._changed(b._reveal);
if (revealHooks && b._reveal !== null) revealHooks._changed(b._reveal);
if (b._gated) {
if (b._collapsed) {
b._fallback = true;
Expand Down Expand Up @@ -717,10 +721,10 @@ function createBoundary<T>(
release(b);
// Shown: the slot leaves its reveal order (a later re-arm gates nobody
// else; the frontier moves on).
if (b._reveal !== null) {
if (revealHooks && b._reveal !== null) {
const controller = b._reveal;
b._reveal = null;
revealHooks!._changed(controller);
revealHooks._changed(controller);
}
return value;
},
Expand Down
4 changes: 3 additions & 1 deletion packages/signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,11 +706,13 @@ export function recompute(el: Computed<any>, create: boolean = false): void {
// confirm is a landing too — the lane's end commits it) and notifies
// only a correction: an equal truth re-runs nothing (lanes.ts). (As the
// lane's own work the lane judged it — `laneStage`.)
// (The hook first: a build without lanes folds the arm away.)
if (
laneOutcomeHook &&
!create &&
lane === null &&
el._config & CONFIG_GUESS &&
laneOutcomeHook!(el, value, errored)
laneOutcomeHook(el, value, errored)
) {
// The truth, staged under the lane's parent.
} else if (errored) {
Expand Down
59 changes: 25 additions & 34 deletions packages/signals/src/core/lanes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ export function laneRead(c: Computed<any> | null, el: Signal<any> | Computed<any
// An errored member's lane value predates its error; the error is the answer (#4106).
if (!guess && status & STATUS_ERROR) return NOT_PENDING;
if (c === null) return laneValueOf(el);
const pending = !guess && status & STATUS_PENDING;
if (!tracking || c._config & CONFIG_CHILDREN_FORBIDDEN) {
// An untracked read inside lane work derives from the lane like a
// tracked one, minus the subscription — `untrack` is about
Expand All @@ -576,51 +577,41 @@ export function laneRead(c: Computed<any> | null, el: Signal<any> | Computed<any
// lanes read by one pass link, as for tracked reads). Outside lane
// work, the screen.
if (passLane === null || c._config & CONFIG_CHILDREN_FORBIDDEN) return display(el);
enterLane(l, c);
if (!guess && status & STATUS_PENDING) return NOT_PENDING;
return laneValueOf(el);
}
// D-28: a leaf a verdict read moved into the lane still throws on a flight
// that has never shown — the pass leaves the lane, and its frame holds as
// if the read had come first (S-39, #4275).
if (
(c as any)._type === EFFECT_RENDER &&
passLane?._parent!._verdict === passLane &&
!guess &&
status & STATUS_PENDING &&
el._x!._lane === NOT_PENDING
)
setPassLane(null);
if ((c as any)._type === EFFECT_RENDER && (passLane === null || !sameLane(passLane, l))) {
if (!guess) {
// A flight whose value has never shown: nothing to show — the leaf
// waits on it, and its own frame holds (A15, #3334), not the lane's
// parent. The landing lands the frame: its own pass (the flush
// joined its transaction) reads the answer it waited on — the lane's
// staging, revealed at this seam.
if (status & STATUS_PENDING && el._x!._lane === NOT_PENDING) return NOT_PENDING;
} else if ((c as any)._type === EFFECT_RENDER) {
// A flight whose value has never shown: nothing to show — the leaf
// waits on it, and its own frame holds (A15, #3334), not the lane's
// parent. The landing lands the frame: its own pass (the flush joined
// its transaction) reads the answer it waited on — the lane's staging,
// revealed at this seam.
const never = pending && el._x!._lane === NOT_PENDING;
// D-28: a leaf a verdict read moved into the lane still throws on such
// a flight — the pass leaves the lane, and its frame holds as if the
// read had come first (S-39, #4275).
if (never && passLane?._parent!._verdict === passLane) setPassLane(null);
if (passLane === null || !sameLane(passLane, l)) {
if (never) return NOT_PENDING;
if (
!guess &&
el._pendingValue !== NOT_PENDING &&
c._config & CONFIG_HELD &&
flushTransaction !== null &&
resolveTx(flushTransaction) === txOf(c)
)
return el._pendingValue;
}
if (l._held || (!guess && status & STATUS_PENDING)) {
if (!guess && !l._shown && status & STATUS_UNINITIALIZED) throw new NotReadyError(null);
// Re-derived at the reveal; once per pass (the pass may also be a stale
// reader of a transaction, REACTIVE_FRAME_READ — both reruns apply).
if (!(c._flags & REACTIVE_SCREEN_READ)) {
c._flags |= REACTIVE_FRAME_READ | REACTIVE_SCREEN_READ;
l._reruns.push(c);
if (l._held || pending) {
if (!guess && !l._shown && status & STATUS_UNINITIALIZED) throw new NotReadyError(null);
// Re-derived at the reveal; once per pass (the pass may also be a stale
// reader of a transaction, REACTIVE_FRAME_READ — both reruns apply).
if (!(c._flags & REACTIVE_SCREEN_READ)) {
c._flags |= REACTIVE_FRAME_READ | REACTIVE_SCREEN_READ;
l._reruns.push(c);
}
return display(el);
}
return display(el);
}
}
enterLane(l, c);
if (!guess && status & STATUS_PENDING) return NOT_PENDING;
return laneValueOf(el);
return pending ? NOT_PENDING : laneValueOf(el);
}

/** The value a guess covered: the node's committed value — a store slot's
Expand Down
11 changes: 7 additions & 4 deletions packages/signals/src/core/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,14 @@ export function schedule() {
*/
export const ROOT_ERROR_HOOK: unique symbol = Symbol.for("solid-js/root-error-hook") as any;

const HALTED = "[REACTIVITY_HALTED]";

export function haltReactivity(cause?: unknown): void {
if (halted) return;
halted = true;
// A cleanup error collected before the halt is dropped (owner.ts).
rethrowCleanupError(true);
let message = "[REACTIVITY_HALTED]";
let message = HALTED;
if (__DEV__) {
message +=
" An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
Expand Down Expand Up @@ -785,7 +787,7 @@ function notifyHalted(): void {
__DEV__
? "[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error." +
haltedAt
: "[REACTIVITY_HALTED]"
: HALTED
);
}

Expand Down Expand Up @@ -1016,11 +1018,12 @@ export class GlobalQueue implements IQueue {
// This flush's own runs, the lanes' reveals, and the landings' held runs
// are collected apart and ordered below.
const own = this._queues;
const lanes: [QueueCallback[], QueueCallback[]] = (this._queues = [[], []]);
const lanes: [QueueCallback[], QueueCallback[]] = [[], []];
// The passes that read this frame's stagings as the screen read held
// writes if it parked: the lane seam re-derives them on the committed
// world (a lane's runs wait this round).
if (laneSeamsHook) {
this._queues = lanes;
laneSeamsHook(t !== null ? stagedReaders : null);
if (stagedReaders.length) stagedReaders.length = 0;
}
Expand Down Expand Up @@ -1179,7 +1182,7 @@ function land(u: Transaction): void {
// (`_nodes` still lists them).
if (__OBSERVE__ && attrHooks !== null) attrHooks.transitionSettled(u);
const frame = reruns(u);
if (u._marks !== null) releaseMarksHook!(u._marks);
if (releaseMarksHook && u._marks !== null) releaseMarksHook(u._marks);
// Old children die in the commits (cleanups first), then the stashed
// effects run ahead of this flush's own. A node a lane took over since
// (a guess written over the staged truth) is the lane's to land.
Expand Down
2 changes: 1 addition & 1 deletion packages/signals/src/store/affects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function walk(
if (t !== undefined) {
if (t.pb !== null) entry._scope.add(t.v);
// The writer's view: the tick's own unflushed guesses are in motion too.
if (t.fam?.opt === true) raw = optHooks!.view(t, raw, true);
if (optHooks && t.fam?.opt === true) raw = optHooks.view(t, raw, true);
const nodes = t.n;
if (nodes !== null)
for (const k of Reflect.ownKeys(nodes))
Expand Down
3 changes: 3 additions & 0 deletions packages/signals/src/store/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export interface OptHooks {
* into the draft. */
reconcile(draft: any, incoming: any, keyFn: ((item: any) => any) | null): void;
}
// Set before any family is optimistic (`fam.opt`): a call site checks the
// hooks first, so a build that does not import optimistic.ts folds the arm
// away.
export let optHooks: OptHooks | null = null;
export function installOptHooks(hooks: OptHooks): void {
optHooks = hooks;
Expand Down
6 changes: 3 additions & 3 deletions packages/signals/src/store/reconcile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export function reconcileState(
// truth but an optimistic edit — the keyed diff is written into the draft
// (a matched row keeps its proxy, its changed leaves become its guesses),
// and the setter's exit turns the draft into guesses like any.
if (userWriting() && t.fam?.opt === true) {
optHooks!.reconcile(state, unwrapValue(value), keyFn);
if (optHooks && userWriting() && t.fam?.opt === true) {
optHooks.reconcile(state, unwrapValue(value), keyFn);
return;
}
// Replace-mode root handed another store's proxy: chain to it (§7b).
Expand Down Expand Up @@ -100,7 +100,7 @@ function applyAdopt(t: StoreTarget, incoming: any, keyFn: KeyFn | null, proj = f
// Q-D: a landing on an optimistic family is reconciled against the VIEW
// — key matching from the arrangement the guesses made (R28), so a row
// the guess added keeps its proxy when the server returns it.
const prevView = t.fam?.opt === true ? optHooks!.view(t, old) : old;
const prevView = optHooks && t.fam?.opt === true ? optHooks.view(t, old) : old;
const nextArr = Array.isArray(incoming);
const shallow = t.s === true;
adoptPB(t, incoming, false);
Expand Down
26 changes: 13 additions & 13 deletions packages/signals/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,8 @@ function ensurePB(target: StoreTarget): Record<PropertyKey, any> {
// An optimistic edit composes on the view the user saw and becomes
// guesses at the setter's exit: nothing is staged, nothing folds
// (optimistic.ts).
if (userWrite && target.fam?.opt === true && !pendingNotify.has(target))
return (target.pb = optHooks!.draft(target));
if (optHooks && userWrite && target.fam?.opt === true && !pendingNotify.has(target))
return (target.pb = optHooks.draft(target));
if (pb !== null && !isOwned(pb)) {
// An adopted object awaiting its fold is the user's: the draft writes a
// clone of it. The clone is the batch's staging — unless the adoption is
Expand Down Expand Up @@ -1142,12 +1142,12 @@ function notifyWrites(t: StoreTarget): void {
// Optimistic channel: a user's writes on an optimistic family become
// guesses on the nodes (lanes); the draft is discarded, the committed
// backing never touched.
if (userWrite && t.fam?.opt === true) {
if (optHooks && userWrite && t.fam?.opt === true) {
// The draft is discarded first: the diff reads the target (its nodes
// are born from the committed frame, not the draft).
t.pb = null;
t.wk = null;
t.pb = optHooks!.writes(t, pb);
t.pb = optHooks.writes(t, pb);
return;
}
const old = t.v;
Expand Down Expand Up @@ -1888,10 +1888,10 @@ function enumerationSource(target: StoreTarget): Record<PropertyKey, any> {
// A container's arrangement guess is not a backing: the view composes
// over the base it is shown over.
const base = asBacking(readNode(getContainerNode(target)));
return target.fam?.opt === true ? optHooks!.view(target, base) : base;
return optHooks && target.fam?.opt === true ? optHooks.view(target, base) : base;
}
const src = readSource(target);
return optRead(target) ? optHooks!.view(target, src) : src;
return optHooks && optRead(target) ? optHooks.view(target, src) : src;
}

/** A read on an optimistic family that composes the lanes' values: not the
Expand Down Expand Up @@ -2165,8 +2165,8 @@ const traps: ProxyHandler<StoreTarget> = {
node._value = present; // the link follows the inner store (S4)
if (node._config & CONFIG_OVERRIDE) return !!nv;
return present;
} else if (optRead(target)) {
const guessed = optHooks!.has(target, key);
} else if (optHooks && optRead(target)) {
const guessed = optHooks.has(target, key);
if (guessed !== undefined) return guessed;
}
}
Expand Down Expand Up @@ -2207,8 +2207,8 @@ const traps: ProxyHandler<StoreTarget> = {
return undefined;
} else src = readSource(target, key, true);
let desc: PropertyDescriptor | undefined;
if (!enumerating && optRead(target)) {
const guessed = optHooks!.descriptor(target, key);
if (optHooks && !enumerating && optRead(target)) {
const guessed = optHooks.descriptor(target, key);
if (guessed === null) return undefined;
desc = guessed;
}
Expand Down Expand Up @@ -2412,8 +2412,8 @@ export function storeSetter<T>(proxy: T, fn: (draft: T) => T | void, guard = tru
// A returned replacement: on an optimistic family a user's is itself an
// optimistic edit — diffed against the view as guesses (`next` parity);
// otherwise it is adopted as the incoming truth.
if (guard && target.fam?.opt === true && !writeOverride)
optHooks!.writes(target, unwrapValue(result));
if (optHooks && guard && target.fam?.opt === true && !writeOverride)
optHooks.writes(target, unwrapValue(result));
else adoptPB(target, unwrapValue(result));
}
// A34 (3), #3612 — the derived store's twin of `setMemo`: a user write to
Expand Down Expand Up @@ -2614,7 +2614,7 @@ function snapshotWalk(value: any, seen: Map<object, any>, fam: StoreFamily | nul
if (t.ovl) materializePB(t);
backing = t.pb!;
}
if (optRead(t)) backing = optHooks!.view(t, backing);
if (optHooks && optRead(t)) backing = optHooks.view(t, backing);
if (backing === src) break;
src = backing;
}
Expand Down
Loading
Loading