@rangojs/router 0.16.0 → 0.18.0
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/README.md +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
|
@@ -69,11 +69,15 @@ export function stringToStream(str: string): ReadableStream<Uint8Array> {
|
|
|
69
69
|
*/
|
|
70
70
|
export async function rscSerialize(
|
|
71
71
|
value: unknown,
|
|
72
|
+
onError?: (error: unknown) => void,
|
|
72
73
|
): Promise<string | undefined> {
|
|
73
74
|
if (value === undefined || value === null) return undefined;
|
|
74
75
|
|
|
75
76
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
76
|
-
const stream = renderToReadableStream(value, {
|
|
77
|
+
const stream = renderToReadableStream(value, {
|
|
78
|
+
temporaryReferences,
|
|
79
|
+
onError,
|
|
80
|
+
});
|
|
77
81
|
return streamToString(stream);
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -102,10 +106,16 @@ export async function rscDeserialize<T>(
|
|
|
102
106
|
*
|
|
103
107
|
* Returns null only on serialization failure.
|
|
104
108
|
*/
|
|
105
|
-
export async function serializeResult(
|
|
109
|
+
export async function serializeResult(
|
|
110
|
+
value: unknown,
|
|
111
|
+
onError?: (error: unknown) => void,
|
|
112
|
+
): Promise<string | null> {
|
|
106
113
|
try {
|
|
107
114
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
108
|
-
const stream = renderToReadableStream(value, {
|
|
115
|
+
const stream = renderToReadableStream(value, {
|
|
116
|
+
temporaryReferences,
|
|
117
|
+
onError,
|
|
118
|
+
});
|
|
109
119
|
return await streamToString(stream);
|
|
110
120
|
} catch (error) {
|
|
111
121
|
// Returning null silently turns a non-serializable cache value into a
|
|
@@ -145,9 +155,17 @@ export const deserializeComponent: (encoded: string) => Promise<unknown> =
|
|
|
145
155
|
* Serialize segments for storage.
|
|
146
156
|
* Each segment's component, layout, loading, and loaderData are RSC-serialized.
|
|
147
157
|
* Metadata is preserved as-is.
|
|
158
|
+
*
|
|
159
|
+
* `onError` goes to every Flight encode. A component that throws while
|
|
160
|
+
* encoding (e.g. an async server component in the tree) does not reject the
|
|
161
|
+
* encode: Flight reports it through onError and writes an error row
|
|
162
|
+
* (`1:E{...}`) that throws when the decoded tree renders. cacheRoute and the
|
|
163
|
+
* prerender producers (prerender-match.ts) pass it to refuse such an entry;
|
|
164
|
+
* without it the encode is unchanged.
|
|
148
165
|
*/
|
|
149
166
|
export async function serializeSegments(
|
|
150
167
|
segments: ResolvedSegment[],
|
|
168
|
+
onError?: (error: unknown) => void,
|
|
151
169
|
): Promise<SerializedSegmentData[]> {
|
|
152
170
|
return Promise.all(
|
|
153
171
|
segments.map(async (segment): Promise<SerializedSegmentData> => {
|
|
@@ -162,6 +180,7 @@ export async function serializeSegments(
|
|
|
162
180
|
// Serialize the component to RSC stream
|
|
163
181
|
const stream = renderToReadableStream(componentResolved, {
|
|
164
182
|
temporaryReferences,
|
|
183
|
+
onError,
|
|
165
184
|
});
|
|
166
185
|
|
|
167
186
|
// RSC-serialize loading: "null" string distinguishes explicit null from undefined
|
|
@@ -169,7 +188,7 @@ export async function serializeSegments(
|
|
|
169
188
|
segment.loading !== undefined
|
|
170
189
|
? segment.loading === null
|
|
171
190
|
? "null"
|
|
172
|
-
: await rscSerialize(segment.loading)
|
|
191
|
+
: await rscSerialize(segment.loading, onError)
|
|
173
192
|
: undefined;
|
|
174
193
|
|
|
175
194
|
// Await loaderData / loaderDataPromise if they're Promises
|
|
@@ -190,9 +209,9 @@ export async function serializeSegments(
|
|
|
190
209
|
encodedLoaderDataPromise,
|
|
191
210
|
] = await Promise.all([
|
|
192
211
|
streamToString(stream),
|
|
193
|
-
segment.layout ? rscSerialize(segment.layout) : undefined,
|
|
194
|
-
rscSerialize(loaderDataResolved),
|
|
195
|
-
rscSerialize(loaderDataPromiseResolved),
|
|
212
|
+
segment.layout ? rscSerialize(segment.layout, onError) : undefined,
|
|
213
|
+
rscSerialize(loaderDataResolved, onError),
|
|
214
|
+
rscSerialize(loaderDataPromiseResolved, onError),
|
|
196
215
|
]);
|
|
197
216
|
|
|
198
217
|
return {
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-isolate memo of fresh PPR shell reads (issue #941, decision 1;
|
|
3
|
+
* docs/design/shell-entry-layout.md).
|
|
4
|
+
*
|
|
5
|
+
* A warm isolate serving the same shell many times a second re-reads, and
|
|
6
|
+
* re-parses, the same stored entry on every HIT. The store keeps the last
|
|
7
|
+
* fresh read per key for a short window (`memo.shellMs`) and serves the next
|
|
8
|
+
* HIT from memory. What the memo does NOT skip is the part that decides
|
|
9
|
+
* whether the entry may be served: the store still runs its tag-marker check
|
|
10
|
+
* on every read, so with KV bound `updateTag()` / `revalidateTag()` stay
|
|
11
|
+
* immediate across isolates. The isolate that runs the invalidation drops its
|
|
12
|
+
* own copies and keeps reads that were in flight from memoizing them again
|
|
13
|
+
* (RecentTagInvalidations). What another isolate can serve for up to one
|
|
14
|
+
* window: a newer capture of the same key; a KV-less purge-mode purge; on
|
|
15
|
+
* Vercel, an invalidation from another region (tag markers are regional) and
|
|
16
|
+
* a platform `expireTag` issued outside rango (it writes no tag marker).
|
|
17
|
+
*
|
|
18
|
+
* Store instances are usually created per request, so the memo must outlive
|
|
19
|
+
* them: CFCacheStore keeps one module-level memo per isolate, VercelCacheStore
|
|
20
|
+
* one per runtime-cache handle (the handle carries the namespace). One byte
|
|
21
|
+
* budget (`memo.shellMaxBytes`) caps a memo; storing sweeps entries past their
|
|
22
|
+
* window, then evicts the least recently used. Measured on a 3.2 MB shell
|
|
23
|
+
* (Node 22 heap): CFCacheStore holds 1.08x what it counts (prelude plus
|
|
24
|
+
* snapshot JSON); VercelCacheStore holds 0.93x (envelope JSON plus the
|
|
25
|
+
* decoded prelude).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-isolate memos a built-in store keeps (`CFCacheStore`: one per isolate;
|
|
30
|
+
* `VercelCacheStore`: one per `cache` handle). A window of 0 turns that memo
|
|
31
|
+
* off.
|
|
32
|
+
*/
|
|
33
|
+
export interface StoreMemoOptions {
|
|
34
|
+
/**
|
|
35
|
+
* PPR shell memo window (ms): after a shell read, the next HITs of that key
|
|
36
|
+
* within the window are served from memory, with no store read and no
|
|
37
|
+
* parse. Only a fresh shell whose snapshot read completed is kept. The
|
|
38
|
+
* tag-marker check still runs on every HIT, and the isolate that runs
|
|
39
|
+
* `updateTag()` / `revalidateTag()` drops its own copies, so neither that
|
|
40
|
+
* request nor a later one on the same isolate gets the invalidated shell.
|
|
41
|
+
* What ANOTHER isolate can serve for up to one window:
|
|
42
|
+
* - a newer capture of the same key;
|
|
43
|
+
* - `CFCacheStore` without KV, in purge mode: a purged shell. The purge
|
|
44
|
+
* removes the stored entry, not other isolates' memos;
|
|
45
|
+
* - `VercelCacheStore`: an invalidation from another region (the tag
|
|
46
|
+
* markers are a regional `cache.set`; only `expireTag` is global), and a
|
|
47
|
+
* platform `expireTag` issued outside rango.
|
|
48
|
+
*
|
|
49
|
+
* The mutating user is not served these: the response of a request that
|
|
50
|
+
* ran `updateTag()` / `revalidateTag()` sets the fresh-reads cookie, and
|
|
51
|
+
* requests carrying it skip the memos. Set `{ shellMs: 0, markerFreshMs: 0 }`
|
|
52
|
+
* where every user's next request must see an invalidation (with KV,
|
|
53
|
+
* `CFCacheStore`'s marker memo alone can serve an invalidated shell for up
|
|
54
|
+
* to `markerMaxStaleMs`). Default 2000.
|
|
55
|
+
*/
|
|
56
|
+
shellMs?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Total bytes (prelude plus snapshot) the shell memo may hold; the least
|
|
59
|
+
* recently used shells are evicted first, and a shell larger than the whole
|
|
60
|
+
* budget is not kept. Default 16 MiB. 0 turns the memo off.
|
|
61
|
+
*/
|
|
62
|
+
shellMaxBytes?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Tag-marker memo for PPR shell reads: a tag's invalidation marker read
|
|
65
|
+
* within this many ms is used without a store read. Default 1000 on
|
|
66
|
+
* `CFCacheStore` (with KV; without KV there are no markers), 300 on
|
|
67
|
+
* `VercelCacheStore`. 0 turns the marker memo off. Cached data families
|
|
68
|
+
* (`cache()`, `"use cache"`, responses) keep reading their markers.
|
|
69
|
+
*/
|
|
70
|
+
markerFreshMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* The oldest memoized marker a shell read still uses while a background
|
|
73
|
+
* read refreshes it; an older one blocks on the store read. Default 10000
|
|
74
|
+
* on `CFCacheStore`, 2000 on `VercelCacheStore`. The isolate that runs
|
|
75
|
+
* `updateTag()` / `revalidateTag()` writes the new marker into its memo,
|
|
76
|
+
* and the response sets the fresh-reads cookie, so the same user's next
|
|
77
|
+
* requests skip both memos on any isolate.
|
|
78
|
+
*/
|
|
79
|
+
markerMaxStaleMs?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Default memo window (ms). */
|
|
83
|
+
export const DEFAULT_SHELL_MEMO_MS = 2000;
|
|
84
|
+
|
|
85
|
+
/** Default total memo size per isolate (bytes). */
|
|
86
|
+
export const DEFAULT_SHELL_MEMO_MAX_BYTES: number = 16 * 1024 * 1024;
|
|
87
|
+
|
|
88
|
+
/** A store's memo options, validated. */
|
|
89
|
+
export interface ResolvedShellMemoOptions {
|
|
90
|
+
shellMs: number;
|
|
91
|
+
shellMaxBytes: number;
|
|
92
|
+
markerFreshMs: number;
|
|
93
|
+
markerMaxStaleMs: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate a store's `memo` option; the marker memo's defaults are per store
|
|
98
|
+
* (isolate-tag-memo.ts). A non-finite value (`NaN` from `Number(env.UNSET)`,
|
|
99
|
+
* `Infinity`, null) falls back to the default, like the CFCacheStore read
|
|
100
|
+
* budgets; a finite 0 or negative passes through and turns that memo off.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveShellMemoOptions(
|
|
103
|
+
memo: StoreMemoOptions | undefined,
|
|
104
|
+
markerDefaults: { markerFreshMs: number; markerMaxStaleMs: number },
|
|
105
|
+
): ResolvedShellMemoOptions {
|
|
106
|
+
const finite = (value: number | undefined, fallback: number): number =>
|
|
107
|
+
typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
108
|
+
return {
|
|
109
|
+
shellMs: finite(memo?.shellMs, DEFAULT_SHELL_MEMO_MS),
|
|
110
|
+
shellMaxBytes: finite(memo?.shellMaxBytes, DEFAULT_SHELL_MEMO_MAX_BYTES),
|
|
111
|
+
markerFreshMs: finite(memo?.markerFreshMs, markerDefaults.markerFreshMs),
|
|
112
|
+
markerMaxStaleMs: finite(
|
|
113
|
+
memo?.markerMaxStaleMs,
|
|
114
|
+
markerDefaults.markerMaxStaleMs,
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Headroom on the fresh-reads cookie's lifetime: revalidateTag() writes its
|
|
121
|
+
* markers in the background, so they can land after the response that sets
|
|
122
|
+
* the cookie, and the memos' windows count from then.
|
|
123
|
+
*/
|
|
124
|
+
export const FRESH_READS_MARGIN_MS = 1000;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* How long the fresh-reads cookie keeps a mutating user off a store's memos:
|
|
128
|
+
* the longest either memo can still hold a value from before the mutation
|
|
129
|
+
* (the shell window; the marker memo's max-stale cap when the store reads
|
|
130
|
+
* markers through it, counted from the start of the read that filled it),
|
|
131
|
+
* plus FRESH_READS_MARGIN_MS. 0 when both memos are off: no cookie.
|
|
132
|
+
*/
|
|
133
|
+
export function freshReadsWindowMs(
|
|
134
|
+
memo: ResolvedShellMemoOptions,
|
|
135
|
+
markerMemo: boolean,
|
|
136
|
+
): number {
|
|
137
|
+
const window = Math.max(
|
|
138
|
+
memo.shellMs > 0 && memo.shellMaxBytes > 0 ? memo.shellMs : 0,
|
|
139
|
+
markerMemo && memo.markerFreshMs > 0
|
|
140
|
+
? Math.max(memo.markerFreshMs, memo.markerMaxStaleMs)
|
|
141
|
+
: 0,
|
|
142
|
+
);
|
|
143
|
+
return window > 0 ? window + FRESH_READS_MARGIN_MS : 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether a shell is neither stale nor expired: the only kind a memo keeps or
|
|
148
|
+
* serves (a stale read stays with the store, which schedules the recapture).
|
|
149
|
+
*/
|
|
150
|
+
export function isShellFresh(
|
|
151
|
+
staleAt: number,
|
|
152
|
+
expiresAt: number,
|
|
153
|
+
now: number = Date.now(),
|
|
154
|
+
): boolean {
|
|
155
|
+
return !(staleAt > 0 && now > staleAt) && now <= expiresAt;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether a shell's tags include any of `tags`. */
|
|
159
|
+
export function shellHasAnyTag(
|
|
160
|
+
shellTags: readonly string[] | undefined,
|
|
161
|
+
tags: readonly string[],
|
|
162
|
+
): boolean {
|
|
163
|
+
return shellTags?.some((tag) => tags.includes(tag)) === true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface ShellMemoSlot<T> {
|
|
167
|
+
value: T;
|
|
168
|
+
bytes: number;
|
|
169
|
+
storedAt: number;
|
|
170
|
+
/** storedAt + the storing store's window: past it, set() sweeps the slot. */
|
|
171
|
+
expiresAt: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class ShellMemo<T> {
|
|
175
|
+
private readonly slots = new Map<string, ShellMemoSlot<T>>();
|
|
176
|
+
private totalBytes = 0;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The value stored under `key` less than `windowMs` ago, or undefined.
|
|
180
|
+
* A hit becomes the most recently used entry.
|
|
181
|
+
*/
|
|
182
|
+
get(key: string, windowMs: number, now: number = Date.now()): T | undefined {
|
|
183
|
+
if (windowMs <= 0) return undefined;
|
|
184
|
+
const slot = this.slots.get(key);
|
|
185
|
+
if (!slot) return undefined;
|
|
186
|
+
this.slots.delete(key);
|
|
187
|
+
if (now - slot.storedAt >= windowMs) {
|
|
188
|
+
this.totalBytes -= slot.bytes;
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
this.slots.set(key, slot);
|
|
192
|
+
return slot.value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Store `value` (`bytes` counted against `maxBytes`). Entries past their own
|
|
197
|
+
* window are dropped first, so a key that is never read again does not hold
|
|
198
|
+
* memory until the cap pushes it out; then the least recently used entries
|
|
199
|
+
* are evicted until the value fits. A value larger than the whole budget, or
|
|
200
|
+
* a window of 0, is not stored.
|
|
201
|
+
*/
|
|
202
|
+
set(
|
|
203
|
+
key: string,
|
|
204
|
+
value: T,
|
|
205
|
+
bytes: number,
|
|
206
|
+
windowMs: number,
|
|
207
|
+
maxBytes: number,
|
|
208
|
+
now: number = Date.now(),
|
|
209
|
+
): void {
|
|
210
|
+
this.delete(key);
|
|
211
|
+
if (windowMs <= 0 || bytes > maxBytes) return;
|
|
212
|
+
for (const [other, slot] of this.slots) {
|
|
213
|
+
if (now >= slot.expiresAt) this.delete(other);
|
|
214
|
+
}
|
|
215
|
+
for (const [oldest, slot] of this.slots) {
|
|
216
|
+
if (this.totalBytes + bytes <= maxBytes) break;
|
|
217
|
+
this.slots.delete(oldest);
|
|
218
|
+
this.totalBytes -= slot.bytes;
|
|
219
|
+
}
|
|
220
|
+
this.slots.set(key, {
|
|
221
|
+
value,
|
|
222
|
+
bytes,
|
|
223
|
+
storedAt: now,
|
|
224
|
+
expiresAt: now + windowMs,
|
|
225
|
+
});
|
|
226
|
+
this.totalBytes += bytes;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
delete(key: string): void {
|
|
230
|
+
const slot = this.slots.get(key);
|
|
231
|
+
if (!slot) return;
|
|
232
|
+
this.slots.delete(key);
|
|
233
|
+
this.totalBytes -= slot.bytes;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Drop every entry `predicate` selects. */
|
|
237
|
+
deleteWhere(predicate: (value: T) => boolean): void {
|
|
238
|
+
for (const [key, slot] of this.slots) {
|
|
239
|
+
if (predicate(slot.value)) this.delete(key);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Bytes currently held. */
|
|
244
|
+
get size(): number {
|
|
245
|
+
return this.totalBytes;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
clear(): void {
|
|
249
|
+
this.slots.clear();
|
|
250
|
+
this.totalBytes = 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface TagInvalidationRecord {
|
|
255
|
+
/** The latest invalidation's timestamp. */
|
|
256
|
+
at: number;
|
|
257
|
+
/** Invalidations of the tag still writing their markers or purge. */
|
|
258
|
+
pending: number;
|
|
259
|
+
/** When the record may be dropped once nothing is pending. */
|
|
260
|
+
heldUntil: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Tags this isolate invalidated recently, and when. `invalidateTags()` drops
|
|
265
|
+
* the isolate's memoized shells for its tags at once, but a read that was in
|
|
266
|
+
* flight (its store read done, its snapshot still arriving) can memoize one
|
|
267
|
+
* of them again while the markers or the purge are still being written. With
|
|
268
|
+
* KV that copy is harmless (every memo hit reads the markers); in KV-less
|
|
269
|
+
* purge mode the memo-hit check sees only the request's own marker memo, so
|
|
270
|
+
* nothing would reject it for a whole window, and the mutating user's next
|
|
271
|
+
* request on this isolate got the invalidated shell.
|
|
272
|
+
*
|
|
273
|
+
* A record lives from the invalidation's start until `holdMs` after it
|
|
274
|
+
* settles, which covers a read in flight when it settled (its snapshot read
|
|
275
|
+
* is bounded by the store's read budget, 170 ms by default on CFCacheStore).
|
|
276
|
+
* While it lives, a shell carrying the tag and tagged at or before the
|
|
277
|
+
* invalidation is neither memoized nor served from the memo. It only adds
|
|
278
|
+
* rejections: a shell tagged after the invalidation is not affected, and no
|
|
279
|
+
* marker read is skipped. Records are per tag across namespaces, like the
|
|
280
|
+
* memo drop in `invalidateTags()`: another namespace's shell with the same
|
|
281
|
+
* tag skips the memo for the record's life.
|
|
282
|
+
*/
|
|
283
|
+
export class RecentTagInvalidations {
|
|
284
|
+
private readonly records = new Map<string, TagInvalidationRecord>();
|
|
285
|
+
|
|
286
|
+
/** An invalidation of `tags` at `at` started. */
|
|
287
|
+
begin(tags: readonly string[], at: number, now: number = Date.now()): void {
|
|
288
|
+
for (const [tag, record] of this.records) {
|
|
289
|
+
if (record.pending === 0 && now >= record.heldUntil) {
|
|
290
|
+
this.records.delete(tag);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const tag of tags) {
|
|
294
|
+
const record = this.records.get(tag);
|
|
295
|
+
if (record) {
|
|
296
|
+
record.at = Math.max(record.at, at);
|
|
297
|
+
record.pending++;
|
|
298
|
+
} else {
|
|
299
|
+
this.records.set(tag, { at, pending: 1, heldUntil: 0 });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The invalidation begun for `tags` settled: hold its records `holdMs`. */
|
|
305
|
+
settle(
|
|
306
|
+
tags: readonly string[],
|
|
307
|
+
holdMs: number,
|
|
308
|
+
now: number = Date.now(),
|
|
309
|
+
): void {
|
|
310
|
+
for (const tag of tags) {
|
|
311
|
+
const record = this.records.get(tag);
|
|
312
|
+
if (!record) continue;
|
|
313
|
+
record.pending = Math.max(0, record.pending - 1);
|
|
314
|
+
record.heldUntil = Math.max(record.heldUntil, now + holdMs);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Whether a shell carrying `tags`, tagged at `taggedAt`, predates a live
|
|
320
|
+
* record of one of them (a tagged shell with no tag time is treated as
|
|
321
|
+
* predating it).
|
|
322
|
+
*/
|
|
323
|
+
covers(
|
|
324
|
+
tags: readonly string[] | undefined,
|
|
325
|
+
taggedAt: number | undefined,
|
|
326
|
+
now: number = Date.now(),
|
|
327
|
+
): boolean {
|
|
328
|
+
if (!tags || this.records.size === 0) return false;
|
|
329
|
+
for (const tag of tags) {
|
|
330
|
+
const record = this.records.get(tag);
|
|
331
|
+
if (!record) continue;
|
|
332
|
+
if (record.pending === 0 && now >= record.heldUntil) continue;
|
|
333
|
+
if (taggedAt === undefined || taggedAt <= record.at) return true;
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
clear(): void {
|
|
339
|
+
this.records.clear();
|
|
340
|
+
}
|
|
341
|
+
}
|