@rangojs/router 0.18.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/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -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 +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +24 -4
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- 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/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/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/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- 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 +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +89 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- 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-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- 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 +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +960 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- 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 +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +64 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -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 +4 -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 +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +197 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +105 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- 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 +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- 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 +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +28 -3
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +150 -25
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- 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 +30 -10
- package/src/rsc/handler.ts +12 -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 +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +189 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +123 -84
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -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 +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- 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 +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -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 +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
// and in-flight reads, the PPR shell read's own marker record, and the prefix
|
|
7
7
|
// and sentinel of the optional per-colo L1 marker cache. The maps are module
|
|
8
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.
|
|
9
11
|
|
|
10
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
|
|
@@ -24,32 +30,6 @@ export const TAG_MARKER_CACHE_PREFIX = "__tagmarker__/";
|
|
|
24
30
|
*/
|
|
25
31
|
export const TAG_MARKER_ABSENT = "none";
|
|
26
32
|
|
|
27
|
-
/**
|
|
28
|
-
* Request context -> store instance -> T, both levels weakly held. Also
|
|
29
|
-
* VercelCacheStore's request tag mask (#973).
|
|
30
|
-
*/
|
|
31
|
-
export type PerRequestStoreMap<T> = WeakMap<object, WeakMap<object, T>>;
|
|
32
|
-
|
|
33
|
-
/** The value for (ctx, store), created on first use. */
|
|
34
|
-
export function perRequestStoreValue<T>(
|
|
35
|
-
map: PerRequestStoreMap<T>,
|
|
36
|
-
ctx: object,
|
|
37
|
-
store: object,
|
|
38
|
-
create: () => T,
|
|
39
|
-
): T {
|
|
40
|
-
let byStore = map.get(ctx);
|
|
41
|
-
if (!byStore) {
|
|
42
|
-
byStore = new WeakMap();
|
|
43
|
-
map.set(ctx, byStore);
|
|
44
|
-
}
|
|
45
|
-
let value = byStore.get(store);
|
|
46
|
-
if (value === undefined) {
|
|
47
|
-
value = create();
|
|
48
|
-
byStore.set(store, value);
|
|
49
|
-
}
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
/**
|
|
54
34
|
* Per-request memo of tag-invalidation markers (tag -> latest invalidatedAt, or
|
|
55
35
|
* null when no marker exists). Keyed first by the request context object (so it
|
|
@@ -73,92 +53,23 @@ export function perRequestStoreValue<T>(
|
|
|
73
53
|
* Without the memo, isGloballyInvalidated() issues a KV read per tag on every
|
|
74
54
|
* tagged cache read, so a page composed of many segments/items sharing a tag
|
|
75
55
|
* pays that cost N times. The memo collapses it to one KV read per distinct tag
|
|
76
|
-
* per (request, store).
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* invalidation from the memo, without waiting for the KV put that
|
|
80
|
-
* revalidateTag() leaves to waitUntil.
|
|
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.
|
|
81
59
|
*
|
|
82
60
|
* It does NOT span requests, so a hot single-entry route still pays one KV read
|
|
83
61
|
* per request; that read hits Cloudflare KV's own edge read cache for hot keys.
|
|
84
62
|
* A context derived from the request (Object.create(reqCtx): a PPR HIT tail, a
|
|
85
|
-
* shell capture) keeps a memo of its own, so its marker reads stay its own
|
|
86
|
-
* starts from the request's mask (requestMasks) instead of empty, and takes
|
|
87
|
-
* every mask the request sets later (requestMemos).
|
|
63
|
+
* shell capture) keeps a memo of its own, so its marker reads stay its own.
|
|
88
64
|
*/
|
|
89
65
|
const tagMarkerMemo: PerRequestStoreMap<Map<string, number | null>> =
|
|
90
66
|
new WeakMap();
|
|
91
67
|
|
|
92
|
-
/**
|
|
93
|
-
* The tags each request invalidated through each store, with the
|
|
94
|
-
* invalidatedAt its mask carries (#973). Keyed by the request's root context
|
|
95
|
-
* (RequestContext._requestRoot) rather than by the context that called, so a
|
|
96
|
-
* derived context sees the request's mask and a mask it sets reaches the
|
|
97
|
-
* request. Its values are unconfirmed until the KV put lands: a marker read
|
|
98
|
-
* that finds one of them publishes nothing to other requests
|
|
99
|
-
* (CFCacheStore.fetchTagMarker).
|
|
100
|
-
*/
|
|
101
|
-
const requestMasks: PerRequestStoreMap<Map<string, number>> = new WeakMap();
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Every memo of a request, whichever context owns it, per (root, store): a
|
|
105
|
-
* mask set later reaches the memos that already exist (maskRequestTags), not
|
|
106
|
-
* only the caller's. They live as long as the request does.
|
|
107
|
-
*/
|
|
108
|
-
const requestMemos: PerRequestStoreMap<Set<Map<string, number | null>>> =
|
|
109
|
-
new WeakMap();
|
|
110
|
-
|
|
111
|
-
function requestRoot(ctx: object): object {
|
|
112
|
-
return (ctx as { _requestRoot?: object })._requestRoot ?? ctx;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
68
|
export function getTagMarkerMemo(
|
|
116
69
|
ctx: object,
|
|
117
70
|
store: object,
|
|
118
71
|
): Map<string, number | null> {
|
|
119
|
-
return perRequestStoreValue(tagMarkerMemo, ctx, store, () =>
|
|
120
|
-
const root = requestRoot(ctx);
|
|
121
|
-
const memo = new Map<string, number | null>(
|
|
122
|
-
requestMasks.get(root)?.get(store),
|
|
123
|
-
);
|
|
124
|
-
perRequestStoreValue(requestMemos, root, store, () => new Set()).add(memo);
|
|
125
|
-
return memo;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Mask `tags` at `at` for the rest of the request: in the request's mask and
|
|
131
|
-
* in every memo the request's contexts hold for `store` (a memo created later
|
|
132
|
-
* starts from the mask).
|
|
133
|
-
*/
|
|
134
|
-
export function maskRequestTags(
|
|
135
|
-
ctx: object,
|
|
136
|
-
store: object,
|
|
137
|
-
tags: readonly string[],
|
|
138
|
-
at: number,
|
|
139
|
-
): void {
|
|
140
|
-
const root = requestRoot(ctx);
|
|
141
|
-
const mask = perRequestStoreValue(
|
|
142
|
-
requestMasks,
|
|
143
|
-
root,
|
|
144
|
-
store,
|
|
145
|
-
() => new Map<string, number>(),
|
|
146
|
-
);
|
|
147
|
-
getTagMarkerMemo(ctx, store);
|
|
148
|
-
const memos = requestMemos.get(root)?.get(store) ?? [];
|
|
149
|
-
for (const tag of tags) {
|
|
150
|
-
mask.set(tag, at);
|
|
151
|
-
for (const memo of memos) memo.set(tag, at);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Whether this request masked `tag` through `store` (maskRequestTags). */
|
|
156
|
-
export function isRequestMasked(
|
|
157
|
-
ctx: object,
|
|
158
|
-
store: object,
|
|
159
|
-
tag: string,
|
|
160
|
-
): boolean {
|
|
161
|
-
return requestMasks.get(requestRoot(ctx))?.get(store)?.has(tag) ?? false;
|
|
72
|
+
return perRequestStoreValue(tagMarkerMemo, ctx, store, () => new Map());
|
|
162
73
|
}
|
|
163
74
|
|
|
164
75
|
/**
|
|
@@ -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
|
|
@@ -134,6 +134,29 @@ export function captureHandleOwners(
|
|
|
134
134
|
return owners;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** How restoreHandles delivers a record's loader-owned values. */
|
|
138
|
+
export interface OwnedPushDelivery {
|
|
139
|
+
/**
|
|
140
|
+
* Asked once per owner delivered through pushReplayed
|
|
141
|
+
* (setupLoaderAccess _claimLoaderPushes); an owner it refuses is skipped.
|
|
142
|
+
*/
|
|
143
|
+
claim?: (loaderId: string) => boolean;
|
|
144
|
+
/**
|
|
145
|
+
* A PPR shell's record (capture, document HIT tail, navigation replay): the
|
|
146
|
+
* loaders the route runs on the live lane (liveLaneLoaderIds), marked on
|
|
147
|
+
* the store (HandleStore.markLiveLane). A hole's values are replayed
|
|
148
|
+
* unclaimed, so its own cache() HIT redelivers its entry's pushes in their
|
|
149
|
+
* place (HandleStore.redeliverReplays), and its run ending without a push
|
|
150
|
+
* drops them (HandleStore.settleLoaderRun).
|
|
151
|
+
*/
|
|
152
|
+
liveLane?: ReadonlySet<string>;
|
|
153
|
+
/**
|
|
154
|
+
* A document HIT tail: every owner outside `liveLane` goes through
|
|
155
|
+
* pushRestored and stands, unclaimed.
|
|
156
|
+
*/
|
|
157
|
+
restore?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
/**
|
|
138
161
|
* Restore handle data from a cached snapshot into the handle store.
|
|
139
162
|
* Used when serving cached segments to replay their handle data: a route
|
|
@@ -141,21 +164,32 @@ export function captureHandleOwners(
|
|
|
141
164
|
*
|
|
142
165
|
* A segment with `owners` is replaced with empty arrays and re-pushed in
|
|
143
166
|
* recorded order, an owned value through pushReplayed: if the owning loader
|
|
144
|
-
* runs
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* (
|
|
148
|
-
* loader
|
|
149
|
-
*
|
|
150
|
-
*
|
|
167
|
+
* runs (a client navigation replaying a ppr route's cache() record runs its
|
|
168
|
+
* `ssr: false` loaders), its live pushes replace the recorded ones in place
|
|
169
|
+
* instead of appending a second copy. `owned.claim` is asked once per
|
|
170
|
+
* owner, so the loader's own cache() HIT does not replay its pushes a second
|
|
171
|
+
* time (loader-cache.ts replayLoaderHandles). A record without owners
|
|
172
|
+
* restores as a plain replay.
|
|
173
|
+
*
|
|
174
|
+
* `owned.restore`: a document HIT tail restoring the shell's doc record
|
|
175
|
+
* (withCacheLookup). A bake-lane owner's values are the prelude's, so they go
|
|
176
|
+
* through pushRestored and stand: a run of that loader on the HIT (a
|
|
177
|
+
* promise-carrying `ssr: false` loader, one whose record asks for a run)
|
|
178
|
+
* reads the store, and its settled pushes are dropped, not swapped in.
|
|
179
|
+
* Nothing is claimed, so the loader's own cache() HIT still replays the
|
|
180
|
+
* pushes the record could not keep (the thenable ones pushRestored lets
|
|
181
|
+
* through). A loader the route also runs on the live lane is a hole: its
|
|
182
|
+
* values keep pushReplayed, unclaimed, and its live run replaces them
|
|
183
|
+
* (#936), even inside a restored loader's body (`owned.liveLane`).
|
|
151
184
|
*/
|
|
152
185
|
export function restoreHandles(
|
|
153
186
|
handles: Record<string, SegmentHandleData>,
|
|
154
187
|
handleStore: HandleStore,
|
|
155
188
|
owners?: HandleOwners,
|
|
156
|
-
|
|
189
|
+
owned?: OwnedPushDelivery,
|
|
157
190
|
): void {
|
|
158
191
|
let delivers: Map<string, boolean> | undefined;
|
|
192
|
+
if (owned?.liveLane) handleStore.markLiveLane(owned.liveLane);
|
|
159
193
|
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
160
194
|
if (Object.keys(segHandles).length === 0) continue;
|
|
161
195
|
const segOwners = owners?.[segId];
|
|
@@ -174,10 +208,19 @@ export function restoreHandles(
|
|
|
174
208
|
handleStore.push(handleName, segId, values[i]);
|
|
175
209
|
continue;
|
|
176
210
|
}
|
|
211
|
+
const hole = owned?.liveLane?.has(owner) === true;
|
|
212
|
+
if (owned?.restore && !hole) {
|
|
213
|
+
handleStore.pushRestored(handleName, segId, values[i], owner);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (hole) {
|
|
217
|
+
handleStore.pushReplayed(handleName, segId, values[i], owner);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
177
220
|
delivers ??= new Map();
|
|
178
221
|
let deliver = delivers.get(owner);
|
|
179
222
|
if (deliver === undefined) {
|
|
180
|
-
deliver = claim ? claim(owner) : true;
|
|
223
|
+
deliver = owned?.claim ? owned.claim(owner) : true;
|
|
181
224
|
delivers.set(owner, deliver);
|
|
182
225
|
}
|
|
183
226
|
if (deliver) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The isolate's order of tag invalidations and cache execution starts
|
|
3
|
+
* (#973, #977): what the write gate in tag-invalidation.ts
|
|
4
|
+
* (predatesInvalidation) and the "use cache" join check read to tell whether
|
|
5
|
+
* an execution's value may predate an invalidation of one of its tags.
|
|
6
|
+
* Imports nothing, so request-context.ts can take each request's start here
|
|
7
|
+
* without a cycle through tag-invalidation.ts.
|
|
8
|
+
*
|
|
9
|
+
* - `seq` (#973): a counter, not Date.now(). Workers advance the clock only
|
|
10
|
+
* on I/O, so an execution started right after an invalidation would share
|
|
11
|
+
* its millisecond and read as older.
|
|
12
|
+
* - `tags` (#977): tag -> the `seq` of its latest invalidation, from every
|
|
13
|
+
* request, oldest first. Bounded: dropping the oldest raises `forgotten`,
|
|
14
|
+
* and an execution that started before a forgotten invalidation counts as
|
|
15
|
+
* invalidated, whatever its tags (a miss, never a stale read).
|
|
16
|
+
*
|
|
17
|
+
* Keyed by tag name alone, not by router or store: two routers in one
|
|
18
|
+
* isolate that share a tag name skip each other's writes started before an
|
|
19
|
+
* invalidation. That costs a miss, never a stale read.
|
|
20
|
+
*
|
|
21
|
+
* On globalThis: a second evaluated copy of this module (a duplicated
|
|
22
|
+
* package, a dev re-evaluation) must read the invalidations the first one
|
|
23
|
+
* recorded, or its writers pass the gate.
|
|
24
|
+
*/
|
|
25
|
+
interface IsolateOrder {
|
|
26
|
+
seq: number;
|
|
27
|
+
tags: Map<string, number>;
|
|
28
|
+
forgotten: number;
|
|
29
|
+
}
|
|
30
|
+
const order: IsolateOrder = ((globalThis as any)[
|
|
31
|
+
Symbol.for("rangojs-router:tag-invalidation-order")
|
|
32
|
+
] ??= { seq: 0, tags: new Map(), forgotten: 0 });
|
|
33
|
+
const ISOLATE_INVALIDATION_HISTORY = 1024;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where a cache execution ("use cache", a loader's own cache(), a route
|
|
37
|
+
* cache() render, a request's document) started: its position in this
|
|
38
|
+
* isolate's order, and the store clock for invalidations this isolate did
|
|
39
|
+
* not see.
|
|
40
|
+
*/
|
|
41
|
+
export interface ExecutionStart {
|
|
42
|
+
readonly seq: number;
|
|
43
|
+
readonly at: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Record an execution's start, before it reads its data. */
|
|
47
|
+
export function executionStart(): ExecutionStart {
|
|
48
|
+
return { seq: order.seq, at: Date.now() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether this isolate invalidated any of `tags` after `since` (the `seq` of
|
|
53
|
+
* an ExecutionStart), in any request: the execution's value may predate the
|
|
54
|
+
* invalidation, so it is neither joined nor written (#973, #977).
|
|
55
|
+
*/
|
|
56
|
+
export function invalidatedSince(
|
|
57
|
+
tags: readonly string[] | undefined,
|
|
58
|
+
since: number,
|
|
59
|
+
): boolean {
|
|
60
|
+
if (!tags || tags.length === 0) return false;
|
|
61
|
+
if (since < order.forgotten) return true;
|
|
62
|
+
for (const tag of tags) {
|
|
63
|
+
const at = order.tags.get(tag);
|
|
64
|
+
if (at !== undefined && at > since) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record an invalidation of `tags` in this isolate's order
|
|
71
|
+
* (updateTag()/revalidateTag(), before any store sees it).
|
|
72
|
+
*/
|
|
73
|
+
export function markInvalidated(tags: readonly string[]): void {
|
|
74
|
+
const at = ++order.seq;
|
|
75
|
+
for (const tag of tags) {
|
|
76
|
+
order.tags.delete(tag);
|
|
77
|
+
order.tags.set(tag, at);
|
|
78
|
+
}
|
|
79
|
+
for (const [tag, seq] of order.tags) {
|
|
80
|
+
if (order.tags.size <= ISOLATE_INVALIDATION_HISTORY) break;
|
|
81
|
+
order.tags.delete(tag);
|
|
82
|
+
order.forgotten = seq;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-request tag mask behind read-your-own-writes (#973).
|
|
3
|
+
*
|
|
4
|
+
* updateTag() and revalidateTag() call a store's invalidateTags() inside the
|
|
5
|
+
* request, and a built-in store masks the tags here before its first await:
|
|
6
|
+
* every later read in that request, through any context derived from it,
|
|
7
|
+
* treats an entry carrying one of the tags, written at or before the mask, as
|
|
8
|
+
* invalidated, without waiting for the durable write (the KV marker put, the
|
|
9
|
+
* tag purge, expireTag()) that revalidateTag() leaves to waitUntil. The mask
|
|
10
|
+
* only ever turns this request's hits into misses: if the durable write then
|
|
11
|
+
* fails, the request paid extra misses, never a stale read, and other
|
|
12
|
+
* requests read the durable state. CFCacheStore and VercelCacheStore share it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Request context -> store instance -> T, both levels weakly held. */
|
|
16
|
+
export type PerRequestStoreMap<T> = WeakMap<object, WeakMap<object, T>>;
|
|
17
|
+
|
|
18
|
+
/** The value for (ctx, store), created on first use. */
|
|
19
|
+
export function perRequestStoreValue<T>(
|
|
20
|
+
map: PerRequestStoreMap<T>,
|
|
21
|
+
ctx: object,
|
|
22
|
+
store: object,
|
|
23
|
+
create: () => T,
|
|
24
|
+
): T {
|
|
25
|
+
let byStore = map.get(ctx);
|
|
26
|
+
if (!byStore) {
|
|
27
|
+
byStore = new WeakMap();
|
|
28
|
+
map.set(ctx, byStore);
|
|
29
|
+
}
|
|
30
|
+
let value = byStore.get(store);
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
value = create();
|
|
33
|
+
byStore.set(store, value);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The request a context belongs to: RequestContext._requestRoot, which
|
|
40
|
+
* createRequestContext defines and a derived context (Object.create: a shell
|
|
41
|
+
* capture, a PPR HIT tail) inherits. A bare test context is its own root.
|
|
42
|
+
*/
|
|
43
|
+
export function requestRoot<T extends object>(
|
|
44
|
+
ctx: T & { _requestRoot?: T },
|
|
45
|
+
): T {
|
|
46
|
+
return ctx._requestRoot ?? ctx;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Request root -> store -> tag -> the invalidatedAt its mask carries. */
|
|
50
|
+
const requestMasks: PerRequestStoreMap<Map<string, number>> = new WeakMap();
|
|
51
|
+
|
|
52
|
+
/** Mask `tags` at `at` through `store` for the rest of `ctx`'s request. */
|
|
53
|
+
export function maskRequestTags(
|
|
54
|
+
ctx: object,
|
|
55
|
+
store: object,
|
|
56
|
+
tags: readonly string[],
|
|
57
|
+
at: number,
|
|
58
|
+
): void {
|
|
59
|
+
const mask = perRequestStoreValue(
|
|
60
|
+
requestMasks,
|
|
61
|
+
requestRoot(ctx),
|
|
62
|
+
store,
|
|
63
|
+
() => new Map<string, number>(),
|
|
64
|
+
);
|
|
65
|
+
for (const tag of tags) mask.set(tag, at);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether `ctx`'s request masked one of `tags` through `store` at or after
|
|
70
|
+
* `taggedAt`. `>=`, as the stores' marker checks: an entry written in the
|
|
71
|
+
* invalidation's millisecond may hold a value computed before it, so it
|
|
72
|
+
* misses (a false miss, never a stale read).
|
|
73
|
+
*/
|
|
74
|
+
export function maskedForRequest(
|
|
75
|
+
ctx: object | undefined,
|
|
76
|
+
store: object,
|
|
77
|
+
tags: readonly string[] | undefined,
|
|
78
|
+
taggedAt: number,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (!ctx || !tags?.length) return false;
|
|
81
|
+
const mask = requestMasks.get(requestRoot(ctx))?.get(store);
|
|
82
|
+
if (!mask) return false;
|
|
83
|
+
for (const tag of tags) {
|
|
84
|
+
const at = mask.get(tag);
|
|
85
|
+
if (at !== undefined && at >= taggedAt) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A marker read the write gate issued (#977) and has not settled: the
|
|
92
|
+
* store's answer for one tag and the millisecond it was issued in.
|
|
93
|
+
*/
|
|
94
|
+
interface GateRead {
|
|
95
|
+
issuedAt: number;
|
|
96
|
+
answer: Promise<unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Request root -> store -> tag -> the gate read in flight for it. */
|
|
100
|
+
const gateReads: PerRequestStoreMap<Map<string, GateRead>> = new WeakMap();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A marker read for a write gate (isTagsInvalidatedSince, from
|
|
104
|
+
* tag-invalidation.ts predatesInvalidation), shared by the gates of a
|
|
105
|
+
* request that ask while it is in flight. A page's writes (its "use cache"
|
|
106
|
+
* misses, its route record, its document) finish together and often carry
|
|
107
|
+
* the same tags, and each read its own marker before: one KV or
|
|
108
|
+
* runtime-cache read per tag per write.
|
|
109
|
+
*
|
|
110
|
+
* A read answers only for invalidations before it was issued. So a gate
|
|
111
|
+
* shares one still in flight, issued at or after its execution started
|
|
112
|
+
* (`sinceMs` is the millisecond after that start), and the read is dropped
|
|
113
|
+
* once it settles: a gate asking later reads again, and a shared read
|
|
114
|
+
* leaves open at most one marker round-trip. Reusing a settled read let a
|
|
115
|
+
* gate asking long after it miss another isolate's invalidation in
|
|
116
|
+
* between (a capture's putShell answered by a "use cache" write's read
|
|
117
|
+
* from the start of the capture), and kept a timed-out read's fail-open
|
|
118
|
+
* answer for the rest of the request. Without a request context `read()`
|
|
119
|
+
* runs unshared.
|
|
120
|
+
*/
|
|
121
|
+
export function gateMarkerRead<T>(
|
|
122
|
+
ctx: object | undefined,
|
|
123
|
+
store: object,
|
|
124
|
+
tag: string,
|
|
125
|
+
sinceMs: number,
|
|
126
|
+
read: () => Promise<T>,
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
if (!ctx) return read();
|
|
129
|
+
const reads = perRequestStoreValue(
|
|
130
|
+
gateReads,
|
|
131
|
+
requestRoot(ctx),
|
|
132
|
+
store,
|
|
133
|
+
() => new Map<string, GateRead>(),
|
|
134
|
+
);
|
|
135
|
+
const inFlight = reads.get(tag);
|
|
136
|
+
if (inFlight && inFlight.issuedAt >= sinceMs - 1) {
|
|
137
|
+
return inFlight.answer as Promise<T>;
|
|
138
|
+
}
|
|
139
|
+
const answer = read();
|
|
140
|
+
const entry: GateRead = { issuedAt: Date.now(), answer };
|
|
141
|
+
reads.set(tag, entry);
|
|
142
|
+
const settled = (): void => {
|
|
143
|
+
if (reads.get(tag) === entry) reads.delete(tag);
|
|
144
|
+
};
|
|
145
|
+
answer.then(settled, settled);
|
|
146
|
+
return answer;
|
|
147
|
+
}
|