@rsc-kit/core 0.29.10 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/action.d.ts +69 -9
  2. package/dist/action.js +78 -3
  3. package/dist/action.js.map +1 -1
  4. package/dist/conformance.d.ts +1 -0
  5. package/dist/conformance.js +24 -0
  6. package/dist/conformance.js.map +1 -1
  7. package/dist/headers.d.ts +0 -5
  8. package/dist/headers.js +0 -5
  9. package/dist/headers.js.map +1 -1
  10. package/dist/host.d.ts +3 -8
  11. package/dist/host.js +21 -73
  12. package/dist/host.js.map +1 -1
  13. package/dist/hostCalls.d.ts +7 -0
  14. package/dist/hostCalls.js +7 -1
  15. package/dist/hostCalls.js.map +1 -1
  16. package/dist/inlineFlight.d.ts +9 -0
  17. package/dist/inlineFlight.js +126 -0
  18. package/dist/inlineFlight.js.map +1 -0
  19. package/dist/js/Form.d.ts +9 -2
  20. package/dist/js/Form.js +11 -2
  21. package/dist/js/Form.js.map +1 -1
  22. package/dist/js/SegmentBoundary.js +4 -14
  23. package/dist/js/SegmentBoundary.js.map +1 -1
  24. package/dist/js/createViteRscApp.js +5 -8
  25. package/dist/js/createViteRscApp.js.map +1 -1
  26. package/dist/js/inlinePayload.d.ts +6 -0
  27. package/dist/js/inlinePayload.js +56 -0
  28. package/dist/js/inlinePayload.js.map +1 -0
  29. package/dist/js/pagePayload.d.ts +1 -1
  30. package/dist/js/pagePayload.js +2 -2
  31. package/dist/js/pagePayload.js.map +1 -1
  32. package/dist/js/useAction.d.ts +9 -6
  33. package/dist/js/useAction.js.map +1 -1
  34. package/dist/testHost.d.ts +6 -2
  35. package/dist/testHost.js +6 -2
  36. package/dist/testHost.js.map +1 -1
  37. package/dist/vite.js +61 -24
  38. package/dist/vite.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/bootPayloads.d.ts +0 -18
  41. package/dist/bootPayloads.js +0 -81
  42. package/dist/bootPayloads.js.map +0 -1
@@ -88,6 +88,13 @@ export interface HostCallReply {
88
88
  * rate-limited visitor indistinguishable from a broken server.
89
89
  */
90
90
  refusalStatus?: number;
91
+ /**
92
+ * What a deliberate refusal carries beside its message (`error`): the
93
+ * records blocking a delete, say. Raised as the action client's own
94
+ * refusal, so an action passes it to the page as `result.refusal`, checked
95
+ * against the schema the action declared - see `refuse()`.
96
+ */
97
+ refusalData?: unknown;
91
98
  }
92
99
  /**
93
100
  * A batch on the wire: several calls in one POST, answered in order.
package/dist/hostCalls.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // gets the same semantics the socket has, without the framing.
13
13
  import { headers as incomingHeaders, passSetCookies } from './request.js';
14
14
  import { revalidate } from './revalidate.js';
15
- import { ActionValidationError } from './action.js';
15
+ import { ActionRefusal, ActionValidationError } from './action.js';
16
16
  import { redirect } from './redirect.js';
17
17
  import { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js';
18
18
  import { notFound } from './notFound.js';
@@ -329,6 +329,12 @@ export function httpHostCalls(options) {
329
329
  // reached the page as "something went wrong".
330
330
  if (reply?.refusalStatus === 404)
331
331
  notFound();
332
+ // A refusal with data is the backend's refuse(): its message was written
333
+ // for the person asking, so it is raised as one, not as a failure whose
334
+ // message onError would replace.
335
+ if (reply?.error !== undefined && reply.refusalData !== undefined) {
336
+ throw new ActionRefusal(reply.error, reply.refusalData, reply.refusalStatus ?? 409);
337
+ }
332
338
  if (reply?.error !== undefined) {
333
339
  const failure = new Error(`Host call ${JSON.stringify(name)} failed: ${reply.error}`);
334
340
  // Carried on the error rather than thrown as another class: the host
@@ -1 +1 @@
1
- {"version":3,"file":"hostCalls.js","sourceRoot":"","sources":["../src/hostCalls.ts"],"names":[],"mappings":"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","sourcesContent":["// Host calls over HTTP, so a host in any language can answer them.\n//\n// A server component writing `await rpc('Orders.recent')` needs the host\n// mid-render. The Laravel path answers on a second unix socket with its own\n// framing (PROTOCOL.md, \"The callback channel\"), which every non-JS host has\n// to reimplement before it can render a single page that fetches anything.\n// This is the same conversation over an ordinary POST.\n//\n// The transport is the only thing that changes. The engine still sees one\n// function, the deferred-release ordering in streaming.ts still applies, and a\n// reply may still carry `revalidate` — a Go host that answers this endpoint\n// gets the same semantics the socket has, without the framing.\n\nimport { headers as incomingHeaders, passSetCookies } from './request.js'\nimport { revalidate } from './revalidate.js'\nimport type { RevalidateTarget } from './routes.js'\nimport { ActionValidationError } from './action.js'\nimport { redirect } from './redirect.js'\nimport { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js'\nimport { notFound } from './notFound.js'\n\nexport interface HttpHostCallsOptions {\n /**\n * Where calls are POSTed. A loopback address or a unix socket url — see the\n * note on `secret` for why this must not be a public one.\n */\n endpoint: string\n /**\n * Sent as `X-RSC-Host-Secret`, and required rather than optional.\n *\n * This endpoint runs functions by name on behalf of a render. PROTOCOL.md\n * Part 3b draws the line at client-supplied input deciding what RUNS, and an\n * unauthenticated callback endpoint is on the wrong side of it: anyone who\n * can reach it can invoke any registered host function directly, with\n * whatever arguments they like and none of the app's routing in front. A\n * default of \"off\" would be the kind that ships.\n */\n secret: string\n /**\n * Request headers copied from the render's own request onto the call.\n *\n * This is what makes a host call run as the person browsing: the backend's\n * session middleware reads its cookie and finds the same user. Nothing else\n * is forwarded, because everything else is either meaningless to the backend\n * or actively wrong — `content-length` and `content-type` describe this POST,\n * not the page request.\n */\n forwardHeaders?: string[]\n /** Defaults to 30s. A render blocked on a host that never answers is a hung request. */\n timeoutMs?: number\n /** Injectable for tests and for a runtime whose fetch is not global. */\n fetch?: typeof fetch\n /**\n * What to do with whatever a reply reported as invalidated.\n *\n * Defaults to marking it on the engine, which is the only thing that makes\n * `Revalidate(ctx, \"orders\")` on the host side mean anything: an action's\n * answer carries the re-rendered region only if the target reached the\n * revalidation scope before the action returned. Without it a host reports\n * what it dirtied, nothing listens, and the browser is told nothing — the\n * page simply shows stale data with no error anywhere.\n */\n onRevalidate?: (targets: string[]) => void\n}\n\nconst DEFAULT_FORWARDED = ['cookie', 'authorization']\n\nexport interface HostCallReply {\n result?: unknown\n error?: string\n revalidate?: string[]\n /**\n * Field name to messages, when the host refused the input.\n *\n * The same shape everything else here already uses: Laravel's own\n * `$e->errors()`, the socket protocol's `validation_errors`, and what\n * `issuesToErrors` turns a Standard Schema result into. Dot-joined for a\n * nested field, the empty string for a message about the form rather than\n * any one field.\n */\n validationErrors?: Record<string, string[]>\n /** The caller has no session. Becomes the engine's own authentication error. */\n unauthenticated?: boolean\n /** The caller has a session and still may not. */\n unauthorized?: boolean\n /**\n * Where the host says this request should go instead.\n *\n * Answered with a 200 and this field, never as a 3xx: fetch follows a\n * redirect transparently, so a real one would send the host call itself to\n * the destination and hand whatever came back to the render as the\n * function's result.\n */\n redirect?: string\n /**\n * Where an unexpected failure happened in the backend, sent only in\n * development (Laravel's app.debug, Go's CallbackHandler.Debug): the\n * thrown type, its message, and frames newest first.\n */\n debug?: { type?: string; message?: string; trace?: string[] }\n /** The status to redirect with. Defaults to 307, which preserves the method. */\n redirectStatus?: number\n /**\n * The status a refusal should be answered with.\n *\n * A middleware that aborted meant what it aborted with — throttle answers\n * 429, a signed-url check 403 — and collapsing those to 500 makes a\n * rate-limited visitor indistinguishable from a broken server.\n */\n refusalStatus?: number\n}\n\n/**\n * A batch on the wire: several calls in one POST, answered in order.\n *\n * Calls issued in the same tick of a render - sibling components each\n * awaiting rpc() - travel together, so a page's parallel reads cost the host\n * one request rather than one each. A host that has never heard of the\n * envelope answers it as a malformed single call, and the calls are sent one\n * at a time from then on; nothing is lost but the saving.\n */\nexport interface HostCallBatch {\n calls: { function: string; args: unknown[] }[]\n}\n\nexport interface HostCallBatchReply {\n /** One per call, in order. Each carries the status that call would have had. */\n replies: (HostCallReply & { status?: number })[]\n}\n\n/**\n * A batch answered as it goes: one line per call, each as the host finishes\n * it, in whatever order that is.\n *\n * `application/x-ndjson`, each line a reply with the call's `index` in the\n * batch. A host that answers this way lets a fast call resolve while a slow\n * sibling is still running - which is what keeps a page's boundaries\n * streaming independently when their reads travelled together. A host that\n * answers the whole batch at once, as one JSON object of `replies`, is read\n * as before; the saving of the batch stays, the independence does not.\n */\nexport type HostCallBatchLine = HostCallReply & { index: number; status?: number }\n\n/** How many calls one POST carries at most. */\nconst BATCH_LIMIT = 50\n\ntype Settled = { status: number; reply: HostCallReply | null; text: string; setCookies?: string[] }\n\ninterface Pending {\n name: string\n args: unknown[]\n resolve: (settled: Settled) => void\n reject: (error: unknown) => void\n}\n\ninterface Bucket {\n headers: Record<string, string>\n calls: Pending[]\n}\n\n// After the current I/O, before the next timer: what a render issued\n// synchronously - and across the microtasks between one component's awaits -\n// is in the bucket by then. setImmediate where the runtime has it, a zero\n// timer where it does not (a Worker).\nconst nextTick: (fn: () => void) => void =\n typeof setImmediate === 'function' ? (fn) => setImmediate(fn) : (fn) => setTimeout(fn, 0)\n\n/**\n * The function to hand to `installHostFn`, or to `hostCalls` on the JS host.\n */\n/**\n * The backend's trace, on the error the render sees.\n *\n * As its cause - an Error named for the backend's type, whose stack is the\n * backend's frames - so Node, Bun and the dev overlay print it under the\n * renderer's own stack; and appended to that stack, for whatever prints only\n * the one. Before this a failed rpc() pointed at a line of JavaScript, and the\n * PHP or Go that failed was a separate search through another log.\n */\nfunction withBackendTrace(failure: Error, debug: NonNullable<HostCallReply['debug']>): void {\n const frames = (debug.trace ?? []).map((frame) => ' at ' + frame).join('\\n')\n const cause = new Error(debug.message ?? '')\n\n cause.name = 'Backend ' + (debug.type ?? 'Error')\n cause.stack = cause.name + ': ' + (debug.message ?? '') + (frames ? '\\n' + frames : '')\n\n failure.cause = cause\n failure.stack = (failure.stack ?? String(failure)) + '\\nCaused in the backend by ' + cause.stack\n}\n\nexport function httpHostCalls(\n options: HttpHostCallsOptions,\n): (name: string, ...args: unknown[]) => Promise<unknown> {\n const {\n endpoint,\n secret,\n forwardHeaders = DEFAULT_FORWARDED,\n timeoutMs = 30_000,\n fetch: fetchImpl,\n onRevalidate,\n } = options\n\n if (!secret) {\n throw new Error('httpHostCalls requires a secret — see the note on the option.')\n }\n\n const forwarded = forwardHeaders.map((name) => name.toLowerCase())\n\n // One bucket per set of forwarded headers: two visitors' renders in flight\n // at once must not share a request, because the host reads the cookie off\n // the request to know whose session a call runs under.\n const buckets = new Map<string, Bucket>()\n\n async function forwardedHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n 'x-rsc-host-secret': secret,\n }\n\n // Outside a render — a prerender pass, or a host calling one directly —\n // there is no request to forward from, and that is not an error. The call\n // simply carries no session, which is exactly what a build-time render\n // should be doing.\n //\n // Only that one condition is swallowed. A blanket catch here is the wrong\n // shape: anything else going wrong while reading the request would come\n // out as a call with no session, which is not a failure the caller sees —\n // it is the visitor silently becoming anonymous, and the page rendering as\n // though they were logged out.\n try {\n const from = await incomingHeaders()\n\n for (const key of forwarded) {\n const value = from.get(key)\n if (value !== null) headers[key] = value\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (!message.startsWith('No request in scope')) throw error\n }\n\n return headers\n }\n\n /** One POST. `label` names what is being sent, for the error messages. */\n async function post(\n body: unknown,\n headers: Record<string, string>,\n label: string,\n ): Promise<{ status: number; text: string; setCookies: string[] }> {\n const response = await send(body, headers, label)\n\n // Read the body before branching on status: a host that reports the error\n // in JSON with a 500 is saying something more useful than \"500\", and\n // throwing on the status alone discards it.\n return {\n status: response.status,\n text: await response.text(),\n // A login's session cookie: kept to put on the page's response.\n setCookies: response.headers.getSetCookie?.() ?? [],\n }\n }\n\n /** The POST itself, headers in hand, body still to read. */\n async function send(body: unknown, headers: Record<string, string>, label: string): Promise<Response> {\n const doFetch = fetchImpl ?? globalThis.fetch\n\n // AbortSignal.timeout is not on every runtime this engine targets, so the\n // controller is written out rather than assumed.\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n\n let response: Response\n\n try {\n response = await doFetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n signal: controller.signal,\n })\n } catch (error) {\n if (controller.signal.aborted) {\n throw new Error(`Host call ${label} timed out after ${timeoutMs}ms`)\n }\n\n throw new Error(\n `Host call ${label} could not reach the host at ${endpoint}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n } finally {\n clearTimeout(timer)\n }\n\n return response\n }\n\n function parse(text: string): HostCallReply | null {\n try {\n return text ? (JSON.parse(text) as HostCallReply) : null\n } catch {\n return null\n }\n }\n\n async function single(name: string, args: unknown[], headers: Record<string, string>): Promise<Settled> {\n const { status, text, setCookies } = await post({ function: name, args }, headers, JSON.stringify(name))\n\n return { status, reply: parse(text), text, setCookies }\n }\n\n /** Read a streamed batch, one reply per line, resolving each call as its line lands. */\n async function resolveAsLinesArrive(body: ReadableStream<Uint8Array>, calls: Pending[]): Promise<void> {\n const answered = new Set<number>()\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffered = ''\n\n const take = (line: string) => {\n const trimmed = line.trim()\n\n if (!trimmed) return\n\n let parsed: Partial<HostCallBatchLine> | null\n\n try {\n parsed = JSON.parse(trimmed) as Partial<HostCallBatchLine>\n } catch {\n return\n }\n\n const index = parsed?.index\n\n if (typeof index !== 'number' || !calls[index] || answered.has(index)) return\n\n const { index: _, status: own, ...reply } = parsed as HostCallBatchLine\n\n answered.add(index)\n calls[index]!.resolve({ status: own ?? 200, reply, text: JSON.stringify(reply) })\n }\n\n try {\n for (;;) {\n const { done, value } = await reader.read()\n\n if (done) break\n\n buffered += decoder.decode(value, { stream: true })\n\n let at: number\n\n while ((at = buffered.indexOf('\\n')) !== -1) {\n take(buffered.slice(0, at))\n buffered = buffered.slice(at + 1)\n }\n }\n\n buffered += decoder.decode()\n take(buffered)\n } catch (error) {\n for (const [i, call] of calls.entries()) {\n if (!answered.has(i)) call.reject(error)\n }\n\n return\n }\n\n // The host closed the batch with a call unanswered: a fault on its side,\n // reported to the call rather than left pending forever.\n for (const [i, call] of calls.entries()) {\n if (!answered.has(i)) {\n call.reject(new Error(`Host call ${JSON.stringify(call.name)} was not answered in its batch`))\n }\n }\n }\n\n /**\n * Send a bucket. One call goes as itself, so a host that speaks only the\n * single shape - or a request that happened to be alone - sees nothing\n * new on the wire.\n */\n async function flush(bucket: Bucket): Promise<void> {\n const { headers, calls } = bucket\n\n if (calls.length === 1) {\n const [only] = calls\n\n try {\n only.resolve(await single(only.name, only.args, headers))\n } catch (error) {\n only.reject(error)\n }\n\n return\n }\n\n const label = `batch of ${calls.length} (${calls.map((c) => c.name).join(', ')})`\n let response: Response\n\n try {\n response = await send(\n { calls: calls.map((c) => ({ function: c.name, args: c.args })) } satisfies HostCallBatch,\n headers,\n label,\n )\n } catch (error) {\n // The host could not be reached, or did not answer in time. Not\n // re-sent one by one: the calls may have run, and an action run twice\n // is worse than one reported as failed.\n for (const call of calls) call.reject(error)\n\n return\n }\n\n // Answered as it goes: each call resolves the moment its line arrives,\n // so a component waiting on a fast read paints while a slow sibling's\n // is still running. The batch saved the round trips; this keeps the\n // streaming the batch would otherwise have cost.\n if (response.status < 400 && (response.headers.get('content-type') ?? '').includes('x-ndjson') && response.body) {\n await resolveAsLinesArrive(response.body, calls)\n\n return\n }\n\n const status = response.status\n const text = await response.text()\n const parsed = parse(text) as Partial<HostCallBatchReply> | null\n const replies = parsed?.replies\n\n if (status < 400 && Array.isArray(replies) && replies.length === calls.length) {\n calls.forEach((call, i) => {\n const { status: own, ...reply } = replies[i]!\n\n call.resolve({ status: own ?? 200, reply, text: JSON.stringify(reply) })\n })\n\n return\n }\n\n // Not a batch answer. Taking batches is part of the protocol - the\n // conformance suite checks it - so this is a backend that does not, or\n // one that failed before reading the calls: wrong secret, down, too many.\n // Every call fails with what the backend said, and none is sent again:\n // they may have run, and an action run twice is worse than one reported.\n const said = parsed && typeof (parsed as HostCallReply).error === 'string' ? (parsed as HostCallReply).error : text.slice(0, 200)\n const failure = new Error(\n `Host call ${label} was not answered as a batch (${status}${said ? `: ${said}` : ''}). ` +\n 'A backend answers { \"calls\": [...] } line by line or as { \"replies\": [...] }: ' +\n 'https://docs.rsc-kit.dev/hosts/your-own-backend#batches',\n )\n\n for (const call of calls) call.reject(failure)\n }\n\n function enqueue(name: string, args: unknown[], headers: Record<string, string>): Promise<Settled> {\n return new Promise<Settled>((resolve, reject) => {\n const key = JSON.stringify(headers)\n let bucket = buckets.get(key)\n\n if (!bucket) {\n bucket = { headers, calls: [] }\n buckets.set(key, bucket)\n\n const mine = bucket\n\n nextTick(() => {\n if (buckets.get(key) === mine) buckets.delete(key)\n\n void flush(mine)\n })\n }\n\n bucket.calls.push({ name, args, resolve, reject })\n\n if (bucket.calls.length >= BATCH_LIMIT) {\n buckets.delete(key)\n\n void flush(bucket)\n }\n })\n }\n\n /**\n * What a reply means, in the caller's own async context.\n *\n * Deliberately not done where the response arrives: a batch is answered in\n * a timer, outside every caller's request scope, and `redirect()` and\n * `revalidate()` both write to that scope. Interpreting here, after the\n * caller's await resumed, puts each reply back inside the render it belongs\n * to.\n */\n function interpret(name: string, { status, reply, text, setCookies }: Settled): unknown {\n // First, whatever the outcome: a login that then redirects still logged\n // the visitor in. In the caller's context, where the page's response is.\n if (setCookies?.length) passSetCookies(setCookies)\n\n // Refusing the input is not the call failing — it is the call answering.\n //\n // Thrown rather than returned, so a handler stops where it is instead of\n // carrying on with data the host rejected. createActionClient catches this\n // on the way out and returns { validationErrors }, which is the shape\n // useForm reads: React serialises a REJECTED server action opaquely, so a\n // validation error that stays thrown reaches the browser as \"an error\n // occurred\" with every field it named gone.\n //\n // Checked before `error`, so a host that sends both is read as a refusal\n // rather than as a failure with no fields.\n //\n // One error for both places a call is made. In an action, the mark is\n // what returns the fields to the form. In a page's render there is no\n // form: refusalStatus makes the page answer 422 rather than 500, and the\n // message says which call refused which fields, where \"Validation failed\"\n // read like an action had run.\n if (reply?.validationErrors) {\n const refused = new ActionValidationError(reply.validationErrors)\n const fields = Object.keys(reply.validationErrors).map((field) => field || '(the input as a whole)')\n\n refused.message = `Host call ${JSON.stringify(name)} refused its input: ${fields.join(', ')}`\n ;(refused as ActionValidationError & { refusalStatus?: number }).refusalStatus = 422\n\n throw refused\n }\n\n // Raised as the engine's own redirect, so it travels the path every other\n // redirect travels — a real 3xx above a Suspense boundary, the digest\n // below one — rather than becoming an error the page has to interpret.\n if (reply?.redirect) {\n redirect(reply.redirect as never, reply.redirectStatus ?? 307)\n }\n\n if (reply?.unauthenticated) {\n throw new ServerAuthenticationError(reply.error ?? 'Unauthenticated.')\n }\n\n if (reply?.unauthorized) {\n throw new ServerAuthorizationError(reply.error ?? 'This action is unauthorized.')\n }\n\n // The backend says there is no such thing - Go's Refuse(404), Laravel's\n // abort(404), a record this caller cannot see. Raised as the engine's\n // own notFound(), so the page answers with not-found.tsx and a 404,\n // exactly as a component calling notFound() would; as an error it\n // reached the page as \"something went wrong\".\n if (reply?.refusalStatus === 404) notFound()\n\n if (reply?.error !== undefined) {\n const failure = new Error(`Host call ${JSON.stringify(name)} failed: ${reply.error}`)\n\n // Carried on the error rather than thrown as another class: the host\n // chose a status and the only job here is not to lose it on the way to\n // whoever writes the response.\n if (reply.refusalStatus) {\n ;(failure as Error & { refusalStatus?: number }).refusalStatus = reply.refusalStatus\n }\n\n if (reply.debug) withBackendTrace(failure, reply.debug)\n\n throw failure\n }\n\n if (status >= 400) {\n throw new Error(\n `Host call ${JSON.stringify(name)} failed: ${status}`.trim() +\n (text ? ` — ${text.slice(0, 200)}` : ''),\n )\n }\n\n if (reply === null) {\n throw new Error(\n `Host call ${JSON.stringify(name)} returned a body that is not JSON: ${text.slice(0, 200)}`,\n )\n }\n\n if (reply.revalidate?.length) {\n if (onRevalidate) onRevalidate(reply.revalidate)\n // Named by the host over the wire - Rsc::revalidate('orders') in PHP -\n // so nothing here can check it; the renderer refuses an unknown name.\n else for (const target of reply.revalidate) revalidate(target as RevalidateTarget)\n }\n\n return reply.result ?? null\n }\n\n return async function hostCall(name: string, ...args: unknown[]): Promise<unknown> {\n const headers = await forwardedHeaders()\n const settled = await enqueue(name, args, headers)\n\n return interpret(name, settled)\n }\n}\n"]}
1
+ {"version":3,"file":"hostCalls.js","sourceRoot":"","sources":["../src/hostCalls.ts"],"names":[],"mappings":"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","sourcesContent":["// Host calls over HTTP, so a host in any language can answer them.\n//\n// A server component writing `await rpc('Orders.recent')` needs the host\n// mid-render. The Laravel path answers on a second unix socket with its own\n// framing (PROTOCOL.md, \"The callback channel\"), which every non-JS host has\n// to reimplement before it can render a single page that fetches anything.\n// This is the same conversation over an ordinary POST.\n//\n// The transport is the only thing that changes. The engine still sees one\n// function, the deferred-release ordering in streaming.ts still applies, and a\n// reply may still carry `revalidate` — a Go host that answers this endpoint\n// gets the same semantics the socket has, without the framing.\n\nimport { headers as incomingHeaders, passSetCookies } from './request.js'\nimport { revalidate } from './revalidate.js'\nimport type { RevalidateTarget } from './routes.js'\nimport { ActionRefusal, ActionValidationError } from './action.js'\nimport { redirect } from './redirect.js'\nimport { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js'\nimport { notFound } from './notFound.js'\n\nexport interface HttpHostCallsOptions {\n /**\n * Where calls are POSTed. A loopback address or a unix socket url — see the\n * note on `secret` for why this must not be a public one.\n */\n endpoint: string\n /**\n * Sent as `X-RSC-Host-Secret`, and required rather than optional.\n *\n * This endpoint runs functions by name on behalf of a render. PROTOCOL.md\n * Part 3b draws the line at client-supplied input deciding what RUNS, and an\n * unauthenticated callback endpoint is on the wrong side of it: anyone who\n * can reach it can invoke any registered host function directly, with\n * whatever arguments they like and none of the app's routing in front. A\n * default of \"off\" would be the kind that ships.\n */\n secret: string\n /**\n * Request headers copied from the render's own request onto the call.\n *\n * This is what makes a host call run as the person browsing: the backend's\n * session middleware reads its cookie and finds the same user. Nothing else\n * is forwarded, because everything else is either meaningless to the backend\n * or actively wrong — `content-length` and `content-type` describe this POST,\n * not the page request.\n */\n forwardHeaders?: string[]\n /** Defaults to 30s. A render blocked on a host that never answers is a hung request. */\n timeoutMs?: number\n /** Injectable for tests and for a runtime whose fetch is not global. */\n fetch?: typeof fetch\n /**\n * What to do with whatever a reply reported as invalidated.\n *\n * Defaults to marking it on the engine, which is the only thing that makes\n * `Revalidate(ctx, \"orders\")` on the host side mean anything: an action's\n * answer carries the re-rendered region only if the target reached the\n * revalidation scope before the action returned. Without it a host reports\n * what it dirtied, nothing listens, and the browser is told nothing — the\n * page simply shows stale data with no error anywhere.\n */\n onRevalidate?: (targets: string[]) => void\n}\n\nconst DEFAULT_FORWARDED = ['cookie', 'authorization']\n\nexport interface HostCallReply {\n result?: unknown\n error?: string\n revalidate?: string[]\n /**\n * Field name to messages, when the host refused the input.\n *\n * The same shape everything else here already uses: Laravel's own\n * `$e->errors()`, the socket protocol's `validation_errors`, and what\n * `issuesToErrors` turns a Standard Schema result into. Dot-joined for a\n * nested field, the empty string for a message about the form rather than\n * any one field.\n */\n validationErrors?: Record<string, string[]>\n /** The caller has no session. Becomes the engine's own authentication error. */\n unauthenticated?: boolean\n /** The caller has a session and still may not. */\n unauthorized?: boolean\n /**\n * Where the host says this request should go instead.\n *\n * Answered with a 200 and this field, never as a 3xx: fetch follows a\n * redirect transparently, so a real one would send the host call itself to\n * the destination and hand whatever came back to the render as the\n * function's result.\n */\n redirect?: string\n /**\n * Where an unexpected failure happened in the backend, sent only in\n * development (Laravel's app.debug, Go's CallbackHandler.Debug): the\n * thrown type, its message, and frames newest first.\n */\n debug?: { type?: string; message?: string; trace?: string[] }\n /** The status to redirect with. Defaults to 307, which preserves the method. */\n redirectStatus?: number\n /**\n * The status a refusal should be answered with.\n *\n * A middleware that aborted meant what it aborted with — throttle answers\n * 429, a signed-url check 403 — and collapsing those to 500 makes a\n * rate-limited visitor indistinguishable from a broken server.\n */\n refusalStatus?: number\n /**\n * What a deliberate refusal carries beside its message (`error`): the\n * records blocking a delete, say. Raised as the action client's own\n * refusal, so an action passes it to the page as `result.refusal`, checked\n * against the schema the action declared - see `refuse()`.\n */\n refusalData?: unknown\n}\n\n/**\n * A batch on the wire: several calls in one POST, answered in order.\n *\n * Calls issued in the same tick of a render - sibling components each\n * awaiting rpc() - travel together, so a page's parallel reads cost the host\n * one request rather than one each. A host that has never heard of the\n * envelope answers it as a malformed single call, and the calls are sent one\n * at a time from then on; nothing is lost but the saving.\n */\nexport interface HostCallBatch {\n calls: { function: string; args: unknown[] }[]\n}\n\nexport interface HostCallBatchReply {\n /** One per call, in order. Each carries the status that call would have had. */\n replies: (HostCallReply & { status?: number })[]\n}\n\n/**\n * A batch answered as it goes: one line per call, each as the host finishes\n * it, in whatever order that is.\n *\n * `application/x-ndjson`, each line a reply with the call's `index` in the\n * batch. A host that answers this way lets a fast call resolve while a slow\n * sibling is still running - which is what keeps a page's boundaries\n * streaming independently when their reads travelled together. A host that\n * answers the whole batch at once, as one JSON object of `replies`, is read\n * as before; the saving of the batch stays, the independence does not.\n */\nexport type HostCallBatchLine = HostCallReply & { index: number; status?: number }\n\n/** How many calls one POST carries at most. */\nconst BATCH_LIMIT = 50\n\ntype Settled = { status: number; reply: HostCallReply | null; text: string; setCookies?: string[] }\n\ninterface Pending {\n name: string\n args: unknown[]\n resolve: (settled: Settled) => void\n reject: (error: unknown) => void\n}\n\ninterface Bucket {\n headers: Record<string, string>\n calls: Pending[]\n}\n\n// After the current I/O, before the next timer: what a render issued\n// synchronously - and across the microtasks between one component's awaits -\n// is in the bucket by then. setImmediate where the runtime has it, a zero\n// timer where it does not (a Worker).\nconst nextTick: (fn: () => void) => void =\n typeof setImmediate === 'function' ? (fn) => setImmediate(fn) : (fn) => setTimeout(fn, 0)\n\n/**\n * The function to hand to `installHostFn`, or to `hostCalls` on the JS host.\n */\n/**\n * The backend's trace, on the error the render sees.\n *\n * As its cause - an Error named for the backend's type, whose stack is the\n * backend's frames - so Node, Bun and the dev overlay print it under the\n * renderer's own stack; and appended to that stack, for whatever prints only\n * the one. Before this a failed rpc() pointed at a line of JavaScript, and the\n * PHP or Go that failed was a separate search through another log.\n */\nfunction withBackendTrace(failure: Error, debug: NonNullable<HostCallReply['debug']>): void {\n const frames = (debug.trace ?? []).map((frame) => ' at ' + frame).join('\\n')\n const cause = new Error(debug.message ?? '')\n\n cause.name = 'Backend ' + (debug.type ?? 'Error')\n cause.stack = cause.name + ': ' + (debug.message ?? '') + (frames ? '\\n' + frames : '')\n\n failure.cause = cause\n failure.stack = (failure.stack ?? String(failure)) + '\\nCaused in the backend by ' + cause.stack\n}\n\nexport function httpHostCalls(\n options: HttpHostCallsOptions,\n): (name: string, ...args: unknown[]) => Promise<unknown> {\n const {\n endpoint,\n secret,\n forwardHeaders = DEFAULT_FORWARDED,\n timeoutMs = 30_000,\n fetch: fetchImpl,\n onRevalidate,\n } = options\n\n if (!secret) {\n throw new Error('httpHostCalls requires a secret — see the note on the option.')\n }\n\n const forwarded = forwardHeaders.map((name) => name.toLowerCase())\n\n // One bucket per set of forwarded headers: two visitors' renders in flight\n // at once must not share a request, because the host reads the cookie off\n // the request to know whose session a call runs under.\n const buckets = new Map<string, Bucket>()\n\n async function forwardedHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n 'x-rsc-host-secret': secret,\n }\n\n // Outside a render — a prerender pass, or a host calling one directly —\n // there is no request to forward from, and that is not an error. The call\n // simply carries no session, which is exactly what a build-time render\n // should be doing.\n //\n // Only that one condition is swallowed. A blanket catch here is the wrong\n // shape: anything else going wrong while reading the request would come\n // out as a call with no session, which is not a failure the caller sees —\n // it is the visitor silently becoming anonymous, and the page rendering as\n // though they were logged out.\n try {\n const from = await incomingHeaders()\n\n for (const key of forwarded) {\n const value = from.get(key)\n if (value !== null) headers[key] = value\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (!message.startsWith('No request in scope')) throw error\n }\n\n return headers\n }\n\n /** One POST. `label` names what is being sent, for the error messages. */\n async function post(\n body: unknown,\n headers: Record<string, string>,\n label: string,\n ): Promise<{ status: number; text: string; setCookies: string[] }> {\n const response = await send(body, headers, label)\n\n // Read the body before branching on status: a host that reports the error\n // in JSON with a 500 is saying something more useful than \"500\", and\n // throwing on the status alone discards it.\n return {\n status: response.status,\n text: await response.text(),\n // A login's session cookie: kept to put on the page's response.\n setCookies: response.headers.getSetCookie?.() ?? [],\n }\n }\n\n /** The POST itself, headers in hand, body still to read. */\n async function send(body: unknown, headers: Record<string, string>, label: string): Promise<Response> {\n const doFetch = fetchImpl ?? globalThis.fetch\n\n // AbortSignal.timeout is not on every runtime this engine targets, so the\n // controller is written out rather than assumed.\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n\n let response: Response\n\n try {\n response = await doFetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n signal: controller.signal,\n })\n } catch (error) {\n if (controller.signal.aborted) {\n throw new Error(`Host call ${label} timed out after ${timeoutMs}ms`)\n }\n\n throw new Error(\n `Host call ${label} could not reach the host at ${endpoint}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n } finally {\n clearTimeout(timer)\n }\n\n return response\n }\n\n function parse(text: string): HostCallReply | null {\n try {\n return text ? (JSON.parse(text) as HostCallReply) : null\n } catch {\n return null\n }\n }\n\n async function single(name: string, args: unknown[], headers: Record<string, string>): Promise<Settled> {\n const { status, text, setCookies } = await post({ function: name, args }, headers, JSON.stringify(name))\n\n return { status, reply: parse(text), text, setCookies }\n }\n\n /** Read a streamed batch, one reply per line, resolving each call as its line lands. */\n async function resolveAsLinesArrive(body: ReadableStream<Uint8Array>, calls: Pending[]): Promise<void> {\n const answered = new Set<number>()\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffered = ''\n\n const take = (line: string) => {\n const trimmed = line.trim()\n\n if (!trimmed) return\n\n let parsed: Partial<HostCallBatchLine> | null\n\n try {\n parsed = JSON.parse(trimmed) as Partial<HostCallBatchLine>\n } catch {\n return\n }\n\n const index = parsed?.index\n\n if (typeof index !== 'number' || !calls[index] || answered.has(index)) return\n\n const { index: _, status: own, ...reply } = parsed as HostCallBatchLine\n\n answered.add(index)\n calls[index]!.resolve({ status: own ?? 200, reply, text: JSON.stringify(reply) })\n }\n\n try {\n for (;;) {\n const { done, value } = await reader.read()\n\n if (done) break\n\n buffered += decoder.decode(value, { stream: true })\n\n let at: number\n\n while ((at = buffered.indexOf('\\n')) !== -1) {\n take(buffered.slice(0, at))\n buffered = buffered.slice(at + 1)\n }\n }\n\n buffered += decoder.decode()\n take(buffered)\n } catch (error) {\n for (const [i, call] of calls.entries()) {\n if (!answered.has(i)) call.reject(error)\n }\n\n return\n }\n\n // The host closed the batch with a call unanswered: a fault on its side,\n // reported to the call rather than left pending forever.\n for (const [i, call] of calls.entries()) {\n if (!answered.has(i)) {\n call.reject(new Error(`Host call ${JSON.stringify(call.name)} was not answered in its batch`))\n }\n }\n }\n\n /**\n * Send a bucket. One call goes as itself, so a host that speaks only the\n * single shape - or a request that happened to be alone - sees nothing\n * new on the wire.\n */\n async function flush(bucket: Bucket): Promise<void> {\n const { headers, calls } = bucket\n\n if (calls.length === 1) {\n const [only] = calls\n\n try {\n only.resolve(await single(only.name, only.args, headers))\n } catch (error) {\n only.reject(error)\n }\n\n return\n }\n\n const label = `batch of ${calls.length} (${calls.map((c) => c.name).join(', ')})`\n let response: Response\n\n try {\n response = await send(\n { calls: calls.map((c) => ({ function: c.name, args: c.args })) } satisfies HostCallBatch,\n headers,\n label,\n )\n } catch (error) {\n // The host could not be reached, or did not answer in time. Not\n // re-sent one by one: the calls may have run, and an action run twice\n // is worse than one reported as failed.\n for (const call of calls) call.reject(error)\n\n return\n }\n\n // Answered as it goes: each call resolves the moment its line arrives,\n // so a component waiting on a fast read paints while a slow sibling's\n // is still running. The batch saved the round trips; this keeps the\n // streaming the batch would otherwise have cost.\n if (response.status < 400 && (response.headers.get('content-type') ?? '').includes('x-ndjson') && response.body) {\n await resolveAsLinesArrive(response.body, calls)\n\n return\n }\n\n const status = response.status\n const text = await response.text()\n const parsed = parse(text) as Partial<HostCallBatchReply> | null\n const replies = parsed?.replies\n\n if (status < 400 && Array.isArray(replies) && replies.length === calls.length) {\n calls.forEach((call, i) => {\n const { status: own, ...reply } = replies[i]!\n\n call.resolve({ status: own ?? 200, reply, text: JSON.stringify(reply) })\n })\n\n return\n }\n\n // Not a batch answer. Taking batches is part of the protocol - the\n // conformance suite checks it - so this is a backend that does not, or\n // one that failed before reading the calls: wrong secret, down, too many.\n // Every call fails with what the backend said, and none is sent again:\n // they may have run, and an action run twice is worse than one reported.\n const said = parsed && typeof (parsed as HostCallReply).error === 'string' ? (parsed as HostCallReply).error : text.slice(0, 200)\n const failure = new Error(\n `Host call ${label} was not answered as a batch (${status}${said ? `: ${said}` : ''}). ` +\n 'A backend answers { \"calls\": [...] } line by line or as { \"replies\": [...] }: ' +\n 'https://docs.rsc-kit.dev/hosts/your-own-backend#batches',\n )\n\n for (const call of calls) call.reject(failure)\n }\n\n function enqueue(name: string, args: unknown[], headers: Record<string, string>): Promise<Settled> {\n return new Promise<Settled>((resolve, reject) => {\n const key = JSON.stringify(headers)\n let bucket = buckets.get(key)\n\n if (!bucket) {\n bucket = { headers, calls: [] }\n buckets.set(key, bucket)\n\n const mine = bucket\n\n nextTick(() => {\n if (buckets.get(key) === mine) buckets.delete(key)\n\n void flush(mine)\n })\n }\n\n bucket.calls.push({ name, args, resolve, reject })\n\n if (bucket.calls.length >= BATCH_LIMIT) {\n buckets.delete(key)\n\n void flush(bucket)\n }\n })\n }\n\n /**\n * What a reply means, in the caller's own async context.\n *\n * Deliberately not done where the response arrives: a batch is answered in\n * a timer, outside every caller's request scope, and `redirect()` and\n * `revalidate()` both write to that scope. Interpreting here, after the\n * caller's await resumed, puts each reply back inside the render it belongs\n * to.\n */\n function interpret(name: string, { status, reply, text, setCookies }: Settled): unknown {\n // First, whatever the outcome: a login that then redirects still logged\n // the visitor in. In the caller's context, where the page's response is.\n if (setCookies?.length) passSetCookies(setCookies)\n\n // Refusing the input is not the call failing — it is the call answering.\n //\n // Thrown rather than returned, so a handler stops where it is instead of\n // carrying on with data the host rejected. createActionClient catches this\n // on the way out and returns { validationErrors }, which is the shape\n // useForm reads: React serialises a REJECTED server action opaquely, so a\n // validation error that stays thrown reaches the browser as \"an error\n // occurred\" with every field it named gone.\n //\n // Checked before `error`, so a host that sends both is read as a refusal\n // rather than as a failure with no fields.\n //\n // One error for both places a call is made. In an action, the mark is\n // what returns the fields to the form. In a page's render there is no\n // form: refusalStatus makes the page answer 422 rather than 500, and the\n // message says which call refused which fields, where \"Validation failed\"\n // read like an action had run.\n if (reply?.validationErrors) {\n const refused = new ActionValidationError(reply.validationErrors)\n const fields = Object.keys(reply.validationErrors).map((field) => field || '(the input as a whole)')\n\n refused.message = `Host call ${JSON.stringify(name)} refused its input: ${fields.join(', ')}`\n ;(refused as ActionValidationError & { refusalStatus?: number }).refusalStatus = 422\n\n throw refused\n }\n\n // Raised as the engine's own redirect, so it travels the path every other\n // redirect travels — a real 3xx above a Suspense boundary, the digest\n // below one — rather than becoming an error the page has to interpret.\n if (reply?.redirect) {\n redirect(reply.redirect as never, reply.redirectStatus ?? 307)\n }\n\n if (reply?.unauthenticated) {\n throw new ServerAuthenticationError(reply.error ?? 'Unauthenticated.')\n }\n\n if (reply?.unauthorized) {\n throw new ServerAuthorizationError(reply.error ?? 'This action is unauthorized.')\n }\n\n // The backend says there is no such thing - Go's Refuse(404), Laravel's\n // abort(404), a record this caller cannot see. Raised as the engine's\n // own notFound(), so the page answers with not-found.tsx and a 404,\n // exactly as a component calling notFound() would; as an error it\n // reached the page as \"something went wrong\".\n if (reply?.refusalStatus === 404) notFound()\n\n // A refusal with data is the backend's refuse(): its message was written\n // for the person asking, so it is raised as one, not as a failure whose\n // message onError would replace.\n if (reply?.error !== undefined && reply.refusalData !== undefined) {\n throw new ActionRefusal(reply.error, reply.refusalData, reply.refusalStatus ?? 409)\n }\n\n if (reply?.error !== undefined) {\n const failure = new Error(`Host call ${JSON.stringify(name)} failed: ${reply.error}`)\n\n // Carried on the error rather than thrown as another class: the host\n // chose a status and the only job here is not to lose it on the way to\n // whoever writes the response.\n if (reply.refusalStatus) {\n ;(failure as Error & { refusalStatus?: number }).refusalStatus = reply.refusalStatus\n }\n\n if (reply.debug) withBackendTrace(failure, reply.debug)\n\n throw failure\n }\n\n if (status >= 400) {\n throw new Error(\n `Host call ${JSON.stringify(name)} failed: ${status}`.trim() +\n (text ? ` — ${text.slice(0, 200)}` : ''),\n )\n }\n\n if (reply === null) {\n throw new Error(\n `Host call ${JSON.stringify(name)} returned a body that is not JSON: ${text.slice(0, 200)}`,\n )\n }\n\n if (reply.revalidate?.length) {\n if (onRevalidate) onRevalidate(reply.revalidate)\n // Named by the host over the wire - Rsc::revalidate('orders') in PHP -\n // so nothing here can check it; the renderer refuses an unknown name.\n else for (const target of reply.revalidate) revalidate(target as RevalidateTarget)\n }\n\n return reply.result ?? null\n }\n\n return async function hostCall(name: string, ...args: unknown[]): Promise<unknown> {\n const headers = await forwardedHeaders()\n const settled = await enqueue(name, args, headers)\n\n return interpret(name, settled)\n }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /** The script that declares the payload is coming, and what the page is built from. */
2
+ export declare function inlineFlightStart(layouts: string): string;
3
+ /**
4
+ * The HTML with the payload interleaved, and an end marker once both have
5
+ * finished. `prelude` - the declaration - goes after the first flush, never
6
+ * ahead of the HTML: in front of the doctype it would put the page in quirks
7
+ * mode.
8
+ */
9
+ export declare function inlineFlight(html: ReadableStream<Uint8Array>, flight: ReadableStream<Uint8Array>, prelude?: string): ReadableStream<Uint8Array>;
@@ -0,0 +1,126 @@
1
+ // The page's payload, streamed inside its own document.
2
+ //
3
+ // Hydrating server components needs the serialised tree as well as the HTML.
4
+ // It used to be a second request - and a second render of the whole page, its
5
+ // reads and its queries, a moment after the document's render had made exactly
6
+ // that tree. Now the render's payload is written into the document as it
7
+ // streams, a script per chunk between React's HTML, and the runtime reads it
8
+ // from there. One render per visit, nothing held anywhere, and the slow parts
9
+ // of the page stream in the payload the moment they do in the HTML.
10
+ //
11
+ // React writes its HTML in pieces that can end mid-tag, but always all the
12
+ // pieces of one flush in the same task. So a payload chunk is written on the
13
+ // next task, never between two pieces of one flush. (The rsc-html-stream
14
+ // package does the same.)
15
+ //
16
+ // Read in the browser by js/inlinePayload.ts.
17
+ const encoder = new TextEncoder();
18
+ /** The script that declares the payload is coming, and what the page is built from. */
19
+ export function inlineFlightStart(layouts) {
20
+ return `<script>self.__rsc_f=self.__rsc_f||[];self.__rsc_l=${script(layouts)}</script>`;
21
+ }
22
+ /**
23
+ * The HTML with the payload interleaved, and an end marker once both have
24
+ * finished. `prelude` - the declaration - goes after the first flush, never
25
+ * ahead of the HTML: in front of the doctype it would put the page in quirks
26
+ * mode.
27
+ */
28
+ export function inlineFlight(html, flight, prelude = "") {
29
+ const htmlReader = html.getReader();
30
+ const flightReader = flight.getReader();
31
+ const decoder = new TextDecoder("utf-8", { fatal: true });
32
+ let pending = prelude ? [prelude] : [];
33
+ let scheduled = null;
34
+ let flightDone = false;
35
+ let htmlDone = false;
36
+ let closed = false;
37
+ return new ReadableStream({
38
+ start(controller) {
39
+ const flush = () => {
40
+ scheduled = null;
41
+ if (closed)
42
+ return;
43
+ if (pending.length) {
44
+ controller.enqueue(encoder.encode(pending.join("")));
45
+ pending = [];
46
+ }
47
+ if (flightDone && htmlDone) {
48
+ closed = true;
49
+ controller.enqueue(encoder.encode("<script>self.__rsc_f.push(null)</script>"));
50
+ controller.close();
51
+ }
52
+ };
53
+ // After the task React is flushing in, so never between its pieces.
54
+ const later = () => {
55
+ scheduled ??= setTimeout(flush, 0);
56
+ };
57
+ const readHtml = async () => {
58
+ for (;;) {
59
+ const { done, value } = await htmlReader.read();
60
+ if (done)
61
+ break;
62
+ if (!closed)
63
+ controller.enqueue(value);
64
+ // Anything the payload had waiting goes after this flush, not in it.
65
+ if (pending.length)
66
+ later();
67
+ }
68
+ htmlDone = true;
69
+ later();
70
+ };
71
+ const readFlight = async () => {
72
+ for (;;) {
73
+ const { done, value } = await flightReader.read();
74
+ if (done)
75
+ break;
76
+ pending.push(`<script>self.__rsc_f.push(${chunk(value)})</script>`);
77
+ later();
78
+ }
79
+ // A character split across the last two chunks would be held here.
80
+ const tail = decoder.decode();
81
+ if (tail)
82
+ pending.push(`<script>self.__rsc_f.push(${script(tail)})</script>`);
83
+ flightDone = true;
84
+ later();
85
+ };
86
+ const fail = (error) => {
87
+ if (closed)
88
+ return;
89
+ closed = true;
90
+ void htmlReader.cancel(error).catch(() => { });
91
+ void flightReader.cancel(error).catch(() => { });
92
+ controller.error(error);
93
+ };
94
+ readHtml().catch(fail);
95
+ readFlight().catch(fail);
96
+ },
97
+ cancel(reason) {
98
+ closed = true;
99
+ if (scheduled)
100
+ clearTimeout(scheduled);
101
+ void htmlReader.cancel(reason).catch(() => { });
102
+ void flightReader.cancel(reason).catch(() => { });
103
+ },
104
+ });
105
+ // Text as a string, which compresses with the page. A chunk that is not
106
+ // UTF-8 - a typed array in the payload - goes as base64.
107
+ function chunk(value) {
108
+ try {
109
+ return script(decoder.decode(value, { stream: true }));
110
+ }
111
+ catch {
112
+ let binary = "";
113
+ for (const byte of value)
114
+ binary += String.fromCharCode(byte);
115
+ return `{b:${script(btoa(binary))}}`;
116
+ }
117
+ }
118
+ }
119
+ /** A string literal safe inside a <script>: no way to close the element or open a comment. */
120
+ function script(text) {
121
+ return JSON.stringify(text)
122
+ .replace(/</g, "\\u003c")
123
+ .replace(/\u2028/g, "\\u2028")
124
+ .replace(/\u2029/g, "\\u2029");
125
+ }
126
+ //# sourceMappingURL=inlineFlight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inlineFlight.js","sourceRoot":"","sources":["../src/inlineFlight.ts"],"names":[],"mappings":"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","sourcesContent":["// The page's payload, streamed inside its own document.\n//\n// Hydrating server components needs the serialised tree as well as the HTML.\n// It used to be a second request - and a second render of the whole page, its\n// reads and its queries, a moment after the document's render had made exactly\n// that tree. Now the render's payload is written into the document as it\n// streams, a script per chunk between React's HTML, and the runtime reads it\n// from there. One render per visit, nothing held anywhere, and the slow parts\n// of the page stream in the payload the moment they do in the HTML.\n//\n// React writes its HTML in pieces that can end mid-tag, but always all the\n// pieces of one flush in the same task. So a payload chunk is written on the\n// next task, never between two pieces of one flush. (The rsc-html-stream\n// package does the same.)\n//\n// Read in the browser by js/inlinePayload.ts.\n\nconst encoder = new TextEncoder();\n\n/** The script that declares the payload is coming, and what the page is built from. */\nexport function inlineFlightStart(layouts: string): string {\n return `<script>self.__rsc_f=self.__rsc_f||[];self.__rsc_l=${script(layouts)}</script>`;\n}\n\n/**\n * The HTML with the payload interleaved, and an end marker once both have\n * finished. `prelude` - the declaration - goes after the first flush, never\n * ahead of the HTML: in front of the doctype it would put the page in quirks\n * mode.\n */\nexport function inlineFlight(\n html: ReadableStream<Uint8Array>,\n flight: ReadableStream<Uint8Array>,\n prelude = \"\",\n): ReadableStream<Uint8Array> {\n const htmlReader = html.getReader();\n const flightReader = flight.getReader();\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n let pending: string[] = prelude ? [prelude] : [];\n let scheduled: ReturnType<typeof setTimeout> | null = null;\n let flightDone = false;\n let htmlDone = false;\n let closed = false;\n\n return new ReadableStream<Uint8Array>({\n start(controller) {\n const flush = () => {\n scheduled = null;\n\n if (closed) return;\n if (pending.length) {\n controller.enqueue(encoder.encode(pending.join(\"\")));\n pending = [];\n }\n if (flightDone && htmlDone) {\n closed = true;\n controller.enqueue(encoder.encode(\"<script>self.__rsc_f.push(null)</script>\"));\n controller.close();\n }\n };\n // After the task React is flushing in, so never between its pieces.\n const later = () => {\n scheduled ??= setTimeout(flush, 0);\n };\n\n const readHtml = async () => {\n for (;;) {\n const { done, value } = await htmlReader.read();\n\n if (done) break;\n if (!closed) controller.enqueue(value);\n // Anything the payload had waiting goes after this flush, not in it.\n if (pending.length) later();\n }\n htmlDone = true;\n later();\n };\n\n const readFlight = async () => {\n for (;;) {\n const { done, value } = await flightReader.read();\n\n if (done) break;\n pending.push(`<script>self.__rsc_f.push(${chunk(value)})</script>`);\n later();\n }\n // A character split across the last two chunks would be held here.\n const tail = decoder.decode();\n\n if (tail) pending.push(`<script>self.__rsc_f.push(${script(tail)})</script>`);\n flightDone = true;\n later();\n };\n\n const fail = (error: unknown) => {\n if (closed) return;\n closed = true;\n void htmlReader.cancel(error).catch(() => {});\n void flightReader.cancel(error).catch(() => {});\n controller.error(error);\n };\n\n readHtml().catch(fail);\n readFlight().catch(fail);\n },\n cancel(reason) {\n closed = true;\n if (scheduled) clearTimeout(scheduled);\n void htmlReader.cancel(reason).catch(() => {});\n void flightReader.cancel(reason).catch(() => {});\n },\n });\n\n // Text as a string, which compresses with the page. A chunk that is not\n // UTF-8 - a typed array in the payload - goes as base64.\n function chunk(value: Uint8Array): string {\n try {\n return script(decoder.decode(value, { stream: true }));\n } catch {\n let binary = \"\";\n\n for (const byte of value) binary += String.fromCharCode(byte);\n\n return `{b:${script(btoa(binary))}}`;\n }\n }\n}\n\n/** A string literal safe inside a <script>: no way to close the element or open a comment. */\nfunction script(text: string): string {\n return JSON.stringify(text)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n"]}
package/dist/js/Form.d.ts CHANGED
@@ -34,7 +34,7 @@ interface FieldBinding<V> {
34
34
  * is caught on `field()`, which is closed to the declared names.
35
35
  */
36
36
  type FieldPath<T> = (keyof T & string) | (string & {});
37
- interface FormRenderProps<T extends Record<string, unknown> = Record<string, unknown>> {
37
+ interface FormRenderProps<T extends Record<string, unknown> = Record<string, unknown>, Refusal = unknown> {
38
38
  pending: boolean;
39
39
  /** What is being submitted, while it is. */
40
40
  data: T;
@@ -63,6 +63,13 @@ interface FormRenderProps<T extends Record<string, unknown> = Record<string, unk
63
63
  * React's stand-in and not something to show.
64
64
  */
65
65
  formError: string | undefined;
66
+ /**
67
+ * What the action's refusal carried beside `formError` - the records
68
+ * blocking a delete, say - when the action refused with `refuse()` and
69
+ * declared its shape with `.refusal(schema)`. Typed with `useForm<T, R>()`.
70
+ * Goes when `formError` does.
71
+ */
72
+ formRefusal: Refusal | undefined;
66
73
  clearErrors: (...fields: FieldPath<T>[]) => void;
67
74
  reset: () => void;
68
75
  /**
@@ -196,7 +203,7 @@ export declare function useFormStore<T extends Record<string, unknown>>(initial?
196
203
  * Not React's `useFormStatus`, which is a different hook with a different
197
204
  * answer - which is why this one is not called that.
198
205
  */
199
- export declare function useForm<T extends Record<string, unknown> = Record<string, unknown>>(): FormRenderProps<T>;
206
+ export declare function useForm<T extends Record<string, unknown> = Record<string, unknown>, Refusal = unknown>(): FormRenderProps<T, Refusal>;
200
207
  /**
201
208
  * One field, subscribed on its own - for a large controlled form.
202
209
  *
package/dist/js/Form.js CHANGED
@@ -26,7 +26,7 @@ function resultOf(value) {
26
26
  if (result.validationErrors)
27
27
  return { errors: result.validationErrors };
28
28
  if (result.serverError)
29
- return { serverError: result.serverError };
29
+ return { serverError: result.serverError, refusal: result.refusal };
30
30
  if (typeof result.redirected === "string")
31
31
  return { redirected: result.redirected };
32
32
  return null;
@@ -37,6 +37,7 @@ const FormStatusContext = createContext({
37
37
  dirty: false,
38
38
  error: () => undefined,
39
39
  formError: undefined,
40
+ formRefusal: undefined,
40
41
  clearErrors: () => { },
41
42
  reset: () => { },
42
43
  succeeded: false,
@@ -193,6 +194,11 @@ export default function Form({ action, method: methodProp, defaultValues, store:
193
194
  const [posted, formAction] = useActionState(bindable ? boundSubmit(action) : async () => null, null);
194
195
  const postedRefusal = resultOf(posted);
195
196
  const [errors, setErrors] = useState(() => postedRefusal?.errors ?? {});
197
+ // A refusal's data, tied to the exact form-level error it came with: when
198
+ // that entry is cleared or replaced - the next submit, a success,
199
+ // clearErrors() - the data goes with it, without every place that touches
200
+ // the errors having to know.
201
+ const [refusal, setRefusal] = useState(null);
196
202
  const [touched, setTouched] = useState({});
197
203
  /**
198
204
  * Mark a field visited, and check it.
@@ -451,7 +457,9 @@ export default function Form({ action, method: methodProp, defaultValues, store:
451
457
  else {
452
458
  // Under the form's own key, so every place that clears the
453
459
  // errors clears it too.
454
- setErrors({ "": [refused.serverError ?? "The action was refused."] });
460
+ const formLevel = [refused.serverError ?? "The action was refused."];
461
+ setErrors({ "": formLevel });
462
+ setRefusal(refused.refusal === undefined ? null : { for: formLevel, data: refused.refusal });
455
463
  onError?.({}, new Error(refused.serverError));
456
464
  }
457
465
  return;
@@ -543,6 +551,7 @@ export default function Form({ action, method: methodProp, defaultValues, store:
543
551
  field,
544
552
  error,
545
553
  formError: errors[""]?.[0],
554
+ formRefusal: refusal && errors[""] === refusal.for ? refusal.data : undefined,
546
555
  clearErrors,
547
556
  reset: resetForm,
548
557
  };