@rsc-kit/core 0.29.9 → 0.29.10
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/bootPayloads.d.ts +18 -0
- package/dist/bootPayloads.js +81 -0
- package/dist/bootPayloads.js.map +1 -0
- package/dist/headers.d.ts +5 -0
- package/dist/headers.js +5 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +10 -0
- package/dist/host.js +81 -5
- package/dist/host.js.map +1 -1
- package/dist/js/SegmentBoundary.js +20 -4
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +8 -1
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/pagePayload.d.ts +1 -1
- package/dist/js/pagePayload.js +2 -2
- package/dist/js/pagePayload.js.map +1 -1
- package/dist/vite.js +26 -9
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** How long a payload waits for its browser. */
|
|
2
|
+
export declare const BOOT_HOLD_MS = 10000;
|
|
3
|
+
export interface HeldPayload {
|
|
4
|
+
payload: ReadableStream;
|
|
5
|
+
/** The headers the boot answer carries beside the payload: the layout chain, the depth. */
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
at: number;
|
|
8
|
+
}
|
|
9
|
+
/** A token no one can guess: 128 bits, url-safe. */
|
|
10
|
+
export declare function bootToken(): string;
|
|
11
|
+
/** Keep a render's payload for the boot fetch that follows its document. */
|
|
12
|
+
export declare function holdBootPayload(token: string, payload: ReadableStream, headers: Record<string, string>, now?: number): void;
|
|
13
|
+
/** The payload for a token, once. Null for a token not held, redeemed or expired. */
|
|
14
|
+
export declare function takeBootPayload(token: string, now?: number): HeldPayload | null;
|
|
15
|
+
/** How many are held, for tests. */
|
|
16
|
+
export declare function heldBootPayloads(): number;
|
|
17
|
+
/** Forget everything, for tests. */
|
|
18
|
+
export declare function clearBootPayloads(): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// One render per visit.
|
|
2
|
+
//
|
|
3
|
+
// Hydrating server components needs the serialised tree, not the HTML, and a
|
|
4
|
+
// document does not carry its own payload - inlined, decode and hydration
|
|
5
|
+
// become one long task (#231). So the client fetches it, and that fetch used
|
|
6
|
+
// to render the whole page a second time: the same reads, the same queries,
|
|
7
|
+
// a moment after the document's render had made exactly that payload and
|
|
8
|
+
// thrown it away.
|
|
9
|
+
//
|
|
10
|
+
// Now the document's render keeps its payload here for the few seconds it
|
|
11
|
+
// takes the browser to come back for it, under a token written into the
|
|
12
|
+
// document that only that browser holds. The boot fetch redeems the token
|
|
13
|
+
// and is answered with the payload the document was made from - which is
|
|
14
|
+
// also the one it should hydrate against: two renders a moment apart could
|
|
15
|
+
// disagree, and the one that made the HTML cannot.
|
|
16
|
+
//
|
|
17
|
+
// A token is redeemed once. A payload nobody comes back for - a route with
|
|
18
|
+
// no runtime, a browser that left, a Worker isolate other than the one that
|
|
19
|
+
// rendered - is cancelled when it expires, and the boot fetch that misses
|
|
20
|
+
// renders as it always did. Missing costs a render; it never costs a wrong
|
|
21
|
+
// answer.
|
|
22
|
+
/** How long a payload waits for its browser. */
|
|
23
|
+
export const BOOT_HOLD_MS = 10_000;
|
|
24
|
+
/** At most this many held at once; the oldest is dropped first. */
|
|
25
|
+
const MAX_HELD = 500;
|
|
26
|
+
const held = new Map();
|
|
27
|
+
/** A token no one can guess: 128 bits, url-safe. */
|
|
28
|
+
export function bootToken() {
|
|
29
|
+
const bytes = new Uint8Array(16);
|
|
30
|
+
crypto.getRandomValues(bytes);
|
|
31
|
+
let out = "";
|
|
32
|
+
for (const b of bytes)
|
|
33
|
+
out += b.toString(16).padStart(2, "0");
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** Keep a render's payload for the boot fetch that follows its document. */
|
|
37
|
+
export function holdBootPayload(token, payload, headers, now = Date.now()) {
|
|
38
|
+
sweep(now);
|
|
39
|
+
if (held.size >= MAX_HELD) {
|
|
40
|
+
const oldest = held.keys().next().value;
|
|
41
|
+
if (oldest !== undefined)
|
|
42
|
+
drop(oldest);
|
|
43
|
+
}
|
|
44
|
+
held.set(token, { payload, headers, at: now });
|
|
45
|
+
}
|
|
46
|
+
/** The payload for a token, once. Null for a token not held, redeemed or expired. */
|
|
47
|
+
export function takeBootPayload(token, now = Date.now()) {
|
|
48
|
+
const found = held.get(token);
|
|
49
|
+
if (!found)
|
|
50
|
+
return null;
|
|
51
|
+
held.delete(token);
|
|
52
|
+
if (now - found.at >= BOOT_HOLD_MS) {
|
|
53
|
+
void found.payload.cancel().catch(() => { });
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
/** Let go of what nobody came back for. */
|
|
59
|
+
function sweep(now) {
|
|
60
|
+
for (const [token, entry] of held) {
|
|
61
|
+
if (now - entry.at < BOOT_HOLD_MS)
|
|
62
|
+
break;
|
|
63
|
+
drop(token);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function drop(token) {
|
|
67
|
+
const entry = held.get(token);
|
|
68
|
+
held.delete(token);
|
|
69
|
+
if (entry)
|
|
70
|
+
void entry.payload.cancel().catch(() => { });
|
|
71
|
+
}
|
|
72
|
+
/** How many are held, for tests. */
|
|
73
|
+
export function heldBootPayloads() {
|
|
74
|
+
return held.size;
|
|
75
|
+
}
|
|
76
|
+
/** Forget everything, for tests. */
|
|
77
|
+
export function clearBootPayloads() {
|
|
78
|
+
for (const token of [...held.keys()])
|
|
79
|
+
drop(token);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=bootPayloads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootPayloads.js","sourceRoot":"","sources":["../src/bootPayloads.ts"],"names":[],"mappings":"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","sourcesContent":["// One render per visit.\n//\n// Hydrating server components needs the serialised tree, not the HTML, and a\n// document does not carry its own payload - inlined, decode and hydration\n// become one long task (#231). So the client fetches it, and that fetch used\n// to render the whole page a second time: the same reads, the same queries,\n// a moment after the document's render had made exactly that payload and\n// thrown it away.\n//\n// Now the document's render keeps its payload here for the few seconds it\n// takes the browser to come back for it, under a token written into the\n// document that only that browser holds. The boot fetch redeems the token\n// and is answered with the payload the document was made from - which is\n// also the one it should hydrate against: two renders a moment apart could\n// disagree, and the one that made the HTML cannot.\n//\n// A token is redeemed once. A payload nobody comes back for - a route with\n// no runtime, a browser that left, a Worker isolate other than the one that\n// rendered - is cancelled when it expires, and the boot fetch that misses\n// renders as it always did. Missing costs a render; it never costs a wrong\n// answer.\n\n/** How long a payload waits for its browser. */\nexport const BOOT_HOLD_MS = 10_000;\n\n/** At most this many held at once; the oldest is dropped first. */\nconst MAX_HELD = 500;\n\nexport interface HeldPayload {\n payload: ReadableStream;\n /** The headers the boot answer carries beside the payload: the layout chain, the depth. */\n headers: Record<string, string>;\n at: number;\n}\n\nconst held = new Map<string, HeldPayload>();\n\n/** A token no one can guess: 128 bits, url-safe. */\nexport function bootToken(): string {\n const bytes = new Uint8Array(16);\n\n crypto.getRandomValues(bytes);\n\n let out = \"\";\n\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n\n return out;\n}\n\n/** Keep a render's payload for the boot fetch that follows its document. */\nexport function holdBootPayload(\n token: string,\n payload: ReadableStream,\n headers: Record<string, string>,\n now = Date.now(),\n): void {\n sweep(now);\n\n if (held.size >= MAX_HELD) {\n const oldest = held.keys().next().value;\n\n if (oldest !== undefined) drop(oldest);\n }\n\n held.set(token, { payload, headers, at: now });\n}\n\n/** The payload for a token, once. Null for a token not held, redeemed or expired. */\nexport function takeBootPayload(token: string, now = Date.now()): HeldPayload | null {\n const found = held.get(token);\n\n if (!found) return null;\n\n held.delete(token);\n\n if (now - found.at >= BOOT_HOLD_MS) {\n void found.payload.cancel().catch(() => {});\n\n return null;\n }\n\n return found;\n}\n\n/** Let go of what nobody came back for. */\nfunction sweep(now: number): void {\n for (const [token, entry] of held) {\n if (now - entry.at < BOOT_HOLD_MS) break;\n\n drop(token);\n }\n}\n\nfunction drop(token: string): void {\n const entry = held.get(token);\n\n held.delete(token);\n\n if (entry) void entry.payload.cancel().catch(() => {});\n}\n\n/** How many are held, for tests. */\nexport function heldBootPayloads(): number {\n return held.size;\n}\n\n/** Forget everything, for tests. */\nexport function clearBootPayloads(): void {\n for (const token of [...held.keys()]) drop(token);\n}\n"]}
|
package/dist/headers.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare const HEADER: {
|
|
|
10
10
|
readonly segmentDepth: 'X-RSC-Segment-Depth';
|
|
11
11
|
/** The chain to send back next time. */
|
|
12
12
|
readonly layouts: 'X-RSC-Layouts';
|
|
13
|
+
/**
|
|
14
|
+
* On the boot fetch: the token the document carried, redeeming the payload
|
|
15
|
+
* its own render kept. See bootPayloads.ts.
|
|
16
|
+
*/
|
|
17
|
+
readonly boot: 'X-RSC-Boot';
|
|
13
18
|
/** Identifies the build, so a client can notice it is talking to an old one. */
|
|
14
19
|
readonly version: 'X-RSC-Version';
|
|
15
20
|
/** The slot an intercepted navigation is targeting. */
|
package/dist/headers.js
CHANGED
|
@@ -19,6 +19,11 @@ export const HEADER = {
|
|
|
19
19
|
segmentDepth: 'X-RSC-Segment-Depth',
|
|
20
20
|
/** The chain to send back next time. */
|
|
21
21
|
layouts: 'X-RSC-Layouts',
|
|
22
|
+
/**
|
|
23
|
+
* On the boot fetch: the token the document carried, redeeming the payload
|
|
24
|
+
* its own render kept. See bootPayloads.ts.
|
|
25
|
+
*/
|
|
26
|
+
boot: 'X-RSC-Boot',
|
|
22
27
|
/** Identifies the build, so a client can notice it is talking to an old one. */
|
|
23
28
|
version: 'X-RSC-Version',
|
|
24
29
|
/** The slot an intercepted navigation is targeting. */
|
package/dist/headers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,0EAA0E;AAC1E,sEAAsE;AACtE,wEAAwE;AACxE,2EAA2E;AAC3E,gCAAgC;AAChC,EAAE;AACF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB;;;OAGG;IACH,GAAG,EAAE,OAAO;IAEZ,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB;IAE1B,kEAAkE;IAClE,YAAY,EAAE,qBAAqB;IAEnC,wCAAwC;IACxC,OAAO,EAAE,eAAe;IAExB,gFAAgF;IAChF,OAAO,EAAE,eAAe;IAExB,uDAAuD;IACvD,SAAS,EAAE,iBAAiB;IAE5B,gDAAgD;IAChD,OAAO,EAAE,eAAe;IAExB,uEAAuE;IACvE,UAAU,EAAE,kBAAkB;IAE9B,0CAA0C;IAC1C,MAAM,EAAE,cAAc;IAEtB;;;;;;;OAOG;IACH,QAAQ,EAAE,gBAAgB;IAE1B;;;;;;OAMG;IACH,WAAW,EAAE,oBAAoB;IAEjC;;;;;OAKG;IACH,UAAU,EAAE,cAAc;IAC1B;;;;;;;;;OASG;IACH;;;;;;;OAOG;IACH,KAAK,EAAE,aAAa;IAEpB,SAAS,EAAE,aAAa;IACxB,mEAAmE;IACnE,YAAY,EAAE,iBAAiB;IAC/B,uDAAuD;IACvD,aAAa,EAAE,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,eAAe;IAC1B,+EAA+E;IAC/E,WAAW,EAAE,eAAe;IAC5B,8FAA8F;IAC9F,UAAU,EAAE,cAAc;CAClB,CAAA;AAEV,sBAAsB;AACtB,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAA;AAE5D,2BAA2B;AAC3B,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,CAAC,GAAG;IACV,MAAM,CAAC,QAAQ;IACf,MAAM,CAAC,UAAU;IACjB,MAAM,CAAC,SAAS;IAChB,MAAM,CAAC,OAAO;CACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAA;AAE7C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,oCAAoC,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qCAAqC,CAAA","sourcesContent":["// The wire protocol, as names rather than string literals.\n//\n// A host writing these out by hand is how they drift. Two of them are not\n// negotiable at all — the client sends them or asks for them at fixed\n// addresses, and a host that spells one differently fails silently: the\n// request falls through, the decoder never runs, and a button does nothing\n// with no error on either side.\n//\n// PROTOCOL.md describes what each one means. This is the same list, importable.\n\nexport const HEADER = {\n /**\n * Present on a request for a Flight payload rather than the document. One\n * url serves both, which is why every answer must Vary on it.\n */\n rsc: 'X-RSC',\n\n /** The layout chain the client has mounted, outermost first, comma-separated. */\n segments: 'X-RSC-Segments',\n\n /** The boundary the answer replaces. 0 means a whole document. */\n segmentDepth: 'X-RSC-Segment-Depth',\n\n /** The chain to send back next time. */\n layouts: 'X-RSC-Layouts',\n\n /** Identifies the build, so a client can notice it is talking to an old one. */\n version: 'X-RSC-Version',\n\n /** The slot an intercepted navigation is targeting. */\n intercept: 'X-RSC-Intercept',\n\n /** Where the navigation or action came from. */\n referer: 'X-RSC-Referer',\n\n /** One named region of the current page to render and return alone. */\n revalidate: 'X-RSC-Revalidate',\n\n /** The server reference being invoked. */\n action: 'X-RSC-Action',\n\n /**\n * Go here instead.\n *\n * Used for a redirect the client has to perform itself: a failed action, and\n * a navigation whose render redirected. Never a 3xx for those — `fetch`\n * follows one transparently, so the client would get the destination's HTML\n * where it expected a Flight payload and decode it as one.\n */\n redirect: 'X-RSC-Redirect',\n\n /**\n * The body's real type.\n *\n * The body itself goes out as application/octet-stream so a host that parses\n * multipart cannot consume it before the action does — reading Content-Type\n * instead leaves an upload undecodable.\n */\n contentType: 'X-RSC-Content-Type',\n\n /**\n * Where the browser posts server actions.\n *\n * Not a convention this can choose: the client runtime posts here\n * unconditionally.\n */\n actionPath: '/_rsc/action',\n /**\n * Where the browser reads queries.\n *\n * A GET, and that is the whole point of it being a separate address: a POST\n * is cacheable by nothing, so a read routed through the action endpoint can\n * never be kept by an HTTP cache, a CDN or a service worker. Only functions\n * declared with `query()` are reachable here — the endpoint checks the mark\n * before invoking anything, because otherwise this address would run any\n * registered action over GET.\n */\n /**\n * Sent on every read, and required by the query endpoint.\n *\n * A GET with no unusual header is a simple request, so any page could\n * trigger one with an <img> and the visitor's cookies would ride along.\n * This header is not CORS-safelisted, so the browser preflights it — the\n * same thing that makes a POST carrying X-RSC-Action safe.\n */\n query: 'X-RSC-Query',\n\n queryPath: '/_rsc/query',\n /** Build-time shell for a PPR route, for an edge cache to hold. */\n pprShellPath: '/_rsc/ppr-shell',\n /** Finish a shell an edge cache is already serving. */\n pprResumePath: '/_rsc/ppr-resume',\n /**\n * On a refresh a change triggered: which names moved, to which versions\n * (`stock@5,orders@2`). A shared section renders once per such change, and\n * every tab asking with the same one gets that render.\n */\n changedBy: 'X-RSC-Changed',\n /** The stream a tab hears on when a name its page refreshes on has changed. */\n changesPath: '/_rsc/changes',\n /** Whether this renderer can serve: up, and its backend answering. GET only, never cached. */\n healthPath: '/_rsc/health',\n} as const\n\n/** Flight payload. */\nexport const FLIGHT_TYPE = 'text/x-component; charset=utf-8'\n\n/** A rendered document. */\nexport const HTML_TYPE = 'text/html; charset=utf-8'\n\n/**\n * Every request header that changes what a response body contains.\n *\n * One url answers with a whole document, a partial payload, a named region or\n * an interceptor depending on these — so a cache keyed on the url alone, or on\n * `X-RSC` alone, will hand one client another's answer. They are materially\n * different bodies, not variations in presentation.\n */\nexport const VARY_ON_RSC = [\n HEADER.rsc,\n HEADER.segments,\n HEADER.revalidate,\n HEADER.intercept,\n HEADER.referer,\n].join(', ')\n\n/**\n * What a response says about being stored, when it must not be.\n *\n * `Vary` alone is not enough on the platform this targets. Cloudflare ignores\n * `Vary` for everything except `Accept-Encoding`, and this package ships a\n * Workers story as a headline feature — so one url answering with a document,\n * a Flight payload, a segment and an interceptor, keyed only on the url, hands\n * one visitor another's body. Anything narrowed by a request header, and\n * anything behind a guard, says outright that it is not to be stored.\n */\nexport const PER_CLIENT = 'private, no-store'\n\n/**\n * And what a frozen, unguarded page says.\n *\n * Explicit rather than absent: with no directive at all, whether an\n * intermediary stores it comes down to that intermediary's heuristics, which\n * is not a decision this package should be leaving to chance in either\n * direction.\n */\nexport const REVALIDATE = 'public, max-age=0, must-revalidate'\n\n/**\n * And what a page rendered for one request says.\n *\n * It may have read the visitor's cookies, and nothing in the headers - which\n * leave before the render has finished - can know whether it did. So a shared\n * cache may not keep it; the visitor's own browser may, and must ask again.\n */\nexport const PER_REQUEST = 'private, max-age=0, must-revalidate'\n"]}
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,0EAA0E;AAC1E,sEAAsE;AACtE,wEAAwE;AACxE,2EAA2E;AAC3E,gCAAgC;AAChC,EAAE;AACF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB;;;OAGG;IACH,GAAG,EAAE,OAAO;IAEZ,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB;IAE1B,kEAAkE;IAClE,YAAY,EAAE,qBAAqB;IAEnC,wCAAwC;IACxC,OAAO,EAAE,eAAe;IAExB;;;OAGG;IACH,IAAI,EAAE,YAAY;IAElB,gFAAgF;IAChF,OAAO,EAAE,eAAe;IAExB,uDAAuD;IACvD,SAAS,EAAE,iBAAiB;IAE5B,gDAAgD;IAChD,OAAO,EAAE,eAAe;IAExB,uEAAuE;IACvE,UAAU,EAAE,kBAAkB;IAE9B,0CAA0C;IAC1C,MAAM,EAAE,cAAc;IAEtB;;;;;;;OAOG;IACH,QAAQ,EAAE,gBAAgB;IAE1B;;;;;;OAMG;IACH,WAAW,EAAE,oBAAoB;IAEjC;;;;;OAKG;IACH,UAAU,EAAE,cAAc;IAC1B;;;;;;;;;OASG;IACH;;;;;;;OAOG;IACH,KAAK,EAAE,aAAa;IAEpB,SAAS,EAAE,aAAa;IACxB,mEAAmE;IACnE,YAAY,EAAE,iBAAiB;IAC/B,uDAAuD;IACvD,aAAa,EAAE,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,eAAe;IAC1B,+EAA+E;IAC/E,WAAW,EAAE,eAAe;IAC5B,8FAA8F;IAC9F,UAAU,EAAE,cAAc;CAClB,CAAA;AAEV,sBAAsB;AACtB,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAA;AAE5D,2BAA2B;AAC3B,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,CAAC,GAAG;IACV,MAAM,CAAC,QAAQ;IACf,MAAM,CAAC,UAAU;IACjB,MAAM,CAAC,SAAS;IAChB,MAAM,CAAC,OAAO;CACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAA;AAE7C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,oCAAoC,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qCAAqC,CAAA","sourcesContent":["// The wire protocol, as names rather than string literals.\n//\n// A host writing these out by hand is how they drift. Two of them are not\n// negotiable at all — the client sends them or asks for them at fixed\n// addresses, and a host that spells one differently fails silently: the\n// request falls through, the decoder never runs, and a button does nothing\n// with no error on either side.\n//\n// PROTOCOL.md describes what each one means. This is the same list, importable.\n\nexport const HEADER = {\n /**\n * Present on a request for a Flight payload rather than the document. One\n * url serves both, which is why every answer must Vary on it.\n */\n rsc: 'X-RSC',\n\n /** The layout chain the client has mounted, outermost first, comma-separated. */\n segments: 'X-RSC-Segments',\n\n /** The boundary the answer replaces. 0 means a whole document. */\n segmentDepth: 'X-RSC-Segment-Depth',\n\n /** The chain to send back next time. */\n layouts: 'X-RSC-Layouts',\n\n /**\n * On the boot fetch: the token the document carried, redeeming the payload\n * its own render kept. See bootPayloads.ts.\n */\n boot: 'X-RSC-Boot',\n\n /** Identifies the build, so a client can notice it is talking to an old one. */\n version: 'X-RSC-Version',\n\n /** The slot an intercepted navigation is targeting. */\n intercept: 'X-RSC-Intercept',\n\n /** Where the navigation or action came from. */\n referer: 'X-RSC-Referer',\n\n /** One named region of the current page to render and return alone. */\n revalidate: 'X-RSC-Revalidate',\n\n /** The server reference being invoked. */\n action: 'X-RSC-Action',\n\n /**\n * Go here instead.\n *\n * Used for a redirect the client has to perform itself: a failed action, and\n * a navigation whose render redirected. Never a 3xx for those — `fetch`\n * follows one transparently, so the client would get the destination's HTML\n * where it expected a Flight payload and decode it as one.\n */\n redirect: 'X-RSC-Redirect',\n\n /**\n * The body's real type.\n *\n * The body itself goes out as application/octet-stream so a host that parses\n * multipart cannot consume it before the action does — reading Content-Type\n * instead leaves an upload undecodable.\n */\n contentType: 'X-RSC-Content-Type',\n\n /**\n * Where the browser posts server actions.\n *\n * Not a convention this can choose: the client runtime posts here\n * unconditionally.\n */\n actionPath: '/_rsc/action',\n /**\n * Where the browser reads queries.\n *\n * A GET, and that is the whole point of it being a separate address: a POST\n * is cacheable by nothing, so a read routed through the action endpoint can\n * never be kept by an HTTP cache, a CDN or a service worker. Only functions\n * declared with `query()` are reachable here — the endpoint checks the mark\n * before invoking anything, because otherwise this address would run any\n * registered action over GET.\n */\n /**\n * Sent on every read, and required by the query endpoint.\n *\n * A GET with no unusual header is a simple request, so any page could\n * trigger one with an <img> and the visitor's cookies would ride along.\n * This header is not CORS-safelisted, so the browser preflights it — the\n * same thing that makes a POST carrying X-RSC-Action safe.\n */\n query: 'X-RSC-Query',\n\n queryPath: '/_rsc/query',\n /** Build-time shell for a PPR route, for an edge cache to hold. */\n pprShellPath: '/_rsc/ppr-shell',\n /** Finish a shell an edge cache is already serving. */\n pprResumePath: '/_rsc/ppr-resume',\n /**\n * On a refresh a change triggered: which names moved, to which versions\n * (`stock@5,orders@2`). A shared section renders once per such change, and\n * every tab asking with the same one gets that render.\n */\n changedBy: 'X-RSC-Changed',\n /** The stream a tab hears on when a name its page refreshes on has changed. */\n changesPath: '/_rsc/changes',\n /** Whether this renderer can serve: up, and its backend answering. GET only, never cached. */\n healthPath: '/_rsc/health',\n} as const\n\n/** Flight payload. */\nexport const FLIGHT_TYPE = 'text/x-component; charset=utf-8'\n\n/** A rendered document. */\nexport const HTML_TYPE = 'text/html; charset=utf-8'\n\n/**\n * Every request header that changes what a response body contains.\n *\n * One url answers with a whole document, a partial payload, a named region or\n * an interceptor depending on these — so a cache keyed on the url alone, or on\n * `X-RSC` alone, will hand one client another's answer. They are materially\n * different bodies, not variations in presentation.\n */\nexport const VARY_ON_RSC = [\n HEADER.rsc,\n HEADER.segments,\n HEADER.revalidate,\n HEADER.intercept,\n HEADER.referer,\n].join(', ')\n\n/**\n * What a response says about being stored, when it must not be.\n *\n * `Vary` alone is not enough on the platform this targets. Cloudflare ignores\n * `Vary` for everything except `Accept-Encoding`, and this package ships a\n * Workers story as a headline feature — so one url answering with a document,\n * a Flight payload, a segment and an interceptor, keyed only on the url, hands\n * one visitor another's body. Anything narrowed by a request header, and\n * anything behind a guard, says outright that it is not to be stored.\n */\nexport const PER_CLIENT = 'private, no-store'\n\n/**\n * And what a frozen, unguarded page says.\n *\n * Explicit rather than absent: with no directive at all, whether an\n * intermediary stores it comes down to that intermediary's heuristics, which\n * is not a decision this package should be leaving to chance in either\n * direction.\n */\nexport const REVALIDATE = 'public, max-age=0, must-revalidate'\n\n/**\n * And what a page rendered for one request says.\n *\n * It may have read the visitor's cookies, and nothing in the headers - which\n * leave before the render has finished - can know whether it did. So a shared\n * cache may not keep it; the visitor's own browser may, and must ask again.\n */\nexport const PER_REQUEST = 'private, max-age=0, must-revalidate'\n"]}
|
package/dist/host.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface RscEngine {
|
|
|
36
36
|
props: Record<string, unknown>;
|
|
37
37
|
}[], loadings?: string[], parallelSlots?: Record<string, string>, slotOverrides?: Record<string, unknown>, nonce?: string, pageKey?: string, bootstrap?: boolean): Promise<{
|
|
38
38
|
htmlStream: ReadableStream;
|
|
39
|
+
/**
|
|
40
|
+
* The same render as a Flight payload, and the token the document
|
|
41
|
+
* carries for it. The host holds the one under the other for the boot
|
|
42
|
+
* fetch - see bootPayloads.ts. An engine built before this answers
|
|
43
|
+
* without them, and every boot renders, as it used to.
|
|
44
|
+
*/
|
|
45
|
+
rscPayload?: ReadableStream;
|
|
46
|
+
bootToken?: string;
|
|
39
47
|
}>;
|
|
40
48
|
/**
|
|
41
49
|
* A form posted to the page's own url before the page had a runtime to
|
|
@@ -77,6 +85,8 @@ export interface RscEngine {
|
|
|
77
85
|
* filled here - see `unresumable` below.
|
|
78
86
|
*/
|
|
79
87
|
replayed?: () => boolean;
|
|
88
|
+
/** The render as a payload, for the boot fetch; see handleRscHtmlStream. */
|
|
89
|
+
rscPayload?: ReadableStream;
|
|
80
90
|
}>;
|
|
81
91
|
handleRscRevalidate?(target: string, page: unknown,
|
|
82
92
|
/** Set on a refresh a change triggered: a shared section renders once per key. */
|
package/dist/host.js
CHANGED
|
@@ -48,6 +48,7 @@ export function pageSaidNotFound(request) {
|
|
|
48
48
|
}
|
|
49
49
|
export { revalidate } from "./revalidate.js";
|
|
50
50
|
import { isCrawler } from "./crawler.js";
|
|
51
|
+
import { bootToken, holdBootPayload, takeBootPayload } from "./bootPayloads.js";
|
|
51
52
|
import { currentNotFound, withRedirect } from "./redirect.js";
|
|
52
53
|
import { compressed } from "./compress.js";
|
|
53
54
|
import { withCache } from "./cache.js";
|
|
@@ -487,6 +488,16 @@ export function createRscHandler(options) {
|
|
|
487
488
|
const identify = Boolean(manifest.build?.identify);
|
|
488
489
|
/** How a response was answered, for X-RSC-Kit: a file the build wrote, or a shell of one. */
|
|
489
490
|
const servedFrom = new WeakMap();
|
|
491
|
+
/** Whether a stored shell carries a script - a runtime that will boot. Read once per shell. */
|
|
492
|
+
const bootsByKey = new Map();
|
|
493
|
+
function shellBoots(key, shell) {
|
|
494
|
+
let boots = bootsByKey.get(key);
|
|
495
|
+
if (boots === undefined) {
|
|
496
|
+
boots = shell.includes("<script");
|
|
497
|
+
bootsByKey.set(key, boots);
|
|
498
|
+
}
|
|
499
|
+
return boots;
|
|
500
|
+
}
|
|
490
501
|
/** What a stored document's head names, read once per file, for the Link header. */
|
|
491
502
|
const hinted = new WeakMap();
|
|
492
503
|
/**
|
|
@@ -865,14 +876,25 @@ export function createRscHandler(options) {
|
|
|
865
876
|
// boundary rejects here — before a byte is written, with a status line
|
|
866
877
|
// still available. Nothing is buffered to make that true.
|
|
867
878
|
let htmlStream;
|
|
879
|
+
// The render's payload and the token the document carries for it.
|
|
880
|
+
// Held for the boot fetch when the document goes out as a 200;
|
|
881
|
+
// let go on every other exit, or the tee buffers it for nothing.
|
|
882
|
+
let rscPayload;
|
|
883
|
+
let token;
|
|
884
|
+
const letGo = () => void rscPayload?.cancel().catch(() => { });
|
|
868
885
|
try {
|
|
869
|
-
|
|
886
|
+
const rendered = formPost
|
|
870
887
|
? await engine.handleRscFormPost(match.route.component, props, layouts, match.route.loadings, match.route.slots, {}, undefined, url.pathname, true, formPost)
|
|
871
888
|
: await engine.handleRscHtmlStream(match.route.component, props, layouts, match.route.loadings, match.route.slots, {}, undefined, url.pathname,
|
|
872
889
|
// A route that ships no runtime gets no bootstrap and no segment
|
|
873
890
|
// boundary — the boundary is itself a client component, so leaving
|
|
874
891
|
// it in means no page could ever be JS-free.
|
|
875
|
-
true)
|
|
892
|
+
true);
|
|
893
|
+
htmlStream = rendered.htmlStream;
|
|
894
|
+
// Not after a post: the answer to one is the result of something
|
|
895
|
+
// that happened once, and a payload of it is not a page to keep.
|
|
896
|
+
if (!formPost)
|
|
897
|
+
({ rscPayload, bootToken: token } = rendered);
|
|
876
898
|
}
|
|
877
899
|
catch (error) {
|
|
878
900
|
// A rejected shell is how a redirect above every boundary arrives:
|
|
@@ -906,14 +928,27 @@ export function createRscHandler(options) {
|
|
|
906
928
|
throw error;
|
|
907
929
|
}
|
|
908
930
|
const early = taken();
|
|
909
|
-
if (early)
|
|
931
|
+
if (early) {
|
|
932
|
+
letGo();
|
|
910
933
|
return redirectResponse(early, false);
|
|
934
|
+
}
|
|
911
935
|
// Above every boundary, so the shell resolving means the page did not
|
|
912
936
|
// refuse itself. Deeper than that and the shell is already on the wire
|
|
913
937
|
// — the digest carries it to the boundary instead, and the status
|
|
914
938
|
// stays 200 because the status line has gone.
|
|
915
|
-
if (currentNotFound())
|
|
939
|
+
if (currentNotFound()) {
|
|
940
|
+
letGo();
|
|
916
941
|
return saidNotFound(request);
|
|
942
|
+
}
|
|
943
|
+
if (rscPayload && token) {
|
|
944
|
+
holdBootPayload(token, rscPayload, {
|
|
945
|
+
[HEADER.segmentDepth]: "0",
|
|
946
|
+
[HEADER.layouts]: chain.join(","),
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
letGo();
|
|
951
|
+
}
|
|
917
952
|
return new Response(appendLateRedirect(htmlStream, taken, missing), {
|
|
918
953
|
headers: withVersion({
|
|
919
954
|
"Content-Type": HTML_TYPE,
|
|
@@ -932,6 +967,27 @@ export function createRscHandler(options) {
|
|
|
932
967
|
});
|
|
933
968
|
});
|
|
934
969
|
}
|
|
970
|
+
// The boot fetch, redeeming the payload its own document's render kept:
|
|
971
|
+
// answered from it, nothing rendered, the guard already passed by the
|
|
972
|
+
// render that made it a moment ago for this same browser. A token not
|
|
973
|
+
// held - expired, another isolate, an older document - falls through to
|
|
974
|
+
// the render every boot used to be. See bootPayloads.ts.
|
|
975
|
+
const redeeming = request.headers.get(HEADER.boot);
|
|
976
|
+
if (redeeming !== null && request.headers.get(HEADER.segments) === null) {
|
|
977
|
+
const kept = takeBootPayload(redeeming);
|
|
978
|
+
if (kept) {
|
|
979
|
+
const answer = new Response(kept.payload, {
|
|
980
|
+
headers: withVersion({
|
|
981
|
+
"Content-Type": FLIGHT_TYPE,
|
|
982
|
+
...kept.headers,
|
|
983
|
+
Vary: VARY_ON_RSC,
|
|
984
|
+
"Cache-Control": PER_CLIENT,
|
|
985
|
+
}),
|
|
986
|
+
});
|
|
987
|
+
servedFrom.set(answer, "held");
|
|
988
|
+
return answer;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
935
991
|
const from = sharedDepth(request.headers.get(HEADER.segments), chain);
|
|
936
992
|
// A boot - X-RSC and no segments - of a document served from a pattern
|
|
937
993
|
// shell: the shell was rendered for no url, and the client hydrates
|
|
@@ -1316,7 +1372,7 @@ export function createRscHandler(options) {
|
|
|
1316
1372
|
// stream's end - see LATE_NOINDEX.
|
|
1317
1373
|
let missing = () => false;
|
|
1318
1374
|
const resume = engine.handleRscResume;
|
|
1319
|
-
const { htmlStream, replayed } = await withRedirect((_, said) => {
|
|
1375
|
+
const { htmlStream, replayed, rscPayload } = await withRedirect((_, said) => {
|
|
1320
1376
|
missing = said;
|
|
1321
1377
|
return resume(route.route.component, forShape,
|
|
1322
1378
|
// Empty props, because that is what the build passed. Resuming replays
|
|
@@ -1337,12 +1393,32 @@ export function createRscHandler(options) {
|
|
|
1337
1393
|
// a hole, and the resume fills it with the real one. Nothing is looked
|
|
1338
1394
|
// up before the shell leaves.
|
|
1339
1395
|
//
|
|
1396
|
+
// The render's payload, kept for the boot fetch under a token written
|
|
1397
|
+
// right after the shell - the shell is a build artifact, so the token
|
|
1398
|
+
// cannot be in it. Only a shell that carries a script boots a runtime
|
|
1399
|
+
// that will come back for one. A pattern shell's resume renders with
|
|
1400
|
+
// no page key, so React can line its slots up; the client files the
|
|
1401
|
+
// page under the url it is at when the payload carries none - see
|
|
1402
|
+
// SegmentBoundary. See bootPayloads.ts.
|
|
1403
|
+
const token = rscPayload && shellBoots(shellKey, shell) ? bootToken() : null;
|
|
1404
|
+
if (token) {
|
|
1405
|
+
holdBootPayload(token, rscPayload, {
|
|
1406
|
+
[HEADER.segmentDepth]: "0",
|
|
1407
|
+
[HEADER.layouts]: layoutChain(route.route.layouts, route.params).join(","),
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
void rscPayload?.cancel().catch(() => { });
|
|
1412
|
+
}
|
|
1340
1413
|
// The shell first, then whatever the resume writes. React's own script
|
|
1341
1414
|
// travels with the resumed segments and moves them into place, so this is
|
|
1342
1415
|
// a plain concatenation and the holes land without hydration.
|
|
1343
1416
|
const body = new ReadableStream({
|
|
1344
1417
|
async start(controller) {
|
|
1345
1418
|
controller.enqueue(new TextEncoder().encode(shell));
|
|
1419
|
+
if (token) {
|
|
1420
|
+
controller.enqueue(new TextEncoder().encode("<script>self.__rsc_boot=" + JSON.stringify(token) + "</script>"));
|
|
1421
|
+
}
|
|
1346
1422
|
const reader = htmlStream.getReader();
|
|
1347
1423
|
try {
|
|
1348
1424
|
while (true) {
|