@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
CacheGetResult,
|
|
35
35
|
CacheItemResult,
|
|
36
36
|
CacheItemOptions,
|
|
37
|
+
ShellCacheEntry,
|
|
37
38
|
} from "../types.js";
|
|
38
39
|
import {
|
|
39
40
|
_getRequestContext,
|
|
@@ -136,6 +137,16 @@ const warnedNoKvReadInvalidation = new Set<string>();
|
|
|
136
137
|
*/
|
|
137
138
|
const warnedTagInvalidationTtlFloor = new Set<string>();
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Stores (by namespace) already warned that tag invalidation is writing KV
|
|
142
|
+
* markers with no expiry (tagInvalidationTtl unset), so the unbounded-growth
|
|
143
|
+
* warning fires once per process rather than once per invalidateTags call
|
|
144
|
+
* (CFCacheStore is constructed per request; invalidateTags runs per marker
|
|
145
|
+
* batch). Distinct from the floor warning: that one only fires for a positive
|
|
146
|
+
* below-floor value, never for the unset (no-expiry) default that this bounds.
|
|
147
|
+
*/
|
|
148
|
+
const warnedNoTagInvalidationTtl = new Set<string>();
|
|
149
|
+
|
|
139
150
|
// ============================================================================
|
|
140
151
|
// Types
|
|
141
152
|
// ============================================================================
|
|
@@ -189,6 +200,35 @@ interface KVItemEnvelope {
|
|
|
189
200
|
ta?: number;
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
/**
|
|
204
|
+
* KV envelope for PPR shell cache entries.
|
|
205
|
+
* @internal
|
|
206
|
+
*/
|
|
207
|
+
interface KVShellEnvelope {
|
|
208
|
+
/** base64-encoded prelude bytes */
|
|
209
|
+
p: string;
|
|
210
|
+
/** postponed state JSON, or null (DATA variant — no holes) */
|
|
211
|
+
po: string | null;
|
|
212
|
+
/** React.version captured at prerender time */
|
|
213
|
+
rv: string;
|
|
214
|
+
/** Build version captured at prerender time (ShellCacheEntry.buildVersion) */
|
|
215
|
+
bv?: string;
|
|
216
|
+
/** createdAt (ms epoch) */
|
|
217
|
+
c: number;
|
|
218
|
+
/** When entry becomes stale (ms epoch) */
|
|
219
|
+
s: number;
|
|
220
|
+
/** When entry hard-expires (ms epoch) */
|
|
221
|
+
e: number;
|
|
222
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
223
|
+
t?: string[];
|
|
224
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
225
|
+
ta?: number;
|
|
226
|
+
/** initialTheme the capture render was built with (resume theme fidelity) */
|
|
227
|
+
i?: string;
|
|
228
|
+
/** Capture data snapshot: recorded cache-store hits/writes for HIT parity */
|
|
229
|
+
sn?: import("../types.js").ShellSnapshotRecord[];
|
|
230
|
+
}
|
|
231
|
+
|
|
192
232
|
/**
|
|
193
233
|
* KV envelope for document cache entries.
|
|
194
234
|
* @internal
|
|
@@ -312,19 +352,30 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
312
352
|
// kv - yet every tagged read still serves stale data with no other signal.
|
|
313
353
|
// Surface that misconfiguration.
|
|
314
354
|
if (!this.kv && (this.tagCacheTtl > 0 || this.onRevalidateTag)) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
`{ kv } for distributed tag invalidation.`,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
355
|
+
this.warnOncePerNamespace(
|
|
356
|
+
warnedNoKvReadInvalidation,
|
|
357
|
+
`[CFCacheStore] tagCacheTtl/onRevalidateTag is configured without a KV ` +
|
|
358
|
+
`namespace, so tag invalidation has NO read-side effect: tagged reads ` +
|
|
359
|
+
`are never treated as invalidated and serve stale data. Configure ` +
|
|
360
|
+
`{ kv } for distributed tag invalidation.`,
|
|
361
|
+
);
|
|
325
362
|
}
|
|
326
363
|
}
|
|
327
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Warn about a namespace-scoped misconfiguration once per namespace per
|
|
367
|
+
* isolate. `seen` is the module-level Set for that message family -- Sets
|
|
368
|
+
* are module-level (not instance fields) so re-constructed stores in the
|
|
369
|
+
* same isolate don't re-warn.
|
|
370
|
+
* @internal
|
|
371
|
+
*/
|
|
372
|
+
private warnOncePerNamespace(seen: Set<string>, message: string): void {
|
|
373
|
+
const id = this.namespace ?? "default";
|
|
374
|
+
if (seen.has(id)) return;
|
|
375
|
+
seen.add(id);
|
|
376
|
+
console.warn(message);
|
|
377
|
+
}
|
|
378
|
+
|
|
328
379
|
/**
|
|
329
380
|
* Validate a consumer-supplied tagInvalidationTtl against CF KV's expirationTtl
|
|
330
381
|
* floor. A finite value below KV_MIN_EXPIRATION_TTL is raised to it (with a
|
|
@@ -340,16 +391,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
340
391
|
if (value == null) return undefined;
|
|
341
392
|
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
342
393
|
if (value < KV_MIN_EXPIRATION_TTL) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
`TTL+SWR or invalidated entries can resurrect when the marker expires.`,
|
|
351
|
-
);
|
|
352
|
-
}
|
|
394
|
+
this.warnOncePerNamespace(
|
|
395
|
+
warnedTagInvalidationTtlFloor,
|
|
396
|
+
`[CFCacheStore] tagInvalidationTtl ${value} is below Cloudflare KV's ` +
|
|
397
|
+
`${KV_MIN_EXPIRATION_TTL}s expirationTtl floor; raising to ` +
|
|
398
|
+
`${KV_MIN_EXPIRATION_TTL}. It must still exceed your largest entry ` +
|
|
399
|
+
`TTL+SWR or invalidated entries can resurrect when the marker expires.`,
|
|
400
|
+
);
|
|
353
401
|
return KV_MIN_EXPIRATION_TTL;
|
|
354
402
|
}
|
|
355
403
|
return value;
|
|
@@ -1574,6 +1622,143 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1574
1622
|
}
|
|
1575
1623
|
}
|
|
1576
1624
|
|
|
1625
|
+
// ============================================================================
|
|
1626
|
+
// Shell Cache Methods (PPR shell resume) — KV-only in v1
|
|
1627
|
+
// ============================================================================
|
|
1628
|
+
//
|
|
1629
|
+
// Unlike the segment/item/document tiers, the shell family has NO Cache-API L1
|
|
1630
|
+
// tier: the prelude bytes + postponed blob are large and version-coupled, and a
|
|
1631
|
+
// per-colo L1 for them is a deliberate follow-up (see the PPR shell-resume
|
|
1632
|
+
// design doc). Shell entries live only in KV (the global tier), so the family
|
|
1633
|
+
// requires a configured KV namespace; without one, getShell/putShell no-op and
|
|
1634
|
+
// the shell-cache middleware fails open to a full HTML render. Tag invalidation
|
|
1635
|
+
// still applies: shell entries carry tags/taggedAt and are checked against the
|
|
1636
|
+
// same KV markers isGloballyInvalidated() reads for every other tier.
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Get a cached PPR shell entry by key from KV (no L1). Applies the KV read
|
|
1640
|
+
* budget, corrupt-entry eviction, hard-expiry, and tag invalidation exactly
|
|
1641
|
+
* like kvGetItem, minus the L1 promote. SWR is a plain staleness flag — KV has
|
|
1642
|
+
* no REVALIDATING herd guard, so the shell-cache middleware's module-level
|
|
1643
|
+
* in-flight set is the recapture stampede guard.
|
|
1644
|
+
*/
|
|
1645
|
+
async getShell(
|
|
1646
|
+
key: string,
|
|
1647
|
+
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
1648
|
+
if (!this.kv) return null;
|
|
1649
|
+
try {
|
|
1650
|
+
const kvKey = this.toKVKey(`shell:${key}`);
|
|
1651
|
+
const { value: envelope, timedOut } =
|
|
1652
|
+
await this.kvGetOrEvict<KVShellEnvelope>(
|
|
1653
|
+
kvKey,
|
|
1654
|
+
(e) =>
|
|
1655
|
+
typeof e.p === "string" &&
|
|
1656
|
+
(e.po === null || typeof e.po === "string") &&
|
|
1657
|
+
typeof e.rv === "string" &&
|
|
1658
|
+
typeof e.e === "number" &&
|
|
1659
|
+
typeof e.s === "number",
|
|
1660
|
+
"getShell",
|
|
1661
|
+
);
|
|
1662
|
+
// A timeout, a missing key, or an already-evicted corrupt entry is a miss.
|
|
1663
|
+
if (timedOut || !envelope) return null;
|
|
1664
|
+
|
|
1665
|
+
const now = Date.now();
|
|
1666
|
+
if (now > envelope.e) return null;
|
|
1667
|
+
|
|
1668
|
+
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const shouldRevalidate = envelope.s > 0 && now > envelope.s;
|
|
1673
|
+
return {
|
|
1674
|
+
entry: {
|
|
1675
|
+
prelude: envelope.p,
|
|
1676
|
+
postponed: envelope.po,
|
|
1677
|
+
reactVersion: envelope.rv,
|
|
1678
|
+
buildVersion: envelope.bv,
|
|
1679
|
+
initialTheme: envelope.i,
|
|
1680
|
+
snapshot: envelope.sn,
|
|
1681
|
+
createdAt: envelope.c,
|
|
1682
|
+
},
|
|
1683
|
+
shouldRevalidate,
|
|
1684
|
+
};
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Store a PPR shell entry in KV with TTL and optional SWR window. Non-blocking
|
|
1693
|
+
* (waitUntil) like the other KV writes. The tags/taggedAt ride in the envelope
|
|
1694
|
+
* so isGloballyInvalidated() can invalidate the shell via the shared KV markers.
|
|
1695
|
+
*/
|
|
1696
|
+
async putShell(
|
|
1697
|
+
key: string,
|
|
1698
|
+
entry: ShellCacheEntry,
|
|
1699
|
+
ttlSeconds?: number,
|
|
1700
|
+
swrSeconds?: number,
|
|
1701
|
+
tags?: string[],
|
|
1702
|
+
): Promise<void> {
|
|
1703
|
+
// KV-only tier: needs a KV namespace and waitUntil (writes are non-blocking).
|
|
1704
|
+
if (!this.kv || !this.waitUntil) return;
|
|
1705
|
+
try {
|
|
1706
|
+
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
1707
|
+
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
1708
|
+
const totalTtl = ttl + swrWindow;
|
|
1709
|
+
// KV requires expirationTtl >= 60s; skip a shorter-lived shell rather than
|
|
1710
|
+
// letting kv.put reject inside waitUntil (mirrors setItem/kvSetSegment).
|
|
1711
|
+
if (totalTtl < 60) return;
|
|
1712
|
+
|
|
1713
|
+
const staleAt = Date.now() + ttl * 1000;
|
|
1714
|
+
const taggedAt =
|
|
1715
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1716
|
+
|
|
1717
|
+
const kvKey = this.toKVKey(`shell:${key}`);
|
|
1718
|
+
// A key over the KV limit makes kv.put reject deep inside waitUntil; report
|
|
1719
|
+
// and skip the doomed write (mirrors kvSetSegment).
|
|
1720
|
+
const kvKeyBytes = kvKeyByteLength(kvKey);
|
|
1721
|
+
if (kvKeyBytes > KV_MAX_KEY_BYTES) {
|
|
1722
|
+
reportCacheError(
|
|
1723
|
+
new Error(
|
|
1724
|
+
`shell cache key produces a ${kvKeyBytes}-byte KV key, over the ` +
|
|
1725
|
+
`${KV_MAX_KEY_BYTES}-byte limit; the shell was not persisted.`,
|
|
1726
|
+
),
|
|
1727
|
+
"cache-write",
|
|
1728
|
+
"[CFCacheStore] putShell",
|
|
1729
|
+
);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
this.waitUntil(() =>
|
|
1734
|
+
reportingAsync(
|
|
1735
|
+
() => {
|
|
1736
|
+
const envelope: KVShellEnvelope = {
|
|
1737
|
+
p: entry.prelude,
|
|
1738
|
+
po: entry.postponed,
|
|
1739
|
+
rv: entry.reactVersion,
|
|
1740
|
+
bv: entry.buildVersion,
|
|
1741
|
+
c: entry.createdAt,
|
|
1742
|
+
s: staleAt,
|
|
1743
|
+
e: staleAt + swrWindow * 1000,
|
|
1744
|
+
t: tags,
|
|
1745
|
+
ta: taggedAt,
|
|
1746
|
+
i: entry.initialTheme,
|
|
1747
|
+
sn: entry.snapshot,
|
|
1748
|
+
};
|
|
1749
|
+
return this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
1750
|
+
expirationTtl: totalTtl,
|
|
1751
|
+
});
|
|
1752
|
+
},
|
|
1753
|
+
"cache-write",
|
|
1754
|
+
"[CFCacheStore] putShell",
|
|
1755
|
+
),
|
|
1756
|
+
);
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1577
1762
|
// ============================================================================
|
|
1578
1763
|
// Key Helpers
|
|
1579
1764
|
// ============================================================================
|
|
@@ -2091,6 +2276,22 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2091
2276
|
const failedTags = new Set<string>();
|
|
2092
2277
|
const errors: unknown[] = [];
|
|
2093
2278
|
if (this.kv) {
|
|
2279
|
+
// Markers written with no expiry (tagInvalidationTtl unset) never expire,
|
|
2280
|
+
// so high-cardinality tags accumulate KV keys unboundedly with no reaper.
|
|
2281
|
+
// Warn once per namespace at the batch entry point (not per marker write,
|
|
2282
|
+
// which would fire once per tag). Kept separate from the floor warning:
|
|
2283
|
+
// that path only fires for a positive below-floor value, never the unset
|
|
2284
|
+
// default sanitizeTagInvalidationTtl passes through as undefined.
|
|
2285
|
+
if (!this.tagInvalidationTtl) {
|
|
2286
|
+
this.warnOncePerNamespace(
|
|
2287
|
+
warnedNoTagInvalidationTtl,
|
|
2288
|
+
`[CFCacheStore] invalidateTags is writing KV markers with no expiry ` +
|
|
2289
|
+
`(tagInvalidationTtl is unset): high-cardinality tags accumulate KV ` +
|
|
2290
|
+
`keys unboundedly (storage + list-scan cost) with no reaper. Set ` +
|
|
2291
|
+
`tagInvalidationTtl above your largest entry TTL+SWR to bound marker ` +
|
|
2292
|
+
`growth; setting it too small resurrects invalidated entries.`,
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2094
2295
|
await Promise.all(
|
|
2095
2296
|
tags.map(async (tag) => {
|
|
2096
2297
|
const markerKey = this.tagMarkerKey(tag);
|
|
@@ -2267,6 +2468,28 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2267
2468
|
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
2268
2469
|
|
|
2269
2470
|
const kvKey = this.toKVKey(key);
|
|
2471
|
+
|
|
2472
|
+
// Reject an oversized data-segment KV key the same way tag-marker keys are
|
|
2473
|
+
// rejected in invalidateTags(). A key over KV_MAX_KEY_BYTES makes kv.put()
|
|
2474
|
+
// fail, so the segment silently never lands in L2 (KV) and every cold-colo
|
|
2475
|
+
// or TTL-expired read re-renders instead of serving stale. Segment keys can
|
|
2476
|
+
// grow with user-controlled inputs (e.g. a route's search params), so report
|
|
2477
|
+
// a clear, actionable error and skip the doomed write rather than letting it
|
|
2478
|
+
// reject deep inside waitUntil as an opaque cache-write failure.
|
|
2479
|
+
const kvKeyBytes = kvKeyByteLength(kvKey);
|
|
2480
|
+
if (kvKeyBytes > KV_MAX_KEY_BYTES) {
|
|
2481
|
+
reportCacheError(
|
|
2482
|
+
new Error(
|
|
2483
|
+
`cache segment key produces a ${kvKeyBytes}-byte KV key, over the ` +
|
|
2484
|
+
`${KV_MAX_KEY_BYTES}-byte limit; the segment was not persisted to KV (L2). ` +
|
|
2485
|
+
`Reduce the cache-key inputs (e.g. large search params on this route).`,
|
|
2486
|
+
),
|
|
2487
|
+
"cache-write",
|
|
2488
|
+
"[CFCacheStore] kvSetSegment",
|
|
2489
|
+
);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2270
2493
|
const expiresAt = staleAt + swrWindow * 1000;
|
|
2271
2494
|
|
|
2272
2495
|
this.waitUntil(() =>
|
|
@@ -15,10 +15,11 @@ import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
|
15
15
|
import { hasPerClientSignal } from "../browser/cookie-name.js";
|
|
16
16
|
import {
|
|
17
17
|
getRequestContext,
|
|
18
|
+
runWithRequestContext,
|
|
18
19
|
type RequestContext,
|
|
19
20
|
} from "../server/request-context.js";
|
|
20
21
|
import { mayNeedSSR } from "../rsc/ssr-setup.js";
|
|
21
|
-
import {
|
|
22
|
+
import { cacheKeyBase } from "./cache-key-utils.js";
|
|
22
23
|
import { runBackground } from "./background-task.js";
|
|
23
24
|
import { reportCacheError } from "./cache-error.js";
|
|
24
25
|
|
|
@@ -126,20 +127,31 @@ function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
|
126
127
|
// ============================================================================
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
* Add cache
|
|
130
|
+
* Add the cache-status header (HIT/STALE/MISS) to a response.
|
|
131
|
+
*
|
|
132
|
+
* The response we get here is always a fresh instance — the store rebuilds a
|
|
133
|
+
* new Response per getResponse(), and the miss path wraps a fresh Response
|
|
134
|
+
* around the tee'd body — so mutating its headers in place is safe and avoids
|
|
135
|
+
* cloning every header + allocating a new Response on every cache hit. Only when
|
|
136
|
+
* the headers are immutable (a guarded Response rejects set() with a TypeError)
|
|
137
|
+
* do we fall back to rebuilding the Response with a mutable Headers.
|
|
130
138
|
*/
|
|
131
139
|
function addCacheStatusHeader(
|
|
132
140
|
response: Response,
|
|
133
141
|
status: "HIT" | "STALE" | "MISS",
|
|
134
142
|
): Response {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
try {
|
|
144
|
+
response.headers.set(CACHE_STATUS_HEADER, status);
|
|
145
|
+
return response;
|
|
146
|
+
} catch {
|
|
147
|
+
const headers = new Headers(response.headers);
|
|
148
|
+
headers.set(CACHE_STATUS_HEADER, status);
|
|
149
|
+
return new Response(response.body, {
|
|
150
|
+
status: response.status,
|
|
151
|
+
statusText: response.statusText,
|
|
152
|
+
headers,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
/**
|
|
@@ -176,7 +188,13 @@ export interface DocumentCacheOptions<TEnv = any> {
|
|
|
176
188
|
skipPaths?: string[];
|
|
177
189
|
|
|
178
190
|
/**
|
|
179
|
-
* Custom cache key generator
|
|
191
|
+
* Custom cache key generator.
|
|
192
|
+
*
|
|
193
|
+
* Replaces the default `host + pathname + search` key entirely. On a
|
|
194
|
+
* multi-domain deployment served by one function you MUST include `url.host`
|
|
195
|
+
* (or an equivalent tenant discriminator) yourself — the default key is
|
|
196
|
+
* host-namespaced, but a custom generator's output is used verbatim, so
|
|
197
|
+
* omitting host bleeds one hostname's cached response to another.
|
|
180
198
|
*/
|
|
181
199
|
keyGenerator?: (url: URL) => string;
|
|
182
200
|
|
|
@@ -299,17 +317,17 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
299
317
|
isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
300
318
|
const typeSuffix = isRscRequest ? ":rsc" : ":html";
|
|
301
319
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
320
|
+
// Default key rides the shared host-namespaced base (cacheKeyBase) so the
|
|
321
|
+
// segment tier (cache-scope.ts) and this document tier cannot drift on the
|
|
322
|
+
// host-namespacing rule -- see the contract on cacheKeyBase.
|
|
323
|
+
// The keyGenerator branch is left untouched: a consumer-supplied generator
|
|
324
|
+
// owns its own namespacing (auto-prefixing host would silently change their
|
|
325
|
+
// existing keys and double any host they already include).
|
|
310
326
|
const cacheKey = keyGenerator
|
|
311
327
|
? keyGenerator(url) + segmentHash + typeSuffix
|
|
312
|
-
:
|
|
328
|
+
: cacheKeyBase(url.host, url.pathname, url.searchParams) +
|
|
329
|
+
segmentHash +
|
|
330
|
+
typeSuffix;
|
|
313
331
|
// 1. Check cache
|
|
314
332
|
const cached = await store.getResponse(cacheKey);
|
|
315
333
|
|
|
@@ -330,7 +348,13 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
330
348
|
|
|
331
349
|
runBackground(requestCtx, async () => {
|
|
332
350
|
try {
|
|
333
|
-
|
|
351
|
+
// Re-establish the request-context ALS around the background
|
|
352
|
+
// re-render: next() re-runs the full handler pipeline, and on
|
|
353
|
+
// workerd a waitUntil task runs detached from the request's I/O
|
|
354
|
+
// context, so a handler/component reading getRequestContext() would
|
|
355
|
+
// otherwise throw. Same fix as the route-level/use-cache background
|
|
356
|
+
// revalidation paths.
|
|
357
|
+
const fresh = await runWithRequestContext(requestCtx, () => next());
|
|
334
358
|
const directives = shouldCacheResponse(fresh);
|
|
335
359
|
|
|
336
360
|
if (directives && fresh.body) {
|
|
@@ -348,10 +372,15 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
348
372
|
log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
|
|
349
373
|
}
|
|
350
374
|
} catch (error) {
|
|
375
|
+
// Pass requestCtx explicitly: this runs in a detached waitUntil task
|
|
376
|
+
// where the ALS context is gone, so onError only fires if we hand it
|
|
377
|
+
// the captured context (reportCacheError falls back to _getRequestContext
|
|
378
|
+
// otherwise, which is null here).
|
|
351
379
|
reportCacheError(
|
|
352
380
|
error,
|
|
353
381
|
"cache-write",
|
|
354
382
|
"[DocumentCache] revalidation",
|
|
383
|
+
requestCtx,
|
|
355
384
|
);
|
|
356
385
|
}
|
|
357
386
|
});
|
|
@@ -401,10 +430,14 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
401
430
|
collectRequestTags(requestCtx),
|
|
402
431
|
);
|
|
403
432
|
} catch (error) {
|
|
433
|
+
// Detached waitUntil task — pass the captured requestCtx so onError
|
|
434
|
+
// fires even though the ALS context is gone (see the revalidation
|
|
435
|
+
// catch above).
|
|
404
436
|
reportCacheError(
|
|
405
437
|
error,
|
|
406
438
|
"cache-write",
|
|
407
439
|
"[DocumentCache] cache write",
|
|
440
|
+
requestCtx,
|
|
408
441
|
);
|
|
409
442
|
}
|
|
410
443
|
});
|