Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/observe-request-record.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/web": patch
---

New **`"request"` record** on `OBSERVE.records` (`CallRequestEvent`, `CallRequestListener`; augmenting `HostRecordTypes` through `solid-js` beside `"call"`): one per server-function request the browser **sent**, delivered at the send — after argument serialization and `prepareRequest`, immediately before the transport's `fetch` receives the request — so a network panel can show a call that is still in flight, or one that never settles, which the `"call"` record (delivered at settle) cannot. `{ side: "client", id, name?, at, method: "GET" | "POST", origin? }`: `id`, `name`, `method` and `origin` as on the call's `CallEvent`; `at` is `performance.now()` at the send (not the call's dispatch — `CallEvent.at` is when the call was made, and the gap is what building the request cost); `side` is the client's today, the server half (the request arrived, ahead of its `"invocation"`) additive later, the way `"frame"` has two halves.

- **`CallLive` is one object across a call's records** — documented and pinned: the `live` handed to the `"request"` listener is, by identity, the `live` handed to the `"call"` listener at settle, filled in as the call proceeds (`args` from the start, `request` at the send, `response` and `result`/`error` at settle — on every settle, whether or not a `"call"` record goes out, so a `"request"` listener alone reads the outcome off the object it holds). An in-process consumer joins a call's request to its settle by identity — the way `origin` joins a record to the interaction's — with no id-plus-time join and no sequence number. It is one mutable object and one `Request` shared by both records: treat it as read-only, and read the request's body through `live.request.clone()` so the other record's listener still can.
- **Gated per call, delivered per emission**: every gate is read once at the call's start — the observation exists when either `"call"` or `"request"` is observed; a `"request"` record is built only if `observed("request")` and a `"call"` record only if `observed("call")` were true then (explicit now — a `"request"` listener alone gets no settle record). Each record is delivered to the listeners installed when it fires, so a listener subscribing mid-call can receive a `"request"` and never that call's `"call"`, or a `"call"` without its `"request"`. `bodies` on a `"request"` subscription governs `live.request` alone: the request is reconstructed when `observed("call", "bodies") || observed("request", "bodies")`, set on `live` before the `"request"` record is delivered; the response clone stays the `"call"` listener's opt-in. `at` is read before the reconstruction, so its cost is not in the send's timestamp.
- **What emits nothing**: a call that failed before its request was built (argument serialization or `prepareRequest` threw) emits no `"request"` — nothing was handed to `fetch` — only its `"call"` settle; a call whose fetch never settles emits `"request"` only; a call an integration answered locally (`intercept`) emits neither. A `fetch` that throws synchronously still emits `"request"` ("left" means handed to `fetch`). A deferred/streaming result emits `"request"` at the send and `"call"` at handoff, as today. A throwing `"request"` listener is reported and cannot break the call; listeners run synchronously before the send, so a slow one delays the fetch — hand work off to a microtask.
- Production artifacts are byte-identical: everything lives behind `IS_OBSERVE`. Not in the diagnostics capture's `RECORD_TYPES` (capture artifacts keep settled summaries only); no performance-tracks change.
23 changes: 18 additions & 5 deletions documentation/solid-2.0/08-dev-diagnostics.md

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions packages/web/server-functions/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ async function createRequest(base, id, options, meta) {
}
init = prepared || init;
}
// The send, as observed: the `"request"` record leaves from here — the
// final address and init, immediately before `fetch` receives them.
if (IS_OBSERVE && observation) observation.request(base, init);
const send = config.fetch || fetch;
return send(base, init);
Expand Down Expand Up @@ -725,8 +727,10 @@ async function initializeResponse(base, id, options, args, meta) {
// arguments ride pre-encoded in the url (wire args empty) — a handler keying
// state by the call (function + arguments) must still see the real ones.
// Observe tier: the `"call"` record (`OBSERVE.records`, see `CallEvent`) —
// the call as the caller awaited it, request through decode; with no
// listener the dispatch runs bare, not even reading the clock. The wrapper
// the call as the caller awaited it, request through decode — and the
// `"request"` record beside it (see `CallRequestEvent`), delivered from
// `createRequest` at the send; with no listener for either the dispatch
// runs bare, not even reading the clock. The wrapper
// exists in the observe and dev artifacts only: `fetchServerFunction` below
// is the dispatch itself where the literal folds, so prod pays neither the
// extra frame nor the promise hop.
Expand Down
10 changes: 6 additions & 4 deletions packages/web/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,16 @@ export interface RequestEvent {
export type { CookieOptions } from "./cookies.js";

// This runtime's records on `OBSERVE.records` (`"invocation"`, `"render"`,
// `"call"`, `"frame"`), and with them the `HostRecordTypes` augmentation
// that module declares: the published types resolve to this entry under
// every condition, so this re-export is what puts the augmentation in a
// consumer's program.
// `"call"`, `"request"`, `"frame"`), and with them the `HostRecordTypes`
// augmentation that module declares: the published types resolve to this
// entry under every condition, so this re-export is what puts the
// augmentation in a consumer's program.
export type {
CallEvent,
CallListener,
CallLive,
CallRequestEvent,
CallRequestListener,
FrameAppliedEvent,
FrameEvent,
FrameListener,
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export type {
CallEvent,
CallListener,
CallLive,
CallRequestEvent,
CallRequestListener,
FrameAppliedEvent,
FrameEvent,
FrameListener,
Expand Down
Loading
Loading