Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/frames-residue-4-sref-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

frames: residue 4 — a pending `{$ref}` read is owned by the decode table. A slot record's `{$ref}` to a key its response has not delivered yet is answered by that response's `createJSONDataTable` with a promise the table owns (marked `s = 0`, carrying `c`, the callbacks run when it settles), settled by the key's `data` chunk through the table's own `apply` and rejected by `close(error)` when the response ends (L1 — a value that never comes is an error where it is read, not a silence); a version bump drops the superseded table and its reads. The frames host keeps only the `record.pending` count (a fresh mount waits for its record, the covering `<Loading>` pends on the landing alone) and one fan-out helper; `pendingRef` / `settleWait` / the per-store waits are gone. `FrameHostOptions.closeData(frameId, version, error?)` (new, `@experimental`) tells the integration a response ended; the shared host closes that response's table. The transport awaits `prepareData` before a `slot` chunk whose args carry a `{$ref}`, as it does before a `data` chunk, so `resolve` always has a table. Frames eager client −505 min / −174 br (34,272 / 11,367 → 33,767 / 11,193); the lazy decode chunk +420 / +161.
172 changes: 168 additions & 4 deletions documentation/plans/frames-residue-pass.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions documentation/plans/frames-savings-pass.md

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions packages/web/frames/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,14 @@ export function asyncArg<T>(value: PromiseLike<T> | AsyncIterable<T>): T {
// their own host.
//
// Tables materialize lazily, at first use once the codec module is
// resident — `prepareData` guarantees that before any `data` chunk
// delivers. A `resolve` ahead of the codec (a record's `$ref` sighted
// before its data) answers undefined — "not delivered" — and the host hands
// the fill a pending read the data chunk settles (see `createFrameHost`).
// resident — `prepareData` guarantees that before any chunk that reads
// data delivers (a `data` chunk; a `slot` chunk whose args carry a `$ref`).
// A `resolve` of a key the response has not delivered yet is the table's
// own pending read (the key lives there, so the wait does), settled by the
// key's `data` chunk through `apply` and rejected when the response ends
// (`closeData` → `table.close`, see `createFrameHost`); a version the
// address has moved past takes its table — and its unanswered reads — with
// it.
let sharedHost: any;
let codec: any;
let codecLoading: Promise<unknown> | undefined;
Expand Down Expand Up @@ -361,7 +365,11 @@ export function getFrameHost() {
prepareData: loadCodec,
applyData: (c: any, current?: number) => tableFor(c.id, c.version, current)?.apply(c),
resolve: (ref: any, id: string, version: number, current?: number) =>
tableFor(id, version, current)?.resolve(ref)
tableFor(id, version, current)?.resolve(ref),
// The response's table alone — never minted for a response that
// carried no data, never a superseded version's.
closeData: (id: string, version: number, error?: unknown) =>
tables.get(id)?.get(version)?.close(error)
// No `revive` here: document-face container traces ride slot records
// as inline literals (never `{$ref}`s) and are revived into live
// stores at arg-read by the traces tier, whose install sets this
Expand Down
230 changes: 114 additions & 116 deletions packages/web/frames/src/frame-client.ts

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions packages/web/frames/src/frame-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,22 @@ function applyFrames(response, host, options = {}, observation) {
// only with the tier resident), so it awaits the load, as it awaits
// the codec below.
const needs = chunk.tiers?.map(prepareTier) || [];
// Codec-free until a `data` chunk actually arrives: a host whose
// deserializer loads lazily (`prepareData`) gets awaited here, and
// because the loop is sequential every later chunk — the records
// referencing this data included — queues behind the load. Chunk
// Codec-free until a chunk that READS data actually arrives: a host
// whose deserializer loads lazily (`prepareData`) gets awaited here,
// and because the loop is sequential every later chunk — the
// records referencing this data included — queues behind the load.
// A `slot` chunk whose args carry a `{$ref}` reads data too (its
// refs settle at the write, through its response's table — a key
// not delivered yet is the TABLE's pending read, so the table must
// exist); the producer emits the data ahead of the record that
// names it, so on its wire this await is already answered. Chunk
// ORDER is the only contract downstream (network jitter already
// stretches time between chunks), so nothing else observes the wait.
if (chunk.type === "data") await Promise.all([host.prepareData?.(), ...needs]);
if (
chunk.type === "data" ||
(chunk.type === "slot" && Object.values(chunk.args).some(v => v && v.$ref))
)
await Promise.all([host.prepareData?.(), ...needs]);
host.apply(chunk);
}
result = await reader.next();
Expand Down
83 changes: 79 additions & 4 deletions packages/web/serialization/src/serializer-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,38 @@ export interface JSONCodecOptions {
/**
* A resident, response-scoped decode table over the keyed JSON codec: apply
* each frame `data` chunk with `apply`, resolve `{ $ref }` slot args with
* `resolve`. The frames client host wires one per response
* (`applyData: c => table.apply(c)`).
* `resolve`, and `close` it when the response ends. The frames client host
* wires one per response (`applyData: c => table.apply(c)`,
* `closeData: (…) => table.close(error)`).
*
* A `{ $ref }` to a key no `apply` has delivered yet is a PENDING READ the
* table owns (`@internal` behaviour — the frames client is its consumer): a
* promise marked `s = 0`, settled by the key's `apply` and rejected by
* `close` — the response ended without delivering the key, so a value that
* never comes is an error where it is read, not a silence. Stamped `s`/`v`
* as it settles (`1` fulfilled, `2` rejected — the hydration serializer's
* marks, which a reader adopts synchronously).
*
* Integration-facing; may change (see the entry banner). This serialization
* entry is the single home of the data table — the frames client consumes
* it internally rather than re-exporting it.
*/
export interface JSONDataTable {
apply(chunk: { key?: string; node?: unknown; initial?: boolean }): void;
/**
* The value under `ref`, or the table's pending read of it: a promise
* marked `s = 0` carrying `c`, the callbacks the table runs in the
* `apply` / `close` that settles it — the key delivered (whatever its
* value's shape: a delivered promise is adopted by the read, the
* callbacks do not wait for it) or the table closed.
*/
resolve<T = unknown>(ref: { $ref: string }): T;
/**
* The response has ended: every pending read of a key it never
* delivered rejects — with `error` (the response's own) when given, with
* an `Error` naming the key otherwise. Idempotent.
*/
close(error?: unknown): void;
}

// Container traces cross as RAW seroval streams so their buffered snapshot
Expand Down Expand Up @@ -478,20 +500,73 @@ export function createJSONDataTable(options?: JSONCodecOptions): JSONDataTable;
* nodes patch pending values (promise/stream resolutions) through the shared
* deserializer refs. `resolve` reads a `{ $ref }` back out — the record ids
* double as the reference namespace.
*
* A key the table does not hold yet answers with a PENDING READ the table
* owns: one promise per undelivered key (a second reader of the key gets
* the same one), marked `s = 0` so a consumer can tell it from a delivered
* value that happens to be a promise (an async arg passed whole), settled
* by the key's own `apply` and rejected by `close` — the response ended and
* the key never came (frames-rulings L1). It is stamped `s`/`v` as it
* settles, the marks the hydration serializer puts on a settled promise, so
* a reader that must stay synchronous (a fill's prop read at a t=0 claim)
* adopts the value without a pending beat. Owned here (`then(undefined,
* noop)`): a read nobody makes — the occurrence never mounted — must not
* surface its rejection as unhandled; a reader attaches its own handlers.
* The table does not know what a key is FOR: who waits on a read and what
* runs when it settles is the consumer's — the read carries `c`, the
* callbacks the table runs in the `apply` / `close` that settles it (the
* frames host counts a record's reads and re-applies it when the last one
* lands). Callbacks, not the promise's own `then`: a delivered value may
* itself be a promise (an async arg passed whole), which the read ADOPTS —
* its `then` would fire at the inner value, while the consumer must learn
* of the DELIVERY (a fresh mount runs then, and the fill's own boundary
* holds the inner wait — DR-2's value tier).
*/
export function createJSONDataTable(options) {
const deserialize = createJSONDeserializer(options);
const table = new Map();
// key -> { p, r, j }: the pending read for an undelivered key (`p.c` its
// settle callbacks).
const waits = new Map();
const settle = (w, s, value) => {
const p = w.p;
p.s = s;
p.v = value;
s === 1 ? w.r(value) : w.j(value);
for (const c of p.c) c();
};
return {
apply(record) {
const value = deserialize(record.node);
if (record.initial) table.set(record.key, value);
if (record.initial) {
table.set(record.key, value);
const w = waits.get(record.key);
if (w) {
waits.delete(record.key);
settle(w, 1, value);
}
}
},
get(key) {
return table.get(key);
},
resolve(ref) {
return table.get(ref.$ref);
const key = ref.$ref;
if (table.has(key)) return table.get(key);
let w = waits.get(key);
if (!w) {
waits.set(key, (w = {}));
const p = (w.p = new Promise((r, j) => ((w.r = r), (w.j = j))));
p.s = 0;
p.c = [];
p.then(undefined, () => {});
}
return w.p;
},
close(error) {
for (const [key, w] of waits)
settle(w, 2, error || new Error(`Stream ended without delivering {$ref: "${key}"}`));
waits.clear();
}
};
}
Loading