Skip to content

frames: refetched content lands at the transition's commit - #3759

Merged
ryansolid merged 2 commits into
nextfrom
fix/frames-refetch-commit
Oct 5, 2026
Merged

ryansolid merged 2 commits into
nextfrom
fix/frames-refetch-commit

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Ported onto L2 (2026-10-04)

This PR was written against the pre-L2 scheduler and has been rebased and re-landed on the L2 hold model (next @ bde4299, after #3788–#3791). Authorship is kept on the feature commit; the port is one commit plus a size commit.

Subsumed by L2 (tests ported verbatim, green on untouched L2):

  • Both signals pins — compute-write-joins-transition.test.ts (a compute-half write under a held transaction joins it and is read before the optimistic intent ends) and settle-folds-queued-writes.test.ts. On L2 "a staging read is the frame's" and a write queued before a landing joins the transaction; the pins stay as regression tests.
  • Every frames-optimistic-hold.spec.tsx case, including the four new ones (literal args, {$ref} args on the shared host, a fill inside a nested region, a refetch that renames the nested region).

Ported (red on untouched L2): frames-morph-in-transition.spec.tsx (3/3). The staging design lands as described above, in L2 vocabulary: a response for a showing address is staged under the address, the call resolves to a content token, dynamic delivers it as an address, and the mount's follow effect lands it in two halves — stagedContent.preview from the compute half (the pass of the transaction that delivered the content, so the staged slot args are held with that transaction and commit in the frame whose landing dissolves its optimistic guesses, never a flush behind it) and stagedContent.commit from the effect half (at land). The preview half was verified to be load-bearing on L2: disabling it returns the one-flush optimistic gap (server=false / derived=false) in all three multi-flight specs. Staged data tables (stageTables) on the shared host and root-routed single-flight regions are as in the original.

Switch adaptation and ruling. The L2 carve (e133516) had moved the switch's re-arm and rebind into the compute half so a second switch mid-flight could not deadlock, and called the rebind "plumbing, not display". This PR's switch test needs the rebind at the commit. Maintainer ruling (2026-10-04): the switch is display — one reveal. e133516's wording is superseded (stated in the comment at followAddress). Resolution: the rebind runs in the effect half at the commit; the re-arm stays in the pass; a frameless waiter { apply: settle } is registered on the host under the new address so the switch gate settles on the new address's first content without the rebind — a second switch mid-flight still binds to the live call (call-driven-lifecycle stays green). Consequence: host.get(newAddress) is truthy while a switch pends, so a refetch of the new address that arrives in that window is staged rather than written through.

Public API changes

No new public exports, props, options or parameters beyond those the original description lists. On L2 they are:

  • Behaviour: a response for a showing call that no reader mounts is never shown (previously it morphed every mount of the address on arrival; frames-live.spec.tsx's supersession test mounts the second caller's result).
  • Behaviour: single-flight regions for a showing call land when the cache takes the slice, not when the body arrives.
  • Behaviour: the address accessor a custom ServerComponentHandlerOptions.component receives can deliver a content token (address + "\0" + version) for a refetch of the address it shows; the bundled mounts strip it with contentAddress.
  • Behaviour (L2 port): an address switch's new content lands at the commit of the transaction that read the address, not in the pass; host.get(newAddress) is truthy while the switch pends.
  • @internal surface: Frame.preview?(records, resolve?), FrameHost.preview?(chunk, resolve?); new frame-transport.ts exports contentAddress, stagedContent, STAGED_DATA (a handler option symbol, not on the options type).
  • Signals options: the fills' args signal now uses ownedWrite: true (staged args are written from a compute half). Both mount gate signals already carried ownedWrite on L2 (e133516, the re-arm in the pass); the port adds nothing there.
  • No new diagnostic codes. Changeset: .changeset/frames-refetch-lands-at-commit.md (@solidjs/web, patch).

Tests

Measured on the rebased branch (next @ bde4299), clean build with the native compiler rebuilt:

  • packages/web vitest: 1139 passed, 1 expected fail (1140) (1138 before test(signals): pin #3726 on L2 — derived store sync landing wakes parked readers (supersedes #3732) #3789 added derived-presence-async-3726.spec.tsx).
  • packages/signals vitest: 4919 passed, 3 expected fail, 2 skipped (4924).
  • packages/web tsc: tsconfig.test.json, tsconfig.test.augment.json, tsconfig.json (--noEmit) all clean.
  • packages/signals/scripts/rules-index.mjs --check current.
  • scripts/size gate: every scenario within its cap; check-floor-caps.mjs origin/next passes with the exception line below and fails without it.

New/changed specs: frames-morph-in-transition.spec.tsx (3, new), frames-optimistic-hold.spec.tsx (6, four new), frames-live.spec.tsx (supersession test updated), compute-write-joins-transition.test.ts (2, new), settle-folds-queued-writes.test.ts (2, new).

Size

Re-measured after the rebase against next @ bde4299 (decimal KB; caps at head + 10 B, rounded up to two decimals). The frames client is the whole cost: +2,255 B minified in web/frames, +7 B in the server-functions client slice; the pages' remaining ~210 B minified is attribution drift.

Scenario next @ bde4299 This PR Δ brotli Δ minified Cap
frames: eager client consumer 12,997 B 13,770 B +773 B +2,262 B 13.00 → 13.78 KB
page: base server components 44,048 B 44,762 B +714 B +2,265 B 44.05 → 44.78 KB (floor-caps.json)
page: live server components 47,670 B 48,436 B +766 B +2,265 B 47.67 → 48.45 KB (floor-caps.json)

The L2 port is 21 B minified smaller than the original PR's frames client (the follow effect is shared between the cold mount and adoption, and awaitContent is inlined); the frameless waiter is included. Dropping either half was weighed and rejected (see the ledger notes in scripts/size/scenarios.js). Accepted by the maintainer (2026-10-04).

Size-Exception: frames: eager client consumer 12,997 -> 13,770 B (cap 13.00 -> 13.78 KB); page: base server components 44,048 -> 44,762 B (cap 44.05 -> 44.78 KB); page: live server components 47,670 -> 48,436 B (cap 47.67 -> 48.45 KB) — #3759's staging so refetched content lands at the transition's commit; accepted by the maintainer (2026-10-04).


Original description

Written pre-L2 against next @ 9338c00. Its Size table, Size-Exception line and test counts are superseded by the sections above; it is preserved verbatim for the design rationale.

Summary

A refetch (or a single-flight region) for a server-component call a boundary is showing used to morph the boundary as soon as its body arrived. That tore against the transition that drove it: isPending and optimistic writes over the old slot args (§9.2.2) were still held while the new markup was already on screen, or the optimistic intent ended one flush before the new args landed (a server=false / derived=false frame).

The cache is now the commit point. Such a response is staged:

  • Its chunks buffer under the address. The call resolves (when the body is buffered) to a binding naming the staged version, a content token. dynamic treats addresses as opaque, so the token reaches the mount like an address switch, inside the transition that read the call.
  • The mount lands it from the render effect that follows its address accessor, in two halves:
    • Compute half (stagedContent.preview): the staged slot args are pushed into the live fills under that transition, so a fill deriving optimistic intent over a server arg never reads the old arg once the intent ends. Occurrences whose args add or rename a region are structural and wait.
    • Effect half (stagedContent.commit): the markup, the store, mounts, re-calls and region changes land with everything else the transition holds.
  • On the shared host, the response's data chunks decode into staged tables installed at commit, so {$ref} args resolve against the new response while the content still showing keeps its own tables.
  • Single-flight regions route by root (nested region ids buffer under their root's entry) and show when the integration's cache takes the mutation's slice. Any cache that subscribes to flight data (Solid Router, TanStack Query, …) drives the landing; nothing is router-specific.

Cold mounts and switches to an address nothing shows are unchanged: they write through with header-time resolution, because the shell gate is their hold.

Public API and behaviour changes

  • Behaviour: a response for a showing call that no reader mounts is never shown. Previously it morphed every mount of the address on arrival. (frames-live.spec.tsx's supersession test now mounts the second caller's result.)
  • Behaviour: single-flight regions for a showing call land when the cache takes the slice, not when the body arrives.
  • Behaviour: the address accessor a custom component receives (ServerComponentHandlerOptions.component) can now deliver a content token (address + "\^@" + version) for a refetch of the address it shows. The bundled mounts strip it with contentAddress; a custom component that compares or keys on the raw address would see the token.
  • Internal (@internal) surface:
    • Frame.preview?(records, resolve?) and FrameHost.preview?(chunk, resolve?).
    • New frame-transport.ts exports contentAddress, stagedContent and STAGED_DATA (a handler option symbol, not part of the options type).
  • Signals options: the fills' args signal and both mount gate signals now use ownedWrite: true. Staged args are written from a compute half, and a commit from one mount's render can release another mount's gate.
  • Observe tier: "frame applied" is still recorded when a chunk is buffered, not at commit.

Size

Size-Exception: refetched content lands at the transition's commit; the staging and the two-phase landing are the feature (+715 to +749 B brotli, ~+2.3 KB minified, all in the frames client).

Scenario next @ 9338c00 This PR Cap
frames: eager client consumer 12,977 B 13,726 B 12.98 → 13.73 KB
page: base server components 46,249 B 46,964 B 46.25 → 46.97 KB (frozen)
page: live server components 50,428 B 51,150 B 50.45 → 51.15 KB (frozen)

Re-measured after merging next @ 9338c00 (first measured against 98d35b9 at +749 / +825 / +765 B, page base 46.20 → 47.01 KB). The base page's cap is now lower than first requested (#3741 and #3749 moved the base under the PR); the frames and live page caps are unchanged.

Trimmed before accepting. Dropping either half was weighed and rejected: without the preview the one-flush optimistic gap returns; without staged tables the shown content reads the new response's refs before the commit. Ledger notes are in scripts/size/scenarios.js.

Tests

  • packages/signals/tests/compute-write-joins-transition.test.ts: a compute-half write under a held transaction joins it and is read before the optimistic intent ends (pins the core behaviour the preview relies on).
  • packages/signals/tests/settle-folds-queued-writes.test.ts.
  • packages/web/test/frames-optimistic-hold.spec.tsx: multi-flight with literal args, {$ref} args on the shared host, a fill inside a nested region, and a refetch that renames the nested region (it reproduced a lost rename in the preview, fixed here).
  • packages/web/test/frames-morph-in-transition.spec.tsx: the morph waits for the transition.
  • packages/web/test/frames-live.spec.tsx: supersession test updated (behaviour change above).

Verified locally:

  • packages/web vitest: 1130 passed, 1 expected fail. The two lowercase-on-attribute.spec.tsx failures come from fix!: lowercase on* names are attributes, not event handlers #3753 and a stale local native compiler binary; they pass with JSX_COMPILER=babel.
  • packages/signals vitest: 4836 passed, 3 expected fail, 2 skipped.
  • All four packages/web tsc configs.
  • scripts/size gate, and check-floor-caps.mjs with the exception line.

After merging next @ 9338c00 (clean build, native compiler rebuilt):

  • packages/web vitest: client 1137 passed + 1 expected fail, server 1400 passed + 2 skipped, hydrate 275 passed (frames specs: 92 passed).
  • packages/signals vitest: 4836 passed, 3 expected fail, 2 skipped. packages/solid vitest: 819 passed.
  • pnpm types, packages/web and packages/solid test-types.
  • scripts/size gate, and check-floor-caps.mjs against origin/next with the exception line.

@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f7f415

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
todos-server-example Patch
@solidjs/compiler Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

ryansolid added a commit that referenced this pull request Oct 2, 2026
Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.32 KB 0 B 7.33 KB ✅
signals: + createStore 14.51 KB 0 B 14.53 KB ✅
signals: + isPending/latest 9.45 KB 0 B 9.45 KB ✅
app: render + one signal (the simple-app floor) 9.81 KB 0 B 9.83 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.65 KB 0 B 17.66 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 28.79 KB 0 B 28.80 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.81 KB 0 B 12.82 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.39 KB 0 B 14.39 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.59 KB 0 B 28.61 KB ✅ lazy-page.js 0.04 KB
frames: eager client consumer (frames client + transport, lazy codec) 13.77 KB +773 B (+5.9%) 13.78 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 44.76 KB +714 B (+1.6%) 44.78 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
page: live server components (base + live/GET + action + isPending/latest) 48.44 KB +766 B (+1.6%) 48.45 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.41 KB 0 B 20.42 KB ✅

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body).

@coveralls

coveralls commented Oct 2, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37268989085

Coverage remained the same at 75.991%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1195
Covered Lines: 962
Line Coverage: 80.5%
Relevant Branches: 925
Covered Branches: 649
Branch Coverage: 70.16%
Branches in Coverage %: Yes
Coverage Strength: 27.77 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 19.62%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 187 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ memo + sync render effect only (reference) 32.1 ms 26.8 ms +19.62%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/frames-refetch-commit (4f7f415) with next (bde4299)

Open in CodSpeed

ryansolid added a commit that referenced this pull request Oct 5, 2026
…rted onto L2)

A refetch or single-flight region for a call a boundary is showing is
staged in the transport and the call resolves to a content token; the
mount lands it in the two halves of the render effect that follows its
address accessor. Under the hold model the two halves ride core's seams
unchanged: the compute half is the delivering transaction's pass (its
`preview` writes — the staged slot args — are held with it and commit in
the frame whose landing dissolves the lane's guesses), the effect half is
its `land` (the commit replays the staged chunks). Both signals pins
(`compute-write-joins-transition`, `settle-folds-queued-writes`) and the
optimistic-hold specs were green on untouched L2; the morph-in-transition
specs were red (3/3) and are closed by the staging.

Composes with e133516 (the switch gate re-arms in the pass): the rebind
— display — moves to the effect half so a switch's content waits for the
commit too (`frames-morph-in-transition` switch case), while the gate
settles on the new address's first write through a frameless waiter
registered on the host, so a second switch mid-flight still binds
(`call-driven-lifecycle`). No core seam touched. Size caps not raised:
frames 12,997 -> 13,770 B (cap 13.00 KB), page base 44,017 -> 44,765,
page live 47,656 -> 48,447 — reported for the maintainer's exception.

Signals 4898/0/2, web 1138/0 + server 1411/2 skipped + hydrate 275/0.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 5, 2026
…e base 44.03 -> 44.78, page live 47.67 -> 48.43

Refetched content lands at the transition's commit (the staging, the
content token, the two halves of the mount's follow effect, the staged
data tables, and on L2 the switch's rebind in the effect half with the
frameless host waiter). Measured against `next` @ 1a3f87f (after
#3776): frames 12,997 -> 13,770 B (+773; +2,262 B minified, frames
client +2,255), page base 44,029 -> 44,762 B (+733; +2,265 B minified),
page live 47,654 -> 48,418 B (+764; +2,265 B minified). Caps set at
head + 10 B. Ledger notes in scenarios.js re-applied from the PR's
1b347c7 against these numbers.

Size-Exception: frames: eager client consumer 12,997 -> 13,770 B (cap 13.00 -> 13.78 KB); page: base server components 44,029 -> 44,762 B (cap 44.03 -> 44.78 KB); page: live server components 47,654 -> 48,418 B (cap 47.67 -> 48.43 KB) — #3759's staging so refetched content lands at the transition's commit; accepted by the maintainer (2026-10-04).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the fix/frames-refetch-commit branch from 6f09b62 to 52dd93b Compare October 5, 2026 04:11
ryansolid and others added 2 commits October 4, 2026 22:39
…rted onto L2)

A refetch or single-flight region for a call a boundary is showing is
staged in the transport and the call resolves to a content token; the
mount lands it in the two halves of the render effect that follows its
address accessor. Under the hold model the two halves ride core's seams
unchanged: the compute half is the delivering transaction's pass (its
`preview` writes — the staged slot args — are held with it and commit in
the frame whose landing dissolves the lane's guesses), the effect half is
its `land` (the commit replays the staged chunks). Both signals pins
(`compute-write-joins-transition`, `settle-folds-queued-writes`) and the
optimistic-hold specs were green on untouched L2; the morph-in-transition
specs were red (3/3) and are closed by the staging.

Composes with e133516 (the switch gate re-arms in the pass): the rebind
— display — moves to the effect half so a switch's content waits for the
commit too (`frames-morph-in-transition` switch case), while the gate
settles on the new address's first write through a frameless waiter
registered on the host, so a second switch mid-flight still binds
(`call-driven-lifecycle`). No core seam touched. Size caps not raised:
frames 12,997 -> 13,770 B (cap 13.00 KB), page base 44,017 -> 44,765,
page live 47,656 -> 48,447 — reported for the maintainer's exception.

Signals 4898/0/2, web 1138/0 + server 1411/2 skipped + hydrate 275/0.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…e base 44.05 -> 44.78, page live 47.67 -> 48.45

Refetched content lands at the transition's commit (the staging, the
content token, the two halves of the mount's follow effect, the staged
data tables, and on L2 the switch's rebind in the effect half with the
frameless host waiter). Measured against `next` @ bde4299 (after
#3790/#3791): frames 12,997 -> 13,770 B (+773; +2,262 B minified, frames
client +2,255), page base 44,048 -> 44,762 B (+714; +2,265 B minified),
page live 47,670 -> 48,436 B (+766; +2,265 B minified). Caps set at
head + 10 B. Ledger notes in scenarios.js re-applied from the PR's
1b347c7 against these numbers, after #3743's page-base note.

Size-Exception: frames: eager client consumer 12,997 -> 13,770 B (cap 13.00 -> 13.78 KB); page: base server components 44,048 -> 44,762 B (cap 44.05 -> 44.78 KB); page: live server components 47,670 -> 48,436 B (cap 47.67 -> 48.45 KB) — #3759's staging so refetched content lands at the transition's commit; accepted by the maintainer (2026-10-04).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the fix/frames-refetch-commit branch from 52dd93b to 4f7f415 Compare October 5, 2026 05:42
@ryansolid
ryansolid merged commit 203ab1a into next Oct 5, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants