@rsc-kit/core 0.30.39 → 0.30.42
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 +79 -2
- package/dist/cache.js +151 -5
- package/dist/cache.js.map +1 -1
- package/dist/changed.js +2 -1
- package/dist/changed.js.map +1 -1
- package/dist/conformance.d.ts +1 -0
- package/dist/conformance.js +27 -0
- package/dist/conformance.js.map +1 -1
- package/dist/host.d.ts +28 -17
- package/dist/host.js +109 -31
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +27 -11
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/createViteRscApp.js +3 -4
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +3 -1
- package/dist/js/errors.js +5 -2
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.d.ts +16 -1
- package/dist/js/navigate.js +42 -11
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/payloadKey.d.ts +32 -0
- package/dist/js/payloadKey.js +61 -0
- package/dist/js/payloadKey.js.map +1 -0
- package/dist/js/updateStore.js +6 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js.map +1 -1
- package/dist/pageCache.d.ts +47 -0
- package/dist/pageCache.js +177 -0
- package/dist/pageCache.js.map +1 -0
- package/dist/prerender.js +4 -0
- package/dist/prerender.js.map +1 -1
- package/dist/request.d.ts +36 -0
- package/dist/request.js +80 -0
- package/dist/request.js.map +1 -1
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +119 -14
- package/dist/vite.js.map +1 -1
- package/package.json +3 -3
package/dist/cache.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memoise a function for the length of one request
|
|
2
|
+
* Memoise a function for the length of one request - or, given a key and a
|
|
3
|
+
* lifetime, across requests too.
|
|
3
4
|
*
|
|
4
5
|
* The *promise* is cached, not the value, so callers that arrive while the
|
|
5
6
|
* first is still in flight wait on it rather than starting a second. A
|
|
@@ -10,8 +11,84 @@
|
|
|
10
11
|
* leaks between requests, and shared code does not have to know which side of
|
|
11
12
|
* the boundary it is on — the same reason revalidate() is a no-op outside an
|
|
12
13
|
* action.
|
|
14
|
+
*
|
|
15
|
+
* With options, the answer is also kept in the cache store (see
|
|
16
|
+
* {@link installCacheStore}) for `revalidate` seconds, shared by every
|
|
17
|
+
* request, and by every instance when the store is shared:
|
|
18
|
+
*
|
|
19
|
+
* export const findPage = cache(
|
|
20
|
+
* async (slug: string) => db.pages.find(slug),
|
|
21
|
+
* { key: 'pages.find', revalidate: 3600, tags: ['pages'] },
|
|
22
|
+
* )
|
|
23
|
+
*
|
|
24
|
+
* // wherever pages are saved
|
|
25
|
+
* await changed('pages') // the next findPage() asks the database again
|
|
26
|
+
*
|
|
27
|
+
* Arguments and answer must be JSON. The function runs as nobody - no
|
|
28
|
+
* cookies(), no headers(), no session on a backend call - because its
|
|
29
|
+
* answer is handed to every visitor; a read throws instead of leaking.
|
|
30
|
+
*/
|
|
31
|
+
export declare function cache<A extends unknown[], R>(fn: (...args: A) => R, options?: AcrossRequests<A>): (...args: A) => R;
|
|
32
|
+
/** How a function's answers are kept between requests. */
|
|
33
|
+
export interface AcrossRequests<A extends unknown[] = unknown[]> {
|
|
34
|
+
/**
|
|
35
|
+
* The function's name in the store, the same on every instance: a function
|
|
36
|
+
* has no identity outside this process. Two functions must not share one.
|
|
37
|
+
*/
|
|
38
|
+
key: string;
|
|
39
|
+
/** Seconds an answer is kept before it is asked again. */
|
|
40
|
+
revalidate: number;
|
|
41
|
+
/**
|
|
42
|
+
* Names whose change makes every answer stale at once: `changed('pages')`.
|
|
43
|
+
* The same names `refreshOn` and `changed()` use, so a change that refreshes
|
|
44
|
+
* open tabs also misses here. Given the arguments, for a tag per record.
|
|
45
|
+
*/
|
|
46
|
+
tags?: string[] | ((...args: A) => string[]);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Where answers kept across requests live. Strings in, strings out.
|
|
50
|
+
*
|
|
51
|
+
* // Redis
|
|
52
|
+
* installCacheStore({
|
|
53
|
+
* get: (key) => redis.get(key),
|
|
54
|
+
* set: (key, value, ttl) => redis.set(key, value, 'EX', ttl),
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* // Workers KV (60 seconds is KV's shortest expiry)
|
|
58
|
+
* installCacheStore({
|
|
59
|
+
* get: (key) => env.CACHE.get(key),
|
|
60
|
+
* set: (key, value, ttl) => env.CACHE.put(key, value, { expirationTtl: Math.max(60, ttl) }),
|
|
61
|
+
* })
|
|
62
|
+
*
|
|
63
|
+
* A store may forget anything at any time, and may keep it past `ttl`: the
|
|
64
|
+
* engine checks an answer's age itself.
|
|
65
|
+
*/
|
|
66
|
+
export interface CacheStore {
|
|
67
|
+
get(key: string): Promise<string | null | undefined> | string | null | undefined;
|
|
68
|
+
set(key: string, value: string, ttl: number): Promise<unknown> | unknown;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Answers kept in this process, the most recently used `max` of them (1,000
|
|
72
|
+
* by default). The store without one installed: fast, and one copy per
|
|
73
|
+
* instance, so a change on another instance is seen when the answer expires.
|
|
74
|
+
*/
|
|
75
|
+
export declare function memoryCacheStore(options?: {
|
|
76
|
+
max?: number;
|
|
77
|
+
}): CacheStore;
|
|
78
|
+
/**
|
|
79
|
+
* Choose where answers are kept across requests: a store, or a function that
|
|
80
|
+
* makes one on first use (so the build, which loads the app without its
|
|
81
|
+
* settings, never connects). `null` goes back to memory.
|
|
82
|
+
*/
|
|
83
|
+
export declare function installCacheStore(store: CacheStore | (() => CacheStore) | null): void;
|
|
84
|
+
/** The store in use: the app's, else this process's memory. */
|
|
85
|
+
export declare function cacheStore(): CacheStore;
|
|
86
|
+
/**
|
|
87
|
+
* The build answers are kept for. Set by the host once it knows its own, so
|
|
88
|
+
* a deploy never reads answers a previous build kept - their shape may have
|
|
89
|
+
* changed with the code. Not for apps.
|
|
13
90
|
*/
|
|
14
|
-
export declare function
|
|
91
|
+
export declare function setCacheBuild(build: string | null | undefined): void;
|
|
15
92
|
/** Whether anything is listening, for code that wants to know. */
|
|
16
93
|
export declare function isCaching(): boolean;
|
|
17
94
|
/**
|
package/dist/cache.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// Scoped to the request, so two in flight cannot see each other's answers. The
|
|
17
17
|
// scope is opened by whoever owns the request — the host, the worker, or the
|
|
18
18
|
// prerenderer — and everything below it shares one store.
|
|
19
|
-
import {
|
|
19
|
+
import { versionSource } from "./changed.js";
|
|
20
|
+
import { noteAwaiter, withHelper, withoutRequest } from "./request.js";
|
|
20
21
|
import { resolveScope } from "./revalidate.js";
|
|
21
22
|
const SCOPE = Symbol.for("@rsc-kit/core.cache-scope");
|
|
22
23
|
const globals = globalThis;
|
|
@@ -53,7 +54,8 @@ function nodeFor(root, args) {
|
|
|
53
54
|
return node;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
* Memoise a function for the length of one request
|
|
57
|
+
* Memoise a function for the length of one request - or, given a key and a
|
|
58
|
+
* lifetime, across requests too.
|
|
57
59
|
*
|
|
58
60
|
* The *promise* is cached, not the value, so callers that arrive while the
|
|
59
61
|
* first is still in flight wait on it rather than starting a second. A
|
|
@@ -64,12 +66,29 @@ function nodeFor(root, args) {
|
|
|
64
66
|
* leaks between requests, and shared code does not have to know which side of
|
|
65
67
|
* the boundary it is on — the same reason revalidate() is a no-op outside an
|
|
66
68
|
* action.
|
|
69
|
+
*
|
|
70
|
+
* With options, the answer is also kept in the cache store (see
|
|
71
|
+
* {@link installCacheStore}) for `revalidate` seconds, shared by every
|
|
72
|
+
* request, and by every instance when the store is shared:
|
|
73
|
+
*
|
|
74
|
+
* export const findPage = cache(
|
|
75
|
+
* async (slug: string) => db.pages.find(slug),
|
|
76
|
+
* { key: 'pages.find', revalidate: 3600, tags: ['pages'] },
|
|
77
|
+
* )
|
|
78
|
+
*
|
|
79
|
+
* // wherever pages are saved
|
|
80
|
+
* await changed('pages') // the next findPage() asks the database again
|
|
81
|
+
*
|
|
82
|
+
* Arguments and answer must be JSON. The function runs as nobody - no
|
|
83
|
+
* cookies(), no headers(), no session on a backend call - because its
|
|
84
|
+
* answer is handed to every visitor; a read throws instead of leaking.
|
|
67
85
|
*/
|
|
68
|
-
export function cache(fn) {
|
|
86
|
+
export function cache(fn, options) {
|
|
87
|
+
const run = options ? across(fn, options) : fn;
|
|
69
88
|
return (...args) => {
|
|
70
89
|
const store = scope()?.getStore();
|
|
71
90
|
if (!store)
|
|
72
|
-
return
|
|
91
|
+
return run(...args);
|
|
73
92
|
let root = store.get(fn);
|
|
74
93
|
if (!root) {
|
|
75
94
|
root = {};
|
|
@@ -81,11 +100,138 @@ export function cache(fn) {
|
|
|
81
100
|
noteAwaiter(fn);
|
|
82
101
|
if (node.result)
|
|
83
102
|
return node.result.value;
|
|
84
|
-
const value = withHelper(fn, () =>
|
|
103
|
+
const value = withHelper(fn, () => run(...args));
|
|
85
104
|
node.result = { value };
|
|
86
105
|
return value;
|
|
87
106
|
};
|
|
88
107
|
}
|
|
108
|
+
const STORE = Symbol.for("@rsc-kit/core.cache-store");
|
|
109
|
+
const LOCAL_STORE = Symbol.for("@rsc-kit/core.cache-store-local");
|
|
110
|
+
const BUILD = Symbol.for("@rsc-kit/core.cache-build");
|
|
111
|
+
const IN_FLIGHT = Symbol.for("@rsc-kit/core.cache-in-flight");
|
|
112
|
+
/**
|
|
113
|
+
* Answers kept in this process, the most recently used `max` of them (1,000
|
|
114
|
+
* by default). The store without one installed: fast, and one copy per
|
|
115
|
+
* instance, so a change on another instance is seen when the answer expires.
|
|
116
|
+
*/
|
|
117
|
+
export function memoryCacheStore(options = {}) {
|
|
118
|
+
const max = options.max ?? 1000;
|
|
119
|
+
const entries = new Map();
|
|
120
|
+
return {
|
|
121
|
+
get(key) {
|
|
122
|
+
const entry = entries.get(key);
|
|
123
|
+
if (!entry)
|
|
124
|
+
return undefined;
|
|
125
|
+
entries.delete(key);
|
|
126
|
+
if (entry.until <= Date.now())
|
|
127
|
+
return undefined;
|
|
128
|
+
// Re-inserted, so the Map's order is least recently used first.
|
|
129
|
+
entries.set(key, entry);
|
|
130
|
+
return entry.value;
|
|
131
|
+
},
|
|
132
|
+
set(key, value, ttl) {
|
|
133
|
+
entries.delete(key);
|
|
134
|
+
entries.set(key, { value, until: Date.now() + ttl * 1000 });
|
|
135
|
+
while (entries.size > max)
|
|
136
|
+
entries.delete(entries.keys().next().value);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Choose where answers are kept across requests: a store, or a function that
|
|
142
|
+
* makes one on first use (so the build, which loads the app without its
|
|
143
|
+
* settings, never connects). `null` goes back to memory.
|
|
144
|
+
*/
|
|
145
|
+
export function installCacheStore(store) {
|
|
146
|
+
globals[STORE] = store;
|
|
147
|
+
}
|
|
148
|
+
/** The store in use: the app's, else this process's memory. */
|
|
149
|
+
export function cacheStore() {
|
|
150
|
+
if (typeof globals[STORE] === "function")
|
|
151
|
+
globals[STORE] = globals[STORE]();
|
|
152
|
+
return (globals[STORE] ??
|
|
153
|
+
(globals[LOCAL_STORE] ??= memoryCacheStore()));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The build answers are kept for. Set by the host once it knows its own, so
|
|
157
|
+
* a deploy never reads answers a previous build kept - their shape may have
|
|
158
|
+
* changed with the code. Not for apps.
|
|
159
|
+
*/
|
|
160
|
+
export function setCacheBuild(build) {
|
|
161
|
+
globals[BUILD] = build ?? "";
|
|
162
|
+
}
|
|
163
|
+
let warnedStore = false;
|
|
164
|
+
/** A store that fails is a slower page, not a broken one: said once, then asked around. */
|
|
165
|
+
function storeFailed(error) {
|
|
166
|
+
if (warnedStore)
|
|
167
|
+
return;
|
|
168
|
+
warnedStore = true;
|
|
169
|
+
console.warn("[rsc-kit] the cache store failed, so answers are being worked out each time instead:", error);
|
|
170
|
+
}
|
|
171
|
+
function across(fn, options) {
|
|
172
|
+
if (!options.key)
|
|
173
|
+
throw new Error("[rsc-kit] cache() across requests needs a key");
|
|
174
|
+
if (!(options.revalidate > 0))
|
|
175
|
+
throw new Error(`[rsc-kit] cache() ${options.key}: revalidate is seconds, above 0`);
|
|
176
|
+
// Whole seconds for the store, which may round anyway; the age is checked exactly.
|
|
177
|
+
const ttl = Math.ceil(options.revalidate);
|
|
178
|
+
return async (...args) => {
|
|
179
|
+
const tags = typeof options.tags === "function" ? options.tags(...args) : options.tags ?? [];
|
|
180
|
+
const versions = tags.length
|
|
181
|
+
? await versionSource().changed(Object.fromEntries(tags.map((tag) => [tag, 0])), 0)
|
|
182
|
+
: {};
|
|
183
|
+
const key = [
|
|
184
|
+
"rsc-cache",
|
|
185
|
+
globals[BUILD] ?? "",
|
|
186
|
+
options.key,
|
|
187
|
+
JSON.stringify(args),
|
|
188
|
+
tags.map((tag) => `${tag}@${versions[tag] ?? 0}`).join(","),
|
|
189
|
+
].join("|");
|
|
190
|
+
// One ask per key at a time in this process: a burst of visitors to a
|
|
191
|
+
// page nobody has asked for yet starts one lookup, not one each.
|
|
192
|
+
const inFlight = (globals[IN_FLIGHT] ??=
|
|
193
|
+
new Map());
|
|
194
|
+
const pending = inFlight.get(key);
|
|
195
|
+
if (pending)
|
|
196
|
+
return pending;
|
|
197
|
+
const work = (async () => {
|
|
198
|
+
const store = cacheStore();
|
|
199
|
+
let raw;
|
|
200
|
+
try {
|
|
201
|
+
raw = await store.get(key);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
storeFailed(error);
|
|
205
|
+
}
|
|
206
|
+
if (raw) {
|
|
207
|
+
const kept = JSON.parse(raw);
|
|
208
|
+
if (Date.now() - kept.at < options.revalidate * 1000)
|
|
209
|
+
return kept.v;
|
|
210
|
+
}
|
|
211
|
+
const value = await withoutRequest(() => fn(...args));
|
|
212
|
+
const kept = { at: Date.now() };
|
|
213
|
+
if (value !== undefined)
|
|
214
|
+
kept.v = value;
|
|
215
|
+
// Every caller gets what the store would give back - a miss as well as
|
|
216
|
+
// a hit - so a Date is a string the first time too, not only after.
|
|
217
|
+
const text = JSON.stringify(kept);
|
|
218
|
+
try {
|
|
219
|
+
await store.set(key, text, ttl);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
storeFailed(error);
|
|
223
|
+
}
|
|
224
|
+
return JSON.parse(text).v;
|
|
225
|
+
})();
|
|
226
|
+
inFlight.set(key, work);
|
|
227
|
+
try {
|
|
228
|
+
return await work;
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
inFlight.delete(key);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
89
235
|
/** Whether anything is listening, for code that wants to know. */
|
|
90
236
|
export function isCaching() {
|
|
91
237
|
return scope()?.getStore() !== undefined;
|
package/dist/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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","sourcesContent":["// Asking the same question twice in one request.\n//\n// import { cache } from '@rsc-kit/core/cache'\n//\n// export const currentUser = cache(async () => db.user(await sessionId()))\n//\n// A guard checks who you are; the layout wants their name; the page wants their\n// permissions. Three calls, one request, one answer — without threading the\n// result through props that only exist to carry it.\n//\n// This is the same shape as React's cache(), and deliberately not React's: the\n// engine's own boundaries are not React's. A guard runs before any component\n// does, which is exactly where the duplication starts, and React has no scope\n// open there.\n//\n// Scoped to the request, so two in flight cannot see each other's answers. The\n// scope is opened by whoever owns the request — the host, the worker, or the\n// prerenderer — and everything below it shares one store.\n\nimport { noteAwaiter, withHelper } from \"./request.js\";\nimport { resolveScope } from \"./revalidate.js\";\n\n/** One request's memo table. */\ntype Store = Map<unknown, ArgNode>;\n\n/**\n * Arguments keyed the way React keys them: a tree, one level per argument.\n *\n * Primitives compare by value and objects by identity, which falls out of\n * using a Map for the first and a WeakMap for the second. Serialising the\n * arguments instead would make `f({ a: 1 })` and `f({ a: 1 })` the same call —\n * which they are not, if the object is mutable.\n */\ninterface ArgNode {\n /** The answer for the argument list that ends here. */\n result?: { value: unknown };\n byValue?: Map<unknown, ArgNode>;\n byIdentity?: WeakMap<object, ArgNode>;\n}\n\nconst SCOPE = Symbol.for(\"@rsc-kit/core.cache-scope\");\n\nconst globals = globalThis as Record<symbol | string, unknown>;\n\nlet ready: Promise<void> | null = null;\n\ninterface Scope {\n getStore(): Store | undefined;\n run<T>(store: Store, fn: () => T): T;\n}\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null;\n}\n\nfunction isObject(value: unknown): value is object {\n return (\n (typeof value === \"object\" && value !== null) || typeof value === \"function\"\n );\n}\n\n/** Walk to the node for this argument list, creating it as we go. */\nfunction nodeFor(root: ArgNode, args: unknown[]): ArgNode {\n let node = root;\n\n for (const arg of args) {\n if (isObject(arg)) {\n node.byIdentity ??= new WeakMap();\n\n let next = node.byIdentity.get(arg);\n\n if (!next) {\n next = {};\n node.byIdentity.set(arg, next);\n }\n\n node = next;\n } else {\n node.byValue ??= new Map();\n\n let next = node.byValue.get(arg);\n\n if (!next) {\n next = {};\n node.byValue.set(arg, next);\n }\n\n node = next;\n }\n }\n\n return node;\n}\n\n/**\n * Memoise a function for the length of one request.\n *\n * The *promise* is cached, not the value, so callers that arrive while the\n * first is still in flight wait on it rather than starting a second. A\n * rejection is cached too: within one request the answer to a question does\n * not change because you asked again.\n *\n * Outside a request this calls straight through. Nothing is stored, nothing\n * leaks between requests, and shared code does not have to know which side of\n * the boundary it is on — the same reason revalidate() is a no-op outside an\n * action.\n */\nexport function cache<A extends unknown[], R>(\n fn: (...args: A) => R,\n): (...args: A) => R {\n return (...args: A): R => {\n const store = scope()?.getStore();\n\n if (!store) return fn(...args);\n\n let root = store.get(fn);\n\n if (!root) {\n root = {};\n store.set(fn, root);\n }\n\n const node = nodeFor(root, args);\n\n // Who asked, every time: the body runs once, and a read it makes can\n // then name every component that waited on it, not only the first.\n noteAwaiter(fn);\n\n if (node.result) return node.result.value as R;\n\n const value = withHelper(fn, () => fn(...args));\n\n node.result = { value };\n\n return value;\n };\n}\n\n/** Whether anything is listening, for code that wants to know. */\nexport function isCaching(): boolean {\n return scope()?.getStore() !== undefined;\n}\n\n/**\n * Run one request with a memo table of its own.\n *\n * Opened by whoever owns the request. Nesting is harmless and does not create a\n * second table — an inner call reuses the one already open, so a host that\n * wraps and an engine that also wraps do not end up with two.\n */\nexport async function withCache<T>(run: () => Promise<T>): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope;\n });\n\n await ready;\n }\n\n const existing = scope()!.getStore();\n\n if (existing) return await run();\n\n return await scope()!.run(new Map(), run);\n}\n\n/**\n * Forget every answer this request has cached so far.\n *\n * For the render that follows a mutation. An action's request memoises what\n * the middleware and the action read before the row was changed, and a\n * revalidation rendered in the same request would read those answers back:\n * a port renamed its agent and the sidebar the action re-rendered still\n * said the old name, while the row was updated. What was true before the\n * write is not a memo of what is true after it.\n */\nexport function forgetCached(): void {\n scope()?.getStore()?.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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","sourcesContent":["// Asking the same question twice in one request.\n//\n// import { cache } from '@rsc-kit/core/cache'\n//\n// export const currentUser = cache(async () => db.user(await sessionId()))\n//\n// A guard checks who you are; the layout wants their name; the page wants their\n// permissions. Three calls, one request, one answer — without threading the\n// result through props that only exist to carry it.\n//\n// This is the same shape as React's cache(), and deliberately not React's: the\n// engine's own boundaries are not React's. A guard runs before any component\n// does, which is exactly where the duplication starts, and React has no scope\n// open there.\n//\n// Scoped to the request, so two in flight cannot see each other's answers. The\n// scope is opened by whoever owns the request — the host, the worker, or the\n// prerenderer — and everything below it shares one store.\n\nimport { versionSource } from \"./changed.js\";\nimport { noteAwaiter, withHelper, withoutRequest } from \"./request.js\";\nimport { resolveScope } from \"./revalidate.js\";\n\n/** One request's memo table. */\ntype Store = Map<unknown, ArgNode>;\n\n/**\n * Arguments keyed the way React keys them: a tree, one level per argument.\n *\n * Primitives compare by value and objects by identity, which falls out of\n * using a Map for the first and a WeakMap for the second. Serialising the\n * arguments instead would make `f({ a: 1 })` and `f({ a: 1 })` the same call —\n * which they are not, if the object is mutable.\n */\ninterface ArgNode {\n /** The answer for the argument list that ends here. */\n result?: { value: unknown };\n byValue?: Map<unknown, ArgNode>;\n byIdentity?: WeakMap<object, ArgNode>;\n}\n\nconst SCOPE = Symbol.for(\"@rsc-kit/core.cache-scope\");\n\nconst globals = globalThis as Record<symbol | string, unknown>;\n\nlet ready: Promise<void> | null = null;\n\ninterface Scope {\n getStore(): Store | undefined;\n run<T>(store: Store, fn: () => T): T;\n}\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null;\n}\n\nfunction isObject(value: unknown): value is object {\n return (\n (typeof value === \"object\" && value !== null) || typeof value === \"function\"\n );\n}\n\n/** Walk to the node for this argument list, creating it as we go. */\nfunction nodeFor(root: ArgNode, args: unknown[]): ArgNode {\n let node = root;\n\n for (const arg of args) {\n if (isObject(arg)) {\n node.byIdentity ??= new WeakMap();\n\n let next = node.byIdentity.get(arg);\n\n if (!next) {\n next = {};\n node.byIdentity.set(arg, next);\n }\n\n node = next;\n } else {\n node.byValue ??= new Map();\n\n let next = node.byValue.get(arg);\n\n if (!next) {\n next = {};\n node.byValue.set(arg, next);\n }\n\n node = next;\n }\n }\n\n return node;\n}\n\n/**\n * Memoise a function for the length of one request - or, given a key and a\n * lifetime, across requests too.\n *\n * The *promise* is cached, not the value, so callers that arrive while the\n * first is still in flight wait on it rather than starting a second. A\n * rejection is cached too: within one request the answer to a question does\n * not change because you asked again.\n *\n * Outside a request this calls straight through. Nothing is stored, nothing\n * leaks between requests, and shared code does not have to know which side of\n * the boundary it is on — the same reason revalidate() is a no-op outside an\n * action.\n *\n * With options, the answer is also kept in the cache store (see\n * {@link installCacheStore}) for `revalidate` seconds, shared by every\n * request, and by every instance when the store is shared:\n *\n * export const findPage = cache(\n * async (slug: string) => db.pages.find(slug),\n * { key: 'pages.find', revalidate: 3600, tags: ['pages'] },\n * )\n *\n * // wherever pages are saved\n * await changed('pages') // the next findPage() asks the database again\n *\n * Arguments and answer must be JSON. The function runs as nobody - no\n * cookies(), no headers(), no session on a backend call - because its\n * answer is handed to every visitor; a read throws instead of leaking.\n */\nexport function cache<A extends unknown[], R>(\n fn: (...args: A) => R,\n options?: AcrossRequests<A>,\n): (...args: A) => R {\n const run = options ? (across(fn as never, options) as unknown as (...args: A) => R) : fn;\n\n return (...args: A): R => {\n const store = scope()?.getStore();\n\n if (!store) return run(...args);\n\n let root = store.get(fn);\n\n if (!root) {\n root = {};\n store.set(fn, root);\n }\n\n const node = nodeFor(root, args);\n\n // Who asked, every time: the body runs once, and a read it makes can\n // then name every component that waited on it, not only the first.\n noteAwaiter(fn);\n\n if (node.result) return node.result.value as R;\n\n const value = withHelper(fn, () => run(...args));\n\n node.result = { value };\n\n return value;\n };\n}\n\n// ── Across requests ──────────────────────────────────────────────────────────\n//\n// Where answers are kept is the app's choice, as with the versions behind\n// changed(): memory by default, or anything with a get and a set - Redis,\n// Workers KV, a table. The store keeps strings and nothing else; the engine\n// owns the key, the lifetime and the invalidation.\n//\n// Invalidation is a version, not a delete. A key carries the current version\n// of each of its tags, read from the same source changed() bumps, so after\n// changed('pages') the next ask builds a different key and misses. The old\n// entry is never read again and expires on its own. That is why a store needs\n// no delete, no scan and no purge - and why a change made on one instance is\n// seen by the next ask on every other, when the versions are shared.\n\n/** How a function's answers are kept between requests. */\nexport interface AcrossRequests<A extends unknown[] = unknown[]> {\n /**\n * The function's name in the store, the same on every instance: a function\n * has no identity outside this process. Two functions must not share one.\n */\n key: string;\n /** Seconds an answer is kept before it is asked again. */\n revalidate: number;\n /**\n * Names whose change makes every answer stale at once: `changed('pages')`.\n * The same names `refreshOn` and `changed()` use, so a change that refreshes\n * open tabs also misses here. Given the arguments, for a tag per record.\n */\n tags?: string[] | ((...args: A) => string[]);\n}\n\n/**\n * Where answers kept across requests live. Strings in, strings out.\n *\n * // Redis\n * installCacheStore({\n * get: (key) => redis.get(key),\n * set: (key, value, ttl) => redis.set(key, value, 'EX', ttl),\n * })\n *\n * // Workers KV (60 seconds is KV's shortest expiry)\n * installCacheStore({\n * get: (key) => env.CACHE.get(key),\n * set: (key, value, ttl) => env.CACHE.put(key, value, { expirationTtl: Math.max(60, ttl) }),\n * })\n *\n * A store may forget anything at any time, and may keep it past `ttl`: the\n * engine checks an answer's age itself.\n */\nexport interface CacheStore {\n get(key: string): Promise<string | null | undefined> | string | null | undefined;\n set(key: string, value: string, ttl: number): Promise<unknown> | unknown;\n}\n\nconst STORE = Symbol.for(\"@rsc-kit/core.cache-store\");\nconst LOCAL_STORE = Symbol.for(\"@rsc-kit/core.cache-store-local\");\nconst BUILD = Symbol.for(\"@rsc-kit/core.cache-build\");\nconst IN_FLIGHT = Symbol.for(\"@rsc-kit/core.cache-in-flight\");\n\n/**\n * Answers kept in this process, the most recently used `max` of them (1,000\n * by default). The store without one installed: fast, and one copy per\n * instance, so a change on another instance is seen when the answer expires.\n */\nexport function memoryCacheStore(options: { max?: number } = {}): CacheStore {\n const max = options.max ?? 1000;\n const entries = new Map<string, { value: string; until: number }>();\n\n return {\n get(key) {\n const entry = entries.get(key);\n\n if (!entry) return undefined;\n\n entries.delete(key);\n\n if (entry.until <= Date.now()) return undefined;\n\n // Re-inserted, so the Map's order is least recently used first.\n entries.set(key, entry);\n\n return entry.value;\n },\n set(key, value, ttl) {\n entries.delete(key);\n entries.set(key, { value, until: Date.now() + ttl * 1000 });\n\n while (entries.size > max) entries.delete(entries.keys().next().value!);\n },\n };\n}\n\n/**\n * Choose where answers are kept across requests: a store, or a function that\n * makes one on first use (so the build, which loads the app without its\n * settings, never connects). `null` goes back to memory.\n */\nexport function installCacheStore(\n store: CacheStore | (() => CacheStore) | null,\n): void {\n globals[STORE] = store;\n}\n\n/** The store in use: the app's, else this process's memory. */\nexport function cacheStore(): CacheStore {\n if (typeof globals[STORE] === \"function\")\n globals[STORE] = (globals[STORE] as () => CacheStore)();\n\n return (\n (globals[STORE] as CacheStore | null | undefined) ??\n ((globals[LOCAL_STORE] as CacheStore | undefined) ??= memoryCacheStore())\n );\n}\n\n/**\n * The build answers are kept for. Set by the host once it knows its own, so\n * a deploy never reads answers a previous build kept - their shape may have\n * changed with the code. Not for apps.\n */\nexport function setCacheBuild(build: string | null | undefined): void {\n globals[BUILD] = build ?? \"\";\n}\n\nlet warnedStore = false;\n\n/** A store that fails is a slower page, not a broken one: said once, then asked around. */\nfunction storeFailed(error: unknown): void {\n if (warnedStore) return;\n warnedStore = true;\n console.warn(\n \"[rsc-kit] the cache store failed, so answers are being worked out each time instead:\",\n error,\n );\n}\n\ninterface Kept {\n /** The answer. Absent when it was undefined, which JSON cannot spell. */\n v?: unknown;\n /** When it was worked out, in ms. */\n at: number;\n}\n\nfunction across<A extends unknown[], R>(\n fn: (...args: A) => Promise<R> | R,\n options: AcrossRequests<A>,\n): (...args: A) => Promise<R> {\n if (!options.key) throw new Error(\"[rsc-kit] cache() across requests needs a key\");\n if (!(options.revalidate > 0))\n throw new Error(`[rsc-kit] cache() ${options.key}: revalidate is seconds, above 0`);\n\n // Whole seconds for the store, which may round anyway; the age is checked exactly.\n const ttl = Math.ceil(options.revalidate);\n\n return async (...args: A): Promise<R> => {\n const tags =\n typeof options.tags === \"function\" ? options.tags(...args) : options.tags ?? [];\n const versions = tags.length\n ? await versionSource().changed(Object.fromEntries(tags.map((tag) => [tag, 0])), 0)\n : {};\n const key = [\n \"rsc-cache\",\n (globals[BUILD] as string | undefined) ?? \"\",\n options.key,\n JSON.stringify(args),\n tags.map((tag) => `${tag}@${versions[tag] ?? 0}`).join(\",\"),\n ].join(\"|\");\n\n // One ask per key at a time in this process: a burst of visitors to a\n // page nobody has asked for yet starts one lookup, not one each.\n const inFlight = ((globals[IN_FLIGHT] as Map<string, Promise<unknown>> | undefined) ??=\n new Map());\n const pending = inFlight.get(key);\n\n if (pending) return pending as Promise<R>;\n\n const work = (async (): Promise<R> => {\n const store = cacheStore();\n let raw: string | null | undefined;\n\n try {\n raw = await store.get(key);\n } catch (error) {\n storeFailed(error);\n }\n\n if (raw) {\n const kept = JSON.parse(raw) as Kept;\n\n if (Date.now() - kept.at < options.revalidate * 1000) return kept.v as R;\n }\n\n const value = await withoutRequest(() => fn(...args));\n const kept: Kept = { at: Date.now() };\n\n if (value !== undefined) kept.v = value;\n\n // Every caller gets what the store would give back - a miss as well as\n // a hit - so a Date is a string the first time too, not only after.\n const text = JSON.stringify(kept);\n\n try {\n await store.set(key, text, ttl);\n } catch (error) {\n storeFailed(error);\n }\n\n return (JSON.parse(text) as Kept).v as R;\n })();\n\n inFlight.set(key, work);\n\n try {\n return await work;\n } finally {\n inFlight.delete(key);\n }\n };\n}\n\n/** Whether anything is listening, for code that wants to know. */\nexport function isCaching(): boolean {\n return scope()?.getStore() !== undefined;\n}\n\n/**\n * Run one request with a memo table of its own.\n *\n * Opened by whoever owns the request. Nesting is harmless and does not create a\n * second table — an inner call reuses the one already open, so a host that\n * wraps and an engine that also wraps do not end up with two.\n */\nexport async function withCache<T>(run: () => Promise<T>): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope;\n });\n\n await ready;\n }\n\n const existing = scope()!.getStore();\n\n if (existing) return await run();\n\n return await scope()!.run(new Map(), run);\n}\n\n/**\n * Forget every answer this request has cached so far.\n *\n * For the render that follows a mutation. An action's request memoises what\n * the middleware and the action read before the row was changed, and a\n * revalidation rendered in the same request would read those answers back:\n * a port renamed its agent and the sidebar the action re-rendered still\n * said the old name, while the row was updated. What was true before the\n * write is not a memo of what is true after it.\n */\nexport function forgetCached(): void {\n scope()?.getStore()?.clear();\n}\n"]}
|
package/dist/changed.js
CHANGED
|
@@ -424,7 +424,8 @@ export function versionSource() {
|
|
|
424
424
|
// a choice rather than a fallback, and is not warned.
|
|
425
425
|
if (production()) {
|
|
426
426
|
console.warn("[rsc-kit] refreshOn versions are kept in this process only: a change made on another " +
|
|
427
|
-
"instance, or in a worker, will not reach the tabs this one serves
|
|
427
|
+
"instance, or in a worker, will not reach the tabs this one serves, nor make the answers it " +
|
|
428
|
+
"keeps with cache() tags miss before they expire. Install a store every " +
|
|
428
429
|
"process shares with installVersionSource() in instrumentation.ts - sqlVersions for MySQL " +
|
|
429
430
|
"or SQLite, postgresVersions for Postgres, createVersions for anything else: " +
|
|
430
431
|
"https://docs.rsc-kit.dev/guides/live-data#where-the-versions-live. For a deploy that is one " +
|