diff --git a/.changeset/unread-derive-holds-nothing.md b/.changeset/unread-derive-holds-nothing.md new file mode 100644 index 0000000000..8c57020203 --- /dev/null +++ b/.changeset/unread-derive-holds-nothing.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +`createStore(fn)` that nothing reads no longer holds a plain write with its derive's flight: a derive over a `latest()`-reading memo observed the flight as a frame reader although its store had no reader. diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index bfc9b2c8a5..6138eba09c 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -127,7 +127,10 @@ function route(c: Computed, t: Transaction): void { function observeFlight(c: Computed, el: Computed): boolean { if (el._config & CONFIG_INPUTS_PUBLISHED || (el._config & CONFIG_HELD && bornInto(c, txOf(el)))) return false; - if (globalQueue._running) { + // A derived store's derive whose store has no reader (`fam.live` empty: + // no leaf has a node) is no display: it observes nothing and holds nothing + // (F-3, D-11) — it stays live only to keep the store's data. + if (globalQueue._running && (c as any).fam?.live.size !== 0) { joinFuture(null); if (!(el._config & CONFIG_HELD)) queuePendingNode(el); verdictRead(c, flushTransaction!, true); diff --git a/packages/web/test/async-gen/store-unread-derive-e3.spec.ts b/packages/web/test/async-gen/store-unread-derive-e3.spec.ts new file mode 100644 index 0000000000..a0b2c95c69 --- /dev/null +++ b/packages/web/test/async-gen/store-unread-derive-e3.spec.ts @@ -0,0 +1,38 @@ +/** + * @vitest-environment jsdom + */ +// async-gen rerun on rc.20, E3 (a derived store `createStore(d => { d.v = m() })` +// reads like `m`): a store nobody reads, over an async memo whose input reads +// `latest()`. Its derive stays live (the store keeps its data unobserved), and +// reading a verdict memo makes its pass a verdict reader, so at the re-ask it +// observed the flight as a frame reader and held the flush with it: a plain +// write to `s0` beside it waited for `m1`'s answer. A derive whose store has +// no reader is no display and holds nothing (F-3, D-11); the memo form, whose +// `m1` nobody reads either, shows the write at once. +import { expect, test } from "vitest"; +import { runScenario } from "./run.js"; + +// [s0=1 m0=memo(latest(s0)+s0 !%4=2) m1=async(m0 !%4=1)] :: S1 t s0=0 t +const sc: any = { + signals: [1], + memos: [ + { + async: false, + deps: [ + { src: { k: "s", i: 0 }, how: "latest" }, + { src: { k: "s", i: 0 }, how: "plain" } + ], + poison: 2 + }, + { async: true, deps: [{ src: { k: "m", i: 0 }, how: "plain" }], poison: 1 } + ], + tree: { t: "leaf", text: [], attrs: [{ how: "plain", src: { k: "s", i: 0 } }], grouped: false }, + steps: [{ t: "settle", pick: 1 }, { t: "task" }, { t: "write", s: 0, v: 0 }, { t: "task" }] +}; + +test("E3: an unread store's derive does not hold a plain write", async () => { + const memo = await runScenario(sc, {}); + const store = await runScenario(sc, { store: true }); + expect(store.screens).toEqual(memo.screens); + expect(store.final).toBe(memo.final); +}); diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index b797b2a3d9..0c4f9878e8 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -17,7 +17,7 @@ }, "page: live server components (base + live/GET + action + isPending/latest)": { "cap": "38.91 KB", - "minified": 117544 + "minified": 117566 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.29 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index d3ef4fffbe..7bcbb47f12 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1445,9 +1445,10 @@ module.exports = [ // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 11.10 KB -> 11.12 KB, recorded minified 30,596 -> 30,641 B (+45 B), measured locally (linux) at 11,109 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): 11.12 KB -> 11.17 KB, recorded minified 30,641 -> 30,771 B (+130 B), measured locally (linux) at 11,153 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A verdict reader born into a hold joins it at its re-pass (#4471, #4467, 2026-10-30, re-measured over next @ 749b0d7): cap unchanged at 11.17 KB, recorded minified 30,771 -> 30,800 B (+29 B), measured locally (linux) at 11,166 B brotli. Correctness (D-25, D-13); Size-Exception in #4471 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.17 KB", + // A derived store with no live leaf skips its flight's observe (#4479, 2026-10-30, re-measured over next @ 663ecb0): 11.17 KB -> 11.19 KB, recorded minified 30,800 -> 30,822 B (+22 B), measured locally (linux) at 11,177 B brotli. Correctness (F-3, D-11); Size-Exception in #4479 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "11.19 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30800, + capMinified: 30822, alias }, { @@ -5617,6 +5618,7 @@ module.exports = [ // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 38.83 KB -> 38.85 KB, recorded minified 117,331 -> 117,376 B (+45 B), measured locally (linux) at 38,839 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): cap unchanged at 38.85 KB, recorded minified 117,376 -> 117,514 B (+138 B), measured locally (linux) at 38,849 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A verdict reader born into a hold joins it at its re-pass (#4471, #4467, 2026-10-30, re-measured over next @ 749b0d7): 38.85 KB -> 38.91 KB, recorded minified 117,514 -> 117,544 B (+30 B), measured locally (linux) at 38,898 B brotli. Correctness (D-25, D-13); Size-Exception in #4471 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // A derived store with no live leaf skips its flight's observe (#4479, 2026-10-30, re-measured over next @ 663ecb0): cap unchanged at 38.91 KB, recorded minified 117,544 -> 117,566 B (+22 B), measured locally (linux) at 38,849 B brotli. Correctness (F-3, D-11); Size-Exception in #4479 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"], capMinified: floorMinified["page: live server components (base + live/GET + action + isPending/latest)"], @@ -5936,11 +5938,12 @@ module.exports = [ // #4428 fallback memo lazy (+7 B over next @ ab3d2add, which sits +15 over this record; 2026-10-30): 42.73 KB -> 42.75 KB, recorded minified 125,518 -> 125,540 B (+22 B), measured locally (linux) at 42,735 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): 42.75 KB -> 42.79 KB, recorded minified 125,540 -> 125,655 B (+115 B), measured locally (linux) at 42,777 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A verdict reader born into a hold joins it at its re-pass (#4471, #4467, 2026-10-30, re-measured over next @ 749b0d7): 42.79 KB -> 42.83 KB, recorded minified 125,655 -> 125,684 B (+29 B), measured locally (linux) at 42,817 B brotli. Correctness (D-25, D-13); Size-Exception in #4471 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // A derived store with no live leaf skips its flight's observe (#4479, 2026-10-30, re-measured over next @ 663ecb0): cap unchanged at 42.83 KB, recorded minified 125,684 -> 125,706 B (+22 B), measured locally (linux) at 42,822 B brotli. Correctness (F-3, D-11); Size-Exception in #4479 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "42.83 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 123,191 -> 123,249 B (+58 B minified over next's record), measured locally (linux) at 41,899 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 123,203 -> 123,224 B (+21 B minified over next's record), measured locally (linux) at 41,966 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 123,224 -> 123,733 B (+403 B minified over next), measured locally (linux) at 42,119 B brotli (+169 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 125684, + capMinified: 125706, alias: pageAlias }, { @@ -6344,11 +6347,12 @@ module.exports = [ // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): cap unchanged at 48.44 KB, recorded minified 142,735 -> 142,780 B (+45 B), measured locally (linux) at 48,394 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): 48.44 KB -> 48.51 KB, recorded minified 142,780 -> 142,918 B (+138 B), measured locally (linux) at 48,498 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A verdict reader born into a hold joins it at its re-pass (#4471, #4467, 2026-10-30, re-measured over next @ 749b0d7): cap unchanged at 48.51 KB, recorded minified 142,918 -> 142,947 B (+29 B), measured locally (linux) at 48,462 B brotli. Correctness (D-25, D-13); Size-Exception in #4471 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // A derived store with no live leaf skips its flight's observe (#4479, 2026-10-30, re-measured over next @ 663ecb0): cap unchanged at 48.51 KB, recorded minified 142,947 -> 142,969 B (+22 B), measured locally (linux) at 48,496 B brotli. Correctness (F-3, D-11); Size-Exception in #4479 for the floor. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "48.51 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 140,451 -> 140,509 B (+58 B minified over next's record), measured locally (linux) at 47,516 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 140,463 -> 140,485 B (+22 B minified over next's record), measured locally (linux) at 47,547 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 140,485 -> 140,983 B (+405 B minified over next), measured locally (linux) at 47,722 B brotli (+162 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 142947, + capMinified: 142969, alias: pageAlias, conditions: solidConditions, compile: { hydratable: true }