@rsc-kit/core 0.30.45 → 0.30.46
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/pageCache.d.ts +7 -0
- package/dist/pageCache.js +26 -4
- package/dist/pageCache.js.map +1 -1
- package/dist/purge.d.ts +5 -4
- package/dist/purge.js +13 -8
- package/dist/purge.js.map +1 -1
- package/package.json +1 -1
package/dist/pageCache.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ export declare function noteReadsSoFar(entry: PageCaching): void;
|
|
|
34
34
|
* or a copy older than the page would ever have been kept for.
|
|
35
35
|
*/
|
|
36
36
|
export declare function publicCacheControl(revalidate: number, status: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* The same lifetime for a CDN that reads CDN-Cache-Control (RFC 9213), which
|
|
39
|
+
* is meant for it alone: there max-age is the CDN's own, not a browser's. Some
|
|
40
|
+
* edges keep a page only on this header, and Cache-Control stays beside it for
|
|
41
|
+
* every cache that does not read it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cdnCacheControl(revalidate: number, status: number): string;
|
|
37
44
|
/**
|
|
38
45
|
* The tags of every cache() answer a kept page used, as a CDN reads them:
|
|
39
46
|
* `Cache-Tag: pages,post:1`. A purge by any of them drops the copy. A tag the
|
package/dist/pageCache.js
CHANGED
|
@@ -110,9 +110,20 @@ function bypass(response, body, entry, refused) {
|
|
|
110
110
|
headers.set("Cache-Control", PER_CLIENT);
|
|
111
111
|
headers.set("Cache-Status", `rsc-kit; fwd=bypass; detail=${detailOf(refused)}`);
|
|
112
112
|
headers.delete("ETag");
|
|
113
|
+
// A CDN that reads it reads it over Cache-Control, so a private answer never has it.
|
|
114
|
+
headers.delete("CDN-Cache-Control");
|
|
113
115
|
logRefusal(entry, refused);
|
|
114
116
|
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
115
117
|
}
|
|
118
|
+
/** How long a shared cache keeps a public answer, which both of its headers state. */
|
|
119
|
+
function sharedLifetime(revalidate, status) {
|
|
120
|
+
const notFound = status === 404;
|
|
121
|
+
const stale = notFound ? NOT_FOUND_LIMIT : STALE_WINDOW;
|
|
122
|
+
return {
|
|
123
|
+
fresh: notFound ? Math.min(revalidate, NOT_FOUND_LIMIT) : revalidate,
|
|
124
|
+
windows: `stale-while-revalidate=${stale}, stale-if-error=${stale}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
116
127
|
/**
|
|
117
128
|
* What a public answer says: kept by shared caches for N seconds, refreshed
|
|
118
129
|
* in the background for a day after that, never by the browser. A 404 for at
|
|
@@ -123,9 +134,18 @@ function bypass(response, body, entry, refused) {
|
|
|
123
134
|
* or a copy older than the page would ever have been kept for.
|
|
124
135
|
*/
|
|
125
136
|
export function publicCacheControl(revalidate, status) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
const { fresh, windows } = sharedLifetime(revalidate, status);
|
|
138
|
+
return `public, max-age=0, s-maxage=${fresh}, ${windows}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The same lifetime for a CDN that reads CDN-Cache-Control (RFC 9213), which
|
|
142
|
+
* is meant for it alone: there max-age is the CDN's own, not a browser's. Some
|
|
143
|
+
* edges keep a page only on this header, and Cache-Control stays beside it for
|
|
144
|
+
* every cache that does not read it.
|
|
145
|
+
*/
|
|
146
|
+
export function cdnCacheControl(revalidate, status) {
|
|
147
|
+
const { fresh, windows } = sharedLifetime(revalidate, status);
|
|
148
|
+
return `max-age=${fresh}, ${windows}`;
|
|
129
149
|
}
|
|
130
150
|
/** A tag a Cache-Tag list can carry: visible ASCII, no comma, which separates them. */
|
|
131
151
|
const TAG = /^[\x21-\x2b\x2d-\x7e]+$/;
|
|
@@ -192,7 +212,9 @@ saidNotFound = () => false) {
|
|
|
192
212
|
return bypass(response, body, entry, refused);
|
|
193
213
|
const headers = new Headers(response.headers);
|
|
194
214
|
const etag = await etagOf(body);
|
|
195
|
-
|
|
215
|
+
const status = saidNotFound() ? 404 : response.status;
|
|
216
|
+
headers.set("Cache-Control", publicCacheControl(entry.revalidate, status));
|
|
217
|
+
headers.set("CDN-Cache-Control", cdnCacheControl(entry.revalidate, status));
|
|
196
218
|
headers.set("ETag", etag);
|
|
197
219
|
const tags = cacheTagHeader(cacheTagsRead());
|
|
198
220
|
if (tags)
|
package/dist/pageCache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pageCache.js","sourceRoot":"","sources":["../src/pageCache.ts"],"names":[],"mappings":"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","sourcesContent":["// A page a shared cache may keep: `export const revalidate = N`.\n//\n// The page is rendered whole, judged, then sent. Public only when every check\n// passed for that render; otherwise `private, no-store`, exactly as before,\n// with `Cache-Status: rsc-kit; fwd=bypass; detail=<check>` saying which one\n// failed and a line in the log saying where. Cache-Control is the contract a\n// host caches by; Cache-Status is for counting and for the developer asking\n// why a page was not kept.\n//\n// The checks, in the order they can be made:\n//\n// 1. payload-url - a GET or HEAD, and a payload asked for at its own `?_rsc=<key>` url, and a\n// document never at one: a cache that ignores Vary keys on\n// the url alone, so either the other way round hands one\n// kind of answer to a request for the other\n// 2. the render read nothing that belongs to one visitor: cookies(),\n// headers(), request(), connection() - in the page, a layout, a\n// Suspense'd child, metadata or middleware.ts\n// 3. host-middleware - Laravel route middleware runs against the session\n// 4. set-cookie - nothing set a cookie\n// 5. nonce - no CSP nonce, which a shared copy would give everyone\n// 6. status - 200 or 404; a redirect, a 5xx or a render React reported\n// as failed inside the payload (failed-render) never is\n//\n// A public answer carries `Cache-Tag`: the tags of every cache() answer the\n// render used, so a CDN can purge the copy when one of them changes.\n//\n// Backends render as nobody - see renderAsNobody() - so a session cookie set\n// by the backend on every call does not make every page private.\n\nimport { HEADER, PER_CLIENT } from \"./headers.js\";\nimport { PAYLOAD_PARAM, payloadKey, withoutPayloadKey } from \"./js/payloadKey.js\";\nimport { cacheTagsRead, cacheTagVersionsRead, renderFailed, visitorReads } from \"./request.js\";\nimport type { CacheStore } from \"./cache.js\";\nimport { versionSource } from \"./changed.js\";\n\n/** What a request for a `revalidate` page carries from the moment it arrives. */\nexport interface PageCaching {\n /** Seconds a shared cache may keep the page. */\n revalidate: number;\n /** The url path, for the log line. */\n path: string;\n /** A check that failed before anything rendered, or after the page said notFound(). */\n refused: string | null;\n}\n\n/** A 404 is kept no longer than this, however long its page may be. */\nconst NOT_FOUND_LIMIT = 60;\n\n/** How long a cache may serve a stale page while one request refreshes it, or while the origin is failing. */\nconst STALE_WINDOW = 86_400;\n\nconst caching = new WeakMap<Request, PageCaching>();\n\n/**\n * Decide, for a request as it arrived, whether its answer may be judged at all.\n *\n * Null for anything but a GET or HEAD of a page that exports revalidate - the\n * answer is then what it always was. Keyed by the request the app sees, so the\n * entry in front, which renders a 404, finds it again.\n */\nexport function cachingFor(arrived: Request, revalidate: number | undefined): PageCaching | null {\n // A post is never kept, and its answer already says so.\n if (!revalidate || revalidate <= 0 || (arrived.method !== \"GET\" && arrived.method !== \"HEAD\")) return null;\n\n const url = new URL(arrived.url);\n const entry: PageCaching = { revalidate, path: url.pathname, refused: urlRefusal(arrived, url) };\n\n caching.set(withoutPayloadKey(arrived), entry);\n\n return entry;\n}\n\n/** What cachingFor() decided for this request, if anything. */\nexport function cachingOf(request: Request): PageCaching | null {\n return caching.get(withoutPayloadKey(request)) ?? null;\n}\n\n/**\n * Check 1. A payload answers at `?_rsc=<key>`, with the key its headers give;\n * a document never answers there. Either way round, a cache keying on the url\n * alone would hand a request one kind of answer for the other.\n */\nfunction urlRefusal(arrived: Request, url: URL): string | null {\n const key = url.searchParams.get(PAYLOAD_PARAM);\n const payload = arrived.headers.get(HEADER.rsc) !== null;\n\n if (payload ? key !== payloadKey(arrived.headers) : key !== null) return \"payload-url\";\n\n return null;\n}\n\n/**\n * Note what the render has read so far, for an answer that will be finished\n * somewhere else: the page said notFound(), and the entry in front renders\n * not-found.tsx in a scope of its own, where these reads are gone.\n */\nexport function noteReadsSoFar(entry: PageCaching): void {\n entry.refused ??= readRefusal();\n}\n\nfunction readRefusal(): string | null {\n if (renderFailed()) return \"failed-render\";\n\n const read = visitorReads()[0];\n\n return read ?? null;\n}\n\n/** The detail token a read is reported under: `cookies() in Header` is `cookies`. */\nfunction detailOf(refused: string): string {\n return refused.replace(/\\(.*$/, \"\").replace(/[^a-z-]/gi, \"-\");\n}\n\nconst logged = new Set<string>();\n\nconst REASONS: Record<string, string> = {\n \"payload-url\":\n \"a payload was asked for without its ?_rsc= key, or a document with one. The router always adds it; this was another client.\",\n \"host-middleware\": \"the route has host middleware, which runs against the visitor's session.\",\n \"set-cookie\": \"the answer set a cookie.\",\n nonce: \"the answer carries a CSP nonce, which a shared copy would give every visitor.\",\n status: \"only a 200 or a 404 is kept.\",\n \"failed-render\": \"React reported a failure inside the payload.\",\n};\n\n/** Once per page and reason: a page refused on every request says so once. */\nfunction logRefusal(entry: PageCaching, refused: string): void {\n const key = `${entry.path}\\n${refused}`;\n\n if (logged.has(key) || logged.size > 500) return;\n\n logged.add(key);\n\n // A read names what read, and where: `cookies() in Header`.\n const why = REASONS[refused] ?? `${refused} reads this visitor.`;\n\n console.warn(`[rsc-kit] ${entry.path} exports revalidate, and was sent private: ${why}`);\n}\n\nfunction bypass(response: Response, body: BodyInit | null, entry: PageCaching, refused: string): Response {\n const headers = new Headers(response.headers);\n\n headers.set(\"Cache-Control\", PER_CLIENT);\n headers.set(\"Cache-Status\", `rsc-kit; fwd=bypass; detail=${detailOf(refused)}`);\n headers.delete(\"ETag\");\n logRefusal(entry, refused);\n\n return new Response(body, { status: response.status, statusText: response.statusText, headers });\n}\n\n/**\n * What a public answer says: kept by shared caches for N seconds, refreshed\n * in the background for a day after that, never by the browser. A 404 for at\n * most a minute, so a scanner walking random urls cannot hold pages out.\n *\n * stale-if-error says the same window outright for an origin that is down,\n * erroring or mid-deploy: a cache left to choose may serve the error instead,\n * or a copy older than the page would ever have been kept for.\n */\nexport function publicCacheControl(revalidate: number, status: number): string {\n return status === 404\n ? `public, max-age=0, s-maxage=${Math.min(revalidate, NOT_FOUND_LIMIT)}, stale-while-revalidate=${NOT_FOUND_LIMIT}, stale-if-error=${NOT_FOUND_LIMIT}`\n : `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${STALE_WINDOW}, stale-if-error=${STALE_WINDOW}`;\n}\n\n/** A tag a Cache-Tag list can carry: visible ASCII, no comma, which separates them. */\nconst TAG = /^[\\x21-\\x2b\\x2d-\\x7e]+$/;\n\nlet warnedTag = false;\n\n/**\n * The tags of every cache() answer a kept page used, as a CDN reads them:\n * `Cache-Tag: pages,post:1`. A purge by any of them drops the copy. A tag the\n * header cannot carry is left out, and said once: the copy then goes stale on\n * its own time, not on that change.\n */\nexport function cacheTagHeader(tags: readonly string[]): string | null {\n const kept = tags.filter((tag) => TAG.test(tag));\n\n if (kept.length < tags.length && !warnedTag) {\n warnedTag = true;\n const bad = tags.find((tag) => !TAG.test(tag));\n console.warn(\n `[rsc-kit] cache() tag ${JSON.stringify(bad)} cannot go in a Cache-Tag header (a comma, a space or a non-ASCII letter), so a CDN cannot purge by it.`,\n );\n }\n\n return kept.length ? kept.join(\",\") : null;\n}\n\nasync function etagOf(body: ArrayBuffer): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", body));\n let hex = \"\";\n\n for (let i = 0; i < 12; i++) hex += digest[i].toString(16).padStart(2, \"0\");\n\n return `\"${hex}\"`;\n}\n\nfunction matches(ifNoneMatch: string | null, etag: string): boolean {\n if (!ifNoneMatch) return false;\n\n return ifNoneMatch\n .split(\",\")\n .map((tag) => tag.trim().replace(/^W\\//, \"\"))\n .some((tag) => tag === etag || tag === \"*\");\n}\n\n/**\n * Finish the answer to a `revalidate` page: rendered whole, then judged.\n *\n * Called inside the request's scope, after every header the request will get\n * is on the answer - cookies included - so what it judges is what is sent.\n * Reading the body to its end is what waits for the render: a Suspense'd\n * child that reads cookies() is still running when the headers exist.\n */\nexport async function finishCacheable(\n request: Request,\n response: Response,\n entry: PageCaching,\n /**\n * Whether the answer turned out to be not-found.tsx, asked once the body is\n * read: a page can say notFound() in the middle of its stream, after a 200\n * went out. A payload is a 200 either way - the router reads the page from\n * it - but a not-found one is kept no longer than a 404.\n */\n saidNotFound: () => boolean = () => false,\n): Promise<Response> {\n if (entry.refused) return bypass(response, response.body, entry, entry.refused);\n\n if (response.status !== 200 && response.status !== 404) return bypass(response, response.body, entry, \"status\");\n\n const type = response.headers.get(\"Content-Type\") ?? \"\";\n\n if (!type.startsWith(\"text/html\") && !type.startsWith(\"text/x-component\")) {\n return bypass(response, response.body, entry, \"status\");\n }\n\n const body = await response.arrayBuffer();\n const refused =\n readRefusal() ??\n (response.headers.getSetCookie().length > 0 ? \"set-cookie\" : null) ??\n (/'nonce-/.test(response.headers.get(\"Content-Security-Policy\") ?? \"\") ? \"nonce\" : null);\n\n if (refused) return bypass(response, body, entry, refused);\n\n const headers = new Headers(response.headers);\n const etag = await etagOf(body);\n\n headers.set(\"Cache-Control\", publicCacheControl(entry.revalidate, saidNotFound() ? 404 : response.status));\n headers.set(\"ETag\", etag);\n\n const tags = cacheTagHeader(cacheTagsRead());\n\n if (tags) headers.set(\"Cache-Tag\", tags);\n\n const kept: Keepable = { status: response.status, body, versions: cacheTagVersionsRead(), revalidate: entry.revalidate, notFound: saidNotFound() };\n\n if (response.status === 200 && matches(request.headers.get(\"If-None-Match\"), etag)) {\n headers.delete(\"Content-Length\");\n\n const answer = new Response(null, { status: 304, headers });\n\n keepable.set(answer, kept);\n\n return answer;\n }\n\n const answer = new Response(request.method === \"HEAD\" ? null : body, { status: response.status, statusText: response.statusText, headers });\n\n keepable.set(answer, kept);\n\n return answer;\n}\n\n// The engine's own copy of a public page: ISR on every host.\n//\n// A kept page is versioned, not purged. Its entry carries the version of\n// every tag its cache() answers were read under, and a lookup asks the\n// version source whether any moved since. changed(tag) bumps a version every\n// instance reads, so every pod misses at its next request with nothing sent\n// to it - and a render that was running when the change landed is stored\n// under the older version, so it is never served. Correctness needs only a\n// version source every instance shares; the store decides how often a page\n// is rendered, never whether a stale one is served.\n\n/** What finishCacheable() sent public, kept beside the answer for the page cache to store. */\ninterface Keepable {\n status: number;\n body: ArrayBuffer;\n versions: Record<string, number>;\n revalidate: number;\n /** A not-found page, kept no longer than a 404. */\n notFound: boolean;\n}\n\nconst keepable = new WeakMap<Response, Keepable>();\n\n/** A kept page, as the store holds it. */\ninterface KeptPage {\n /** Status. */\n s: number;\n /** Headers, as name and value pairs. */\n h: [string, string][];\n /** Body, as text: HTML and Flight both are. */\n b: string;\n /** Tag versions it was rendered under. */\n v: Record<string, number>;\n /** When it was rendered, in ms. */\n at: number;\n /** Seconds it is fresh for. */\n r: number;\n /** Seconds past that it may still be served while one render replaces it. */\n w: number;\n}\n\n/** Headers that belong to one answer, never to the kept page. */\nconst NOT_KEPT = new Set([\"date\", \"set-cookie\", \"content-length\", \"x-rsc-kit\", \"x-powered-by\", \"age\"]);\n\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\n/**\n * Pages kept in this process, by the size of what they hold: a page is tens\n * or hundreds of KB, so a count says nothing about memory. Least recently\n * used goes first.\n */\nexport function pageMemoryStore(maxBytes = 64 * 1024 * 1024): CacheStore {\n const entries = new Map<string, { value: string; until: number }>();\n let bytes = 0;\n\n const drop = (key: string) => {\n const entry = entries.get(key);\n\n if (!entry) return;\n\n entries.delete(key);\n bytes -= entry.value.length;\n };\n\n return {\n get(key) {\n const entry = entries.get(key);\n\n if (!entry) return undefined;\n\n if (entry.until <= Date.now()) {\n drop(key);\n\n return undefined;\n }\n\n // Re-inserted, so the Map's order is least recently used first.\n entries.delete(key);\n entries.set(key, entry);\n\n return entry.value;\n },\n set(key, value, ttl) {\n drop(key);\n\n if (value.length > maxBytes) return;\n\n entries.set(key, { value, until: Date.now() + ttl * 1000 });\n bytes += value.length;\n\n while (bytes > maxBytes) drop(entries.keys().next().value!);\n },\n };\n}\n\n/** A kept answer, and whether it is past its time: served, while one render replaces it. */\nexport interface KeptAnswer {\n response: Response;\n stale: boolean;\n}\n\nexport interface PageCache {\n /** The kept answer for this request, or null to render it now. */\n lookup(key: string, request: Request): Promise<KeptAnswer | null>;\n /** Whether a render for this key is running in this process. */\n busy(key: string): boolean;\n /** Keep the answer, when finishCacheable() sent it public. */\n keep(key: string, answer: Response): Promise<void>;\n /** Run one render per key at a time: a burst for a page nobody has kept waits for the first. */\n once<T>(key: string, render: () => Promise<T>): Promise<{ led: boolean; value?: T }>;\n}\n\nlet warnedPageStore = false;\n\n/** A store that fails is a render, not a broken page: said once. */\nfunction pageStoreFailed(error: unknown): void {\n if (warnedPageStore) return;\n warnedPageStore = true;\n console.warn(\"[rsc-kit] the page cache's store failed, so pages are being rendered each time instead:\", error);\n}\n\n/** The engine's page cache, over a store: the app's, or one of its own in memory. */\nexport function createPageCache(store: CacheStore): PageCache {\n const inFlight = new Map<string, Promise<unknown>>();\n\n return {\n async lookup(key, request) {\n let raw: string | null | undefined;\n\n try {\n raw = await store.get(`rsc-page|${key}`);\n } catch (error) {\n pageStoreFailed(error);\n\n return null;\n }\n\n if (!raw) return null;\n\n let kept: KeptPage;\n\n try {\n kept = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const now = Date.now();\n const age = now - kept.at;\n\n // The store may keep it longer than asked; the age is checked here.\n // Past its time it is still served, for a while, as the stale copy a\n // render in the background replaces: no visitor waits on an expiry.\n if (age >= (kept.r + (kept.w ?? 0)) * 1000) return null;\n\n // A tag that moved is never served stale: whoever changed it is\n // looking for the change, so this request renders.\n if (Object.keys(kept.v).length > 0) {\n try {\n const moved = await versionSource().changed(kept.v, 0);\n\n if (Object.keys(moved).some((tag) => moved[tag] !== kept.v[tag])) return null;\n } catch {\n // Not knowing whether it changed is not knowing it did not.\n return null;\n }\n }\n\n const headers = new Headers(kept.h);\n\n headers.set(\"Age\", String(Math.floor((now - kept.at) / 1000)));\n\n const etag = headers.get(\"ETag\");\n\n const stale = age >= kept.r * 1000;\n\n if (kept.s === 200 && etag && matches(request.headers.get(\"If-None-Match\"), etag)) {\n return { response: new Response(null, { status: 304, headers }), stale };\n }\n\n return { response: new Response(request.method === \"HEAD\" ? null : kept.b, { status: kept.s, headers }), stale };\n },\n\n busy(key) {\n return inFlight.has(key);\n },\n\n async keep(key, answer) {\n const kept = keepable.get(answer);\n\n if (!kept) return;\n\n let body: string;\n\n try {\n body = utf8.decode(kept.body);\n } catch {\n // Not text: sent, never kept.\n return;\n }\n\n const headers: [string, string][] = [];\n\n answer.headers.forEach((value, name) => {\n if (!NOT_KEPT.has(name.toLowerCase())) headers.push([name, value]);\n });\n\n const notFound = kept.status === 404 || kept.notFound;\n const revalidate = notFound ? Math.min(kept.revalidate, NOT_FOUND_LIMIT) : kept.revalidate;\n // The same windows the answer's Cache-Control gives a CDN.\n const window = notFound ? NOT_FOUND_LIMIT : STALE_WINDOW;\n const page: KeptPage = { s: kept.status, h: headers, b: body, v: kept.versions, at: Date.now(), r: revalidate, w: window };\n\n try {\n await store.set(`rsc-page|${key}`, JSON.stringify(page), Math.ceil(revalidate + window));\n } catch (error) {\n pageStoreFailed(error);\n }\n },\n\n async once(key, render) {\n const pending = inFlight.get(key);\n\n if (pending) {\n await pending.catch(() => {});\n\n return { led: false };\n }\n\n const work = render();\n\n inFlight.set(key, work);\n\n try {\n return { led: true, value: await work };\n } finally {\n inFlight.delete(key);\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pageCache.js","sourceRoot":"","sources":["../src/pageCache.ts"],"names":[],"mappings":"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","sourcesContent":["// A page a shared cache may keep: `export const revalidate = N`.\n//\n// The page is rendered whole, judged, then sent. Public only when every check\n// passed for that render; otherwise `private, no-store`, exactly as before,\n// with `Cache-Status: rsc-kit; fwd=bypass; detail=<check>` saying which one\n// failed and a line in the log saying where. Cache-Control is the contract a\n// host caches by; Cache-Status is for counting and for the developer asking\n// why a page was not kept.\n//\n// The checks, in the order they can be made:\n//\n// 1. payload-url - a GET or HEAD, and a payload asked for at its own `?_rsc=<key>` url, and a\n// document never at one: a cache that ignores Vary keys on\n// the url alone, so either the other way round hands one\n// kind of answer to a request for the other\n// 2. the render read nothing that belongs to one visitor: cookies(),\n// headers(), request(), connection() - in the page, a layout, a\n// Suspense'd child, metadata or middleware.ts\n// 3. host-middleware - Laravel route middleware runs against the session\n// 4. set-cookie - nothing set a cookie\n// 5. nonce - no CSP nonce, which a shared copy would give everyone\n// 6. status - 200 or 404; a redirect, a 5xx or a render React reported\n// as failed inside the payload (failed-render) never is\n//\n// A public answer carries `Cache-Tag`: the tags of every cache() answer the\n// render used, so a CDN can purge the copy when one of them changes.\n//\n// Backends render as nobody - see renderAsNobody() - so a session cookie set\n// by the backend on every call does not make every page private.\n\nimport { HEADER, PER_CLIENT } from \"./headers.js\";\nimport { PAYLOAD_PARAM, payloadKey, withoutPayloadKey } from \"./js/payloadKey.js\";\nimport { cacheTagsRead, cacheTagVersionsRead, renderFailed, visitorReads } from \"./request.js\";\nimport type { CacheStore } from \"./cache.js\";\nimport { versionSource } from \"./changed.js\";\n\n/** What a request for a `revalidate` page carries from the moment it arrives. */\nexport interface PageCaching {\n /** Seconds a shared cache may keep the page. */\n revalidate: number;\n /** The url path, for the log line. */\n path: string;\n /** A check that failed before anything rendered, or after the page said notFound(). */\n refused: string | null;\n}\n\n/** A 404 is kept no longer than this, however long its page may be. */\nconst NOT_FOUND_LIMIT = 60;\n\n/** How long a cache may serve a stale page while one request refreshes it, or while the origin is failing. */\nconst STALE_WINDOW = 86_400;\n\nconst caching = new WeakMap<Request, PageCaching>();\n\n/**\n * Decide, for a request as it arrived, whether its answer may be judged at all.\n *\n * Null for anything but a GET or HEAD of a page that exports revalidate - the\n * answer is then what it always was. Keyed by the request the app sees, so the\n * entry in front, which renders a 404, finds it again.\n */\nexport function cachingFor(arrived: Request, revalidate: number | undefined): PageCaching | null {\n // A post is never kept, and its answer already says so.\n if (!revalidate || revalidate <= 0 || (arrived.method !== \"GET\" && arrived.method !== \"HEAD\")) return null;\n\n const url = new URL(arrived.url);\n const entry: PageCaching = { revalidate, path: url.pathname, refused: urlRefusal(arrived, url) };\n\n caching.set(withoutPayloadKey(arrived), entry);\n\n return entry;\n}\n\n/** What cachingFor() decided for this request, if anything. */\nexport function cachingOf(request: Request): PageCaching | null {\n return caching.get(withoutPayloadKey(request)) ?? null;\n}\n\n/**\n * Check 1. A payload answers at `?_rsc=<key>`, with the key its headers give;\n * a document never answers there. Either way round, a cache keying on the url\n * alone would hand a request one kind of answer for the other.\n */\nfunction urlRefusal(arrived: Request, url: URL): string | null {\n const key = url.searchParams.get(PAYLOAD_PARAM);\n const payload = arrived.headers.get(HEADER.rsc) !== null;\n\n if (payload ? key !== payloadKey(arrived.headers) : key !== null) return \"payload-url\";\n\n return null;\n}\n\n/**\n * Note what the render has read so far, for an answer that will be finished\n * somewhere else: the page said notFound(), and the entry in front renders\n * not-found.tsx in a scope of its own, where these reads are gone.\n */\nexport function noteReadsSoFar(entry: PageCaching): void {\n entry.refused ??= readRefusal();\n}\n\nfunction readRefusal(): string | null {\n if (renderFailed()) return \"failed-render\";\n\n const read = visitorReads()[0];\n\n return read ?? null;\n}\n\n/** The detail token a read is reported under: `cookies() in Header` is `cookies`. */\nfunction detailOf(refused: string): string {\n return refused.replace(/\\(.*$/, \"\").replace(/[^a-z-]/gi, \"-\");\n}\n\nconst logged = new Set<string>();\n\nconst REASONS: Record<string, string> = {\n \"payload-url\":\n \"a payload was asked for without its ?_rsc= key, or a document with one. The router always adds it; this was another client.\",\n \"host-middleware\": \"the route has host middleware, which runs against the visitor's session.\",\n \"set-cookie\": \"the answer set a cookie.\",\n nonce: \"the answer carries a CSP nonce, which a shared copy would give every visitor.\",\n status: \"only a 200 or a 404 is kept.\",\n \"failed-render\": \"React reported a failure inside the payload.\",\n};\n\n/** Once per page and reason: a page refused on every request says so once. */\nfunction logRefusal(entry: PageCaching, refused: string): void {\n const key = `${entry.path}\\n${refused}`;\n\n if (logged.has(key) || logged.size > 500) return;\n\n logged.add(key);\n\n // A read names what read, and where: `cookies() in Header`.\n const why = REASONS[refused] ?? `${refused} reads this visitor.`;\n\n console.warn(`[rsc-kit] ${entry.path} exports revalidate, and was sent private: ${why}`);\n}\n\nfunction bypass(response: Response, body: BodyInit | null, entry: PageCaching, refused: string): Response {\n const headers = new Headers(response.headers);\n\n headers.set(\"Cache-Control\", PER_CLIENT);\n headers.set(\"Cache-Status\", `rsc-kit; fwd=bypass; detail=${detailOf(refused)}`);\n headers.delete(\"ETag\");\n // A CDN that reads it reads it over Cache-Control, so a private answer never has it.\n headers.delete(\"CDN-Cache-Control\");\n logRefusal(entry, refused);\n\n return new Response(body, { status: response.status, statusText: response.statusText, headers });\n}\n\n/** How long a shared cache keeps a public answer, which both of its headers state. */\nfunction sharedLifetime(revalidate: number, status: number): { fresh: number; windows: string } {\n const notFound = status === 404;\n const stale = notFound ? NOT_FOUND_LIMIT : STALE_WINDOW;\n\n return {\n fresh: notFound ? Math.min(revalidate, NOT_FOUND_LIMIT) : revalidate,\n windows: `stale-while-revalidate=${stale}, stale-if-error=${stale}`,\n };\n}\n\n/**\n * What a public answer says: kept by shared caches for N seconds, refreshed\n * in the background for a day after that, never by the browser. A 404 for at\n * most a minute, so a scanner walking random urls cannot hold pages out.\n *\n * stale-if-error says the same window outright for an origin that is down,\n * erroring or mid-deploy: a cache left to choose may serve the error instead,\n * or a copy older than the page would ever have been kept for.\n */\nexport function publicCacheControl(revalidate: number, status: number): string {\n const { fresh, windows } = sharedLifetime(revalidate, status);\n\n return `public, max-age=0, s-maxage=${fresh}, ${windows}`;\n}\n\n/**\n * The same lifetime for a CDN that reads CDN-Cache-Control (RFC 9213), which\n * is meant for it alone: there max-age is the CDN's own, not a browser's. Some\n * edges keep a page only on this header, and Cache-Control stays beside it for\n * every cache that does not read it.\n */\nexport function cdnCacheControl(revalidate: number, status: number): string {\n const { fresh, windows } = sharedLifetime(revalidate, status);\n\n return `max-age=${fresh}, ${windows}`;\n}\n\n/** A tag a Cache-Tag list can carry: visible ASCII, no comma, which separates them. */\nconst TAG = /^[\\x21-\\x2b\\x2d-\\x7e]+$/;\n\nlet warnedTag = false;\n\n/**\n * The tags of every cache() answer a kept page used, as a CDN reads them:\n * `Cache-Tag: pages,post:1`. A purge by any of them drops the copy. A tag the\n * header cannot carry is left out, and said once: the copy then goes stale on\n * its own time, not on that change.\n */\nexport function cacheTagHeader(tags: readonly string[]): string | null {\n const kept = tags.filter((tag) => TAG.test(tag));\n\n if (kept.length < tags.length && !warnedTag) {\n warnedTag = true;\n const bad = tags.find((tag) => !TAG.test(tag));\n console.warn(\n `[rsc-kit] cache() tag ${JSON.stringify(bad)} cannot go in a Cache-Tag header (a comma, a space or a non-ASCII letter), so a CDN cannot purge by it.`,\n );\n }\n\n return kept.length ? kept.join(\",\") : null;\n}\n\nasync function etagOf(body: ArrayBuffer): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", body));\n let hex = \"\";\n\n for (let i = 0; i < 12; i++) hex += digest[i].toString(16).padStart(2, \"0\");\n\n return `\"${hex}\"`;\n}\n\nfunction matches(ifNoneMatch: string | null, etag: string): boolean {\n if (!ifNoneMatch) return false;\n\n return ifNoneMatch\n .split(\",\")\n .map((tag) => tag.trim().replace(/^W\\//, \"\"))\n .some((tag) => tag === etag || tag === \"*\");\n}\n\n/**\n * Finish the answer to a `revalidate` page: rendered whole, then judged.\n *\n * Called inside the request's scope, after every header the request will get\n * is on the answer - cookies included - so what it judges is what is sent.\n * Reading the body to its end is what waits for the render: a Suspense'd\n * child that reads cookies() is still running when the headers exist.\n */\nexport async function finishCacheable(\n request: Request,\n response: Response,\n entry: PageCaching,\n /**\n * Whether the answer turned out to be not-found.tsx, asked once the body is\n * read: a page can say notFound() in the middle of its stream, after a 200\n * went out. A payload is a 200 either way - the router reads the page from\n * it - but a not-found one is kept no longer than a 404.\n */\n saidNotFound: () => boolean = () => false,\n): Promise<Response> {\n if (entry.refused) return bypass(response, response.body, entry, entry.refused);\n\n if (response.status !== 200 && response.status !== 404) return bypass(response, response.body, entry, \"status\");\n\n const type = response.headers.get(\"Content-Type\") ?? \"\";\n\n if (!type.startsWith(\"text/html\") && !type.startsWith(\"text/x-component\")) {\n return bypass(response, response.body, entry, \"status\");\n }\n\n const body = await response.arrayBuffer();\n const refused =\n readRefusal() ??\n (response.headers.getSetCookie().length > 0 ? \"set-cookie\" : null) ??\n (/'nonce-/.test(response.headers.get(\"Content-Security-Policy\") ?? \"\") ? \"nonce\" : null);\n\n if (refused) return bypass(response, body, entry, refused);\n\n const headers = new Headers(response.headers);\n const etag = await etagOf(body);\n\n const status = saidNotFound() ? 404 : response.status;\n\n headers.set(\"Cache-Control\", publicCacheControl(entry.revalidate, status));\n headers.set(\"CDN-Cache-Control\", cdnCacheControl(entry.revalidate, status));\n headers.set(\"ETag\", etag);\n\n const tags = cacheTagHeader(cacheTagsRead());\n\n if (tags) headers.set(\"Cache-Tag\", tags);\n\n const kept: Keepable = { status: response.status, body, versions: cacheTagVersionsRead(), revalidate: entry.revalidate, notFound: saidNotFound() };\n\n if (response.status === 200 && matches(request.headers.get(\"If-None-Match\"), etag)) {\n headers.delete(\"Content-Length\");\n\n const answer = new Response(null, { status: 304, headers });\n\n keepable.set(answer, kept);\n\n return answer;\n }\n\n const answer = new Response(request.method === \"HEAD\" ? null : body, { status: response.status, statusText: response.statusText, headers });\n\n keepable.set(answer, kept);\n\n return answer;\n}\n\n// The engine's own copy of a public page: ISR on every host.\n//\n// A kept page is versioned, not purged. Its entry carries the version of\n// every tag its cache() answers were read under, and a lookup asks the\n// version source whether any moved since. changed(tag) bumps a version every\n// instance reads, so every pod misses at its next request with nothing sent\n// to it - and a render that was running when the change landed is stored\n// under the older version, so it is never served. Correctness needs only a\n// version source every instance shares; the store decides how often a page\n// is rendered, never whether a stale one is served.\n\n/** What finishCacheable() sent public, kept beside the answer for the page cache to store. */\ninterface Keepable {\n status: number;\n body: ArrayBuffer;\n versions: Record<string, number>;\n revalidate: number;\n /** A not-found page, kept no longer than a 404. */\n notFound: boolean;\n}\n\nconst keepable = new WeakMap<Response, Keepable>();\n\n/** A kept page, as the store holds it. */\ninterface KeptPage {\n /** Status. */\n s: number;\n /** Headers, as name and value pairs. */\n h: [string, string][];\n /** Body, as text: HTML and Flight both are. */\n b: string;\n /** Tag versions it was rendered under. */\n v: Record<string, number>;\n /** When it was rendered, in ms. */\n at: number;\n /** Seconds it is fresh for. */\n r: number;\n /** Seconds past that it may still be served while one render replaces it. */\n w: number;\n}\n\n/** Headers that belong to one answer, never to the kept page. */\nconst NOT_KEPT = new Set([\"date\", \"set-cookie\", \"content-length\", \"x-rsc-kit\", \"x-powered-by\", \"age\"]);\n\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\n/**\n * Pages kept in this process, by the size of what they hold: a page is tens\n * or hundreds of KB, so a count says nothing about memory. Least recently\n * used goes first.\n */\nexport function pageMemoryStore(maxBytes = 64 * 1024 * 1024): CacheStore {\n const entries = new Map<string, { value: string; until: number }>();\n let bytes = 0;\n\n const drop = (key: string) => {\n const entry = entries.get(key);\n\n if (!entry) return;\n\n entries.delete(key);\n bytes -= entry.value.length;\n };\n\n return {\n get(key) {\n const entry = entries.get(key);\n\n if (!entry) return undefined;\n\n if (entry.until <= Date.now()) {\n drop(key);\n\n return undefined;\n }\n\n // Re-inserted, so the Map's order is least recently used first.\n entries.delete(key);\n entries.set(key, entry);\n\n return entry.value;\n },\n set(key, value, ttl) {\n drop(key);\n\n if (value.length > maxBytes) return;\n\n entries.set(key, { value, until: Date.now() + ttl * 1000 });\n bytes += value.length;\n\n while (bytes > maxBytes) drop(entries.keys().next().value!);\n },\n };\n}\n\n/** A kept answer, and whether it is past its time: served, while one render replaces it. */\nexport interface KeptAnswer {\n response: Response;\n stale: boolean;\n}\n\nexport interface PageCache {\n /** The kept answer for this request, or null to render it now. */\n lookup(key: string, request: Request): Promise<KeptAnswer | null>;\n /** Whether a render for this key is running in this process. */\n busy(key: string): boolean;\n /** Keep the answer, when finishCacheable() sent it public. */\n keep(key: string, answer: Response): Promise<void>;\n /** Run one render per key at a time: a burst for a page nobody has kept waits for the first. */\n once<T>(key: string, render: () => Promise<T>): Promise<{ led: boolean; value?: T }>;\n}\n\nlet warnedPageStore = false;\n\n/** A store that fails is a render, not a broken page: said once. */\nfunction pageStoreFailed(error: unknown): void {\n if (warnedPageStore) return;\n warnedPageStore = true;\n console.warn(\"[rsc-kit] the page cache's store failed, so pages are being rendered each time instead:\", error);\n}\n\n/** The engine's page cache, over a store: the app's, or one of its own in memory. */\nexport function createPageCache(store: CacheStore): PageCache {\n const inFlight = new Map<string, Promise<unknown>>();\n\n return {\n async lookup(key, request) {\n let raw: string | null | undefined;\n\n try {\n raw = await store.get(`rsc-page|${key}`);\n } catch (error) {\n pageStoreFailed(error);\n\n return null;\n }\n\n if (!raw) return null;\n\n let kept: KeptPage;\n\n try {\n kept = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const now = Date.now();\n const age = now - kept.at;\n\n // The store may keep it longer than asked; the age is checked here.\n // Past its time it is still served, for a while, as the stale copy a\n // render in the background replaces: no visitor waits on an expiry.\n if (age >= (kept.r + (kept.w ?? 0)) * 1000) return null;\n\n // A tag that moved is never served stale: whoever changed it is\n // looking for the change, so this request renders.\n if (Object.keys(kept.v).length > 0) {\n try {\n const moved = await versionSource().changed(kept.v, 0);\n\n if (Object.keys(moved).some((tag) => moved[tag] !== kept.v[tag])) return null;\n } catch {\n // Not knowing whether it changed is not knowing it did not.\n return null;\n }\n }\n\n const headers = new Headers(kept.h);\n\n headers.set(\"Age\", String(Math.floor((now - kept.at) / 1000)));\n\n const etag = headers.get(\"ETag\");\n\n const stale = age >= kept.r * 1000;\n\n if (kept.s === 200 && etag && matches(request.headers.get(\"If-None-Match\"), etag)) {\n return { response: new Response(null, { status: 304, headers }), stale };\n }\n\n return { response: new Response(request.method === \"HEAD\" ? null : kept.b, { status: kept.s, headers }), stale };\n },\n\n busy(key) {\n return inFlight.has(key);\n },\n\n async keep(key, answer) {\n const kept = keepable.get(answer);\n\n if (!kept) return;\n\n let body: string;\n\n try {\n body = utf8.decode(kept.body);\n } catch {\n // Not text: sent, never kept.\n return;\n }\n\n const headers: [string, string][] = [];\n\n answer.headers.forEach((value, name) => {\n if (!NOT_KEPT.has(name.toLowerCase())) headers.push([name, value]);\n });\n\n const notFound = kept.status === 404 || kept.notFound;\n const revalidate = notFound ? Math.min(kept.revalidate, NOT_FOUND_LIMIT) : kept.revalidate;\n // The same windows the answer's Cache-Control gives a CDN.\n const window = notFound ? NOT_FOUND_LIMIT : STALE_WINDOW;\n const page: KeptPage = { s: kept.status, h: headers, b: body, v: kept.versions, at: Date.now(), r: revalidate, w: window };\n\n try {\n await store.set(`rsc-page|${key}`, JSON.stringify(page), Math.ceil(revalidate + window));\n } catch (error) {\n pageStoreFailed(error);\n }\n },\n\n async once(key, render) {\n const pending = inFlight.get(key);\n\n if (pending) {\n await pending.catch(() => {});\n\n return { led: false };\n }\n\n const work = render();\n\n inFlight.set(key, work);\n\n try {\n return { led: true, value: await work };\n } finally {\n inFlight.delete(key);\n }\n },\n };\n}\n"]}
|
package/dist/purge.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export declare function purgeLater(tags: readonly string[]): void;
|
|
|
39
39
|
* at `s-maxage`. The first failure is logged.
|
|
40
40
|
*/
|
|
41
41
|
export declare function purge(what: Purge): Promise<void>;
|
|
42
|
-
/** Homeport takes at most this many tags
|
|
42
|
+
/** Homeport takes at most this many tags and paths, together, per call. */
|
|
43
43
|
export declare const HOMEPORT_PURGE_LIMIT = 100;
|
|
44
44
|
export interface HomeportPurgeOptions {
|
|
45
45
|
/** Where to post. Default `HOMEPORT_PURGE_URL`, which Homeport sets. */
|
|
@@ -50,8 +50,9 @@ export interface HomeportPurgeOptions {
|
|
|
50
50
|
/**
|
|
51
51
|
* Homeport's purge: `POST {tags, paths, all}` to the url Homeport gives the
|
|
52
52
|
* app, no token - it is reachable only from inside the app's own network.
|
|
53
|
-
* Split into calls of 100 tags and
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Split into calls of 100 tags and paths together. A tag or path too long for
|
|
54
|
+
* it is left out: one would cost the whole call, and the edge keeps no page
|
|
55
|
+
* with a tag it could not purge. Only a 202 is accepted. A 429 is tried once
|
|
56
|
+
* more after its `Retry-After` (at most five seconds); anything else rejects.
|
|
56
57
|
*/
|
|
57
58
|
export declare function homeportPurge(options?: HomeportPurgeOptions): Purger;
|
package/dist/purge.js
CHANGED
|
@@ -93,14 +93,19 @@ export async function purge(what) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
/** Homeport takes at most this many tags
|
|
96
|
+
/** Homeport takes at most this many tags and paths, together, per call. */
|
|
97
97
|
export const HOMEPORT_PURGE_LIMIT = 100;
|
|
98
|
+
/** The longest tag and path Homeport takes, in bytes. */
|
|
99
|
+
const HOMEPORT_TAG_BYTES = 1024;
|
|
100
|
+
const HOMEPORT_PATH_BYTES = 2048;
|
|
101
|
+
const bytes = new TextEncoder();
|
|
98
102
|
/**
|
|
99
103
|
* Homeport's purge: `POST {tags, paths, all}` to the url Homeport gives the
|
|
100
104
|
* app, no token - it is reachable only from inside the app's own network.
|
|
101
|
-
* Split into calls of 100 tags and
|
|
102
|
-
*
|
|
103
|
-
*
|
|
105
|
+
* Split into calls of 100 tags and paths together. A tag or path too long for
|
|
106
|
+
* it is left out: one would cost the whole call, and the edge keeps no page
|
|
107
|
+
* with a tag it could not purge. Only a 202 is accepted. A 429 is tried once
|
|
108
|
+
* more after its `Retry-After` (at most five seconds); anything else rejects.
|
|
104
109
|
*/
|
|
105
110
|
export function homeportPurge(options = {}) {
|
|
106
111
|
return async (what) => {
|
|
@@ -108,15 +113,15 @@ export function homeportPurge(options = {}) {
|
|
|
108
113
|
if (!url)
|
|
109
114
|
throw new Error("homeportPurge() has no url: HOMEPORT_PURGE_URL is not set.");
|
|
110
115
|
const send = options.fetch ?? fetch;
|
|
111
|
-
const tags =
|
|
112
|
-
const paths =
|
|
116
|
+
const tags = (what.tags ?? []).filter((tag) => bytes.encode(tag).length <= HOMEPORT_TAG_BYTES);
|
|
117
|
+
const paths = (what.paths ?? []).filter((path) => bytes.encode(path).length <= HOMEPORT_PATH_BYTES);
|
|
113
118
|
const bodies = [];
|
|
114
119
|
if (what.all)
|
|
115
120
|
bodies.push({ all: true });
|
|
116
121
|
while (tags.length > 0 || paths.length > 0) {
|
|
117
122
|
const body = {};
|
|
118
123
|
const someTags = tags.splice(0, HOMEPORT_PURGE_LIMIT);
|
|
119
|
-
const somePaths = paths.splice(0, HOMEPORT_PURGE_LIMIT);
|
|
124
|
+
const somePaths = paths.splice(0, HOMEPORT_PURGE_LIMIT - someTags.length);
|
|
120
125
|
if (someTags.length > 0)
|
|
121
126
|
body.tags = someTags;
|
|
122
127
|
if (somePaths.length > 0)
|
|
@@ -135,7 +140,7 @@ async function post(send, url, body, retried = false) {
|
|
|
135
140
|
body: JSON.stringify(body),
|
|
136
141
|
});
|
|
137
142
|
await response.body?.cancel();
|
|
138
|
-
if (response.
|
|
143
|
+
if (response.status === 202)
|
|
139
144
|
return;
|
|
140
145
|
if (response.status === 429 && !retried) {
|
|
141
146
|
const seconds = Math.min(Number(response.headers.get("Retry-After")) || 1, 5);
|
package/dist/purge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purge.js","sourceRoot":"","sources":["../src/purge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Telling a CDN that kept pages changed.\n *\n * A `revalidate` page goes out with `Cache-Tag`, and a CDN that purges by tag\n * can drop it the moment one of those tags changes, rather than waiting out\n * `s-maxage`. What calls the purge is the app's: every CDN has its own API, so\n * the engine has one hook, `installPurge()`, and `changed(tag)` calls it with\n * the names that changed - after the answer has gone, through `after()`, so a\n * slow CDN never holds up the action that made the change.\n *\n * Homeport is the one installed for you: with `HOMEPORT_PURGE_URL` set, the\n * hook is `homeportPurge()` unless the app installed another.\n *\n * A Laravel or Go backend changes its names itself (`Rsc::changed()`,\n * `rsckit.Changed()`), and the engine never hears of it, so each backend\n * purges for its own changes. This hook covers `changed()` called here.\n */\n\n// Not imported: @rsc-kit/core/changed reaches this file and a client component\n// may import that, so request.ts - server-only - is found through the global\n// it sets when the server loads it. Without it, there is no request to wait\n// for, and a purge goes out at once.\ntype AfterHooks = { after: (work: () => unknown) => void; requestKey: () => object | null };\n\nfunction hooks(): AfterHooks {\n return (\n ((globalThis as Record<symbol, unknown>)[Symbol.for(\"rsc-kit.after\")] as AfterHooks | undefined) ?? {\n after: (work) => void Promise.resolve().then(work),\n requestKey: () => null,\n }\n );\n}\n\n/** What a purge drops: pages carrying any of these tags, pages at these paths, or everything. */\nexport interface Purge {\n tags?: readonly string[];\n paths?: readonly string[];\n all?: boolean;\n}\n\n/** Sends a purge to a CDN. A rejection is logged; it never fails the change. */\nexport type Purger = (purge: Purge) => Promise<void> | void;\n\n// On the global, like the cache store: an app installs through its own copy\n// of the package, and the engine's copy is the one that calls it.\nconst PURGER = Symbol.for(\"rsc-kit.purger\");\nconst globals = globalThis as unknown as Record<symbol, Purger | null | undefined>;\n\n/** Install the app's purge. `null` takes it out, and Homeport's default with it. */\nexport function installPurge(purger: Purger | null): void {\n globals[PURGER] = purger;\n}\n\n/** The purge in use: the app's, else Homeport's when its url is set, else none. */\nexport function installedPurge(): Purger | null {\n const installed = globals[PURGER];\n\n if (installed !== undefined) return installed;\n\n const url = homeportPurgeUrl();\n\n return url ? homeportPurge({ url }) : null;\n}\n\nfunction homeportPurgeUrl(): string | undefined {\n return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.HOMEPORT_PURGE_URL || undefined;\n}\n\n// The names an action changed go out as one purge when its answer has gone:\n// three changed() calls send one request, not three. Gathered per request,\n// so a request whose after() work never ran cannot hold back another's.\nconst pendingFor = new WeakMap<object, Set<string>>();\n\n/**\n * @internal For changed(): purge these tags once the answer has gone, or at\n * once outside a request. Does nothing when no purge is installed.\n */\nexport function purgeLater(tags: readonly string[]): void {\n if (tags.length === 0 || !installedPurge()) return;\n\n const { after, requestKey } = hooks();\n const key = requestKey();\n\n if (!key) {\n after(() => purge({ tags }));\n\n return;\n }\n\n let pending = pendingFor.get(key);\n\n if (pending) {\n for (const tag of tags) pending.add(tag);\n\n return;\n }\n\n pending = new Set(tags);\n pendingFor.set(key, pending);\n\n const gathered = pending;\n\n after(() => {\n pendingFor.delete(key);\n\n return purge({ tags: [...gathered] });\n });\n}\n\nlet warnedPurge = false;\n\n/**\n * Send a purge now, through the installed purge. For an app's own use - a\n * deploy step, a webhook - as well as changed()'s. Never throws: the change\n * it follows is already made, and a CDN that missed it still drops the copy\n * at `s-maxage`. The first failure is logged.\n */\nexport async function purge(what: Purge): Promise<void> {\n const purger = installedPurge();\n\n if (!purger) return;\n\n try {\n await purger(what);\n } catch (error) {\n if (!warnedPurge) {\n warnedPurge = true;\n console.warn(\n \"[rsc-kit] purging the CDN failed; its copy is dropped at s-maxage instead: \" +\n (error instanceof Error ? error.message : String(error)),\n );\n }\n }\n}\n\n/** Homeport takes at most this many tags, and this many paths, per call. */\nexport const HOMEPORT_PURGE_LIMIT = 100;\n\nexport interface HomeportPurgeOptions {\n /** Where to post. Default `HOMEPORT_PURGE_URL`, which Homeport sets. */\n url?: string;\n /** For tests. */\n fetch?: typeof fetch;\n}\n\n/**\n * Homeport's purge: `POST {tags, paths, all}` to the url Homeport gives the\n * app, no token - it is reachable only from inside the app's own network.\n * Split into calls of 100 tags and 100 paths. A 429 is tried once more after\n * its `Retry-After` (at most five seconds); anything else that is not a 2xx\n * rejects.\n */\nexport function homeportPurge(options: HomeportPurgeOptions = {}): Purger {\n return async (what) => {\n const url = options.url ?? homeportPurgeUrl();\n\n if (!url) throw new Error(\"homeportPurge() has no url: HOMEPORT_PURGE_URL is not set.\");\n\n const send = options.fetch ?? fetch;\n const tags = [...(what.tags ?? [])];\n const paths = [...(what.paths ?? [])];\n const bodies: Purge[] = [];\n\n if (what.all) bodies.push({ all: true });\n\n while (tags.length > 0 || paths.length > 0) {\n const body: Purge = {};\n const someTags = tags.splice(0, HOMEPORT_PURGE_LIMIT);\n const somePaths = paths.splice(0, HOMEPORT_PURGE_LIMIT);\n\n if (someTags.length > 0) body.tags = someTags;\n if (somePaths.length > 0) body.paths = somePaths;\n bodies.push(body);\n }\n\n for (const body of bodies) {\n await post(send, url, body);\n }\n };\n}\n\nasync function post(send: typeof fetch, url: string, body: Purge, retried = false): Promise<void> {\n const response = await send(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n await response.body?.cancel();\n\n if (response.ok) return;\n\n if (response.status === 429 && !retried) {\n const seconds = Math.min(Number(response.headers.get(\"Retry-After\")) || 1, 5);\n\n await new Promise((resolve) => setTimeout(resolve, seconds * 1000));\n\n return post(send, url, body, true);\n }\n\n throw new Error(`Homeport answered ${response.status} to a purge of ${JSON.stringify(body)}.`);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"purge.js","sourceRoot":"","sources":["../src/purge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Telling a CDN that kept pages changed.\n *\n * A `revalidate` page goes out with `Cache-Tag`, and a CDN that purges by tag\n * can drop it the moment one of those tags changes, rather than waiting out\n * `s-maxage`. What calls the purge is the app's: every CDN has its own API, so\n * the engine has one hook, `installPurge()`, and `changed(tag)` calls it with\n * the names that changed - after the answer has gone, through `after()`, so a\n * slow CDN never holds up the action that made the change.\n *\n * Homeport is the one installed for you: with `HOMEPORT_PURGE_URL` set, the\n * hook is `homeportPurge()` unless the app installed another.\n *\n * A Laravel or Go backend changes its names itself (`Rsc::changed()`,\n * `rsckit.Changed()`), and the engine never hears of it, so each backend\n * purges for its own changes. This hook covers `changed()` called here.\n */\n\n// Not imported: @rsc-kit/core/changed reaches this file and a client component\n// may import that, so request.ts - server-only - is found through the global\n// it sets when the server loads it. Without it, there is no request to wait\n// for, and a purge goes out at once.\ntype AfterHooks = { after: (work: () => unknown) => void; requestKey: () => object | null };\n\nfunction hooks(): AfterHooks {\n return (\n ((globalThis as Record<symbol, unknown>)[Symbol.for(\"rsc-kit.after\")] as AfterHooks | undefined) ?? {\n after: (work) => void Promise.resolve().then(work),\n requestKey: () => null,\n }\n );\n}\n\n/** What a purge drops: pages carrying any of these tags, pages at these paths, or everything. */\nexport interface Purge {\n tags?: readonly string[];\n paths?: readonly string[];\n all?: boolean;\n}\n\n/** Sends a purge to a CDN. A rejection is logged; it never fails the change. */\nexport type Purger = (purge: Purge) => Promise<void> | void;\n\n// On the global, like the cache store: an app installs through its own copy\n// of the package, and the engine's copy is the one that calls it.\nconst PURGER = Symbol.for(\"rsc-kit.purger\");\nconst globals = globalThis as unknown as Record<symbol, Purger | null | undefined>;\n\n/** Install the app's purge. `null` takes it out, and Homeport's default with it. */\nexport function installPurge(purger: Purger | null): void {\n globals[PURGER] = purger;\n}\n\n/** The purge in use: the app's, else Homeport's when its url is set, else none. */\nexport function installedPurge(): Purger | null {\n const installed = globals[PURGER];\n\n if (installed !== undefined) return installed;\n\n const url = homeportPurgeUrl();\n\n return url ? homeportPurge({ url }) : null;\n}\n\nfunction homeportPurgeUrl(): string | undefined {\n return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.HOMEPORT_PURGE_URL || undefined;\n}\n\n// The names an action changed go out as one purge when its answer has gone:\n// three changed() calls send one request, not three. Gathered per request,\n// so a request whose after() work never ran cannot hold back another's.\nconst pendingFor = new WeakMap<object, Set<string>>();\n\n/**\n * @internal For changed(): purge these tags once the answer has gone, or at\n * once outside a request. Does nothing when no purge is installed.\n */\nexport function purgeLater(tags: readonly string[]): void {\n if (tags.length === 0 || !installedPurge()) return;\n\n const { after, requestKey } = hooks();\n const key = requestKey();\n\n if (!key) {\n after(() => purge({ tags }));\n\n return;\n }\n\n let pending = pendingFor.get(key);\n\n if (pending) {\n for (const tag of tags) pending.add(tag);\n\n return;\n }\n\n pending = new Set(tags);\n pendingFor.set(key, pending);\n\n const gathered = pending;\n\n after(() => {\n pendingFor.delete(key);\n\n return purge({ tags: [...gathered] });\n });\n}\n\nlet warnedPurge = false;\n\n/**\n * Send a purge now, through the installed purge. For an app's own use - a\n * deploy step, a webhook - as well as changed()'s. Never throws: the change\n * it follows is already made, and a CDN that missed it still drops the copy\n * at `s-maxage`. The first failure is logged.\n */\nexport async function purge(what: Purge): Promise<void> {\n const purger = installedPurge();\n\n if (!purger) return;\n\n try {\n await purger(what);\n } catch (error) {\n if (!warnedPurge) {\n warnedPurge = true;\n console.warn(\n \"[rsc-kit] purging the CDN failed; its copy is dropped at s-maxage instead: \" +\n (error instanceof Error ? error.message : String(error)),\n );\n }\n }\n}\n\n/** Homeport takes at most this many tags and paths, together, per call. */\nexport const HOMEPORT_PURGE_LIMIT = 100;\n\n/** The longest tag and path Homeport takes, in bytes. */\nconst HOMEPORT_TAG_BYTES = 1024;\nconst HOMEPORT_PATH_BYTES = 2048;\n\nconst bytes = new TextEncoder();\n\nexport interface HomeportPurgeOptions {\n /** Where to post. Default `HOMEPORT_PURGE_URL`, which Homeport sets. */\n url?: string;\n /** For tests. */\n fetch?: typeof fetch;\n}\n\n/**\n * Homeport's purge: `POST {tags, paths, all}` to the url Homeport gives the\n * app, no token - it is reachable only from inside the app's own network.\n * Split into calls of 100 tags and paths together. A tag or path too long for\n * it is left out: one would cost the whole call, and the edge keeps no page\n * with a tag it could not purge. Only a 202 is accepted. A 429 is tried once\n * more after its `Retry-After` (at most five seconds); anything else rejects.\n */\nexport function homeportPurge(options: HomeportPurgeOptions = {}): Purger {\n return async (what) => {\n const url = options.url ?? homeportPurgeUrl();\n\n if (!url) throw new Error(\"homeportPurge() has no url: HOMEPORT_PURGE_URL is not set.\");\n\n const send = options.fetch ?? fetch;\n const tags = (what.tags ?? []).filter((tag) => bytes.encode(tag).length <= HOMEPORT_TAG_BYTES);\n const paths = (what.paths ?? []).filter((path) => bytes.encode(path).length <= HOMEPORT_PATH_BYTES);\n const bodies: Purge[] = [];\n\n if (what.all) bodies.push({ all: true });\n\n while (tags.length > 0 || paths.length > 0) {\n const body: Purge = {};\n const someTags = tags.splice(0, HOMEPORT_PURGE_LIMIT);\n const somePaths = paths.splice(0, HOMEPORT_PURGE_LIMIT - someTags.length);\n\n if (someTags.length > 0) body.tags = someTags;\n if (somePaths.length > 0) body.paths = somePaths;\n bodies.push(body);\n }\n\n for (const body of bodies) {\n await post(send, url, body);\n }\n };\n}\n\nasync function post(send: typeof fetch, url: string, body: Purge, retried = false): Promise<void> {\n const response = await send(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n await response.body?.cancel();\n\n if (response.status === 202) return;\n\n if (response.status === 429 && !retried) {\n const seconds = Math.min(Number(response.headers.get(\"Retry-After\")) || 1, 5);\n\n await new Promise((resolve) => setTimeout(resolve, seconds * 1000));\n\n return post(send, url, body, true);\n }\n\n throw new Error(`Homeport answered ${response.status} to a purge of ${JSON.stringify(body)}.`);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rsc-kit/core",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.46",
|
|
4
4
|
"description": "React Server Components as a Vite plugin. Routing, streaming, server actions, typed urls and build-time prerendering — deploy to Bun, Node, Workers, Vercel or Deno.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|