diff --git a/.changeset/frames-a1b-stage-deletion-s-ref.md b/.changeset/frames-a1b-stage-deletion-s-ref.md new file mode 100644 index 000000000..cb3e81bb0 --- /dev/null +++ b/.changeset/frames-a1b-stage-deletion-s-ref.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +frames: A1b + A4 (S-ref) — a slot record's `{$ref}`s settle at the host's write, through the response's own data table (`FrameHostOptions.resolve(ref, frameId, version, current)`); an undelivered key is a pending read the response's `data` chunk settles and its `complete`/`error` rejects (L1 — a value that never comes is an error, not a silence); a fresh mount waits for the record to settle, a mounted occurrence's prop pends and holds its value. Codec data tables are per response (keyed by frame id and version). Deleted: `ServerComponentHandlerOptions.onStream`, `STAGED_DATA` and the staged tables, `FrameHost.resolve`, `FrameHostOptions.isContainer`/`FrameHost.isContainer`, `Frame.rebase`, the frame's record dedupe (`#refArgsUnchanged`, `#slotResolvedRefs`) — a re-sent record's equality is the fill's per-prop memo's — and the frame's error/root value latches (applied state keyed by record identity). `FrameHost.preview`/`Frame.preview` lose their `resolve` parameter and stay: the compute-half preview is what stages a refetch's args with the transaction that read it. diff --git a/.changeset/frames-a4-declared-slot-records.md b/.changeset/frames-a4-declared-slot-records.md new file mode 100644 index 000000000..8acd67bdd --- /dev/null +++ b/.changeset/frames-a4-declared-slot-records.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +frames: A4 (S-record) — the document face declares a slot record at its marker: `_$HY.r["sc:slot::"]` is a pending promise written with the occurrence's markup and settled with the args by the record's data script (the shape a fragment's `_fr` takes), so the adopting client awaits a record that trails its range's reveal through the value's own `.then` instead of polling the registry (C2 (a2) flips). Output shape: ≈ +32–38 B per document slot record (the resolver helpers are shared with the page's fragment declarations); a sync render (`renderToString`) writes the settled value as before. Deleted: the #2968 `setTimeout` re-drain poll and `FrameOptions.recordsPending` / `FrameOptions.drainRecords`. diff --git a/documentation/server-components/frames-rulings.md b/documentation/server-components/frames-rulings.md index f0e654613..a6eb86400 100644 --- a/documentation/server-components/frames-rulings.md +++ b/documentation/server-components/frames-rulings.md @@ -3,9 +3,9 @@ **Status: ruled.** **3.1, ruled 2026-10-05** (hydration-done follows non-SC Solid 2) and the **Principle** below, which is the maintainer's (three statements, 2026-10-05); every other ruling was re-derived from it and -marked *recommended-by-principle* where the principle decides a reading — -and on **2026-10-06** the maintainer nodded the lot (*"other than that lets -do your recommendations"*): **1.3, 1.4 full, 1.6 (i), 2.3, 3.3** are ruled +marked _recommended-by-principle_ where the principle decides a reading — +and on **2026-10-06** the maintainer nodded the lot (_"other than that lets +do your recommendations"_): **1.3, 1.4 full, 1.6 (i), 2.3, 3.3** are ruled as recommended; **3.6 is ruled (iii)** (the consumer parks); the one wire fact (whether `slot` may trail `html`) is ruled by leaving the order unspecified and pinning the client's tolerance; **3.5 is closed**, @@ -74,11 +74,11 @@ is a response too — version 0, the t = 0 frame (DR-4). **A server component's output — its frame markup, its records, its traces — is rendered data like any other async data in Solid 2, and follows the rules Solid 2 already has for async data; the frames layer adds a transport, never -a second model.** The maintainer's three statements, verbatim: *"SCs are no -different than other rendered data."* *"Hydration ending should follow our -Solid 2 non-SC."* *"SCs participate in `` until their first flush +a second model.** The maintainer's three statements, verbatim: _"SCs are no +different than other rendered data."_ _"Hydration ending should follow our +Solid 2 non-SC."_ _"SCs participate in `` until their first flush the same way [as any async data], and can have their own internal loading -states that the client doesn't care about."* Every ruling below is therefore +states that the client doesn't care about."_ Every ruling below is therefore one of two things — the frames **form of a rule the core already has** (`Restates:` names it: the L2 rulings 1–9 and A-rules of `packages/signals/docs/SPEC-ASYNC-SEMANTICS.md`; the `` rules of @@ -97,29 +97,29 @@ Four corollaries, one per seam and one for the boundary the seams meet at: 1. **Response identity IS async supersession.** An address is a source; a response is a flight answering one question on it; a refetch or a switch - is a **new question** on the same source. L2 ruling 5 (provenance): *"a + is a **new question** on the same source. L2 ruling 5 (provenance): _"a landing asking an older question than the guess it lands beneath is not - its answer … nothing moves on screen."* A18 (supersession, 2026-09-10): - *"a slow source shouldn't leak back in like that"* — only the question's + its answer … nothing moves on screen."_ A18 (supersession, 2026-09-10): + _"a slow source shouldn't leak back in like that"_ — only the question's own answer, a later question's, or mainline supersedes; a store's - *"projection landing still consumes the whole layer (fresh authority - supersedes every tentative write)."* So: a late chunk of a superseded + _"projection landing still consumes the whole layer (fresh authority + supersedes every tentative write)."_ So: a late chunk of a superseded response is dropped, never merged; the store holds the **latest answer**, not a merge of answers; an answer resolves its parts (`{$ref}`) through its own question's context, never the current one's. Seam 1. 2. **Applied state per version IS "a landing replaces the value wholesale".** L2 ruling 1 (one frame concept — a node is committed or staged, a flush - lands or parks), A15 (*"lanes settle as one reveal"*; a stale reader is + lands or parks), A15 (_"lanes settle as one reveal"_; a stale reader is re-derived at the landing), A30 (a frame is replaced by its landing, not by the pass that asked). A landing is applied as a whole and every reader of it re-derives; nothing of the previous frame is consulted. So: a version bump re-applies even a byte-identical root (an equal landing is - still a landing — A18 (a): *"a landing that equals … confirms"*, the frame + still a landing — A18 (a): _"a landing that equals … confirms"_, the frame is the new question's); a **reveal is a landing** (content becoming shown is the moment readers of it re-derive — A15's reveal corollary); "applied" is a cache of the store keyed by the landing. Seam 2. -3. **Hydration-done IS non-SC hydration-done — ruled.** *"Hydration ending - should follow our Solid 2 non-SC."* Done is what +3. **Hydration-done IS non-SC hydration-done — ruled.** _"Hydration ending + should follow our Solid 2 non-SC."_ Done is what `hydration.ts:checkHydrationComplete` says: the root pass over and `_pendingBoundaries === 0`; every hold the frames client takes registers **as a pending boundary**, the way a `` resume does @@ -128,20 +128,20 @@ Four corollaries, one per seam and one for the boundary the seams meet at: mean the same thing with or without SC. Seam 3 (3.1, with 3.2 as its mechanism). 4. **A frame is one async value outward; its inner boundaries are the - server's.** *"SCs participate in `` until their first flush the + server's.** _"SCs participate in `` until their first flush the same way, and can have their own internal loading states that the client - doesn't care about."* **Outward:** to its surroundings a frame is one + doesn't care about."_ **Outward:** to its surroundings a frame is one async source. The enclosing `` — and hydration-done, per 3 — waits for the frame's **first flush** exactly as it waits for any async source's first landing (`05-async-data.md` "`Loading` is the UI boundary": - *"branch readiness … after that branch has produced content, subsequent - revalidation should not kick you back into the fallback"*; A29's boundary + _"branch readiness … after that branch has produced content, subsequent + revalidation should not kick you back into the fallback"_; A29's boundary exemption, #3540: an unrevealed boundary shows its fallback now, a - revealed one holds; A33: *"a `` boundary showing its fallback is - the display of everything under it"*), and for **nothing inside it**. A + revealed one holds; A33: _"a `` boundary showing its fallback is + the display of everything under it"_), and for **nothing inside it**. A refetch or switch is a new question on that source — the boundary's - retain/`on` behaviour applies as for any memo. *This is what the shell - gate is* (1.5, 1.6): the boundary's pending state for the frame's first + retain/`on` behaviour applies as for any memo. _This is what the shell + gate is_ (1.5, 1.6): the boundary's pending state for the frame's first flush under the bound address. **Inward:** a server component's own ``/`` are the server's. Their fallbacks, reveals and error outcomes arrive **as markup and segments** (`seg:`/`reveal`/the @@ -152,13 +152,13 @@ Four corollaries, one per seam and one for the boundary the seams meet at: server rendered nothing for it, that is a server-half gap to report, not a client state to invent. **The inward face exempts nothing of the client's:** a fill waiting for its chunk (S1's `prepareArgs`), a record - defer, a `{$ref}` wait are client-side waits *inside* a frame that has + defer, a `{$ref}` wait are client-side waits _inside_ a frame that has had its first flush — the client's own fills — and register under 3 as pending boundaries (3.2). The principle decides five readings the draft left to the maintainer — 1.3 (yes), 1.4 (full), 1.6 (per-address), 2.3 (yes), 3.3 (yes, re-shaped by -corollary 4) — each marked *recommended-by-principle* below; it supports 3.6 +corollary 4) — each marked _recommended-by-principle_ below; it supports 3.6 (iii) through the hydration adoption rule and asks only that 3.5's sentence be confirmed. It argues **against** two things as drafted: 3.3's client error arm (a client `` for a server boundary's failure) and the @@ -167,12 +167,12 @@ position". Code sites corollary 4 says to change are listed under 3.3. ## The seams, the reds, the duplicates -| seam | question | reds (pins that fail on `next`) | duplicates (audit §4) | -| --- | --- | --- | --- | -| **1. Response identity** | which response owns a record, a `{$ref}` wait, a data table, the shell gate | **C5** (a, b, e) a superseded response's late `data` lands in the current table — R4; **C6** (a1, b2) a held `slot:*` record outlives its response and resolves through the next one's data — R5; **C17** (a, c) the shell gate answers to the frame's registered address, which lags the binding — R8 | two table spaces (`tables` + `stageTables`' `staged`, 183 B); two ref-resolution paths (`host.resolve(ref, frameId)` + the `resolve` parameter threaded through `preview` → `#refsUnresolved`/`#refArgsUnchanged`/`#resolveArgs`/`#resolveRef`); two dedupes (`argsEquivalent` 217 B at apply, `#refArgsUnchanged` 534 B at sync — the first exists because refs are response-scoped and the store is not); two shell gates (`boundaryComponent` + the adopted face in `adoptBoundary`, ≈ 150 B duplicated); `preview`'s data half (the `resolve` it threads). _No red, same question:_ the `_$SC` bootstrap twice — the document's t = 0 address record reaches the mount by `documentAddress` scanning `_$SC.a` (audit S9) | -| **2. Applied state per version** | what a version bump resets; when a reveal is an apply | **C7** (c) a byte-identical v2 root never re-applies, so v2's segment waits for a placeholder v1 removed — R2; **C2** (a2, b; and the harness's C2 replay — R3 rediscovered: record before adoption, fragment revealed after) and **C4** (d) a fragment reveal into adopted content is not a sync trigger — R3 | two version spaces (`FrameImpl.#version` beside `store.version`; `rebase`, ≈ 60–100 B); applied state in seven fields reset at three sites (`#resetStreamState` ×5, `rebind` ×2, the root apply ×1), one of them (`#appliedRootValue`) reset at only one; two reveal engines (`web.js`'s `$df`/`$dfl` and the frame's `#revealSegment`/`#showFallback`, ≈ 1.3 KB on one side — DR-4); the #2978 cascade (`claimRegionFragments` + the `fr.subscribe` body ≈ 250 B) as the document face's half of a sync | -| **3. Hydration-done accounting** | what `done` counts; when a hold lifts; what a claim owes and reads | **C3** (a; and the harness's C3 replay — R1 rediscovered) hydration reports done while an adopted occurrence is still deferred — R1; **C18** (×3: two records drained after the parser finished, a live op before the drain, the live pump's catch-up read) a recordless occurrence is classified while the document still holds its record undrained, its render prop evaluated argless → `TypeError` → `REACTIVITY_HALTED` — R9, **page-halting**; **C19** (×2: patch before claim, two orders) a trace patch delivered before the fill's claim is never shown; heals only on the next distinct patch — R10 (green on S1); **C12** (c) a rejected server `` the adoption claimed swaps to a blank, unsurfaced — R6; S1's **C3b** shape (the `prepareArgs` wait — held by S1's own pin as the *expected* order); S1's `.fails` (a keyed sibling after a document boundary misses its key) | the #2968 deferral (`recordsPending` + `#recordRefresh` arm + the drain hook ≈ 300 B) — whose bound is the parser's state while the records it waits for sit in a ledger (`_$HY.r`) nothing consults; S1's `#argsRefresh` + `#heldRecords`, and the `{$ref}` wait's non-carrier: three hold kinds, two carriers, no shared accounting; `drainRecords` + `appliedRecords` (DR-4 row 20) — one `host.apply` per record, a sync per apply; the trace's claim reading (`materializeContainerTrace`'s synchronous replay) and the claim pass's non-mutation (`insertExpression`) each right alone, wrong together. _No red, same question:_ two late-boundary waiters (`boundaryWaiters` + `arrivals`, ≈ 150 B duplicated, resolved from one subscription — a hold already counted through the covering ``'s `_fr`; audit S9) | -| outside | — | **C13** (a, b) one sweep, two frames — R7, **confirmed** by `c13-sweep-atomic` on both faces (`a0\|b0 → a1\|b0 → a1\|b1`, identical through `frame:applied` and a `MutationObserver`); the delimiter's shape is in "The server half / wire" below | the asset-loader mirror (audit S10), the three region-rename sites (audit S7) | +| seam | question | reds (pins that fail on `next`) | duplicates (audit §4) | +| -------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **1. Response identity** | which response owns a record, a `{$ref}` wait, a data table, the shell gate | **C5** (a, b, e) a superseded response's late `data` lands in the current table — R4; **C6** (a1, b2) a held `slot:*` record outlives its response and resolves through the next one's data — R5; **C17** (a, c) the shell gate answers to the frame's registered address, which lags the binding — R8 | two table spaces (`tables` + `stageTables`' `staged`, 183 B); two ref-resolution paths (`host.resolve(ref, frameId)` + the `resolve` parameter threaded through `preview` → `#refsUnresolved`/`#refArgsUnchanged`/`#resolveArgs`/`#resolveRef`); two dedupes (`argsEquivalent` 217 B at apply, `#refArgsUnchanged` 534 B at sync — the first exists because refs are response-scoped and the store is not); two shell gates (`boundaryComponent` + the adopted face in `adoptBoundary`, ≈ 150 B duplicated); `preview`'s data half (the `resolve` it threads). _No red, same question:_ the `_$SC` bootstrap twice — the document's t = 0 address record reaches the mount by `documentAddress` scanning `_$SC.a` (audit S9) | +| **2. Applied state per version** | what a version bump resets; when a reveal is an apply | **C7** (c) a byte-identical v2 root never re-applies, so v2's segment waits for a placeholder v1 removed — R2; **C2** (a2, b; and the harness's C2 replay — R3 rediscovered: record before adoption, fragment revealed after) and **C4** (d) a fragment reveal into adopted content is not a sync trigger — R3 | two version spaces (`FrameImpl.#version` beside `store.version`; `rebase`, ≈ 60–100 B); applied state in seven fields reset at three sites (`#resetStreamState` ×5, `rebind` ×2, the root apply ×1), one of them (`#appliedRootValue`) reset at only one; two reveal engines (`web.js`'s `$df`/`$dfl` and the frame's `#revealSegment`/`#showFallback`, ≈ 1.3 KB on one side — DR-4); the #2978 cascade (`claimRegionFragments` + the `fr.subscribe` body ≈ 250 B) as the document face's half of a sync | +| **3. Hydration-done accounting** | what `done` counts; when a hold lifts; what a claim owes and reads | **C3** (a; and the harness's C3 replay — R1 rediscovered) hydration reports done while an adopted occurrence is still deferred — R1; **C18** (×3: two records drained after the parser finished, a live op before the drain, the live pump's catch-up read) a recordless occurrence is classified while the document still holds its record undrained, its render prop evaluated argless → `TypeError` → `REACTIVITY_HALTED` — R9, **page-halting**; **C19** (×2: patch before claim, two orders) a trace patch delivered before the fill's claim is never shown; heals only on the next distinct patch — R10 (green on S1); **C12** (c) a rejected server `` the adoption claimed swaps to a blank, unsurfaced — R6; S1's **C3b** shape (the `prepareArgs` wait — held by S1's own pin as the _expected_ order); S1's `.fails` (a keyed sibling after a document boundary misses its key) | the #2968 deferral (`recordsPending` + `#recordRefresh` arm + the drain hook ≈ 300 B) — whose bound is the parser's state while the records it waits for sit in a ledger (`_$HY.r`) nothing consults; S1's `#argsRefresh` + `#heldRecords`, and the `{$ref}` wait's non-carrier: three hold kinds, two carriers, no shared accounting; `drainRecords` + `appliedRecords` (DR-4 row 20) — one `host.apply` per record, a sync per apply; the trace's claim reading (`materializeContainerTrace`'s synchronous replay) and the claim pass's non-mutation (`insertExpression`) each right alone, wrong together. _No red, same question:_ two late-boundary waiters (`boundaryWaiters` + `arrivals`, ≈ 150 B duplicated, resolved from one subscription — a hold already counted through the covering ``'s `_fr`; audit S9) | +| outside | — | **C13** (a, b) one sweep, two frames — R7, **confirmed** by `c13-sweep-atomic` on both faces (`a0\|b0 → a1\|b0 → a1\|b1`, identical through `frame:applied` and a `MutationObserver`); the delimiter's shape is in "The server half / wire" below | the asset-loader mirror (audit S10), the three region-rename sites (audit S7) | Byte figures are the audit's (minified, page base, exact per function; brotli ≈ 0.29× at this layer). Estimates below carry a sign per direction: @@ -183,7 +183,7 @@ brotli ≈ 0.29× at this layer). Estimates below carry a sign per direction: ## Seam 1 — Response identity The question every red here asks: a thing arrived — whose is it? Today the -answer is given by *where it landed* (the address's current table, the frame's +answer is given by _where it landed_ (the address's current table, the frame's current id, whatever gate is armed), and the rotation that makes "current" mean "newest" happens at different moments for different things: the table at the header (`beginStream`), the record never (`slot:*` survives `clearStreamRecords`), @@ -200,16 +200,16 @@ superseded response lands in the frame that shows the current one.** - **Mechanism today.** The table: `client.ts:tables` is a `Map`; `beginStream(address)` rotates by `tables.set(address, undefined)` and - `ensureTable` creates lazily at *first use* — which `createFrameHost.apply`'s + `ensureTable` creates lazily at _first use_ — which `createFrameHost.apply`'s `data` arm performs with no version read (the transport restamped `chunk.version`; `applyData` never looks). The record: owned by the frame store and versioned at the store (`store.version`, `#version`), not per record; `clearStreamRecords` keeps every `slot:*`. The wait: a `continue` in `FrameImpl.#syncSlots` with no carrier; its answer is `#resolveRef(ref)` → - `host.resolve(ref, this.#options.id)` → `tableFor(id)` — the frame's *current* + `host.resolve(ref, this.#options.id)` → `tableFor(id)` — the frame's _current_ id, so a `rebind` re-routes every held record to the new address's data. - **Lives twice in.** `tables` + `stageTables()`'s `staged` (the staged - response's table is the one case that already *is* response-owned — kept in a + response's table is the one case that already _is_ response-owned — kept in a second map because the first is address-owned); `host.resolve` + the `resolve` parameter; `argsEquivalent` + `#refArgsUnchanged`. - **Decides.** The frame of reference for 1.2–1.4; by itself it flips nothing. @@ -242,7 +242,7 @@ becomes the current one.** ruling §3 11 — so the cell's lazy fill is the codec's, not the response's); installed as the address's current at the header (unstaged) or at `commit` (staged). The response's chunks reach the host through a per-response target - whose `apply` routes `data` into *its* cell — the shape `stage`'s entry + whose `apply` routes `data` into _its_ cell — the shape `stage`'s entry already has. Nothing stamps or compares versions on the data path: a late chunk fills a cell nothing reads. - **Restates:** corollary 1 — A18 (2026-09-10): a superseded flight's landing @@ -259,7 +259,7 @@ a later response's values, and the later response's own record replaces it.** - **Mechanism today.** `#resolveRef(ref, resolve?)` — the host path by frame id, or the `resolve` the staged preview threads down. R5: after `rebind`, A's held record resolves through `tableFor(B)`; at a staged commit, `commit` installs - v2's tables *before* replaying v2's chunks, and the replayed `start`'s flush + v2's tables _before_ replaying v2's chunks, and the replayed `start`'s flush resolves v1's held record through them. - **Lives twice in.** The two resolution paths (host-by-frame-id and the threaded `resolve`): `createFrameHost.preview(chunk, resolve)`, @@ -286,13 +286,13 @@ a later response's values, and the later response's own record replaces it.** - **Restates:** corollary 1 — L2 ruling 5: an answer is judged by the question it answers; A29/A15: a pass derives from the world it was served. A record is an answer whose parts (`{$ref}`) are resolved in its own question's - context; resolving them through the frame's *current* address is the + context; resolving them through the frame's _current_ address is the "slow source leaking back in" A18 forbids. **Recommended-by-principle: yes.** Not frames-specific. ### 1.4 A version bump drops what the previous version never applied -**Slot records outlive a bump only as the dedupe for *mounted* occurrences; a +**Slot records outlive a bump only as the dedupe for _mounted_ occurrences; a record no mount applied belongs to its superseded response and leaves with it.** Two forms; the maintainer picks. @@ -304,7 +304,7 @@ Two forms; the maintainer picks. record cannot resolve through anything. ≈ +50 B. - **Full — the store is one response's.** Every record of the previous version leaves at the bump (the host's `write` and the frame's `apply` alike); what - preserves occurrence state across versions is the *mount's* applied state + preserves occurrence state across versions is the _mount's_ applied state (`#slotArgs`, `#slotResolvedRefs`), which the sync's value compare (`#refArgsUnchanged`) already consults. Then the apply-time dedupe (`argsEquivalent` 217 B, the `slot:` arm of `FrameImpl.apply` ≈ 80 B) has @@ -325,11 +325,11 @@ Two forms; the maintainer picks. supersedes every tentative write)"). The narrow form keeps a merge of two responses in one store, which has no analogue in a node's value. **Recommended-by-principle: the full form.** What stays frames-specific is - the *precondition* — that every response carries its full record set (the + the _precondition_ — that every response carries its full record set (the sink's A5 rule; "may `slot` trail `html`" below) — a property of the wire the principle cannot supply; confirm it before taking the full form. - **Open under either form:** a called occurrence (`prop#n`) found recordless - on a *non-adopt* sync is invoked argless today (the #2968 defer is adopt-only; + on a _non-adopt_ sync is invoked argless today (the #2968 defer is adopt-only; `#syncSlots`' comment calls the recordless-called case "the protocol's invariant broken"). RFC 11 fixes no order between `slot` and `html`; the sink emits the record "at the call, ahead of the markup". C6 (a1) orders @@ -381,7 +381,7 @@ releases through the frameless waiter.** flush the same way". A switch is a new question on the source (A18 provenance), so the superseded address's apply is an older question's landing and releases nothing (L2 ruling 5: "nothing moves on screen"). The - frames-specific residue is only *where the binding lives* (the `rebind` at + frames-specific residue is only _where the binding lives_ (the `rebind` at the commit, ruled 2026-10-04) — a transport fact, not a second gate. ### 1.6 A switch keeps on screen what was on screen @@ -394,7 +394,7 @@ and the superseded address never reveals after the switch was delivered.** - **Mechanism today.** `createMemo(() => gatePromise())` is one memo across addresses; R8 (c): A's html released the gate legitimately (A was the bound address, no switch delivered yet), so the memo holds the element; B's delivery - re-arms it, and a memo pending *with* a value shows the value under + re-arms it, and a memo pending _with_ a value shows the value under async-holds-latest — the `` drops its fallback for content that was never on screen, `waiting → A → B`. - **Two readings.** @@ -426,38 +426,49 @@ and the superseded address never reveals after the switch was delivered.** is those two rules applied to the frame-as-one-value: unrevealed → fallback until B's first flush; revealed A → A until B's first flush; A's late landing is an older question's (ruling 5) and never reveals. (ii) - holds-latest is a display rule for a *value already shown* — A was never + holds-latest is a display rule for a _value already shown_ — A was never shown, so (ii) misapplies it. **Recommended-by-principle: (i).** Not frames-specific. - **Decides.** **C17 (c)** under (i). ### Fix shape — seam 1 -| step | collapse (−) | carrier (+) | net (min B, est.) | pins that flip | touches | -| --- | --- | --- | --- | --- | --- | -| 1a — 1.2, data owned by response | `stageTables` 183; `beginStream` 32; `tableFor`/`ensureTable`'s lazy-create ≈ 84; `stage`'s `data ? … : streams` fallbacks ≈ 60 | per-response cell at `bump` + the unstaged per-response target (the staged entry's shape, smaller) ≈ 140 | **≈ −220** | C5 (a), (b), (e) | `ServerComponentHandlerOptions.onStream` (public: the rotation it signalled is now the cell's install — delete or redefine); `STAGED_DATA` (@internal) generalizes to every response | -| 1b — 1.3, record carries its resolver | `host.resolve` 53 + `FrameHostOptions.resolve` wiring ≈ 40; the `resolve` parameter across `preview` ×2, `#refsUnresolved`, `#refArgsUnchanged`, `#resolveArgs`, `#resolveRef` ≈ 70 | the stamp at chunk→records ≈ 40 | **≈ −120** | C6 (a1), (b2) | `FrameHost.resolve(ref, frameId)` / `FrameHostOptions.resolve` (public, experimental — no caller); `FrameHost.preview`/`Frame.preview` signatures (@internal) | -| 1c — 1.4 full, the store is one response's | `argsEquivalent` 217; the `slot:` arm of `FrameImpl.apply` ≈ 80; `clearStreamRecords`' filter + `root` ≈ 60 | — | **≈ −350** (narrow form: ≈ +50) | none directly; closes the C6 class | the sink's A5 rule must be confirmed (no wire change if it holds) | -| 1d — 1.5 + 1.6 (i), the gate | the second gate (`adoptBoundary`'s face) ≈ 150 | `shellGate()` helper's waiter check ≈ 20; per-address gate memo ≈ 80 | **≈ −50** (1.5 alone: ≈ −130) | C17 (a); C17 (c) under (i) | none public | -| **seam 1** | | | **≈ −740** (≈ −215 br) | 7 of the 22 | | +| step | collapse (−) | carrier (+) | net (min B, est.) | pins that flip | touches | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1a — 1.2, data owned by response | `stageTables` 183; `beginStream` 32; `tableFor`/`ensureTable`'s lazy-create ≈ 84; `stage`'s `data ? … : streams` fallbacks ≈ 60 | per-response cell at `bump` + the unstaged per-response target (the staged entry's shape, smaller) ≈ 140 | **≈ −220** | C5 (a), (b), (e) | `ServerComponentHandlerOptions.onStream` (public: the rotation it signalled is now the cell's install — delete or redefine); `STAGED_DATA` (@internal) generalizes to every response | +| 1b — 1.3, record carries its resolver | `host.resolve` 53 + `FrameHostOptions.resolve` wiring ≈ 40; the `resolve` parameter across `preview` ×2, `#refsUnresolved`, `#refArgsUnchanged`, `#resolveArgs`, `#resolveRef` ≈ 70 | the stamp at chunk→records ≈ 40 | **≈ −120** | C6 (a1), (b2) | `FrameHost.resolve(ref, frameId)` / `FrameHostOptions.resolve` (public, experimental — no caller); `FrameHost.preview`/`Frame.preview` signatures (@internal) | +| 1c — 1.4 full, the store is one response's | `argsEquivalent` 217; the `slot:` arm of `FrameImpl.apply` ≈ 80; `clearStreamRecords`' filter + `root` ≈ 60 | — | **≈ −350** (narrow form: ≈ +50) | none directly; closes the C6 class | the sink's A5 rule must be confirmed (no wire change if it holds) | +| 1d — 1.5 + 1.6 (i), the gate | the second gate (`adoptBoundary`'s face) ≈ 150 | `shellGate()` helper's waiter check ≈ 20; per-address gate memo ≈ 80 | **≈ −50** (1.5 alone: ≈ −130) | C17 (a); C17 (c) under (i) | none public | +| **seam 1** | | | **≈ −740** (≈ −215 br) | 7 of the 22 | | The audit's **S8** ("one apply path for staged content", ≈ −0.7 KB) splits -here: 1a/1b are its *data* half (`stageTables` folds into response-owned -cells; `preview`'s `resolve` threading goes). Its *markup* half — a staged +here: 1a/1b are its _data_ half (`stageTables` folds into response-owned +cells; `preview`'s `resolve` threading goes). Its _markup_ half — a staged version held in the one store under a not-shown bit, `preview` becoming the ordinary args-update arm — is S8 proper, is a restatement of rulings §3 71–75, and is not decided here (audit §7 Q2). +**Landed (2026-10-06, A1b + A4 on the A2b branch):** 1.2 and 1.3's carriers +as one mechanism — the response's table is keyed by the host's version +(`tableFor(id, version, current)`) and a record's `{$ref}`s settle at the +host's write through it, an undelivered key becoming the response's own +pending read (S-ref; rejected at the stream's end — L1); 1.4 full was +#3830's. 1b's threaded `resolve`, `stageTables`, `STAGED_DATA`, `onStream` +and `FrameHost.resolve` are gone; the `preview` push itself stays (see "As +landed" below for why: the token is the carrier by which a refetch of a +shown address enters the Transaction, and the compute-half push is what +stages its args in that pass). + --- ## Seam 2 — Applied state per version -The question: a frame applied something under version *n*; version *n*+1 +The question: a frame applied something under version _n_; version _n_+1 arrives — what does the frame still believe? Today "applied" is seven fields reset at three sites, and one of them is reset at only one of the two sites that bump the version. And "applied" is also asked of the wrong event: a record -applies when it *arrives* (the flush after a store write), not when the range -it names *appears* — so a reveal that brings no new record applies nothing. +applies when it _arrives_ (the flush after a store write), not when the range +it names _appears_ — so a reveal that brings no new record applies nothing. ### 2.1 The store is the truth; the applied state is a cache of it, keyed by version @@ -504,7 +515,7 @@ version's segments reveal into.** - **Decides.** **C7 (c)** a v2 root byte-identical to v1's resets the segment. (C7 a — all 720 orders — and b hold; d is the differing-root control.) - **Carrier.** One `#applied` record, `{ version, root, revealed, fallbacks, - holes, assets, errorNotified, have }`, created fresh at every bump +holes, assets, errorNotified, have }`, created fresh at every bump (`this.#applied = applied(v)`) and at `rebind` (`applied(undefined)`, plus the root record dropped — the one thing `rebind` does beyond a bump); there is no second site to forget. `rebase()` becomes `#applied.version = undefined` @@ -532,9 +543,9 @@ event seen from two sides, and either one completes the pair.** seam's `content()`). The document face does not: a `$df` into adopted markup notifies `adoptBoundary`'s `fr.subscribe`, which runs `claimRegionFragments` (#2978) and `drainRecords` (#2968) — a sync happens only if the drain finds a - *new* record (`drainRecords` → `host.apply` → `#flush` → `#syncSlots`). R3: a + _new_ record (`drainRecords` → `host.apply` → `#flush` → `#syncSlots`). R3: a reveal with no new record syncs nothing (C2 b, C2 a2 at reveal time); a record - drained *before* the reveal ran its sync while the range was inside + drained _before_ the reveal ran its sync while the range was inside `