@rsc-kit/core 0.28.1 → 0.29.1
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.
- package/dist/changed.d.ts +17 -9
- package/dist/changed.js +116 -2
- package/dist/changed.js.map +1 -1
- package/dist/conformance.d.ts +14 -1
- package/dist/conformance.js +116 -5
- package/dist/conformance.js.map +1 -1
- package/dist/host.js +5 -3
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +0 -6
- package/dist/hostCalls.js +27 -21
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/refreshOn.d.ts +20 -2
- package/dist/js/refreshOn.js +33 -2
- package/dist/js/refreshOn.js.map +1 -1
- package/dist/js/section.d.ts +11 -0
- package/dist/js/section.js.map +1 -1
- package/dist/request.d.ts +8 -0
- package/dist/request.js +31 -0
- package/dist/request.js.map +1 -1
- package/dist/testHost.d.ts +2 -0
- package/dist/testHost.js +12 -1
- package/dist/testHost.js.map +1 -1
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +29 -10
- package/dist/testing.js.map +1 -1
- package/dist/vite.js +51 -2
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/hostCalls.d.ts
CHANGED
|
@@ -40,12 +40,6 @@ export interface HttpHostCallsOptions {
|
|
|
40
40
|
* page simply shows stale data with no error anywhere.
|
|
41
41
|
*/
|
|
42
42
|
onRevalidate?: (targets: string[]) => void;
|
|
43
|
-
/**
|
|
44
|
-
* Send calls issued in the same tick as one POST. On by default; a host
|
|
45
|
-
* that does not understand the envelope is detected on the first batch and
|
|
46
|
-
* sent single calls from then on. `false` never batches.
|
|
47
|
-
*/
|
|
48
|
-
batch?: boolean;
|
|
49
43
|
}
|
|
50
44
|
export interface HostCallReply {
|
|
51
45
|
result?: unknown;
|
package/dist/hostCalls.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// function, the deferred-release ordering in streaming.ts still applies, and a
|
|
11
11
|
// reply may still carry `revalidate` — a Go host that answers this endpoint
|
|
12
12
|
// gets the same semantics the socket has, without the framing.
|
|
13
|
-
import { headers as incomingHeaders } from './request.js';
|
|
13
|
+
import { headers as incomingHeaders, passSetCookies } from './request.js';
|
|
14
14
|
import { revalidate } from './revalidate.js';
|
|
15
15
|
import { ActionValidationError } from './action.js';
|
|
16
16
|
import { redirect } from './redirect.js';
|
|
@@ -45,13 +45,11 @@ function withBackendTrace(failure, debug) {
|
|
|
45
45
|
failure.stack = (failure.stack ?? String(failure)) + '\nCaused in the backend by ' + cause.stack;
|
|
46
46
|
}
|
|
47
47
|
export function httpHostCalls(options) {
|
|
48
|
-
const { endpoint, secret, forwardHeaders = DEFAULT_FORWARDED, timeoutMs = 30_000, fetch: fetchImpl, onRevalidate,
|
|
48
|
+
const { endpoint, secret, forwardHeaders = DEFAULT_FORWARDED, timeoutMs = 30_000, fetch: fetchImpl, onRevalidate, } = options;
|
|
49
49
|
if (!secret) {
|
|
50
50
|
throw new Error('httpHostCalls requires a secret — see the note on the option.');
|
|
51
51
|
}
|
|
52
52
|
const forwarded = forwardHeaders.map((name) => name.toLowerCase());
|
|
53
|
-
// Off for good the first time the host answers a batch as something else.
|
|
54
|
-
let batching = batch;
|
|
55
53
|
// One bucket per set of forwarded headers: two visitors' renders in flight
|
|
56
54
|
// at once must not share a request, because the host reads the cookie off
|
|
57
55
|
// the request to know whose session a call runs under.
|
|
@@ -92,7 +90,12 @@ export function httpHostCalls(options) {
|
|
|
92
90
|
// Read the body before branching on status: a host that reports the error
|
|
93
91
|
// in JSON with a 500 is saying something more useful than "500", and
|
|
94
92
|
// throwing on the status alone discards it.
|
|
95
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
status: response.status,
|
|
95
|
+
text: await response.text(),
|
|
96
|
+
// A login's session cookie: kept to put on the page's response.
|
|
97
|
+
setCookies: response.headers.getSetCookie?.() ?? [],
|
|
98
|
+
};
|
|
96
99
|
}
|
|
97
100
|
/** The POST itself, headers in hand, body still to read. */
|
|
98
101
|
async function send(body, headers, label) {
|
|
@@ -130,8 +133,8 @@ export function httpHostCalls(options) {
|
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
async function single(name, args, headers) {
|
|
133
|
-
const { status, text } = await post({ function: name, args }, headers, JSON.stringify(name));
|
|
134
|
-
return { status, reply: parse(text), text };
|
|
136
|
+
const { status, text, setCookies } = await post({ function: name, args }, headers, JSON.stringify(name));
|
|
137
|
+
return { status, reply: parse(text), text, setCookies };
|
|
135
138
|
}
|
|
136
139
|
/** Read a streamed batch, one reply per line, resolving each call as its line lands. */
|
|
137
140
|
async function resolveAsLinesArrive(body, calls) {
|
|
@@ -236,18 +239,17 @@ export function httpHostCalls(options) {
|
|
|
236
239
|
});
|
|
237
240
|
return;
|
|
238
241
|
}
|
|
239
|
-
// Not a batch answer
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}));
|
|
242
|
+
// Not a batch answer. Taking batches is part of the protocol - the
|
|
243
|
+
// conformance suite checks it - so this is a backend that does not, or
|
|
244
|
+
// one that failed before reading the calls: wrong secret, down, too many.
|
|
245
|
+
// Every call fails with what the backend said, and none is sent again:
|
|
246
|
+
// they may have run, and an action run twice is worse than one reported.
|
|
247
|
+
const said = parsed && typeof parsed.error === 'string' ? parsed.error : text.slice(0, 200);
|
|
248
|
+
const failure = new Error(`Host call ${label} was not answered as a batch (${status}${said ? `: ${said}` : ''}). ` +
|
|
249
|
+
'A backend answers { "calls": [...] } line by line or as { "replies": [...] }: ' +
|
|
250
|
+
'https://docs.rsc-kit.dev/hosts/your-own-backend#batches');
|
|
251
|
+
for (const call of calls)
|
|
252
|
+
call.reject(failure);
|
|
251
253
|
}
|
|
252
254
|
function enqueue(name, args, headers) {
|
|
253
255
|
return new Promise((resolve, reject) => {
|
|
@@ -279,7 +281,11 @@ export function httpHostCalls(options) {
|
|
|
279
281
|
* caller's await resumed, puts each reply back inside the render it belongs
|
|
280
282
|
* to.
|
|
281
283
|
*/
|
|
282
|
-
function interpret(name, { status, reply, text }) {
|
|
284
|
+
function interpret(name, { status, reply, text, setCookies }) {
|
|
285
|
+
// First, whatever the outcome: a login that then redirects still logged
|
|
286
|
+
// the visitor in. In the caller's context, where the page's response is.
|
|
287
|
+
if (setCookies?.length)
|
|
288
|
+
passSetCookies(setCookies);
|
|
283
289
|
// Refusing the input is not the call failing — it is the call answering.
|
|
284
290
|
//
|
|
285
291
|
// Thrown rather than returned, so a handler stops where it is instead of
|
|
@@ -346,7 +352,7 @@ export function httpHostCalls(options) {
|
|
|
346
352
|
}
|
|
347
353
|
return async function hostCall(name, ...args) {
|
|
348
354
|
const headers = await forwardedHeaders();
|
|
349
|
-
const settled =
|
|
355
|
+
const settled = await enqueue(name, args, headers);
|
|
350
356
|
return interpret(name, settled);
|
|
351
357
|
};
|
|
352
358
|
}
|
package/dist/hostCalls.js.map
CHANGED
|
@@ -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 } 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 * Send calls issued in the same tick as one POST. On by default; a host\n * that does not understand the envelope is detected on the first batch and\n * sent single calls from then on. `false` never batches.\n */\n batch?: boolean\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 }\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 batch = true,\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 // Off for good the first time the host answers a batch as something else.\n let batching = batch\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 }> {\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 { status: response.status, text: await response.text() }\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 } = await post({ function: name, args }, headers, JSON.stringify(name))\n\n return { status, reply: parse(text), text }\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: a host that does not know the envelope refused it\n // as one malformed call, before running anything. From here on, one at a\n // time - and these, now.\n batching = false\n\n await Promise.all(\n calls.map(async (call) => {\n try {\n call.resolve(await single(call.name, call.args, headers))\n } catch (error) {\n call.reject(error)\n }\n }),\n )\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 }: Settled): unknown {\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 if (reply?.validationErrors) {\n throw new ActionValidationError(reply.validationErrors)\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 = batching ? await enqueue(name, args, headers) : await single(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 { 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 if (reply?.validationErrors) {\n throw new ActionValidationError(reply.validationErrors)\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"]}
|
package/dist/js/refreshOn.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
/** A prop as it arrives awaited: a page's `params` is a promise of them. */
|
|
3
|
+
type Awaited_<T, Else> = [T] extends [never] ? Else : unknown extends T ? Else : T extends Promise<infer V> ? V : T extends undefined ? Else : T;
|
|
2
4
|
/**
|
|
3
5
|
* What a refreshOn function is given: the page's params and search params,
|
|
4
6
|
* already awaited, beside whatever props the section was rendered with.
|
|
7
|
+
*
|
|
8
|
+
* Typed from P when it says what they are - a page's `PageProps<typeof
|
|
9
|
+
* params>` - so `params.team` is the schema's type, not a string by hand.
|
|
5
10
|
*/
|
|
6
11
|
export type RefreshOnInput<P> = Omit<P, "params" | "searchParams"> & {
|
|
7
|
-
params:
|
|
8
|
-
|
|
12
|
+
params: Awaited_<P extends {
|
|
13
|
+
params?: infer A;
|
|
14
|
+
} ? A : never, Record<string, string>>;
|
|
15
|
+
searchParams: Awaited_<P extends {
|
|
16
|
+
searchParams?: infer S;
|
|
17
|
+
} ? S : never, URLSearchParams | Record<string, unknown>>;
|
|
9
18
|
};
|
|
10
19
|
/** What a section or page refreshes on: names, or a function of its props that makes them. */
|
|
11
20
|
export type RefreshOnList<P> = string[] | ((input: RefreshOnInput<P>) => string[] | Promise<string[]>);
|
|
@@ -27,3 +36,12 @@ export declare function RefreshOn<P>({ target, refreshOn, props, }: {
|
|
|
27
36
|
refreshOn: RefreshOnList<P>;
|
|
28
37
|
props: P;
|
|
29
38
|
}): ReactNode;
|
|
39
|
+
/** Where createTestApp's watched() collects what a render's regions refresh on. */
|
|
40
|
+
export declare const WATCHED: unique symbol;
|
|
41
|
+
/**
|
|
42
|
+
* The names a region refreshes on, for this render: its list, or what its
|
|
43
|
+
* function made of the input - unique, and only non-empty strings. Warns in
|
|
44
|
+
* development when that is none at all.
|
|
45
|
+
*/
|
|
46
|
+
export declare function namesFor<P>(target: string, refreshOn: RefreshOnList<P>, input: RefreshOnInput<P>): Promise<string[]>;
|
|
47
|
+
export {};
|
package/dist/js/refreshOn.js
CHANGED
|
@@ -18,7 +18,10 @@ import { Changes } from "./Changes";
|
|
|
18
18
|
export function RefreshOn({ target, refreshOn, props, }) {
|
|
19
19
|
return createElement(Suspense, { fallback: null }, createElement(Resolve, { target, refreshOn, props }));
|
|
20
20
|
}
|
|
21
|
+
/** Where createTestApp's watched() collects what a render's regions refresh on. */
|
|
22
|
+
export const WATCHED = Symbol.for("rsc-kit.test-watched");
|
|
21
23
|
const warned = new Set();
|
|
24
|
+
const emptyWarned = new Set();
|
|
22
25
|
/**
|
|
23
26
|
* Never throws. An error here would become an error row inside the region's
|
|
24
27
|
* payload, and the browser decodes that as the region failing - a blank
|
|
@@ -46,8 +49,12 @@ async function Resolve({ target, refreshOn, props, }) {
|
|
|
46
49
|
searchParams: ((await given.searchParams) ??
|
|
47
50
|
new URLSearchParams()),
|
|
48
51
|
};
|
|
49
|
-
const
|
|
50
|
-
|
|
52
|
+
const unique = await namesFor(target, refreshOn, input);
|
|
53
|
+
// A test asking what this page watches - createTestApp's watched() - is
|
|
54
|
+
// told here, rather than left to dig the signed names out of the payload.
|
|
55
|
+
const recording = globalThis[WATCHED];
|
|
56
|
+
if (recording)
|
|
57
|
+
recording[target] = unique;
|
|
51
58
|
if (unique.length === 0)
|
|
52
59
|
return null;
|
|
53
60
|
// -1 is a version nothing is at, so every name comes back with its current version.
|
|
@@ -66,4 +73,28 @@ async function Resolve({ target, refreshOn, props, }) {
|
|
|
66
73
|
return null;
|
|
67
74
|
}
|
|
68
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The names a region refreshes on, for this render: its list, or what its
|
|
78
|
+
* function made of the input - unique, and only non-empty strings. Warns in
|
|
79
|
+
* development when that is none at all.
|
|
80
|
+
*/
|
|
81
|
+
export async function namesFor(target, refreshOn, input) {
|
|
82
|
+
const named = typeof refreshOn === "function" ? await refreshOn(input) : refreshOn;
|
|
83
|
+
const unique = [...new Set(named)].filter((name) => typeof name === "string" && name !== "");
|
|
84
|
+
if (unique.length === 0) {
|
|
85
|
+
// Usually a mistake that looks like working code - a param read under
|
|
86
|
+
// the wrong name, a list built from an empty query - and the region
|
|
87
|
+
// then refreshes on nothing. Said in development, once per region; a
|
|
88
|
+
// region that means to watch nothing for some visitors can ignore it.
|
|
89
|
+
if (globalThis.process?.env
|
|
90
|
+
?.NODE_ENV !== "production" &&
|
|
91
|
+
!emptyWarned.has(target)) {
|
|
92
|
+
emptyWarned.add(target);
|
|
93
|
+
console.warn(`[rsc-kit] ${JSON.stringify(target)}'s refreshOn gave no names, so it refreshes on nothing. ` +
|
|
94
|
+
"Check what it reads - a param under another name is undefined.");
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return unique;
|
|
99
|
+
}
|
|
69
100
|
//# sourceMappingURL=refreshOn.js.map
|
package/dist/js/refreshOn.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refreshOn.js","sourceRoot":"","sources":["../../src/js/refreshOn.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"refreshOn.js","sourceRoot":"","sources":["../../src/js/refreshOn.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Suspense, createElement } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { connection, currentPageProps } from \"../request\";\nimport { sign, versionSource } from \"../changed\";\nimport type { SignedName } from \"../changed\";\nimport { Changes } from \"./Changes\";\n\n/** A prop as it arrives awaited: a page's `params` is a promise of them. */\ntype Awaited_<T, Else> = [T] extends [never]\n ? Else\n : unknown extends T\n ? Else // props that do not say: a section's own, with no params among them\n : T extends Promise<infer V>\n ? V\n : T extends undefined\n ? Else\n : T;\n\n/**\n * What a refreshOn function is given: the page's params and search params,\n * already awaited, beside whatever props the section was rendered with.\n *\n * Typed from P when it says what they are - a page's `PageProps<typeof\n * params>` - so `params.team` is the schema's type, not a string by hand.\n */\nexport type RefreshOnInput<P> = Omit<P, \"params\" | \"searchParams\"> & {\n params: Awaited_<P extends { params?: infer A } ? A : never, Record<string, string>>;\n searchParams: Awaited_<\n P extends { searchParams?: infer S } ? S : never,\n URLSearchParams | Record<string, unknown>\n >;\n};\n\n/** What a section or page refreshes on: names, or a function of its props that makes them. */\nexport type RefreshOnList<P> =\n string[] | ((input: RefreshOnInput<P>) => string[] | Promise<string[]>);\n\n/**\n * The names a region refreshes on, read for this render and signed for the tab.\n *\n * A server component, rendered beside the region: it works out the names\n * from the props, asks the source what version each is at, and hands them to\n * the client as `Changes`. Under its own Suspense boundary, so the ask never\n * holds up the region's paint - the data is what the visitor is waiting on,\n * not the version numbers behind it.\n *\n * Renders nothing when the source cannot answer - a backend that is down\n * for this one call. The region still renders; it just does not refresh on\n * its names until the next time it is rendered.\n */\nexport function RefreshOn<P>({\n target,\n refreshOn,\n props,\n}: {\n target: string;\n refreshOn: RefreshOnList<P>;\n props: P;\n}): ReactNode {\n return createElement(\n Suspense,\n { fallback: null },\n createElement(Resolve as never, { target, refreshOn, props }),\n );\n}\n\n/** Where createTestApp's watched() collects what a render's regions refresh on. */\nexport const WATCHED = Symbol.for(\"rsc-kit.test-watched\");\n\nconst warned = new Set<string>();\nconst emptyWarned = new Set<string>();\n\n/**\n * Never throws. An error here would become an error row inside the region's\n * payload, and the browser decodes that as the region failing - a blank\n * document for a version lookup that did not go through. Said once per\n * region instead, and the region renders without refreshing on anything.\n */\nasync function Resolve<P>({\n target,\n refreshOn,\n props,\n}: {\n target: string;\n refreshOn: RefreshOnList<P>;\n props: P;\n}): Promise<ReactNode> {\n try {\n // Per request, never at build. A version read at build is stale by the\n // time anyone loads the page, and a name signed at build is signed with\n // whatever key the build machine had. At build this suspends and the\n // names become a hole in the stored shell, filled per request.\n await connection();\n\n // The page's params and searchParams, under whatever the page passed: a\n // section rendered by its page is given nothing, and alone is given these.\n const given = { ...currentPageProps(), ...(props as object) } as Record<\n string,\n unknown\n >;\n\n // Awaited here, so `({ params }) => [\\`team:${params.team}\\`]` - the way\n // anyone writes it - means the team. A page's params and search params\n // are promises, and read without awaiting they made `team:undefined`:\n // one name for every team, so each refreshed on all the others' changes.\n // Awaiting costs nothing here; the names are made per request anyway.\n const input = {\n ...given,\n params: ((await given.params) ?? {}) as Record<string, string>,\n searchParams: ((await given.searchParams) ??\n new URLSearchParams()) as URLSearchParams,\n } as RefreshOnInput<P>;\n const unique = await namesFor(target, refreshOn, input);\n\n // A test asking what this page watches - createTestApp's watched() - is\n // told here, rather than left to dig the signed names out of the payload.\n const recording = (globalThis as { [WATCHED]?: Record<string, string[]> })[WATCHED];\n\n if (recording) recording[target] = unique;\n\n if (unique.length === 0) return null;\n\n // -1 is a version nothing is at, so every name comes back with its current version.\n const versions = await versionSource().changed(\n Object.fromEntries(unique.map((name) => [name, -1])),\n 0,\n );\n const signed: Record<string, SignedName> = {};\n\n for (const name of unique)\n signed[name] = [versions[name] ?? 0, await sign(name)];\n\n return createElement(Changes, { target, names: signed });\n } catch (error) {\n if (!warned.has(target)) {\n warned.add(target);\n console.warn(\n `[rsc-kit] what ${JSON.stringify(target)} refreshes on could not be read, so it will not: ` +\n (error instanceof Error ? error.message : String(error)),\n );\n }\n\n return null;\n }\n}\n\n/**\n * The names a region refreshes on, for this render: its list, or what its\n * function made of the input - unique, and only non-empty strings. Warns in\n * development when that is none at all.\n */\nexport async function namesFor<P>(\n target: string,\n refreshOn: RefreshOnList<P>,\n input: RefreshOnInput<P>,\n): Promise<string[]> {\n const named =\n typeof refreshOn === \"function\" ? await refreshOn(input) : refreshOn;\n const unique = [...new Set(named)].filter(\n (name) => typeof name === \"string\" && name !== \"\",\n );\n\n if (unique.length === 0) {\n // Usually a mistake that looks like working code - a param read under\n // the wrong name, a list built from an empty query - and the region\n // then refreshes on nothing. Said in development, once per region; a\n // region that means to watch nothing for some visitors can ignore it.\n if (\n (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n ?.NODE_ENV !== \"production\" &&\n !emptyWarned.has(target)\n ) {\n emptyWarned.add(target);\n console.warn(\n `[rsc-kit] ${JSON.stringify(target)}'s refreshOn gave no names, so it refreshes on nothing. ` +\n \"Check what it reads - a param under another name is undefined.\",\n );\n }\n\n return [];\n }\n\n return unique;\n}\n"]}
|
package/dist/js/section.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
2
|
import type { RefreshOnList } from "./refreshOn";
|
|
3
|
+
import type { PageProps } from "../routeSchema";
|
|
4
|
+
export type { RefreshOnInput, RefreshOnList } from "./refreshOn";
|
|
5
|
+
/**
|
|
6
|
+
* A page's `export const refreshOn`, typed from its own url schemas:
|
|
7
|
+
*
|
|
8
|
+
* export const params = z.object({ team: z.string() })
|
|
9
|
+
* export const refreshOn: PageRefreshOn<typeof params> = ({ params }) => [`team:${params.team}:repos`]
|
|
10
|
+
*
|
|
11
|
+
* `params` arrives awaited and parsed; without schemas, it is a record of strings.
|
|
12
|
+
*/
|
|
13
|
+
export type PageRefreshOn<P = never, S = never> = RefreshOnList<PageProps<P, S>>;
|
|
3
14
|
export interface SectionOptions<P> {
|
|
4
15
|
/**
|
|
5
16
|
* What this section refreshes on: names for the data it shows, or a
|
package/dist/js/section.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.js","sourceRoot":"","sources":["../../src/js/section.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"section.js","sourceRoot":"","sources":["../../src/js/section.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAgBxC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,6EAA6E;AAC7E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;AAE5D,yDAAyD;AACzD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;AAoB1D,MAAM,UAAU,OAAO,CACrB,IAAY,EACZ,SAA2B,EAC3B,OAAO,GAAsB,EAAE;IAE/B,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAE9B,2EAA2E;IAC3E,gEAAgE;IAChE,MAAM,KAAK,GAAqB,SAAS;QACvC,CAAC,CAAC,SAAS,gBAAgB,CAAC,KAAQ;YAChC,OAAO,aAAa,CAClB,QAAQ,EACR,IAAI,EACJ,aAAa,CACX,SAAiC,EACjC,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAW,CAC9D,EACD,aAAa,CACX,SAAiC,EACjC,EAAE,GAAG,EAAE,SAAS,EAAE,GAAI,KAAgB,EAAW,CAClD,CACF,CAAC;QACJ,CAAC;QACH,CAAC,CAAC,SAAS,CAAC;IAEd,2EAA2E;IAC3E,qDAAqD;IACrD,SAAS,OAAO,CAAC,KAAQ;QACvB,sEAAsE;QACtE,oDAAoD;QACpD,OAAO,aAAa,CAClB,YAA+C,EAC/C,EAAE,IAAI,EAAE,EACR,aAAa,CAAC,KAA6B,EAAE,KAAc,CAAC,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,WAAW,GAAG,WAAW,IAAI,GAAG,CAAC;IACzC,sEAAsE;IACrE,OAA8C,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;IAC/D,IAAI,OAAO,CAAC,MAAM;QACf,OAA8C,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAEjE,OAAO,OAA2B,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAiB;IAEjB,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IAErD,OAAQ,QAA+C,CAAC,KAAK,CACT,CAAC;AACvD,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,eAAe,CAAC,QAAiB;IAC/C,OAAO,CACL,OAAO,QAAQ,KAAK,UAAU;QAC7B,QAA+C,CAAC,MAAM,CAAC,KAAK,IAAI,CAClE,CAAC;AACJ,CAAC","sourcesContent":["import { Fragment, createElement } from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport { SlotBoundary } from \"./SlotBoundary\";\nimport { RefreshOn } from \"./refreshOn\";\nimport type { RefreshOnList } from \"./refreshOn\";\nimport type { PageProps } from \"../routeSchema\";\n\nexport type { RefreshOnInput, RefreshOnList } from \"./refreshOn\";\n\n/**\n * A page's `export const refreshOn`, typed from its own url schemas:\n *\n * export const params = z.object({ team: z.string() })\n * export const refreshOn: PageRefreshOn<typeof params> = ({ params }) => [`team:${params.team}:repos`]\n *\n * `params` arrives awaited and parsed; without schemas, it is a record of strings.\n */\nexport type PageRefreshOn<P = never, S = never> = RefreshOnList<PageProps<P, S>>;\n\n/**\n * Mark a region of a page as separately refreshable.\n *\n * Deliberately not a client module. The boundary it wraps things in is one,\n * but the things it wraps are server components — an async component that\n * fetches its own data. Marking this file \"use client\" would turn every one of\n * them into a client reference, which async components cannot be.\n *\n * export default section('orders', async function Orders() { ... })\n *\n * The page renders it like any component. What it buys is a name the server\n * can address: `Rsc::revalidate('orders')` re-renders this and nothing else,\n * and the answer replaces it in place.\n *\n * With `refreshOn`, it also says what it depends on, and refreshes itself when\n * that changes - from a webhook, a job, another visitor - with nothing polling:\n *\n * export default section('repos', Repos, {\n * refreshOn: ({ params }) => [`team:${params.team}:repos`],\n * })\n *\n * The name has to be unique within the page. It does not have to be unique in\n * the app: the component is carried on the wrapper it returns, so the server\n * resolves a section through the module the route declares rather than by\n * looking the name up in a registry every section in the app also writes to.\n * Two pages may both call theirs 'stats'.\n */\n\n/** Where the unwrapped component hangs off the wrapper section() returns. */\nconst INNER = Symbol.for(\"@rsc-kit/core.section-component\");\n\n/** Set on the wrapper of a section declared `shared`. */\nconst SHARED = Symbol.for(\"@rsc-kit/core.section-shared\");\n\nexport interface SectionOptions<P> {\n /**\n * What this section refreshes on: names for the data it shows, or a\n * function of the section's props - the page's params and searchParams -\n * that makes them. When one is said to have changed, the section refreshes.\n */\n refreshOn?: RefreshOnList<P>;\n /**\n * The section renders the same for everyone allowed to see the page, so\n * when a change makes many tabs ask for it, one render answers them all.\n * Each tab's guards still run on its own request; only the render is\n * shared. Never set it on a section that shows anything per visitor - their\n * name, their data, what they may do: the first tab's render is what every\n * other tab is shown.\n */\n shared?: boolean;\n}\n\nexport function section<P extends Record<string, unknown>>(\n name: string,\n Component: ComponentType<P>,\n options: SectionOptions<P> = {},\n): ComponentType<P> {\n const { refreshOn } = options;\n\n // The names render inside the boundary, with the component, so a refresh -\n // which renders the inside alone - carries the versions it saw.\n const Inner: ComponentType<P> = refreshOn\n ? function RefreshOnSection(props: P): ReactNode {\n return createElement(\n Fragment,\n null,\n createElement(\n RefreshOn as ComponentType<never>,\n { key: \"refreshOn\", target: name, refreshOn, props } as never,\n ),\n createElement(\n Component as ComponentType<never>,\n { key: \"section\", ...(props as object) } as never,\n ),\n );\n }\n : Component;\n\n // The boundary wraps the component here rather than at the call site, so a\n // page renders a section exactly like anything else.\n function Section(props: P): ReactNode {\n // createElement cannot line its overloads up with a component generic\n // over its own props. The call is the ordinary one.\n return createElement(\n SlotBoundary as ComponentType<{ name: string }>,\n { name },\n createElement(Inner as ComponentType<never>, props as never),\n );\n }\n\n Section.displayName = `Section(${name})`;\n // Reached through the route's own module, never through a shared map.\n (Section as unknown as Record<symbol, unknown>)[INNER] = Inner;\n if (options.shared)\n (Section as unknown as Record<symbol, unknown>)[SHARED] = true;\n\n return Section as ComponentType<P>;\n}\n\n/**\n * The component inside a section module, without its boundary.\n *\n * Revalidation renders this rather than the wrapper: the client swaps what is\n * inside the boundary, so returning the wrapper would nest a new boundary\n * inside the old one on every refresh.\n *\n * Takes the module's own export rather than a name. A name-keyed registry is\n * written to by every section in the app at bundle load — the generated entry\n * imports them all eagerly — so a lookup by name reached any page's region from\n * any url, bounded only by whatever guard happened to sit on the url asked for.\n */\nexport function sectionComponent(\n exported: unknown,\n): ComponentType<Record<string, unknown>> | undefined {\n if (typeof exported !== \"function\") return undefined;\n\n return (exported as unknown as Record<symbol, unknown>)[INNER] as\n ComponentType<Record<string, unknown>> | undefined;\n}\n\n/** Whether a section module's export was declared `shared`. */\nexport function isSharedSection(exported: unknown): boolean {\n return (\n typeof exported === \"function\" &&\n (exported as unknown as Record<symbol, unknown>)[SHARED] === true\n );\n}\n"]}
|
package/dist/request.d.ts
CHANGED
|
@@ -58,6 +58,14 @@ export interface Cookies {
|
|
|
58
58
|
}
|
|
59
59
|
/** Kept for the name it had when it could only read. */
|
|
60
60
|
export type ReadonlyCookies = Cookies;
|
|
61
|
+
/**
|
|
62
|
+
* Cookies a backend set while answering a host call - a login - put on the
|
|
63
|
+
* page's response, as if the page had set them. Only while the response is
|
|
64
|
+
* open: an action or middleware. A call made after the headers went out - a
|
|
65
|
+
* read inside a Suspense boundary - has nothing to put them on, which is why
|
|
66
|
+
* reads do not set cookies; said once, not dropped silently.
|
|
67
|
+
*/
|
|
68
|
+
export declare function passSetCookies(setCookies: string[]): void;
|
|
61
69
|
export declare function responseHeaders(): Headers;
|
|
62
70
|
/**
|
|
63
71
|
* Run a request with a response that can still be changed, and take what was
|
package/dist/request.js
CHANGED
|
@@ -214,6 +214,37 @@ function sealable(draft) {
|
|
|
214
214
|
});
|
|
215
215
|
return proxy;
|
|
216
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Cookies a backend set while answering a host call - a login - put on the
|
|
219
|
+
* page's response, as if the page had set them. Only while the response is
|
|
220
|
+
* open: an action or middleware. A call made after the headers went out - a
|
|
221
|
+
* read inside a Suspense boundary - has nothing to put them on, which is why
|
|
222
|
+
* reads do not set cookies; said once, not dropped silently.
|
|
223
|
+
*/
|
|
224
|
+
export function passSetCookies(setCookies) {
|
|
225
|
+
if (setCookies.length === 0)
|
|
226
|
+
return;
|
|
227
|
+
const open = draft()?.getStore();
|
|
228
|
+
if (!open)
|
|
229
|
+
return;
|
|
230
|
+
if (open.sealed) {
|
|
231
|
+
if (!warnedLateCookie) {
|
|
232
|
+
warnedLateCookie = true;
|
|
233
|
+
console.warn("[rsc-kit] the backend set a cookie while answering a call made after the response was sent, " +
|
|
234
|
+
"so it cannot reach the browser. Set cookies from an action or middleware.");
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
for (const cookie of setCookies) {
|
|
239
|
+
open.headers.append("Set-Cookie", cookie);
|
|
240
|
+
// The jar a later cookies() read sees: this request's own write.
|
|
241
|
+
const pair = cookie.split(";", 1)[0] ?? "";
|
|
242
|
+
const at = pair.indexOf("=");
|
|
243
|
+
if (at > 0)
|
|
244
|
+
open.cookies.set(pair.slice(0, at).trim(), pair.slice(at + 1).trim());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let warnedLateCookie = false;
|
|
217
248
|
export function responseHeaders() {
|
|
218
249
|
return sealable(writable("responseHeaders()"));
|
|
219
250
|
}
|