Skip to content

frames: C2 — live wire tier - #3868

Closed
ryansolid wants to merge 1 commit into
size/frames-residue-4-sref-tablefrom
feat/frames-wire-tier
Closed

ryansolid wants to merge 1 commit into
size/frames-residue-4-sref-tablefrom
feat/frames-wire-tier

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Frames savings pass §3 row C2 — the live wire tier, in the lean form the maintainer ruled, on residue 4's head (55eb4c346, the stack #3867 → #3866 → #3864 → #3863 → #3862 → #3860 → #3849 → next). The live wire moves out of the eager frames client into the sixth lazy chunk, @solidjs/web/frames/wire (frames/src/wire-tier.ts → wire.js, 1,922 min / 934 br):

  • the connection (connect): the per-address connection a live() loop's response runs under — the open-frame count its end is judged by (an end with frames open is a death the loop reconnects from; with none a completion), the join of a second loop onto an address already live (one wire per address, the second loop reads the same store), connection.ended / cancel, the SSE reader selection (wire.open(body) for an event stream, ChunkReader otherwise), proxied so a drained body applies nothing further;
  • supersession (cancel), reached from bump through tierLoads.wire?.r?.cancel;
  • the have-list ledger per mount (have / haveOf, RFC 11 §9.5), fed by the client's #recordHave at its three sites, with encodeHaveList and the FRAME_HAVE_* literals bundled;
  • the resume shape (resume → { position: String(version), headers: { "X-Frame-Have": … } }).

The eager client keeps: applyFrames reading options[LIVE_WIRE] as its reader (one line); handle's live arm — prepareTier("wire").then(() => tierLoads.wire.r.connect(…)) — awaiting residency before the body is read, so a live connection without the tier cannot happen; onLive() { prepareTier("wire") }; resume delegating.

Preload-at-call, as the plan's §1 wire row bounds it: live()'s makeIterable fires responseHandler.onLive() at the call, before its first fetch — the import races only the request; a non-live call never loads the tier. The hook rides responseHandler.onLive (the maintainer's adjustment 2 — no new ServerFunctionsClientConfig key; measured ≈ 45 br lighter on the pages).

Running number: frames eager 33,767 / 11,193 → 32,802 / 10,888 br — 888 B above the ≤ 10 KB target (was 1,193 B above). What remains of the incremental path is C1 (≈ −440) and the claims chunk (≈ −270).

Not in this PR: the trace update-site wait (landed apart, #3861).

Measured before written

Re-measured on this head (the base had moved by residue 1–4 — the sync loop, insert-placed fills, the table-owned pending read — so the phase-1 edits were re-derived: .wt-logs/c2b-edit.mjs, c2b-measure.mjs; edited dist copies of the frames client and the sf client through scripts/size's own bundler). min / br; frames eager, then page base, page live:

variant frames eager page base page live non-SC
base (55eb4c346) 33,767 / 11,193 112,938 / 36,071 124,810 / 39,747 —
del — the ceiling (the wire out whole; unsound alone) −1,432 / −472 −1,404 / −483 −1,399 / −464 0 / 0
lean — the tier owns the connection, the ledger, the resume; the client keeps the arm, the hook, the delegations −965 / −310 −979 / −285 −946 / −300 0 / 0
the carrier (lean − del) ≈ +467 / +162
of which the hook (onLive fired at the call) ≈ 20 / 15 +85 br
the ledger's residue (#recordHave ×3 forwarding) ≈ 112 / 27
the field residue ≈ 106 / 14

Net −310 br on frames eager cleared the −200 stop line, so the tier was built. Built: −965 / −305 (+0 min / +5 br over the copy).

Public API changes

  1. Removed: Frame.have?(): Record<string, string> | undefined from the public Frame interface (frames/src/frame-client.ts). The ledger is the tier's haveOf(frame) (@internal). The maintainer's adjustment 3 — an approved-class removal. frames-live-resume.spec re-pointed.
  2. Moved: encodeHaveList from frames/src/frame-transport.ts into the tier (@internal). decodeHaveList and the FRAME_HAVE_HEADER / FRAME_HAVE_BUDGET constants stay in frame-transport (they were already off the client entry — residue 1, frames: residue 1 — D list, FRAME_HAVE_* off the client entry, lean re-ask #3863; nothing to re-export).
  3. Added: the @solidjs/web/frames/wire export path (the chunk; types/frames/wire-tier.d.ts; every export @internal).
  4. Added: ServerFunctionsClientConfig.responseHandler.onLive?(): void — optional; fired by live() at the call, before its first fetch, per call. ServerFunctionsClientConfig itself gains no key.
  5. Added: ServerComponentHandler.onLive(): void (the frames handler's implementation of 4).

Behaviour, no surface: while the tier is not resident, responseHandler.resume answers undefined (the ledger lives in the tier) — see the degraded case under Pins.

Pins

consistency/tier-wire-preload.spec (new, 3): the hook starts the import synchronously at getRoom(), before any fetch; the response arm awaits residency (chunks sent, nothing applied, the fallback shown, no status) and the release lands the content; X-Frame-Tiers: wire on a live response joins the same load (the loader called once); a non-live call never loads the tier and its refetch bumps nothing of the tier's; reconnect through the tier carries Last-Event-ID and the have-list ({ "": root, "lh:1": hole }), morphs in place, completes to closed; the degraded first connect, pinned as accepted: content a plain call applied with digests while the tier was gated → the live call's first connect carries neither X-Frame-Have nor Last-Event-ID → full snapshot; the release lands the live root; the reconnect after a death carries both from the live root's ledger.

The brief described the degraded case as "sends the ordinal with no have-list". With resume fully delegated (the lean form) the ordinal goes too while the tier is absent. The server never read it for a frame render — liveSource compares Last-Event-ID to a value digest a frame answer does not have; only decodeHaveList(X-Frame-Have) is read in the frame sink — so the outcome is the same full snapshot. Pinned as built; flagged here in case the ordinal was wanted for its own sake.

Every live suite green through the tier: frames-live-resume re-pointed to the tier's haveOf / encodeHaveList; frames-live and frames-live-showing warm it (beforeAll(() => prepareTier("wire")) — resident cells); frames-get, frames-plain-bound, the server frame-live-* / server-functions-live-* suites unchanged.

The coverage closure (server-functions-live-loop.spec, new, 19, client suite): live()'s loop driven from the consumer's side over hand-framed bodies — the codec's chunk stream, and the same payloads as server-sent events with id: lines — ending as each test says (cleanly / dropping / hanging until the request's signal): the non-server-function throw; a handler without intercept (and onLive fired before the fetch); a first-connect failure as the call's rejection (no status, no retry); a death reconnecting on the online wake, the reader's cursor riding back as Last-Event-ID; the fail-fast 4xx closing with the error; 408 / 425 / 429 retrying; Retry-After naming the wait; return() mid-connect (the arriving stream closed, the wire severed) / mid-backoff (the sleep cut) / mid-seed (a late local answer not yielded); invoke's signal mid-stream (a rejection, ended for good) / mid-backoff (the sleep woken, the connect refused); a host without a global EventTarget; live(GET(fn)) at the live address over GET; the handler's local answers (a hit at the call yielded first and adopted; a deferred hit awaited, settling to a value / to nothing; a miss at the call but a hit at connect — one value, no wire).

live's per-function branch coverage, counted as the audit did (§2.4: v8 coverage over the client suite + the hydrate suite, merged; server-functions/src/client.ts, the function live):

branch sites fully covered branch paths
base 55eb4c346 (its own tests) 26 / 59 91 / 129
this head 57 / 60 129 / 132

The three left: return() landing between a value's arrival and its read (L1593), and the resume / seed arms' stopped-while-pending elses (L1527, L1579's implicit else). The brief's "22/61 → ≥ 45/61": the audit's 22/61 was counted on an older head whose live had 61 sites; by sites on this head the closure is 26 → 57 of 60.

Suites: web client 130 / 1,218; hydrate + consistency 89 / 452; server 159 / 1,511; solid 42 / 833; types + test-types clean. Harness: CONSISTENCY_CASES=500 × seeds {3289, 91501}: SC arm 0 findings; generic arm (CONSISTENCY_IGNORE=C1,C9,C19,E) 0 findings.

Size

scripts/size on this head (fresh turbo --force build, native compiler built fresh), vs the base 55eb4c346 and vs next @ 9d89df731 (unchanged since int3's fresh measurement). min / br:

scenario this head vs base vs next cap
frames: eager client consumer 32,802 / 10,888 −965 / −305 −10,612 / −2,899 11.21 → 10.90 KB
page: base server components 112,003 / 35,722 −935 / −349 −33,754 / −9,160 36.09 → 35.74 KB
page: live server components 123,903 / 39,412 −907 / −335 −33,817 / −9,183 39.76 → 39.43 KB
the other 14 scenarios 0 / 0 0 / 0 (hydrating ×3 the base's A0 exceptions; server renderToString +4 / −9 within ±10) unchanged
  • Chunk, reported not counted: wire.js 1,922 min / 934 br (the plan's est. ≈ 1,300 / 470 priced the dispatch; the tier carries the whole connection). The other five lazy chunks unchanged.
  • Live page, honestly: this page LOADS wire.js at its first live() call, so its total transfer is 39,412 + 934 = 40,345 br against the base's 39,747 (+598) — the chunk's own weight plus the carrier, the price of the eager −335 and of the frames-eager −305 on pages that never go live.
  • Caps ratcheted to measured + 10 B at the 0.01 KB step (scenarios.js ledger lines, floor-caps.json with recorded minified); no cap raised (check-floor-caps.mjs against the base: clean). The three pre-existing local FAILs (signals: core floor +11 B, + isPending/latest +23 B, CSR with Show/For +45 B over cap) are byte-identical to every earlier head and to next — the known local-vs-CI brotli difference, not this PR's.

Not done

  • live's last three branch sites (above) — a return() race and two stopped-while-pending elses; not reachable deterministically from the consumer's side.
  • The ordinal on the degraded first connect (flagged under Pins) — the lean form drops it with the have-list; a two-line resume fallback in the client would restore it at ≈ +40 br if wanted.
  • C1 (holes tier) and the claims chunk — the remaining items of the incremental path to ≤ 10 KB.

…ume in a lazy chunk; `live` coverage closure

The live wire moves out of the eager frames client into the sixth lazy
tier, `@solidjs/web/frames/wire` (frames/src/wire-tier.ts → wire.js,
1,922 min / 934 br): `connect` — the per-address connection a live()
loop's response runs under (the open-frame count its end is judged by:
a death with frames open vs a completion; the join of a second loop onto
an address already live; `connection.ended` / `cancel`; the SSE reader
selection, proxied so a drained body applies nothing further), `cancel`
(supersession from `bump`), `have` / `haveOf` (the have-list ledger per
mount, RFC 11 §9.5, fed by the client's `#recordHave` at its three
sites) with `encodeHaveList` and the FRAME_HAVE_* literals bundled, and
`resume` (the ordinal + the have-list header). The eager client keeps:
`applyFrames` reading `options[LIVE_WIRE]` as its reader; `handle`'s
live arm awaiting `prepareTier("wire")` before the body is read; `onLive`
→ `prepareTier("wire")`; `resume` and `bump`'s cancel delegating through
`tierLoads.wire?.r`.

Preload-at-call: `live()`'s makeIterable fires the new optional
`responseHandler.onLive()` before the first fetch, so the import races
only the request; a non-live call never loads the tier. Accepted degraded
case: content applied before the chunk was resident kept no ledger, and
`resume` answers nothing while the tier is absent — that first connect
sends neither X-Frame-Have nor Last-Event-ID and gets a full snapshot
(the server never read the ordinal for a frame render); pinned.

Public surface: `Frame.have?()` removed from the Frame interface (the
ledger is the tier's `haveOf(frame)`); `encodeHaveList` moved from
frame-transport to the tier (@internal); `@solidjs/web/frames/wire`
export path added; `responseHandler.onLive?()` and
`ServerComponentHandler.onLive()` added. `ServerFunctionsClientConfig`
gains no key.

Pins: consistency/tier-wire-preload.spec (3) — the hook starts the import
at the call before any fetch, the arm awaits residency, X-Frame-Tiers:
wire joins the same load, a non-live call never loads the tier,
reconnect through the tier with Last-Event-ID + the have-list, the
degraded first connect. frames-live-resume re-pointed to the tier's
haveOf / encodeHaveList; frames-live / frames-live-showing warm the tier.
The coverage closure: server-functions-live-loop.spec (19, client suite)
drives live()'s loop over hand-framed chunk / SSE bodies — first-connect
failure, the fail-fast 4xx set and 408 / 425 / 429 / Retry-After, the
cursor as Last-Event-ID, the online wake, return() mid-connect /
mid-backoff / mid-seed, invoke's signal mid-stream / mid-backoff, no
global EventTarget, live(GET(fn)), the handler's local answers — `live`
per-function branch coverage (audit §2.4 method, client + hydrate) 26/59
sites (91/129 paths) on the base → 57/60 (129/132).

Size (scripts/size, this head vs the base 55eb4c3): frames eager
33,767 / 11,193 → 32,802 / 10,888 (−965 min / −305 br; cap 11.21 → 10.90
KB); page base 112,938 / 36,071 → 112,003 / 35,722 (−349 br; cap 36.09 →
35.74); page live 124,810 / 39,747 → 123,903 / 39,412 (−335 br eager; cap
39.76 → 39.43; the page loads wire.js at its first live() call — total
transfer 40,345 br, +598 against the base); non-SC scenarios 0 / 0; no
cap raised. Measured before writing on edited dist copies through the
harness's bundler: ceiling −1,432 / −472, lean −965 / −310, the carrier
≈ +467 / +162.

Suites: web client 130 / 1,218, hydrate + consistency 89 / 452, server
159 / 1,511, solid 42 / 833, types + test-types clean; harness 500 ×
{3289, 91501} SC arm 0, generic arm (CONSISTENCY_IGNORE=C1,C9,C19,E) 0.
Docs: savings plan §1 wire row built, §3 row C2 landed, §4 chunk table;
residue doc status + §7e.

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

changeset-bot Bot commented Oct 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9395e3

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 #3860 → #3861 → #3862 → #3863 → #3864 → #3866 → #3867 → #3868 was linear (every head an ancestor of the next), so the integration branch was fast-forwarded to this PR's head d9395e398; this PR's own commit is the top of that chain unchanged. The branch is kept.

On the integrated head, built fresh: frames eager 32,802 min / 10,888 br (−2,899 br vs next @ 9d89df731), page base 112,003 / 35,722 (−9,160), page live 123,903 / 39,412 (−9,183); wire.js 1,922 / 934 lazy. Caps as this PR ratcheted them (10.90 / 35.74 / 39.43 KB); check-floor-caps.mjs against the Phase A base: no cap raised. Suites, test-types, the dist specs and the 500-case harness (both arms, seeds 3289 / 91501) green. The degraded first connect (no have-list, no ordinal) and the ≈ +40 br ordinal fallback are #3860's decision 17.

— Claude via Cursor

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