diff --git a/.changeset/trim-batch-bytes-1026.md b/.changeset/trim-batch-bytes-1026.md new file mode 100644 index 0000000000..45f9bfe0be --- /dev/null +++ b/.changeset/trim-batch-bytes-1026.md @@ -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. diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts index c8103258ea..7b36a765cb 100644 --- a/packages/signals/src/boundaries.ts +++ b/packages/signals/src/boundaries.ts @@ -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; @@ -193,7 +196,7 @@ function caught(b: Boundary, node: Computed, 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 @@ -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 { @@ -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; } @@ -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); } } } @@ -558,7 +562,7 @@ function createBoundary( 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 // `` wrapping it would: created before the owner's context names it. @@ -598,7 +602,7 @@ function createBoundary( 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 => { // Reveal order (reveal.ts): a gated slot shows its fallback — or, in a @@ -612,7 +616,7 @@ function createBoundary( // 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); - 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; @@ -717,10 +721,10 @@ function createBoundary( 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; }, diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index fc84981e29..7f0b970edc 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -706,11 +706,13 @@ export function recompute(el: Computed, 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) { diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index f70e0c2563..22ec77baa7 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -568,6 +568,7 @@ export function laneRead(c: Computed | null, el: Signal | Computed | null, el: Signal | Computed 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; diff --git a/packages/signals/src/store/reconcile.ts b/packages/signals/src/store/reconcile.ts index 3b031aaf09..2b4a664853 100644 --- a/packages/signals/src/store/reconcile.ts +++ b/packages/signals/src/store/reconcile.ts @@ -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). @@ -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); diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 52d67a5bcb..0abec66c32 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -690,8 +690,8 @@ function ensurePB(target: StoreTarget): Record { // 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 @@ -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; @@ -1888,10 +1888,10 @@ function enumerationSource(target: StoreTarget): Record { // 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 @@ -2165,8 +2165,8 @@ const traps: ProxyHandler = { 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; } } @@ -2207,8 +2207,8 @@ const traps: ProxyHandler = { 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; } @@ -2412,8 +2412,8 @@ export function storeSetter(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 @@ -2614,7 +2614,7 @@ function snapshotWalk(value: any, seen: Map, 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; } diff --git a/packages/solid/src/client/hydration.ts b/packages/solid/src/client/hydration.ts index 64cea3c999..73895fbe9b 100644 --- a/packages/solid/src/client/hydration.ts +++ b/packages/solid/src/client/hydration.ts @@ -1272,12 +1272,10 @@ function hydratedCreateErrorBoundary( fn: () => T, fallback: (error: () => unknown, reset: () => void) => U ): Accessor { - if (!sharedConfig.hydrating || noHydrationId()) return coreErrorBoundary(fn, fallback); - markTopLevelSnapshotScope(); - const parent = getOwner()!; - const expectedId = peekNextChildId(parent); - if (sharedConfig.has!(expectedId)) { - const err = sharedConfig.load!(expectedId); + if (sharedConfig.hydrating && !noHydrationId()) { + markTopLevelSnapshotScope(); + const expectedId = peekNextChildId(getOwner()!); + let err: unknown; // The server drew the fallback; the client's own run decides. The // content runs now with its data adopted by id but claiming no DOM — // the markup is the fallback's — so what it throws is the client's @@ -1285,21 +1283,19 @@ function hydratedCreateErrorBoundary( // the server's fallback up behind a hold: a render effect in the content // throwing the server's error until reset(), disposed with the content // when it re-runs (#4273). - if (err !== undefined) { + if (sharedConfig.has!(expectedId) && (err = sharedConfig.load!(expectedId)) !== undefined) { // The content's owner, until a fallback is built. let o: any; const read = coreErrorBoundary( - () => { - const prev = _claimOwner; - o = getOwner(); - // A claim window over no owner: nothing under it claims. - _claimOwner = {} as Owner; - try { - return fn(); - } finally { - _claimOwner = prev; - } - }, + // While hydrating, a claim window over no owner: nothing under it + // claims. (`hydrateWindow` with no id or scope, hydration already on + // and the snapshot root marked above, only swaps the claim owner.) + // A later run claims nothing either way: claims are read only while + // hydrating. + () => + _hydratingValue + ? hydrateWindow("", fn, undefined, ((o = getOwner()), {} as Owner)) + : fn(), (e, reset) => ((o = 0), fallback(e, reset)) ); // No fallback yet: the content ran clean. Hold the server's fallback