Skip to content

frames: residue 1 — D list, FRAME_HAVE_* off the client entry, lean re-ask - #3863

Closed
ryansolid wants to merge 5 commits into
feat/frames-bind-tierfrom
size/frames-residue-1
Closed

ryansolid wants to merge 5 commits into
feat/frames-bind-tierfrom
size/frames-residue-1

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Residue step 1 of the frames size pass (documentation/plans/frames-residue-pass.md §5 rows 2 and 6, + 5a) on feat/frames-bind-tier (#3862, C6) @ e7d6e9e34. Draft; rebases cleanly (the base has not moved). Branch = base + the cherry-pick of the residue doc (dae7d1337 → 2a33c338d, since #3862 does not carry it) + three item commits + the ratchet / docs commit. Every item was measured on an edited copy of this head's dist through the size harness's own bundler before it was written (re-attribution §7 method); the table below is measured vs built.

Summary

  • Frames eager: 36,007 / 11,901 → 35,250 min / 11,654 br (−757 / −247). 1,654 B above the ≤ 10 KB target (was 1,901 B above after C6).
  • The D list, mirror excluded (five of its seven units): documentAddress and its call; the second dispose map folded into fillScopes; the zombie heuristic and the #slotNodes map it read; the imperative branch of #revealSegment replaced by a default seam (revealAtOnce) so there is one reveal path and FrameOptions.reveal stays optional; showing's brand on the bare per-function component.
  • FRAME_HAVE_HEADER / FRAME_HAVE_BUDGET off the client entry's export list. Both stay on @solidjs/web/frames/server, where the header is read.
  • The lean re-ask (§3.3): the server-function client hands its response handler the call it dispatched (or answered locally) as a thunk — ctx.retry on handle, info.retry on intercept — and the frames re-ask for an errored boundary's reset is callFor(address).retry(). The RPC-seam re-ask (reask installer, the two symbol reads, createServerReference on the RPC slot) is gone. GET stays GET by construction: a GET-declared read's thunk is its own query-url dispatch.
  • Not taken (see Skipped): the capture arm (pinned), the s / v stamp (no export to re-use), the _$SC mirror (server half).

Measured-before-written

Edited dist copies through scripts/size's bundler; base = e7d6e9e34 (frames eager 36,007 / 11,901; page base 115,087 / 36,647; page live 126,964 / 40,286). min / br. Non-SC scenarios 0 / 0 on every row.

item frames eager, alone page base page live note
2b documentAddress −95 / −44 −95 / +16 −95 / +19
2c the second dispose map −73 / −24 −73 / +74 −73 / −25
2d the capture arm −272 / −90 −272 / −67 −272 / −14 not taken — pinned
2e the zombie heuristic + #slotNodes −186 / −68 −186 / −22 −186 / −48 the map's only reader was the check
2f the imperative branch — ceiling −121 / −30 −121 / +34 −121 / +81 would make reveal required
2f the imperative branch — default seam (built) −34 / −19 −34 / +54 −34 / −30
2g showing's brand −95 / −20 −98 / +85 −98 / +8
the D list as built (2b, 2c, 2e, 2f-seam, 2g), one copy −721 / −210 −724 / −184 −724 / −208
5a FRAME_HAVE_* off the entry −63 / −25 0 / 0 0 / 0 the pages tree-shook them already
6b the lean re-ask −260 / −99 −230 / −30 −235 / −65 the sf thunks ≈ +80 min on the pages; the pages still shrink
everything above, one copy −1,045 / −320 −935 / −253 −940 / −233
built head −757 / −247 → 35,250 / 11,654 −665 / −159 → 114,422 / 36,488 −670 / −161 → 126,294 / 40,125 the gap to the one-copy row is the +~50 min the source form carries over the dist edit (the dev-guarded early return, reask's function form)

vs next @ 9d89df731 (the pass's recorded measurement, identical in the C3 and integration records; consistent with #3862's deltas): frames eager −8,164 / −2,133; page base −31,335 / −8,394; page live −31,426 / −8,470; the four non-SC scenarios as the base (the hydrating scenarios' +110 / +142 / +52 br vs next are the base's A0 Size-Exceptions — 0 vs base); server renderToString +4 / −9 (as the base).

Public API changes

  • Removed: FRAME_HAVE_HEADER, FRAME_HAVE_BUDGET from the @solidjs/web/frames client entry (@experimental; both remain on @solidjs/web/frames/server). No consumer in the repo or in @solidjs/router's dist imported them from the client entry (frames-live-resume.spec imports them from frame-transport).
  • Removed: createServerReference(id) from the client half of the server-function RPC slot (getServerFunctionRPC(), @internal; A7 had added it). The router reads GET / decodeResponse off the slot; nothing read createServerReference.
  • Changed: callFor(address) (@internal) returns { id, meta, args, retry } — retry() makes the same call again.
  • Changed (type): ServerFunctionsClientConfig.responseHandler.handle's ctx gains retry(): Promise<unknown>; the responseHandler type now declares the existing intercept(info) seam, whose info carries retry() too. createServerComponentHandler's returned handle / intercept signatures carry retry?(). ServerComponentHandlerOptions unchanged.
  • Changed (behaviour of a public option): FrameOptions.reveal omitted → the default seam reveals the segment at once (content inserted before the closing comment, the comment removed), with the content applied as it is revealed exactly as on the boundary path. A placeholder whose closing comment is missing is not revealed (dev error) — before, the imperative path appended at the parent's end and the boundary path threw on the null anchor.
  • Changed (behaviour, @experimental handler): handler.showing(address, id) no longer brands the bare per-function component with COMPONENT_BINDING; it only mints the address's binding. The reference a reader holds at t=0 is the bootstrap's binding for the address (branded there).
  • Behaviour (no surface): an adopted mount with no binding (the per-function placeholder rendered directly, _$SC.r(id)) binds the function id, not an _$SC.a address record. Every serialized reference resolves to the call's binding, and a dynamic mount is called with the live accessor, so the production paths carry the address with the binding; adopted-claim-args-address.spec now mounts the binding (the production shape) and asserts the store the frame bound.
  • Behaviour (no surface): the re-ask carries the original call's per-call options (a caller-supplied signal still owns the wire, as the sf client rules for every dispatch); A7's RPC-seam form carried none.

Pins

  • The nine arms of frames-errored-reset-refetch.spec — (a) catch → reset → new request, (a′), (b) a GET-declared read re-asks over GET with the same args, (c) a re-ask failing on the wire errors the node and a further reset re-asks, (d), (e), (e′), (f), (g) — and hydration/frames-adopted-error-outward.spec (the intercepted call re-asked over the wire) green unchanged.
  • hydration/adopted-claim-args-address.spec: mounts _$SC.r(FID, ADDRESS) (the binding) instead of the bare placeholder + a seeded _$SC.a record; keeps its claim assertions and adds "the store the frame bound is the ADDRESS's, not the wire id's".
  • lifecycle-matrix/remount › "away/back over a t=0 adopted boundary" went red under the capture-arm cut → the cut is not taken (below).
  • D-list pins run: c16-reference-identity, c17-gate-bound-address, c07-store-is-truth, tier-assets-ready (×7 frames without reveal), frames-assets-client, frames-hn-client, preload-links-frame-client, the lifecycle matrix — green.
  • Suites: web client 129 files / 1,194 green; hydrate + consistency 87 / 445; server 159 / 1,511; pnpm types + test-types clean. Harness 500 cases × seeds 3289 / 91501: SC arm 0 findings; generic arm (CONSISTENCY_IGNORE=C1,C9,C19,E) 0 findings.
  • Artifacts: not re-recorded — this branch changes no server output. (The server suite rewrites __artifacts__/*.json on every run; on this machine welcome-status-streamed.json records a different chunk order than frames: C6 — binding-slot tier; _hk on event-slot consumers closes the replay window #3862 committed and write-before-resume.json flips between two chunkings run-to-run, on the untouched base sources too — timing of the streamed render, not this branch; both reverted.)

Size

scripts/size on this head: every frames / page scenario under its cap; check-floor-caps.mjs HEAD: no cap raised. Caps lowered (measured + 10 B at the 0.01 KB step): frames eager 11.92 → 11.67 KB (recorded minified 35,250); page base 36.66 → 36.50 (114,422); page live 40.30 → 40.14 (126,294). The two unrelated caps the ratchet would also lower are left alone. The three scenarios over their cap at the base (signals floor, isPending, CSR) are unchanged by this branch.

Frames eager absolute: 11,654 br — 1,654 B above the ≤ 10 KB target.

Skipped

  • 2d — the capture arm (contentHTML + the host's last-unmount capture; −272 / −90 alone). Pinned: lifecycle-matrix/remount › "away/back over a t=0 adopted boundary: the interior captured at unmount re-materializes instantly, and the refetch (now a real request) morphs it" — the residue doc called it unpinned. Without it, an away/back over a document-adopted (SSR'd) boundary shows the covering <Loading>'s fallback while the refetch is in flight instead of the interior it showed. That is a ruling on holds-latest across a remount of the document face, not a cut; left in.
  • 10 — the s / v stamp (−92 / −30 ceiling, ≈ −20 net). The export it needs does not exist: readHydratedValue is module-private in solid/src/client/hydration.ts ((initP, refresh, options), with a loading-window arm), so taking it is a new solid-js/internal export, not a 0-B re-export of an existing one. Skipped per the brief. Note for the maintainer: the frames copy in slotArgsProxy still lacks 2.0.0-rc.0 | Error Boundary not showing when expected #2997's rejection-observe for s === 2 (the export would fix that too).
  • 2a — the _$SC mirror (−234 / −80). Needs the server to emit the bootstrap unconditionally on SC pages (≈ +200 B of document output) — a server-side change for a later step (§3.7). With documentAddress gone here, the later step is the mirror alone.
  • 2f at its ceiling (making FrameOptions.reveal required: −30 vs the seam's −19). Kept optional; ~10 test files construct frames without reveal.
  • The re-ask's signal: the thunk carries the caller's options whole. Stripping signal costs ~20 min on the sf client for an edge (a caller-aborted call's boundary reset); the sf client's own rule — a caller-supplied signal owns the wire — was kept instead. Flagged in case the maintainer wants the re-ask on a fresh signal.

Docs

  • frames-residue-pass.md (cherry-picked onto this branch): status line; §7 "Landed — residue step 1" with the measured → built table, the two items left out and why, the running number; §5 rows 2 and 6 marked.
  • frames-savings-pass.md §3: row R1.
  • server-components-principles.md: the three zombie-heuristic mentions amended (§9.2.3 / the bind notes / the attribute-slot row).

ryansolid and others added 5 commits October 6, 2026 21:10
… zombie heuristic, the default reveal seam, the brand)

Five of the residue pass's seven D units (frames-residue-pass.md §1.4 /
§2), measured on an edited dist before writing (frames eager, min / br,
each alone against the C6 head 36,007 / 11,901; non-SC scenarios 0 / 0):

- 2b `documentAddress` and its call: −95 / −44. An adopted mount's
  address comes with its binding (every serialized reference resolves to
  the call's binding; a `dynamic` mount is called with the live accessor);
  a mount with no binding — the per-function placeholder rendered
  directly — binds the function id. `adopted-claim-args-address.spec`
  mounts the binding (the production shape) and now asserts the store
  the frame bound.
- 2c the second dispose map folded into `fillScopes`: −73 / −24. A live
  range binding's `insert` lives under the fill's own owner where there
  is one (stream-mounted), else under an owner registered the same way,
  so a re-call disposes whichever path the previous invocation took.
- 2e the zombie heuristic and `#slotNodes` (its only reader): −186 / −68.
  The morph relocates ranges among siblings and recreates nothing (DR-5);
  "mounted" is `#mountedSlots`. Principles §9.2.3 / the bind notes
  amended.
- 2f the imperative branch of `#revealSegment`: −34 / −19 as built (the
  ceiling −121 / −30 would make `FrameOptions.reveal` required; a default
  seam keeps it optional). A frame created without `reveal` reveals
  through `revealAtOnce` — `content()` inserted before the closing
  comment, the comment removed — so the content is applied as it is
  revealed on both paths. A placeholder missing its closing comment is
  not revealed (dev error; `#showFallback` already materialized nothing
  there — the boundary path threw on the null anchor before).
- 2g `showing`'s brand on the bare per-function component: −95 / −20. The
  reference a reader holds at t=0 is the bootstrap's binding for the
  address, branded there.

Not taken: 2d the capture arm (`contentHTML` + the last-unmount capture,
−272 / −90) — `lifecycle-matrix/remount` › "away/back over a t=0 adopted
boundary" pins it (the captured interior shows while the refetch is in
flight); the residue doc called it unpinned. 2a the `_$SC` mirror needs
the server's unconditional bootstrap — a later step.

Together (edited dist): −721 / −210 on frames eager. Pins: web client,
hydrate + consistency, server suites green.

Co-authored-by: Cursor <cursoragent@cursor.com>
…export list

Residue pass §2 row 5a, measured on an edited dist: −63 min / −25 br on
frames eager (the scenario measures the whole entry with its export
list; the pages tree-shake the two constants already — 0 / 0 there).
Both stay exported from `@solidjs/web/frames/server`, where the header
is read; the client sends the have-list itself (`resume`, through the
handler). No consumer in the repo or in @solidjs/router's dist imports
either name from the client entry (`frames-live-resume.spec` imports
them from frame-transport). Public surface removal, `@experimental`.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ient, the RPC-seam re-ask deleted

Residue pass §3.3 (row 6b). A7's re-ask recorded the call per address and
re-invoked it by minting a reference through the RPC slot, copying the
declared metadata and honouring GET — 165 min of `reask` plus two
symbols, because `handle` saw `{ id, meta, args }` and no callable. The
server-function client has the call in hand at both places a frames
handler records one, so it hands the call over as a thunk:

- `dispatchServerFunction`: `ctx.retry = () => fetchServerFunction(base,
  id, sent, args, meta, callArgs)` — the options as the caller sent them
  (captured before this dispatch's controller is spliced in, so the
  re-ask mints its own; a caller-supplied signal rides along, as it owns
  the wire). A GET-declared read's `base` is its query url, so GET stays
  GET by construction.
- the intercept sites (`createServerReference`'s `run`, `GET`'s `run`):
  `info.retry` is the `send` the document's answer pre-empted. (`live`'s
  intercepts carry none: the handler records no live call — the loop owns
  its reconnects.)

The frames client's re-ask is `callFor(address).retry()`; the `reask`
installer in `installServerComponents`, its reads of the RPC slot and the
metadata brand, and `createServerReference` on the client half of the RPC
slot (`provideRPC`) go — the router reads `GET` / `decodeResponse` off the
slot, nothing read `createServerReference`.

Measured on an edited dist: frames eager −260 min / −99 br; page base
−230 / −30; page live −235 / −65 (the pages carry the sf client's +≈80
min thunks and still shrink). Non-SC 0 / 0. Pins: the nine arms of
`frames-errored-reset-refetch` (incl. (b) GET re-asks over GET, (c) a
wire failure errors the node and a further reset re-asks) and
`hydration/frames-adopted-error-outward` (the intercepted call re-asked
over the wire) green unchanged.

Public surface: `responseHandler.handle`'s ctx gains `retry()`;
`responseHandler.intercept` (an existing seam, now on the type) carries
`retry()` on its info; `callFor` (`@internal`) returns the call with its
`retry`; `getServerFunctionRPC()`'s client half loses
`createServerReference(id)` (`@internal`).

Co-authored-by: Cursor <cursoragent@cursor.com>
…27.9 K / R 2.4 K / F 7.9 K / D 1.8 K min; R+D 1,126 br measured), 24 candidate cuts measured on edited dist copies, the path from 11.84 KB (post-C6) to ≤ 10.0: the residue proper stalls at ≈ 10.4 with C2; crossing needs both C1 (holes) and the claims chunk → ≈ 9.67 KB with ≈ 330 B of margin

(cherry picked from commit dae7d13)
…/ live 40.14 KB); residue doc §7 landed table; plan §3 row R1

Caps lowered to measured + 10 B at the 0.01 KB step for the three
scenarios this step shrank — frames eager 11.92 -> 11.67 KB (35,250 min /
11,654 br), page base 36.66 -> 36.50 (114,422 / 36,488), page live 40.30
-> 40.14 (126,294 / 40,125); the two unrelated caps the ratchet would also
lower are left alone; no cap raised (check-floor-caps HEAD clean).

frames-residue-pass.md: status line + §7 — the measured -> built table
for every item of the step, the two it left out and why (the capture arm
is pinned by lifecycle-matrix/remount; readHydratedValue is
module-private), the running number (11,654; 1,654 B above the target).
frames-savings-pass.md §3: row R1.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fce81d2

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

Copy link
Copy Markdown
Member Author

Folded into #3860 (wip/frames-tiers-integration) per the maintainer — "merging is a bottleneck." The stack was linear, so the integration branch was fast-forwarded over this PR's five commits (f7570eb58 … fce81d2c0, incl. the residue doc's cherry-pick) to #3868's head d9395e398. The branch is kept.

On the integrated head the D list, FRAME_HAVE_* off the client entry and the lean re-ask are as this PR built them: the nine arms of frames-errored-reset-refetch and frames-adopted-error-outward green unchanged. The removals (FRAME_HAVE_HEADER / FRAME_HAVE_BUDGET off the client entry, createServerReference off the RPC slot), callFor's { id, meta, args, retry }, ctx.retry / info.retry, the reveal default seam (missing closing comment → not revealed) and showing's brand are in #3860's Public API changes; the kept capture arm, the re-ask's signal, and the s / v stamp's missing export are its decisions 11, 12 and 18.

— Claude via Cursor

@ryansolid ryansolid closed this Oct 7, 2026
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.

1 participant