@rsc-kit/core 0.30.42 → 0.30.44
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/cache.d.ts +2 -0
- package/dist/cache.js +11 -1
- package/dist/cache.js.map +1 -1
- package/dist/host.d.ts +14 -0
- package/dist/host.js +97 -3
- package/dist/host.js.map +1 -1
- package/dist/pageCache.d.ts +38 -0
- package/dist/pageCache.js +192 -6
- package/dist/pageCache.js.map +1 -1
- package/dist/request.d.ts +10 -0
- package/dist/request.js +28 -0
- package/dist/request.js.map +1 -1
- package/dist/vite.js +3 -0
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/pageCache.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CacheStore } from "./cache.js";
|
|
1
2
|
/** What a request for a `revalidate` page carries from the moment it arrives. */
|
|
2
3
|
export interface PageCaching {
|
|
3
4
|
/** Seconds a shared cache may keep the page. */
|
|
@@ -27,8 +28,19 @@ export declare function noteReadsSoFar(entry: PageCaching): void;
|
|
|
27
28
|
* What a public answer says: kept by shared caches for N seconds, refreshed
|
|
28
29
|
* in the background for a day after that, never by the browser. A 404 for at
|
|
29
30
|
* most a minute, so a scanner walking random urls cannot hold pages out.
|
|
31
|
+
*
|
|
32
|
+
* stale-if-error says the same window outright for an origin that is down,
|
|
33
|
+
* erroring or mid-deploy: a cache left to choose may serve the error instead,
|
|
34
|
+
* or a copy older than the page would ever have been kept for.
|
|
30
35
|
*/
|
|
31
36
|
export declare function publicCacheControl(revalidate: number, status: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* The tags of every cache() answer a kept page used, as a CDN reads them:
|
|
39
|
+
* `Cache-Tag: pages,post:1`. A purge by any of them drops the copy. A tag the
|
|
40
|
+
* header cannot carry is left out, and said once: the copy then goes stale on
|
|
41
|
+
* its own time, not on that change.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cacheTagHeader(tags: readonly string[]): string | null;
|
|
32
44
|
/**
|
|
33
45
|
* Finish the answer to a `revalidate` page: rendered whole, then judged.
|
|
34
46
|
*
|
|
@@ -45,3 +57,29 @@ export declare function finishCacheable(request: Request, response: Response, en
|
|
|
45
57
|
* it - but a not-found one is kept no longer than a 404.
|
|
46
58
|
*/
|
|
47
59
|
saidNotFound?: () => boolean): Promise<Response>;
|
|
60
|
+
/**
|
|
61
|
+
* Pages kept in this process, by the size of what they hold: a page is tens
|
|
62
|
+
* or hundreds of KB, so a count says nothing about memory. Least recently
|
|
63
|
+
* used goes first.
|
|
64
|
+
*/
|
|
65
|
+
export declare function pageMemoryStore(maxBytes?: number): CacheStore;
|
|
66
|
+
/** A kept answer, and whether it is past its time: served, while one render replaces it. */
|
|
67
|
+
export interface KeptAnswer {
|
|
68
|
+
response: Response;
|
|
69
|
+
stale: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface PageCache {
|
|
72
|
+
/** The kept answer for this request, or null to render it now. */
|
|
73
|
+
lookup(key: string, request: Request): Promise<KeptAnswer | null>;
|
|
74
|
+
/** Whether a render for this key is running in this process. */
|
|
75
|
+
busy(key: string): boolean;
|
|
76
|
+
/** Keep the answer, when finishCacheable() sent it public. */
|
|
77
|
+
keep(key: string, answer: Response): Promise<void>;
|
|
78
|
+
/** Run one render per key at a time: a burst for a page nobody has kept waits for the first. */
|
|
79
|
+
once<T>(key: string, render: () => Promise<T>): Promise<{
|
|
80
|
+
led: boolean;
|
|
81
|
+
value?: T;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
/** The engine's page cache, over a store: the app's, or one of its own in memory. */
|
|
85
|
+
export declare function createPageCache(store: CacheStore): PageCache;
|
package/dist/pageCache.js
CHANGED
|
@@ -22,14 +22,18 @@
|
|
|
22
22
|
// 6. status - 200 or 404; a redirect, a 5xx or a render React reported
|
|
23
23
|
// as failed inside the payload (failed-render) never is
|
|
24
24
|
//
|
|
25
|
+
// A public answer carries `Cache-Tag`: the tags of every cache() answer the
|
|
26
|
+
// render used, so a CDN can purge the copy when one of them changes.
|
|
27
|
+
//
|
|
25
28
|
// Backends render as nobody - see renderAsNobody() - so a session cookie set
|
|
26
29
|
// by the backend on every call does not make every page private.
|
|
27
30
|
import { HEADER, PER_CLIENT } from "./headers.js";
|
|
28
31
|
import { PAYLOAD_PARAM, payloadKey, withoutPayloadKey } from "./js/payloadKey.js";
|
|
29
|
-
import { renderFailed, visitorReads } from "./request.js";
|
|
32
|
+
import { cacheTagsRead, cacheTagVersionsRead, renderFailed, visitorReads } from "./request.js";
|
|
33
|
+
import { versionSource } from "./changed.js";
|
|
30
34
|
/** A 404 is kept no longer than this, however long its page may be. */
|
|
31
35
|
const NOT_FOUND_LIMIT = 60;
|
|
32
|
-
/** How long a cache may serve a stale page while one request refreshes it. */
|
|
36
|
+
/** How long a cache may serve a stale page while one request refreshes it, or while the origin is failing. */
|
|
33
37
|
const STALE_WINDOW = 86_400;
|
|
34
38
|
const caching = new WeakMap();
|
|
35
39
|
/**
|
|
@@ -113,11 +117,33 @@ function bypass(response, body, entry, refused) {
|
|
|
113
117
|
* What a public answer says: kept by shared caches for N seconds, refreshed
|
|
114
118
|
* in the background for a day after that, never by the browser. A 404 for at
|
|
115
119
|
* most a minute, so a scanner walking random urls cannot hold pages out.
|
|
120
|
+
*
|
|
121
|
+
* stale-if-error says the same window outright for an origin that is down,
|
|
122
|
+
* erroring or mid-deploy: a cache left to choose may serve the error instead,
|
|
123
|
+
* or a copy older than the page would ever have been kept for.
|
|
116
124
|
*/
|
|
117
125
|
export function publicCacheControl(revalidate, status) {
|
|
118
126
|
return status === 404
|
|
119
|
-
? `public, max-age=0, s-maxage=${Math.min(revalidate, NOT_FOUND_LIMIT)}, stale-while-revalidate=${NOT_FOUND_LIMIT}`
|
|
120
|
-
: `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${STALE_WINDOW}`;
|
|
127
|
+
? `public, max-age=0, s-maxage=${Math.min(revalidate, NOT_FOUND_LIMIT)}, stale-while-revalidate=${NOT_FOUND_LIMIT}, stale-if-error=${NOT_FOUND_LIMIT}`
|
|
128
|
+
: `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${STALE_WINDOW}, stale-if-error=${STALE_WINDOW}`;
|
|
129
|
+
}
|
|
130
|
+
/** A tag a Cache-Tag list can carry: visible ASCII, no comma, which separates them. */
|
|
131
|
+
const TAG = /^[\x21-\x2b\x2d-\x7e]+$/;
|
|
132
|
+
let warnedTag = false;
|
|
133
|
+
/**
|
|
134
|
+
* The tags of every cache() answer a kept page used, as a CDN reads them:
|
|
135
|
+
* `Cache-Tag: pages,post:1`. A purge by any of them drops the copy. A tag the
|
|
136
|
+
* header cannot carry is left out, and said once: the copy then goes stale on
|
|
137
|
+
* its own time, not on that change.
|
|
138
|
+
*/
|
|
139
|
+
export function cacheTagHeader(tags) {
|
|
140
|
+
const kept = tags.filter((tag) => TAG.test(tag));
|
|
141
|
+
if (kept.length < tags.length && !warnedTag) {
|
|
142
|
+
warnedTag = true;
|
|
143
|
+
const bad = tags.find((tag) => !TAG.test(tag));
|
|
144
|
+
console.warn(`[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.`);
|
|
145
|
+
}
|
|
146
|
+
return kept.length ? kept.join(",") : null;
|
|
121
147
|
}
|
|
122
148
|
async function etagOf(body) {
|
|
123
149
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", body));
|
|
@@ -168,10 +194,170 @@ saidNotFound = () => false) {
|
|
|
168
194
|
const etag = await etagOf(body);
|
|
169
195
|
headers.set("Cache-Control", publicCacheControl(entry.revalidate, saidNotFound() ? 404 : response.status));
|
|
170
196
|
headers.set("ETag", etag);
|
|
197
|
+
const tags = cacheTagHeader(cacheTagsRead());
|
|
198
|
+
if (tags)
|
|
199
|
+
headers.set("Cache-Tag", tags);
|
|
200
|
+
const kept = { status: response.status, body, versions: cacheTagVersionsRead(), revalidate: entry.revalidate, notFound: saidNotFound() };
|
|
171
201
|
if (response.status === 200 && matches(request.headers.get("If-None-Match"), etag)) {
|
|
172
202
|
headers.delete("Content-Length");
|
|
173
|
-
|
|
203
|
+
const answer = new Response(null, { status: 304, headers });
|
|
204
|
+
keepable.set(answer, kept);
|
|
205
|
+
return answer;
|
|
174
206
|
}
|
|
175
|
-
|
|
207
|
+
const answer = new Response(request.method === "HEAD" ? null : body, { status: response.status, statusText: response.statusText, headers });
|
|
208
|
+
keepable.set(answer, kept);
|
|
209
|
+
return answer;
|
|
210
|
+
}
|
|
211
|
+
const keepable = new WeakMap();
|
|
212
|
+
/** Headers that belong to one answer, never to the kept page. */
|
|
213
|
+
const NOT_KEPT = new Set(["date", "set-cookie", "content-length", "x-rsc-kit", "x-powered-by", "age"]);
|
|
214
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
215
|
+
/**
|
|
216
|
+
* Pages kept in this process, by the size of what they hold: a page is tens
|
|
217
|
+
* or hundreds of KB, so a count says nothing about memory. Least recently
|
|
218
|
+
* used goes first.
|
|
219
|
+
*/
|
|
220
|
+
export function pageMemoryStore(maxBytes = 64 * 1024 * 1024) {
|
|
221
|
+
const entries = new Map();
|
|
222
|
+
let bytes = 0;
|
|
223
|
+
const drop = (key) => {
|
|
224
|
+
const entry = entries.get(key);
|
|
225
|
+
if (!entry)
|
|
226
|
+
return;
|
|
227
|
+
entries.delete(key);
|
|
228
|
+
bytes -= entry.value.length;
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
get(key) {
|
|
232
|
+
const entry = entries.get(key);
|
|
233
|
+
if (!entry)
|
|
234
|
+
return undefined;
|
|
235
|
+
if (entry.until <= Date.now()) {
|
|
236
|
+
drop(key);
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
// Re-inserted, so the Map's order is least recently used first.
|
|
240
|
+
entries.delete(key);
|
|
241
|
+
entries.set(key, entry);
|
|
242
|
+
return entry.value;
|
|
243
|
+
},
|
|
244
|
+
set(key, value, ttl) {
|
|
245
|
+
drop(key);
|
|
246
|
+
if (value.length > maxBytes)
|
|
247
|
+
return;
|
|
248
|
+
entries.set(key, { value, until: Date.now() + ttl * 1000 });
|
|
249
|
+
bytes += value.length;
|
|
250
|
+
while (bytes > maxBytes)
|
|
251
|
+
drop(entries.keys().next().value);
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
let warnedPageStore = false;
|
|
256
|
+
/** A store that fails is a render, not a broken page: said once. */
|
|
257
|
+
function pageStoreFailed(error) {
|
|
258
|
+
if (warnedPageStore)
|
|
259
|
+
return;
|
|
260
|
+
warnedPageStore = true;
|
|
261
|
+
console.warn("[rsc-kit] the page cache's store failed, so pages are being rendered each time instead:", error);
|
|
262
|
+
}
|
|
263
|
+
/** The engine's page cache, over a store: the app's, or one of its own in memory. */
|
|
264
|
+
export function createPageCache(store) {
|
|
265
|
+
const inFlight = new Map();
|
|
266
|
+
return {
|
|
267
|
+
async lookup(key, request) {
|
|
268
|
+
let raw;
|
|
269
|
+
try {
|
|
270
|
+
raw = await store.get(`rsc-page|${key}`);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
pageStoreFailed(error);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (!raw)
|
|
277
|
+
return null;
|
|
278
|
+
let kept;
|
|
279
|
+
try {
|
|
280
|
+
kept = JSON.parse(raw);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
const age = now - kept.at;
|
|
287
|
+
// The store may keep it longer than asked; the age is checked here.
|
|
288
|
+
// Past its time it is still served, for a while, as the stale copy a
|
|
289
|
+
// render in the background replaces: no visitor waits on an expiry.
|
|
290
|
+
if (age >= (kept.r + (kept.w ?? 0)) * 1000)
|
|
291
|
+
return null;
|
|
292
|
+
// A tag that moved is never served stale: whoever changed it is
|
|
293
|
+
// looking for the change, so this request renders.
|
|
294
|
+
if (Object.keys(kept.v).length > 0) {
|
|
295
|
+
try {
|
|
296
|
+
const moved = await versionSource().changed(kept.v, 0);
|
|
297
|
+
if (Object.keys(moved).some((tag) => moved[tag] !== kept.v[tag]))
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Not knowing whether it changed is not knowing it did not.
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const headers = new Headers(kept.h);
|
|
306
|
+
headers.set("Age", String(Math.floor((now - kept.at) / 1000)));
|
|
307
|
+
const etag = headers.get("ETag");
|
|
308
|
+
const stale = age >= kept.r * 1000;
|
|
309
|
+
if (kept.s === 200 && etag && matches(request.headers.get("If-None-Match"), etag)) {
|
|
310
|
+
return { response: new Response(null, { status: 304, headers }), stale };
|
|
311
|
+
}
|
|
312
|
+
return { response: new Response(request.method === "HEAD" ? null : kept.b, { status: kept.s, headers }), stale };
|
|
313
|
+
},
|
|
314
|
+
busy(key) {
|
|
315
|
+
return inFlight.has(key);
|
|
316
|
+
},
|
|
317
|
+
async keep(key, answer) {
|
|
318
|
+
const kept = keepable.get(answer);
|
|
319
|
+
if (!kept)
|
|
320
|
+
return;
|
|
321
|
+
let body;
|
|
322
|
+
try {
|
|
323
|
+
body = utf8.decode(kept.body);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Not text: sent, never kept.
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const headers = [];
|
|
330
|
+
answer.headers.forEach((value, name) => {
|
|
331
|
+
if (!NOT_KEPT.has(name.toLowerCase()))
|
|
332
|
+
headers.push([name, value]);
|
|
333
|
+
});
|
|
334
|
+
const notFound = kept.status === 404 || kept.notFound;
|
|
335
|
+
const revalidate = notFound ? Math.min(kept.revalidate, NOT_FOUND_LIMIT) : kept.revalidate;
|
|
336
|
+
// The same windows the answer's Cache-Control gives a CDN.
|
|
337
|
+
const window = notFound ? NOT_FOUND_LIMIT : STALE_WINDOW;
|
|
338
|
+
const page = { s: kept.status, h: headers, b: body, v: kept.versions, at: Date.now(), r: revalidate, w: window };
|
|
339
|
+
try {
|
|
340
|
+
await store.set(`rsc-page|${key}`, JSON.stringify(page), Math.ceil(revalidate + window));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
pageStoreFailed(error);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
async once(key, render) {
|
|
347
|
+
const pending = inFlight.get(key);
|
|
348
|
+
if (pending) {
|
|
349
|
+
await pending.catch(() => { });
|
|
350
|
+
return { led: false };
|
|
351
|
+
}
|
|
352
|
+
const work = render();
|
|
353
|
+
inFlight.set(key, work);
|
|
354
|
+
try {
|
|
355
|
+
return { led: true, value: await work };
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
inFlight.delete(key);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
};
|
|
176
362
|
}
|
|
177
363
|
//# sourceMappingURL=pageCache.js.map
|
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// 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 { renderFailed, visitorReads } from \"./request.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. */\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 */\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}`\n : `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${STALE_WINDOW}`;\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 if (response.status === 200 && matches(request.headers.get(\"If-None-Match\"), etag)) {\n headers.delete(\"Content-Length\");\n\n return new Response(null, { status: 304, headers });\n }\n\n return new Response(request.method === \"HEAD\" ? null : body, { status: response.status, statusText: response.statusText, headers });\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 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"]}
|
package/dist/request.d.ts
CHANGED
|
@@ -211,6 +211,16 @@ export declare function noteRequestRead(by: string): void;
|
|
|
211
211
|
export declare function renderAsNobody(): void;
|
|
212
212
|
/** Whether the render that is open is one renderAsNobody() marked. */
|
|
213
213
|
export declare function renderingAsNobody(): boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Note the tags of an answer the render used, in a render that may be kept for
|
|
216
|
+
* everyone: the kept copy is stale when any of them changes, and its Cache-Tag
|
|
217
|
+
* lets a CDN purge it then. A no-op anywhere else.
|
|
218
|
+
*/
|
|
219
|
+
export declare function noteCacheTags(tags: readonly string[], versions?: Record<string, number>): void;
|
|
220
|
+
/** The version of each tag noteCacheTags() collected, as it was first read. */
|
|
221
|
+
export declare function cacheTagVersionsRead(): Record<string, number>;
|
|
222
|
+
/** The tags noteCacheTags() collected in the render that is open, in order. */
|
|
223
|
+
export declare function cacheTagsRead(): string[];
|
|
214
224
|
/** What read the visitor in a render renderAsNobody() marked, in order. */
|
|
215
225
|
export declare function visitorReads(): string[];
|
|
216
226
|
/**
|
package/dist/request.js
CHANGED
|
@@ -627,6 +627,34 @@ export function renderAsNobody() {
|
|
|
627
627
|
export function renderingAsNobody() {
|
|
628
628
|
return scope()?.getStore()?.asNobody === true;
|
|
629
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Note the tags of an answer the render used, in a render that may be kept for
|
|
632
|
+
* everyone: the kept copy is stale when any of them changes, and its Cache-Tag
|
|
633
|
+
* lets a CDN purge it then. A no-op anywhere else.
|
|
634
|
+
*/
|
|
635
|
+
export function noteCacheTags(tags, versions = {}) {
|
|
636
|
+
const store = scope()?.getStore();
|
|
637
|
+
if (!store?.request || !store.asNobody || tags.length === 0)
|
|
638
|
+
return;
|
|
639
|
+
const list = (store.tags ??= []);
|
|
640
|
+
const seen = (store.tagVersions ??= {});
|
|
641
|
+
for (const tag of tags) {
|
|
642
|
+
if (!list.includes(tag))
|
|
643
|
+
list.push(tag);
|
|
644
|
+
// The first read wins: a tag that moved mid-render leaves the older
|
|
645
|
+
// version on the kept page, so the next lookup sees the move and misses.
|
|
646
|
+
if (!(tag in seen))
|
|
647
|
+
seen[tag] = versions[tag] ?? 0;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** The version of each tag noteCacheTags() collected, as it was first read. */
|
|
651
|
+
export function cacheTagVersionsRead() {
|
|
652
|
+
return { ...(scope()?.getStore()?.tagVersions ?? {}) };
|
|
653
|
+
}
|
|
654
|
+
/** The tags noteCacheTags() collected in the render that is open, in order. */
|
|
655
|
+
export function cacheTagsRead() {
|
|
656
|
+
return scope()?.getStore()?.tags ?? [];
|
|
657
|
+
}
|
|
630
658
|
/** What read the visitor in a render renderAsNobody() marked, in order. */
|
|
631
659
|
export function visitorReads() {
|
|
632
660
|
return scope()?.getStore()?.perVisitor ?? [];
|