@rangojs/router 0.0.0-experimental.49 → 0.0.0-experimental.4fba1c4c
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 +196 -43
- package/dist/bin/rango.js +269 -96
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2659 -883
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +118 -2
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +734 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +101 -13
- package/src/browser/navigation-client.ts +125 -53
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +90 -30
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +156 -18
- package/src/browser/prefetch/queue.ts +92 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +72 -8
- package/src/browser/react/NavigationProvider.tsx +83 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +87 -22
- package/src/browser/scroll-restoration.ts +29 -19
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +48 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +266 -86
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +74 -47
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +411 -261
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +13 -22
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +70 -97
- package/src/router/match-middleware/cache-store.ts +8 -2
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +103 -4
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +101 -89
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +105 -13
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +236 -112
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +86 -22
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +190 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +195 -57
- package/src/ssr/index.tsx +8 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +179 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +103 -67
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +36 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +188 -93
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +46 -4
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +126 -4
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +130 -26
- package/src/vite/router-discovery.ts +920 -129
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
generateRouteTypesSource,
|
|
5
|
-
|
|
5
|
+
genFileTsPath,
|
|
6
|
+
resolveSearchSchemas,
|
|
6
7
|
} from "./generate-route-types.ts";
|
|
7
8
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
8
9
|
|
|
@@ -175,25 +176,13 @@ export async function discoverAndWriteRouteTypes(
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const filtered: Record<string, Record<string, string>> = {};
|
|
184
|
-
for (const name of Object.keys(routeManifest)) {
|
|
185
|
-
const schema = staticParsed.searchSchemas[name];
|
|
186
|
-
if (schema) filtered[name] = schema;
|
|
187
|
-
}
|
|
188
|
-
if (Object.keys(filtered).length > 0) {
|
|
189
|
-
routeSearchSchemas = filtered;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
179
|
+
routeSearchSchemas = resolveSearchSchemas(
|
|
180
|
+
Object.keys(routeManifest),
|
|
181
|
+
routeSearchSchemas,
|
|
182
|
+
sourceFile,
|
|
183
|
+
);
|
|
193
184
|
|
|
194
|
-
const
|
|
195
|
-
const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
|
|
196
|
-
const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
|
|
185
|
+
const outPath = genFileTsPath(sourceFile);
|
|
197
186
|
|
|
198
187
|
const source = generateRouteTypesSource(
|
|
199
188
|
routeManifest,
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -187,6 +187,32 @@ export class CacheScope {
|
|
|
187
187
|
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Evaluate the cache `condition` predicate. Returns false (skip the cache
|
|
192
|
+
* operation) when the predicate returns false or throws; returns true when
|
|
193
|
+
* there is no condition or no request context to evaluate it against.
|
|
194
|
+
*/
|
|
195
|
+
private conditionAllows(op: "read" | "write"): boolean {
|
|
196
|
+
if (this.config === false || !this.config.condition) return true;
|
|
197
|
+
const requestCtx = getRequestContext();
|
|
198
|
+
if (!requestCtx) return true;
|
|
199
|
+
try {
|
|
200
|
+
if (!this.config.condition(requestCtx)) {
|
|
201
|
+
debugCacheLog(
|
|
202
|
+
`[CacheScope] condition returned false, skipping cache ${op}`,
|
|
203
|
+
);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(
|
|
209
|
+
`[CacheScope] condition function threw, skipping cache ${op}:`,
|
|
210
|
+
error,
|
|
211
|
+
);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
/**
|
|
191
217
|
* Lookup cached segments for a route (single cache entry per request).
|
|
192
218
|
* Returns { segments, shouldRevalidate } or null if cache miss.
|
|
@@ -204,27 +230,7 @@ export class CacheScope {
|
|
|
204
230
|
shouldRevalidate: boolean;
|
|
205
231
|
} | null> {
|
|
206
232
|
if (!this.enabled) return null;
|
|
207
|
-
|
|
208
|
-
// Evaluate condition — skip cache read when condition returns false
|
|
209
|
-
if (this.config !== false && this.config.condition) {
|
|
210
|
-
const requestCtx = getRequestContext();
|
|
211
|
-
if (requestCtx) {
|
|
212
|
-
try {
|
|
213
|
-
if (!this.config.condition(requestCtx)) {
|
|
214
|
-
debugCacheLog(
|
|
215
|
-
`[CacheScope] condition returned false, skipping cache read`,
|
|
216
|
-
);
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
console.error(
|
|
221
|
-
`[CacheScope] condition function threw, skipping cache read:`,
|
|
222
|
-
error,
|
|
223
|
-
);
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
233
|
+
if (!this.conditionAllows("read")) return null;
|
|
228
234
|
|
|
229
235
|
const store = this.getStore();
|
|
230
236
|
if (!store) return null;
|
|
@@ -284,27 +290,7 @@ export class CacheScope {
|
|
|
284
290
|
isIntercept?: boolean,
|
|
285
291
|
): Promise<void> {
|
|
286
292
|
if (!this.enabled || segments.length === 0) return;
|
|
287
|
-
|
|
288
|
-
// Evaluate condition — skip cache write when condition returns false
|
|
289
|
-
if (this.config !== false && this.config.condition) {
|
|
290
|
-
const conditionCtx = getRequestContext();
|
|
291
|
-
if (conditionCtx) {
|
|
292
|
-
try {
|
|
293
|
-
if (!this.config.condition(conditionCtx)) {
|
|
294
|
-
debugCacheLog(
|
|
295
|
-
`[CacheScope] condition returned false, skipping cache write`,
|
|
296
|
-
);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
} catch (error) {
|
|
300
|
-
console.error(
|
|
301
|
-
`[CacheScope] condition function threw, skipping cache write:`,
|
|
302
|
-
error,
|
|
303
|
-
);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
293
|
+
if (!this.conditionAllows("write")) return;
|
|
308
294
|
|
|
309
295
|
const store = this.getStore();
|
|
310
296
|
if (!store) return;
|
|
@@ -328,22 +314,59 @@ export class CacheScope {
|
|
|
328
314
|
// Check if this is a partial request (navigation) vs document request
|
|
329
315
|
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
330
316
|
|
|
317
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
318
|
+
debugCacheLog(
|
|
319
|
+
`[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
331
323
|
requestCtx.waitUntil(async () => {
|
|
324
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
325
|
+
debugCacheLog(
|
|
326
|
+
`[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
332
330
|
await handleStore.settled;
|
|
333
331
|
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
333
|
+
debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// For document requests: only cache if layout segments have components
|
|
337
|
+
// (complete render). Parallel and route segments may legitimately have
|
|
338
|
+
// null components — UI-less @meta parallels return null, and void route
|
|
339
|
+
// handlers produce null when the UI lives in parallel slots/layouts.
|
|
340
|
+
// Partial requests always allow null components (client already has them).
|
|
336
341
|
if (!isPartial) {
|
|
337
|
-
const
|
|
338
|
-
(s) => s.component
|
|
342
|
+
const hasIncompleteLayouts = nonLoaderSegments.some(
|
|
343
|
+
(s) => s.component === null && s.type === "layout",
|
|
339
344
|
);
|
|
340
|
-
if (
|
|
345
|
+
if (hasIncompleteLayouts) {
|
|
346
|
+
const nullSegments = nonLoaderSegments
|
|
347
|
+
.filter((s) => s.component === null && s.type === "layout")
|
|
348
|
+
.map((s) => s.id);
|
|
349
|
+
const error = new Error(
|
|
350
|
+
`[CacheScope] Cache write skipped: layout segments have null components ` +
|
|
351
|
+
`(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
|
|
352
|
+
`layout handlers must return JSX for document requests to be cacheable.`,
|
|
353
|
+
);
|
|
354
|
+
error.name = "CacheScopeInvariantError";
|
|
355
|
+
console.error(error.message);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
341
358
|
}
|
|
342
359
|
|
|
343
360
|
// Collect handle data for non-loader segments only
|
|
344
361
|
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
345
362
|
|
|
346
363
|
try {
|
|
364
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
365
|
+
debugCacheLog(
|
|
366
|
+
`[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
347
370
|
// Serialize non-loader segments only
|
|
348
371
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
349
372
|
|
|
@@ -353,6 +376,10 @@ export class CacheScope {
|
|
|
353
376
|
expiresAt: Date.now() + ttl * 1000,
|
|
354
377
|
};
|
|
355
378
|
|
|
379
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
380
|
+
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
356
383
|
await store.set(key, data, ttl, swr);
|
|
357
384
|
|
|
358
385
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -56,6 +56,15 @@ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
|
|
|
56
56
|
/** Header storing cache status: HIT | REVALIDATING */
|
|
57
57
|
export const CACHE_STATUS_HEADER = "x-edge-cache-status";
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Header stashing the route author's original Cache-Control on L1 document
|
|
61
|
+
* entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
|
|
62
|
+
* `max-age` so the CF Cache API retains the entry across the whole SWR window;
|
|
63
|
+
* getResponse restores this original value before serving so the client and any
|
|
64
|
+
* upstream CDN see the author's intended directive, not the internal edge TTL.
|
|
65
|
+
*/
|
|
66
|
+
const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
|
|
67
|
+
|
|
59
68
|
/**
|
|
60
69
|
* Maximum age in seconds for REVALIDATING status before allowing new revalidation.
|
|
61
70
|
* After this period, a stale entry in REVALIDATING status will trigger revalidation again.
|
|
@@ -67,13 +76,11 @@ export const MAX_REVALIDATION_INTERVAL = 30;
|
|
|
67
76
|
// Types
|
|
68
77
|
// ============================================================================
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
passThroughOnException(): void;
|
|
76
|
-
}
|
|
79
|
+
// Re-exported from the canonical home so cf-cache-store consumers keep
|
|
80
|
+
// importing `ExecutionContext` from this module without a second interface
|
|
81
|
+
// drifting over time.
|
|
82
|
+
export type { ExecutionContext } from "../../types/request-scope.js";
|
|
83
|
+
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
77
84
|
|
|
78
85
|
/**
|
|
79
86
|
* Minimal Cloudflare KV Namespace interface.
|
|
@@ -184,7 +191,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
184
191
|
* Cache version string override. When this changes, all cached entries are
|
|
185
192
|
* effectively invalidated (new keys won't match old entries).
|
|
186
193
|
*
|
|
187
|
-
* Defaults to the auto-generated VERSION from
|
|
194
|
+
* Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
|
|
188
195
|
* Only set this if you need a custom versioning strategy.
|
|
189
196
|
*/
|
|
190
197
|
version?: string;
|
|
@@ -421,7 +428,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
421
428
|
}
|
|
422
429
|
|
|
423
430
|
// L2: persist to KV
|
|
424
|
-
this.kvSetSegment(key, data, staleAt, totalTtl);
|
|
431
|
+
this.kvSetSegment(key, data, staleAt, totalTtl, swrWindow);
|
|
425
432
|
} catch (error) {
|
|
426
433
|
console.error("[CFCacheStore] set failed:", error);
|
|
427
434
|
}
|
|
@@ -480,7 +487,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
480
487
|
const isStale = staleAt > 0 && Date.now() > staleAt;
|
|
481
488
|
|
|
482
489
|
return {
|
|
483
|
-
response,
|
|
490
|
+
response: this.toClientResponse(response),
|
|
484
491
|
shouldRevalidate: isStale,
|
|
485
492
|
};
|
|
486
493
|
} catch (error) {
|
|
@@ -489,6 +496,30 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
489
496
|
}
|
|
490
497
|
}
|
|
491
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Strip internal edge headers and restore the author's Cache-Control before a
|
|
501
|
+
* cached document Response is served to a client. L1 entries carry the
|
|
502
|
+
* internal staleness/status headers and a rewritten Cache-Control; none of
|
|
503
|
+
* those should reach the browser or an upstream CDN.
|
|
504
|
+
*/
|
|
505
|
+
private toClientResponse(response: Response): Response {
|
|
506
|
+
const headers = new Headers(response.headers);
|
|
507
|
+
const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
|
|
508
|
+
if (originalCacheControl !== null) {
|
|
509
|
+
headers.set("Cache-Control", originalCacheControl);
|
|
510
|
+
} else {
|
|
511
|
+
headers.delete("Cache-Control");
|
|
512
|
+
}
|
|
513
|
+
headers.delete(CACHE_ORIG_CC_HEADER);
|
|
514
|
+
headers.delete(CACHE_STALE_AT_HEADER);
|
|
515
|
+
headers.delete(CACHE_STATUS_HEADER);
|
|
516
|
+
return new Response(response.body, {
|
|
517
|
+
status: response.status,
|
|
518
|
+
statusText: response.statusText,
|
|
519
|
+
headers,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
492
523
|
/**
|
|
493
524
|
* Store a Response with TTL and optional SWR window (for document-level caching).
|
|
494
525
|
* When KV is configured, also persists to L2.
|
|
@@ -515,8 +546,14 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
515
546
|
: [null, null]
|
|
516
547
|
: [response.body, null];
|
|
517
548
|
|
|
518
|
-
// Clone and add cache headers
|
|
549
|
+
// Clone and add cache headers. The author's Cache-Control is stashed and
|
|
550
|
+
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
551
|
+
// the SWR window; getResponse restores the original before serving.
|
|
519
552
|
const headers = new Headers(response.headers);
|
|
553
|
+
const originalCacheControl = response.headers.get("Cache-Control");
|
|
554
|
+
if (originalCacheControl !== null) {
|
|
555
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
556
|
+
}
|
|
520
557
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
521
558
|
headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
|
|
522
559
|
|
|
@@ -766,13 +803,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
766
803
|
data: CachedEntryData,
|
|
767
804
|
staleAt: number,
|
|
768
805
|
totalTtl: number,
|
|
806
|
+
swrWindow: number,
|
|
769
807
|
): void {
|
|
770
808
|
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
771
809
|
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
772
810
|
|
|
773
811
|
const kvKey = this.toKVKey(key);
|
|
774
|
-
const
|
|
775
|
-
const expiresAt = staleAt + swrWindow;
|
|
812
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
776
813
|
|
|
777
814
|
this.waitUntil(async () => {
|
|
778
815
|
try {
|
|
@@ -939,6 +976,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
939
976
|
const request = this.keyToRequest(`doc:${key}`);
|
|
940
977
|
|
|
941
978
|
const headers = new Headers(envelope.hd);
|
|
979
|
+
const originalCacheControl = headers.get("Cache-Control");
|
|
980
|
+
if (originalCacheControl !== null) {
|
|
981
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
982
|
+
}
|
|
942
983
|
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
943
984
|
headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
|
|
944
985
|
|
package/src/cache/taint.ts
CHANGED
|
@@ -81,6 +81,61 @@ export function assertNotInsideCacheExec(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Symbol stamped on ctx when resolving handlers inside a cache() DSL boundary.
|
|
86
|
+
* Separate from INSIDE_CACHE_EXEC ("use cache") because cache() allows
|
|
87
|
+
* ctx.set() (children are also cached) but blocks response-level side effects
|
|
88
|
+
* (headers, cookies, status) which are lost on cache hit.
|
|
89
|
+
*/
|
|
90
|
+
export const INSIDE_CACHE_SCOPE: unique symbol = Symbol.for(
|
|
91
|
+
"rango:inside-cache-scope",
|
|
92
|
+
) as any;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Mark ctx as inside a cache() scope. Must be paired with unstampCacheScope.
|
|
96
|
+
*/
|
|
97
|
+
export function stampCacheScope(obj: object): void {
|
|
98
|
+
const current = (obj as any)[INSIDE_CACHE_SCOPE] ?? 0;
|
|
99
|
+
(obj as any)[INSIDE_CACHE_SCOPE] = current + 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove cache() scope mark.
|
|
104
|
+
*/
|
|
105
|
+
export function unstampCacheScope(obj: object): void {
|
|
106
|
+
const current = (obj as any)[INSIDE_CACHE_SCOPE] ?? 0;
|
|
107
|
+
if (current <= 1) {
|
|
108
|
+
delete (obj as any)[INSIDE_CACHE_SCOPE];
|
|
109
|
+
} else {
|
|
110
|
+
(obj as any)[INSIDE_CACHE_SCOPE] = current - 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Throw if ctx is inside a cache() DSL boundary.
|
|
116
|
+
* Call from response-level side effects (header, setCookie, setStatus, etc.)
|
|
117
|
+
* which are lost on cache hit because the handler body is skipped.
|
|
118
|
+
* ctx.set() is allowed inside cache() — children are also cached and can
|
|
119
|
+
* read the value.
|
|
120
|
+
*/
|
|
121
|
+
export function assertNotInsideCacheScope(
|
|
122
|
+
ctx: unknown,
|
|
123
|
+
methodName: string,
|
|
124
|
+
): void {
|
|
125
|
+
if (
|
|
126
|
+
ctx !== null &&
|
|
127
|
+
ctx !== undefined &&
|
|
128
|
+
typeof ctx === "object" &&
|
|
129
|
+
(INSIDE_CACHE_SCOPE as symbol) in (ctx as Record<symbol, unknown>)
|
|
130
|
+
) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
133
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
134
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
84
139
|
/**
|
|
85
140
|
* Brand symbol for functions wrapped by registerCachedFunction().
|
|
86
141
|
* Used at runtime to detect when a "use cache" function is misused
|
package/src/client.rsc.tsx
CHANGED
|
@@ -78,6 +78,9 @@ export {
|
|
|
78
78
|
// Re-export useHref - it's a "use client" hook
|
|
79
79
|
export { useHref } from "./browser/react/use-href.js";
|
|
80
80
|
|
|
81
|
+
// Re-export useReverse - it's a "use client" hook
|
|
82
|
+
export { useReverse } from "./browser/react/use-reverse.js";
|
|
83
|
+
|
|
81
84
|
// Re-export useHandle - it's a "use client" hook
|
|
82
85
|
export { useHandle } from "./browser/react/use-handle.js";
|
|
83
86
|
|