@rangojs/router 0.17.0 → 0.19.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 +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- 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 +224 -72
- 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 +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +9 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- 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 +1094 -376
- 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 +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +47 -3
- package/src/rsc/handler.ts +24 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -15,6 +15,7 @@ import type { ExecutionContext } from "../../types/request-scope.js";
|
|
|
15
15
|
import type { CacheDefaults } from "../types.js";
|
|
16
16
|
import type { RequestContext } from "../../server/request-context.js";
|
|
17
17
|
import type { CloudflareZonePurgeOptions } from "./cf-zone-purge.js";
|
|
18
|
+
import type { StoreMemoOptions } from "../shell-memo.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Minimal Cloudflare KV Namespace interface.
|
|
@@ -340,9 +341,12 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
340
341
|
|
|
341
342
|
/**
|
|
342
343
|
* Latency budget (ms) for reading the BODY of a matched L1 entry
|
|
343
|
-
* (response.json()
|
|
344
|
-
*
|
|
345
|
-
*
|
|
344
|
+
* (response.json(); for a PPR shell, only its head and prelude — the
|
|
345
|
+
* snapshot behind them is read after the HIT commits, under
|
|
346
|
+
* {@link kvReadTimeoutMs}). CF streams the cache body lazily, so the
|
|
347
|
+
* multi-second tail can appear after `match` already resolved; this bounds
|
|
348
|
+
* it. On timeout the read is treated as a miss and falls through to L2/KV or
|
|
349
|
+
* render.
|
|
346
350
|
*
|
|
347
351
|
* Separate from {@link edgeLookupTimeoutMs} because a healthy body read
|
|
348
352
|
* (fetch + JSON parse of a potentially large Flight payload) takes a little
|
|
@@ -356,7 +360,11 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
356
360
|
* Latency budget (ms) for an L2 (KV) read. KV is the last cache tier before a
|
|
357
361
|
* full render and is a global store (~50ms healthy, seconds when degraded);
|
|
358
362
|
* this bounds it so a slow namespace cannot pin the request. On timeout the
|
|
359
|
-
* read is treated as a miss (no L1 promote) and falls through to render.
|
|
363
|
+
* read is treated as a miss (no L1 promote) and falls through to render. For
|
|
364
|
+
* a PPR shell it covers opening the KV value and reading its head and
|
|
365
|
+
* prelude; separately, it bounds the shell's snapshot read (either tier),
|
|
366
|
+
* which runs off the commit path: a timeout there lets the tail run without
|
|
367
|
+
* the snapshot's pins.
|
|
360
368
|
*
|
|
361
369
|
* Defaults to {@link KV_READ_TIMEOUT_MS} (170) -- a few multiples above the
|
|
362
370
|
* ~50ms healthy read, with headroom for legitimate tails (large payloads / far
|
|
@@ -367,6 +375,26 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
367
375
|
*/
|
|
368
376
|
kvReadTimeoutMs?: number;
|
|
369
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Per-isolate memos ({@link StoreMemoOptions}), one set per isolate and
|
|
380
|
+
* shared by every CFCacheStore in it. `shellMs` (default 2000) and
|
|
381
|
+
* `shellMaxBytes` (default 16 MiB) size the PPR shell memo;
|
|
382
|
+
* `markerFreshMs` (default 1000) and `markerMaxStaleMs` (default 10000)
|
|
383
|
+
* the tag-marker memo PPR shell reads use with KV bound (served
|
|
384
|
+
* stale-while-revalidate). A memo HIT still runs the tag-marker check; the
|
|
385
|
+
* isolate that invalidates drops its own shell copies (and keeps reads in
|
|
386
|
+
* flight from memoizing them again) and writes the new marker into its
|
|
387
|
+
* marker memo, and its own `putShell` replaces its copy. Another isolate
|
|
388
|
+
* rejects a memoized shell once its marker memo sees the invalidation (up
|
|
389
|
+
* to `markerMaxStaleMs`). Without KV, in purge mode, the purge does not
|
|
390
|
+
* reach other isolates' memos: they serve the purged shell until their
|
|
391
|
+
* window passes. The response of a request that ran `updateTag()` /
|
|
392
|
+
* `revalidateTag()` sets the fresh-reads cookie, and the same user's
|
|
393
|
+
* requests carrying it skip both memos. `{ shellMs: 0 }` turns the shell
|
|
394
|
+
* memo off, `{ markerFreshMs: 0 }` the marker memo.
|
|
395
|
+
*/
|
|
396
|
+
memo?: StoreMemoOptions;
|
|
397
|
+
|
|
370
398
|
/**
|
|
371
399
|
* Emit a {@link CFCacheReadDebugEvent} per L1 read. `true` logs to console
|
|
372
400
|
* (visible via `wrangler tail`); pass a function to capture events directly.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prelude-first binary layout of a CFCacheStore PPR shell entry, shared by the
|
|
3
|
+
* Cache API body and the KV value (issue #941; docs/design/shell-entry-layout.md):
|
|
4
|
+
*
|
|
5
|
+
* "RSH1" | head length, 8 hex digits | head JSON | prelude bytes | snapshot JSON
|
|
6
|
+
*
|
|
7
|
+
* A document HIT needs the head (versions, freshness, tags, postponed) and the
|
|
8
|
+
* raw prelude before its first byte, and the capture snapshot only in the
|
|
9
|
+
* tail. Laying them out in that order lets the serve path read the head,
|
|
10
|
+
* start the tag-marker read, read exactly `pl` prelude bytes, and commit while
|
|
11
|
+
* the snapshot bytes are still arriving. One record keeps the prelude and the
|
|
12
|
+
* snapshot generation-coupled by construction: a Cache API entry and a KV
|
|
13
|
+
* value are written and read whole, so a split read can never pair one
|
|
14
|
+
* capture's prelude with another's pins.
|
|
15
|
+
*
|
|
16
|
+
* Every byte is UTF-8 text when the prelude is (React's HTML through
|
|
17
|
+
* TextEncoder always is), so the KV value is stored as a string (the
|
|
18
|
+
* KVNamespace contract's `put(key, string)`) and read back as the same bytes
|
|
19
|
+
* with `{ type: "stream" }`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ShellSnapshotRecord } from "../types.js";
|
|
23
|
+
|
|
24
|
+
const MAGIC = [0x52, 0x53, 0x48, 0x31]; // "RSH1"
|
|
25
|
+
/** Magic (4) + head length as 8 hex digits (8). */
|
|
26
|
+
const PREFIX_BYTES = 12;
|
|
27
|
+
|
|
28
|
+
const textEncoder = new TextEncoder();
|
|
29
|
+
const textDecoder = new TextDecoder();
|
|
30
|
+
const strictTextDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
31
|
+
|
|
32
|
+
/** The frame head: every entry field except the prelude and the snapshot. */
|
|
33
|
+
export interface ShellFrameHead {
|
|
34
|
+
/** ShellCacheEntry.reactVersion */
|
|
35
|
+
rv: string;
|
|
36
|
+
/** ShellCacheEntry.buildVersion */
|
|
37
|
+
bv: string;
|
|
38
|
+
/** Capture-generation start time (ms epoch), used by tag marker checks. */
|
|
39
|
+
c: number;
|
|
40
|
+
/** When the entry becomes stale (ms epoch). */
|
|
41
|
+
s: number;
|
|
42
|
+
/** When the entry hard-expires (ms epoch). */
|
|
43
|
+
e: number;
|
|
44
|
+
/** Cache tags (for distributed tag invalidation). */
|
|
45
|
+
t?: string[];
|
|
46
|
+
/** Timestamp when tags were attached (ms epoch). */
|
|
47
|
+
ta?: number;
|
|
48
|
+
/** ShellCacheEntry.initialTheme */
|
|
49
|
+
i?: string;
|
|
50
|
+
/** ShellCacheEntry.docKey; navigation replay needs it after any round trip. */
|
|
51
|
+
dk?: string;
|
|
52
|
+
/** ShellCacheEntry.prunedRecords (diagnostic). */
|
|
53
|
+
pr?: string;
|
|
54
|
+
/** ShellCacheEntry.navigationOnly; its partial-context prelude is not document-safe. */
|
|
55
|
+
no?: true;
|
|
56
|
+
/** ShellCacheEntry.postponed. Absent iff `no` (no document half is stored). */
|
|
57
|
+
po?: string | null;
|
|
58
|
+
/** Prelude byte length (0 for a navigationOnly entry). */
|
|
59
|
+
pl: number;
|
|
60
|
+
/**
|
|
61
|
+
* Snapshot byte length (0 when the entry has none). A body whose snapshot
|
|
62
|
+
* part is any other length was truncated or corrupted.
|
|
63
|
+
*/
|
|
64
|
+
sl: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Validate a parsed head before any field reaches the serve path. */
|
|
68
|
+
function isShellFrameHead(value: unknown): value is ShellFrameHead {
|
|
69
|
+
if (value == null || typeof value !== "object") return false;
|
|
70
|
+
const head = value as Partial<ShellFrameHead>;
|
|
71
|
+
return (
|
|
72
|
+
typeof head.rv === "string" &&
|
|
73
|
+
typeof head.bv === "string" &&
|
|
74
|
+
isFiniteNumber(head.c) &&
|
|
75
|
+
isFiniteNumber(head.s) &&
|
|
76
|
+
isFiniteNumber(head.e) &&
|
|
77
|
+
(head.t === undefined ||
|
|
78
|
+
(Array.isArray(head.t) &&
|
|
79
|
+
head.t.every((tag) => typeof tag === "string"))) &&
|
|
80
|
+
(head.ta === undefined || isFiniteNumber(head.ta)) &&
|
|
81
|
+
(head.i === undefined || typeof head.i === "string") &&
|
|
82
|
+
(head.dk === undefined || typeof head.dk === "string") &&
|
|
83
|
+
(head.pr === undefined || typeof head.pr === "string") &&
|
|
84
|
+
(head.no === undefined || head.no === true) &&
|
|
85
|
+
// Document half: postponed is required unless the entry is navigationOnly.
|
|
86
|
+
(head.po === null ||
|
|
87
|
+
typeof head.po === "string" ||
|
|
88
|
+
(head.po === undefined && head.no === true)) &&
|
|
89
|
+
isByteLength(head.pl) &&
|
|
90
|
+
isByteLength(head.sl)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isByteLength(value: unknown): value is number {
|
|
95
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
99
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Serialize a shell entry into one prelude-first frame. Throws when the head's
|
|
104
|
+
* byte lengths (`pl`, `sl`) do not describe `prelude` and `snapshot`.
|
|
105
|
+
*/
|
|
106
|
+
export function encodeShellFrame(
|
|
107
|
+
head: ShellFrameHead,
|
|
108
|
+
prelude: Uint8Array,
|
|
109
|
+
snapshot?: Uint8Array,
|
|
110
|
+
): Uint8Array<ArrayBuffer> {
|
|
111
|
+
const snapshotLength = snapshot?.length ?? 0;
|
|
112
|
+
if (head.pl !== prelude.length || head.sl !== snapshotLength) {
|
|
113
|
+
throw new Error("shell frame head does not match its prelude/snapshot");
|
|
114
|
+
}
|
|
115
|
+
const headBytes = textEncoder.encode(JSON.stringify(head));
|
|
116
|
+
const frame = new Uint8Array(
|
|
117
|
+
PREFIX_BYTES + headBytes.length + prelude.length + snapshotLength,
|
|
118
|
+
);
|
|
119
|
+
frame.set(MAGIC, 0);
|
|
120
|
+
frame.set(
|
|
121
|
+
textEncoder.encode(headBytes.length.toString(16).padStart(8, "0")),
|
|
122
|
+
MAGIC.length,
|
|
123
|
+
);
|
|
124
|
+
frame.set(headBytes, PREFIX_BYTES);
|
|
125
|
+
frame.set(prelude, PREFIX_BYTES + headBytes.length);
|
|
126
|
+
if (snapshot) {
|
|
127
|
+
frame.set(snapshot, PREFIX_BYTES + headBytes.length + prelude.length);
|
|
128
|
+
}
|
|
129
|
+
return frame;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The frame as the string KV stores. Throws when the prelude is not valid
|
|
134
|
+
* UTF-8, which would not round-trip through a string.
|
|
135
|
+
*/
|
|
136
|
+
export function shellFrameToText(frame: Uint8Array): string {
|
|
137
|
+
return strictTextDecoder.decode(frame);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** UTF-8 JSON of a snapshot, or undefined when there is none to store. */
|
|
141
|
+
export function encodeShellSnapshot(
|
|
142
|
+
snapshot: ShellSnapshotRecord[] | undefined,
|
|
143
|
+
): Uint8Array | undefined {
|
|
144
|
+
return snapshot && snapshot.length > 0
|
|
145
|
+
? textEncoder.encode(JSON.stringify(snapshot))
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse the snapshot bytes that follow the prelude. Zero bytes means the entry
|
|
151
|
+
* carries no snapshot. Throws on anything else that is not a JSON array: the
|
|
152
|
+
* caller treats that as corruption.
|
|
153
|
+
*/
|
|
154
|
+
export function parseShellSnapshot(
|
|
155
|
+
bytes: Uint8Array,
|
|
156
|
+
): ShellSnapshotRecord[] | undefined {
|
|
157
|
+
if (bytes.length === 0) return undefined;
|
|
158
|
+
const parsed: unknown = JSON.parse(textDecoder.decode(bytes));
|
|
159
|
+
if (!Array.isArray(parsed)) {
|
|
160
|
+
throw new Error("shell snapshot is not an array");
|
|
161
|
+
}
|
|
162
|
+
return parsed as ShellSnapshotRecord[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Incremental reader over a streamed frame (a Cache API response body or a KV
|
|
167
|
+
* `{ type: "stream" }` value). The serve path reads the head and exactly the
|
|
168
|
+
* prelude's bytes before it commits; the snapshot is whatever remains.
|
|
169
|
+
*/
|
|
170
|
+
export class ShellFrameReader {
|
|
171
|
+
private readonly reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
172
|
+
private chunks: Uint8Array[] = [];
|
|
173
|
+
private buffered = 0;
|
|
174
|
+
private done = false;
|
|
175
|
+
/** Bytes of the prefix and head, once readHead has read them. */
|
|
176
|
+
headBytes = 0;
|
|
177
|
+
|
|
178
|
+
constructor(body: ReadableStream<Uint8Array>) {
|
|
179
|
+
this.reader = body.getReader();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Exactly `n` bytes, or null when the stream ends first. */
|
|
183
|
+
async take(n: number): Promise<Uint8Array | null> {
|
|
184
|
+
while (this.buffered < n && !this.done) {
|
|
185
|
+
const { done, value } = await this.reader.read();
|
|
186
|
+
if (done) {
|
|
187
|
+
this.done = true;
|
|
188
|
+
} else if (value.length > 0) {
|
|
189
|
+
this.chunks.push(value);
|
|
190
|
+
this.buffered += value.length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (this.buffered < n) return null;
|
|
194
|
+
const out = concat(this.chunks, n);
|
|
195
|
+
const leftover = this.buffered - n;
|
|
196
|
+
const last = this.chunks[this.chunks.length - 1]!;
|
|
197
|
+
this.chunks = leftover > 0 ? [last.subarray(last.length - leftover)] : [];
|
|
198
|
+
this.buffered = leftover;
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The head, or null when the magic, length, or JSON is malformed or short. */
|
|
203
|
+
async readHead(): Promise<ShellFrameHead | null> {
|
|
204
|
+
const prefix = await this.take(PREFIX_BYTES);
|
|
205
|
+
if (!prefix || MAGIC.some((byte, index) => prefix[index] !== byte)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const lengthDigits = textDecoder.decode(prefix.subarray(MAGIC.length));
|
|
209
|
+
if (!/^[0-9a-f]{8}$/.test(lengthDigits)) return null;
|
|
210
|
+
const headBytes = await this.take(Number.parseInt(lengthDigits, 16));
|
|
211
|
+
if (!headBytes) return null;
|
|
212
|
+
this.headBytes = PREFIX_BYTES + headBytes.length;
|
|
213
|
+
let head: unknown;
|
|
214
|
+
try {
|
|
215
|
+
head = JSON.parse(textDecoder.decode(headBytes));
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return isShellFrameHead(head) ? head : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Everything after what was taken, to the end of the stream. */
|
|
223
|
+
async readRest(): Promise<Uint8Array> {
|
|
224
|
+
while (!this.done) {
|
|
225
|
+
const { done, value } = await this.reader.read();
|
|
226
|
+
if (done) {
|
|
227
|
+
this.done = true;
|
|
228
|
+
} else if (value.length > 0) {
|
|
229
|
+
this.chunks.push(value);
|
|
230
|
+
this.buffered += value.length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const out = concat(this.chunks, this.buffered);
|
|
234
|
+
this.chunks = [];
|
|
235
|
+
this.buffered = 0;
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Stop reading and release the body. */
|
|
240
|
+
cancel(): void {
|
|
241
|
+
this.done = true;
|
|
242
|
+
this.reader.cancel().catch(() => {});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The first `n` bytes of `chunks` as one array (no copy for a single chunk). */
|
|
247
|
+
function concat(chunks: Uint8Array[], n: number): Uint8Array {
|
|
248
|
+
if (chunks.length === 1 && chunks[0]!.length === n) return chunks[0]!;
|
|
249
|
+
const out = new Uint8Array(n);
|
|
250
|
+
let offset = 0;
|
|
251
|
+
for (const chunk of chunks) {
|
|
252
|
+
if (offset >= n) break;
|
|
253
|
+
const part = chunk.subarray(0, Math.min(chunk.length, n - offset));
|
|
254
|
+
out.set(part, offset);
|
|
255
|
+
offset += part.length;
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
// Tag-marker memo
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// CFCacheStore's per-request tag-marker state: the data families' marker memo
|
|
6
|
+
// and in-flight reads, the PPR shell read's own marker record, and the prefix
|
|
7
|
+
// and sentinel of the optional per-colo L1 marker cache. The maps are module
|
|
8
|
+
// singletons keyed by the request-context object, then the store INSTANCE.
|
|
9
|
+
// The request's own invalidations are not here: they are the per-request
|
|
10
|
+
// mask (request-tag-mask.ts), which every marker check consults first.
|
|
11
|
+
|
|
12
|
+
import type { MarkerMemoOutcome } from "../isolate-tag-memo.js";
|
|
13
|
+
import {
|
|
14
|
+
perRequestStoreValue,
|
|
15
|
+
type PerRequestStoreMap,
|
|
16
|
+
} from "../request-tag-mask.js";
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Cache-API path prefix for the optional per-colo L1 cache of tag-invalidation
|
|
@@ -42,38 +48,28 @@ export const TAG_MARKER_ABSENT = "none";
|
|
|
42
48
|
* short-circuits when a store has no KV — and, in purge mode, through
|
|
43
49
|
* isL1Invalidated(), which consults ONLY this memo on an L1 hit (no KV read),
|
|
44
50
|
* so a purge-mode store allocates the memo even without KV for same-request
|
|
45
|
-
* read-your-own-writes after updateTag().
|
|
51
|
+
* read-your-own-writes after updateTag()/revalidateTag().
|
|
46
52
|
*
|
|
47
53
|
* Without the memo, isGloballyInvalidated() issues a KV read per tag on every
|
|
48
54
|
* tagged cache read, so a page composed of many segments/items sharing a tag
|
|
49
55
|
* pays that cost N times. The memo collapses it to one KV read per distinct tag
|
|
50
|
-
* per (request, store).
|
|
51
|
-
*
|
|
52
|
-
*
|
|
56
|
+
* per (request, store). It holds only values read from L1 or KV: the request's
|
|
57
|
+
* own invalidations are the mask (request-tag-mask.ts), checked before any
|
|
58
|
+
* marker read, so a value in the memo is always safe to publish.
|
|
53
59
|
*
|
|
54
60
|
* It does NOT span requests, so a hot single-entry route still pays one KV read
|
|
55
61
|
* per request; that read hits Cloudflare KV's own edge read cache for hot keys.
|
|
62
|
+
* A context derived from the request (Object.create(reqCtx): a PPR HIT tail, a
|
|
63
|
+
* shell capture) keeps a memo of its own, so its marker reads stay its own.
|
|
56
64
|
*/
|
|
57
|
-
const tagMarkerMemo
|
|
58
|
-
|
|
59
|
-
WeakMap<object, Map<string, number | null>>
|
|
60
|
-
>();
|
|
65
|
+
const tagMarkerMemo: PerRequestStoreMap<Map<string, number | null>> =
|
|
66
|
+
new WeakMap();
|
|
61
67
|
|
|
62
68
|
export function getTagMarkerMemo(
|
|
63
69
|
ctx: object,
|
|
64
70
|
store: object,
|
|
65
71
|
): Map<string, number | null> {
|
|
66
|
-
|
|
67
|
-
if (!byStore) {
|
|
68
|
-
byStore = new WeakMap();
|
|
69
|
-
tagMarkerMemo.set(ctx, byStore);
|
|
70
|
-
}
|
|
71
|
-
let memo = byStore.get(store);
|
|
72
|
-
if (!memo) {
|
|
73
|
-
memo = new Map();
|
|
74
|
-
byStore.set(store, memo);
|
|
75
|
-
}
|
|
76
|
-
return memo;
|
|
72
|
+
return perRequestStoreValue(tagMarkerMemo, ctx, store, () => new Map());
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
/**
|
|
@@ -85,24 +81,43 @@ export function getTagMarkerMemo(
|
|
|
85
81
|
* collapses those to a single KV read. Entries are dropped once resolved (the
|
|
86
82
|
* value is then in the memo), so this only spans the concurrent read window.
|
|
87
83
|
*/
|
|
88
|
-
const tagMarkerInflight
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
const tagMarkerInflight: PerRequestStoreMap<
|
|
85
|
+
Map<string, Promise<number | null>>
|
|
86
|
+
> = new WeakMap();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A request's PPR shell-read marker reads (issue #941), kept apart from the
|
|
90
|
+
* per-request memo above, HIT or MISS. A shell read may answer a marker from
|
|
91
|
+
* the isolate marker memo (isolate-tag-memo.ts), up to `markerMaxStaleMs`
|
|
92
|
+
* old; the data families must never see that value. Scar tissue: first the
|
|
93
|
+
* shell read wrote into the per-request memo, so a MISS request rendered and
|
|
94
|
+
* stored its `"use cache"` data under a stale marker; then a HIT copied its
|
|
95
|
+
* entry's tags across, which a partial navigation's matchPartial (on the same
|
|
96
|
+
* context as its replay gate's getShell) read, and a `cache()` segment it
|
|
97
|
+
* wrote kept the stale item past `markerMaxStaleMs` under a fresh taggedAt.
|
|
98
|
+
*/
|
|
99
|
+
export interface ShellMarkerReads {
|
|
100
|
+
/** The read of each tag, settled or in flight (collapses concurrent reads). */
|
|
101
|
+
reads: Map<string, Promise<number | null>>;
|
|
102
|
+
/** How the isolate memo answered each tag (the marker row's `memo=`). */
|
|
103
|
+
outcomes: Map<string, MarkerMemoOutcome>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const shellMarkerReads: PerRequestStoreMap<ShellMarkerReads> = new WeakMap();
|
|
107
|
+
|
|
108
|
+
export function getShellMarkerReads(
|
|
109
|
+
ctx: object,
|
|
110
|
+
store: object,
|
|
111
|
+
): ShellMarkerReads {
|
|
112
|
+
return perRequestStoreValue(shellMarkerReads, ctx, store, () => ({
|
|
113
|
+
reads: new Map(),
|
|
114
|
+
outcomes: new Map(),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
92
117
|
|
|
93
118
|
export function getTagMarkerInflight(
|
|
94
119
|
ctx: object,
|
|
95
120
|
store: object,
|
|
96
121
|
): Map<string, Promise<number | null>> {
|
|
97
|
-
|
|
98
|
-
if (!byStore) {
|
|
99
|
-
byStore = new WeakMap();
|
|
100
|
-
tagMarkerInflight.set(ctx, byStore);
|
|
101
|
-
}
|
|
102
|
-
let inflight = byStore.get(store);
|
|
103
|
-
if (!inflight) {
|
|
104
|
-
inflight = new Map();
|
|
105
|
-
byStore.set(store, inflight);
|
|
106
|
-
}
|
|
107
|
-
return inflight;
|
|
122
|
+
return perRequestStoreValue(tagMarkerInflight, ctx, store, () => new Map());
|
|
108
123
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 4. If miss → run handler, cache if response has cache headers
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { requestHeaders } from "../server/request-headers.js";
|
|
16
17
|
import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
17
18
|
import { hasPerClientSignal } from "../browser/cookie-name.js";
|
|
18
19
|
import {
|
|
@@ -24,6 +25,7 @@ import { mayNeedSSR } from "../rsc/ssr-setup.js";
|
|
|
24
25
|
import { cacheKeyBase } from "./cache-key-utils.js";
|
|
25
26
|
import { runBackground } from "./background-task.js";
|
|
26
27
|
import { reportCacheError } from "./cache-error.js";
|
|
28
|
+
import { executionStart, predatesInvalidation } from "./tag-invalidation.js";
|
|
27
29
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
28
30
|
import {
|
|
29
31
|
SEGMENT_FRAGMENT_CAPABILITY_HEADER,
|
|
@@ -109,7 +111,10 @@ function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
|
109
111
|
/**
|
|
110
112
|
* Check if response should be cached based on Cache-Control headers
|
|
111
113
|
*/
|
|
112
|
-
function shouldCacheResponse(
|
|
114
|
+
function shouldCacheResponse(
|
|
115
|
+
response: Response,
|
|
116
|
+
requestCtx?: Pick<RequestContext, "_payloadVisitorTheme">,
|
|
117
|
+
): CacheDirectives | null {
|
|
113
118
|
// Only cache successful responses
|
|
114
119
|
if (response.status !== 200) {
|
|
115
120
|
return null;
|
|
@@ -125,10 +130,34 @@ function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
|
125
130
|
return null;
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
// The payload carries the visitor's theme (#978): the render ran before the
|
|
134
|
+
// response opted in (a Cache-Control written after next()), so
|
|
135
|
+
// payloadInitialTheme could not render the default.
|
|
136
|
+
if (requestCtx?._payloadVisitorTheme) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
const cacheControl = response.headers.get("Cache-Control");
|
|
129
141
|
return parseCacheControl(cacheControl);
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Whether this cache will store the response the current render produces, as
|
|
146
|
+
* far as the render can tell: the middleware runs it (a MISS or a stale
|
|
147
|
+
* refresh) and the response stub already opts in. payloadInitialTheme
|
|
148
|
+
* (rsc/full-payload.ts) then renders the no-cookie default theme (#978). A
|
|
149
|
+
* Cache-Control written after next() is not on the stub yet; for that render
|
|
150
|
+
* shouldCacheResponse refuses a payload marked `_payloadVisitorTheme`.
|
|
151
|
+
*/
|
|
152
|
+
export function documentCacheStoresRender(
|
|
153
|
+
reqCtx: Pick<RequestContext, "_documentCacheRender" | "res">,
|
|
154
|
+
): boolean {
|
|
155
|
+
return (
|
|
156
|
+
reqCtx._documentCacheRender === true &&
|
|
157
|
+
shouldCacheResponse(reqCtx.res) !== null
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
/**
|
|
133
162
|
* Throw the first error the drained render reported through Flight or Fizz
|
|
134
163
|
* onError. A component that throws after the 200 committed does not fail the
|
|
@@ -279,7 +308,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
279
308
|
// are not visible on ctx.url in production.
|
|
280
309
|
const rawUrl = new URL(ctx.request.url);
|
|
281
310
|
const isFragmentRecovery =
|
|
282
|
-
ctx.request.
|
|
311
|
+
requestHeaders(ctx.request).get(SEGMENT_FRAGMENT_RECOVERY_HEADER) === "1";
|
|
283
312
|
|
|
284
313
|
// Only cache GET requests — mutations and other methods must not be cached
|
|
285
314
|
if (ctx.request.method !== "GET") {
|
|
@@ -318,6 +347,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
318
347
|
return next();
|
|
319
348
|
}
|
|
320
349
|
|
|
350
|
+
// The write gate's start (#977): the request's, not this middleware's. A
|
|
351
|
+
// middleware ahead of this one can read tagged data the document bakes,
|
|
352
|
+
// and a stale refresh re-runs the handler over what they set.
|
|
353
|
+
const start = requestCtx._requestStart ?? executionStart();
|
|
354
|
+
|
|
321
355
|
// Determine request type for cache key differentiation.
|
|
322
356
|
// Uses rawUrl for _rsc* param checks and mayNeedSSR for Accept-based
|
|
323
357
|
// detection. Full-document RSC fetches must not share the HTML cache slot.
|
|
@@ -343,7 +377,8 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
343
377
|
// match-time capability gate.
|
|
344
378
|
const fragmentSuffix =
|
|
345
379
|
isPartial &&
|
|
346
|
-
(ctx.request.
|
|
380
|
+
(requestHeaders(ctx.request).get(SEGMENT_FRAGMENT_CAPABILITY_HEADER) ===
|
|
381
|
+
"1" ||
|
|
347
382
|
isFragmentRecovery)
|
|
348
383
|
? ":fragments"
|
|
349
384
|
: "";
|
|
@@ -374,6 +409,9 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
374
409
|
const cached = isFragmentRecovery
|
|
375
410
|
? null
|
|
376
411
|
: await store.getResponse(cacheKey);
|
|
412
|
+
// Every path past the lookup either returns a fresh HIT, which renders
|
|
413
|
+
// nothing, or renders a response this cache may store.
|
|
414
|
+
requestCtx._documentCacheRender = true;
|
|
377
415
|
|
|
378
416
|
if (cached && cached.response.status === 200) {
|
|
379
417
|
if (!cached.shouldRevalidate) {
|
|
@@ -409,7 +447,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
409
447
|
PHASES.background("document-revalidation"),
|
|
410
448
|
async () => {
|
|
411
449
|
const fresh = await next();
|
|
412
|
-
const directives = shouldCacheResponse(fresh);
|
|
450
|
+
const directives = shouldCacheResponse(fresh, requestCtx);
|
|
413
451
|
|
|
414
452
|
if (!directives || !fresh.body) return;
|
|
415
453
|
|
|
@@ -418,12 +456,16 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
418
456
|
// (same render-complete barrier as the miss path).
|
|
419
457
|
const body = await new Response(fresh.body).arrayBuffer();
|
|
420
458
|
throwIfRenderErrored(requestCtx);
|
|
459
|
+
// Not written when one of its tags was invalidated since
|
|
460
|
+
// the render started (#977).
|
|
461
|
+
const tags = collectRequestTags(requestCtx);
|
|
462
|
+
if (await predatesInvalidation(store, tags, start)) return;
|
|
421
463
|
await store.putResponse!(
|
|
422
464
|
cacheKey,
|
|
423
465
|
new Response(body, fresh),
|
|
424
466
|
directives.sMaxAge!,
|
|
425
467
|
directives.staleWhileRevalidate,
|
|
426
|
-
|
|
468
|
+
tags,
|
|
427
469
|
);
|
|
428
470
|
log(
|
|
429
471
|
`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`,
|
|
@@ -456,7 +498,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
456
498
|
const originalResponse = await next();
|
|
457
499
|
|
|
458
500
|
// 3. Cache if response has appropriate headers
|
|
459
|
-
const directives = shouldCacheResponse(originalResponse);
|
|
501
|
+
const directives = shouldCacheResponse(originalResponse, requestCtx);
|
|
460
502
|
|
|
461
503
|
if (directives) {
|
|
462
504
|
log(
|
|
@@ -483,12 +525,16 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
483
525
|
// body and its tag set consistent.
|
|
484
526
|
const body = await new Response(cacheStream).arrayBuffer();
|
|
485
527
|
throwIfRenderErrored(requestCtx);
|
|
528
|
+
// Not written when one of its tags was invalidated since the
|
|
529
|
+
// render started (#977).
|
|
530
|
+
const tags = collectRequestTags(requestCtx);
|
|
531
|
+
if (await predatesInvalidation(store, tags, start)) return;
|
|
486
532
|
await store.putResponse!(
|
|
487
533
|
cacheKey,
|
|
488
534
|
new Response(body, originalResponse),
|
|
489
535
|
directives.sMaxAge!,
|
|
490
536
|
directives.staleWhileRevalidate,
|
|
491
|
-
|
|
537
|
+
tags,
|
|
492
538
|
);
|
|
493
539
|
} catch (error) {
|
|
494
540
|
// Detached waitUntil task — pass the captured requestCtx so onError
|