@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
|
@@ -134,27 +134,62 @@ 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
|
|
140
163
|
* cache() record owns its segments' arrays, so it REPLACES them.
|
|
141
164
|
*
|
|
142
165
|
* A segment with `owners` is replaced with empty arrays and re-pushed in
|
|
143
|
-
* recorded order, an owned value through pushReplayed: the owning loader
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* loader's own cache() HIT does not replay its pushes a second
|
|
148
|
-
* (loader-cache.ts replayLoaderHandles)
|
|
149
|
-
*
|
|
166
|
+
* recorded order, an owned value through pushReplayed: if the owning loader
|
|
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`).
|
|
150
184
|
*/
|
|
151
185
|
export function restoreHandles(
|
|
152
186
|
handles: Record<string, SegmentHandleData>,
|
|
153
187
|
handleStore: HandleStore,
|
|
154
188
|
owners?: HandleOwners,
|
|
155
|
-
|
|
189
|
+
owned?: OwnedPushDelivery,
|
|
156
190
|
): void {
|
|
157
191
|
let delivers: Map<string, boolean> | undefined;
|
|
192
|
+
if (owned?.liveLane) handleStore.markLiveLane(owned.liveLane);
|
|
158
193
|
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
159
194
|
if (Object.keys(segHandles).length === 0) continue;
|
|
160
195
|
const segOwners = owners?.[segId];
|
|
@@ -173,10 +208,19 @@ export function restoreHandles(
|
|
|
173
208
|
handleStore.push(handleName, segId, values[i]);
|
|
174
209
|
continue;
|
|
175
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
|
+
}
|
|
176
220
|
delivers ??= new Map();
|
|
177
221
|
let deliver = delivers.get(owner);
|
|
178
222
|
if (deliver === undefined) {
|
|
179
|
-
deliver = claim ? claim(owner) : true;
|
|
223
|
+
deliver = owned?.claim ? owned.claim(owner) : true;
|
|
180
224
|
delivers.set(owner, deliver);
|
|
181
225
|
}
|
|
182
226
|
if (deliver) {
|
package/src/cache/index.ts
CHANGED
|
@@ -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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-isolate tag-marker memos shared by the built-in stores (issue #941;
|
|
3
|
+
* docs/design/shell-entry-layout.md "The tag-marker memo").
|
|
4
|
+
*
|
|
5
|
+
* A tagged shell HIT waits for its tag-invalidation markers before the first
|
|
6
|
+
* byte. Two things here shorten that wait:
|
|
7
|
+
*
|
|
8
|
+
* - {@link TagNameHints}: which tag NAMES a shell key carried last time, so a
|
|
9
|
+
* HIT can start the marker reads alongside the entry read instead of after
|
|
10
|
+
* the entry's head names its tags. Never marker values.
|
|
11
|
+
* - {@link TagMarkerMemo}: the marker VALUES (a tag's latest invalidatedAt,
|
|
12
|
+
* or null), served stale-while-revalidate. A value younger than
|
|
13
|
+
* `markerFreshMs` is used as is; one younger than `markerMaxStaleMs` is used
|
|
14
|
+
* and refreshed from the store in the background; an older one blocks on
|
|
15
|
+
* the store read as before.
|
|
16
|
+
*
|
|
17
|
+
* Simple reads get SWR; a mutation gets correct reads. The request that runs
|
|
18
|
+
* updateTag()/revalidateTag() reads its own writes through the per-request
|
|
19
|
+
* memo; the isolate that runs it writes the new value through into this memo;
|
|
20
|
+
* and its response sets the fresh-reads cookie (request-context.ts), so the
|
|
21
|
+
* same user's next requests on OTHER isolates skip this memo and the shell
|
|
22
|
+
* memo for as long as either can be stale. Other users can see a marker up to
|
|
23
|
+
* `markerMaxStaleMs` old, on top of the platform's own consistency
|
|
24
|
+
* (Cloudflare KV is eventually consistent across locations, up to about 60 s;
|
|
25
|
+
* Vercel's `tm` markers are regional).
|
|
26
|
+
*
|
|
27
|
+
* Only PPR shell reads use the value memo. Cached data families (`cache()`,
|
|
28
|
+
* `"use cache"`, responses) and the shell write gate
|
|
29
|
+
* (`isTagsInvalidatedSince`) keep reading their markers, in every request,
|
|
30
|
+
* whatever its shell read found: a shell read's memoized values never reach
|
|
31
|
+
* the data families' per-request memo (cf-tag-marker-memo.ts
|
|
32
|
+
* ShellMarkerReads).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
36
|
+
import type { ShellReadStats } from "./types.js";
|
|
37
|
+
|
|
38
|
+
/** Default freshness of a memoized marker on CFCacheStore (ms). */
|
|
39
|
+
export const DEFAULT_CF_MARKER_FRESH_MS = 1000;
|
|
40
|
+
/** Default freshness of a memoized marker on VercelCacheStore (ms). */
|
|
41
|
+
export const DEFAULT_VERCEL_MARKER_FRESH_MS = 300;
|
|
42
|
+
/** Default oldest memoized marker served while it refreshes (ms), CF. */
|
|
43
|
+
export const DEFAULT_CF_MARKER_MAX_STALE_MS = 10_000;
|
|
44
|
+
/**
|
|
45
|
+
* The same cap on VercelCacheStore: the runtime cache expires tags globally
|
|
46
|
+
* within about 300 ms, so a much longer stale window would widen that for
|
|
47
|
+
* other users more than KV's own up-to-60 s cross-location lag does on CF.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_VERCEL_MARKER_MAX_STALE_MS = 2000;
|
|
50
|
+
/** Most tags a marker memo holds (least recently used evicted). */
|
|
51
|
+
const MARKER_MEMO_MAX_ENTRIES = 4096;
|
|
52
|
+
/**
|
|
53
|
+
* Most shell keys a hint map holds (least recently used evicted).
|
|
54
|
+
* @internal Exported for tests that fill the map to its cap.
|
|
55
|
+
*/
|
|
56
|
+
export const TAG_HINTS_MAX_ENTRIES = 2048;
|
|
57
|
+
|
|
58
|
+
/** How a marker read was answered, for the debugPerformance marker row. */
|
|
59
|
+
export type MarkerMemoOutcome = "fresh" | "stale" | "read" | "bypass";
|
|
60
|
+
|
|
61
|
+
interface MarkerSlot {
|
|
62
|
+
value: number | null;
|
|
63
|
+
readAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A memoized marker a read may use: its value, and whether it is stale. */
|
|
67
|
+
export interface MarkerMemoHit {
|
|
68
|
+
value: number | null;
|
|
69
|
+
stale: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A store read of a marker for {@link TagMarkerMemo.readThrough}: the value,
|
|
74
|
+
* and whether it may be memoized (a timed-out read that failed open may not).
|
|
75
|
+
*/
|
|
76
|
+
export interface MarkerFetch {
|
|
77
|
+
value: number | null;
|
|
78
|
+
memoize: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Options of {@link TagMarkerMemo.readThrough}. */
|
|
82
|
+
export interface MarkerReadThroughOptions {
|
|
83
|
+
/** Fresh window (ms); 0 or less reads the store and memoizes nothing. */
|
|
84
|
+
freshMs: number;
|
|
85
|
+
/** The oldest value served while it refreshes (ms). */
|
|
86
|
+
maxStaleMs: number;
|
|
87
|
+
/** The request carries the fresh-reads cookie: read the store. */
|
|
88
|
+
bypass: boolean;
|
|
89
|
+
/** Keeps a background refresh alive after the response (waitUntil). */
|
|
90
|
+
keepAlive?: (pending: Promise<void>) => void;
|
|
91
|
+
/** How the read was answered, for the debugPerformance marker row. */
|
|
92
|
+
onOutcome?: (outcome: MarkerMemoOutcome) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class TagMarkerMemo {
|
|
96
|
+
private readonly slots = new Map<string, MarkerSlot>();
|
|
97
|
+
private readonly refreshing = new Map<string, Promise<void>>();
|
|
98
|
+
|
|
99
|
+
constructor(private readonly maxEntries: number = MARKER_MEMO_MAX_ENTRIES) {}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The memoized marker for `key` if it is younger than `maxStaleMs` (stale
|
|
103
|
+
* when older than `freshMs`), or undefined when the caller must read the
|
|
104
|
+
* store. `freshMs <= 0` disables the memo.
|
|
105
|
+
*/
|
|
106
|
+
lookup(
|
|
107
|
+
key: string,
|
|
108
|
+
freshMs: number,
|
|
109
|
+
maxStaleMs: number,
|
|
110
|
+
now: number = Date.now(),
|
|
111
|
+
): MarkerMemoHit | undefined {
|
|
112
|
+
if (freshMs <= 0) return undefined;
|
|
113
|
+
const slot = this.slots.get(key);
|
|
114
|
+
if (!slot) return undefined;
|
|
115
|
+
const age = now - slot.readAt;
|
|
116
|
+
if (age >= Math.max(freshMs, maxStaleMs)) return undefined;
|
|
117
|
+
this.slots.delete(key);
|
|
118
|
+
this.slots.set(key, slot);
|
|
119
|
+
return { value: slot.value, stale: age >= freshMs };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record a marker the store returned to a read that started at
|
|
124
|
+
* `readStartedAt`. The age counts from the read's start, not its end: a
|
|
125
|
+
* marker written while the read was in flight may be missing from it, so
|
|
126
|
+
* the value is only known to be current as of the start. Markers only move
|
|
127
|
+
* forward, so a value older than one already held (a write-through that
|
|
128
|
+
* landed during the read) is kept at the newer value, with the later stamp.
|
|
129
|
+
*/
|
|
130
|
+
store(
|
|
131
|
+
key: string,
|
|
132
|
+
value: number | null,
|
|
133
|
+
readStartedAt: number = Date.now(),
|
|
134
|
+
): void {
|
|
135
|
+
const held = this.slots.get(key);
|
|
136
|
+
const heldValue = held?.value ?? null;
|
|
137
|
+
const next =
|
|
138
|
+
heldValue !== null && (value === null || heldValue > value)
|
|
139
|
+
? heldValue
|
|
140
|
+
: value;
|
|
141
|
+
this.slots.delete(key);
|
|
142
|
+
this.slots.set(key, {
|
|
143
|
+
value: next,
|
|
144
|
+
readAt: Math.max(held?.readAt ?? readStartedAt, readStartedAt),
|
|
145
|
+
});
|
|
146
|
+
if (this.slots.size > this.maxEntries) {
|
|
147
|
+
const oldest = this.slots.keys().next().value as string;
|
|
148
|
+
this.slots.delete(oldest);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Refresh a stale marker in the background: one read per key at a time,
|
|
154
|
+
* handed to `keepAlive` (the store's waitUntil) so the runtime lets it
|
|
155
|
+
* finish after the response; without it the read runs detached. A read
|
|
156
|
+
* that fails, or may not be memoized, stores nothing.
|
|
157
|
+
*/
|
|
158
|
+
refresh(
|
|
159
|
+
key: string,
|
|
160
|
+
read: () => Promise<MarkerFetch>,
|
|
161
|
+
keepAlive?: (pending: Promise<void>) => void,
|
|
162
|
+
): void {
|
|
163
|
+
if (this.refreshing.has(key)) return;
|
|
164
|
+
const pending = (async (): Promise<void> => {
|
|
165
|
+
const startedAt = Date.now();
|
|
166
|
+
try {
|
|
167
|
+
const { value, memoize } = await read();
|
|
168
|
+
if (memoize) this.store(key, value, startedAt);
|
|
169
|
+
} catch {
|
|
170
|
+
// The next read past the stale window blocks on the store anyway.
|
|
171
|
+
} finally {
|
|
172
|
+
this.refreshing.delete(key);
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
this.refreshing.set(key, pending);
|
|
176
|
+
keepAlive?.(pending);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A marker through the memo, stale-while-revalidate: a value within
|
|
181
|
+
* `freshMs` is used as is; one within `maxStaleMs` is used while one
|
|
182
|
+
* background read refreshes it; otherwise, and for a request carrying the
|
|
183
|
+
* fresh-reads cookie, the store is read (`fetch(false)`) and the result
|
|
184
|
+
* memoized. `fetch(true)` is the background refresh, outside any request.
|
|
185
|
+
*/
|
|
186
|
+
async readThrough(
|
|
187
|
+
key: string,
|
|
188
|
+
fetch: (background: boolean) => Promise<MarkerFetch>,
|
|
189
|
+
options: MarkerReadThroughOptions,
|
|
190
|
+
): Promise<number | null> {
|
|
191
|
+
if (options.freshMs <= 0) return (await fetch(false)).value;
|
|
192
|
+
if (!options.bypass) {
|
|
193
|
+
const hit = this.lookup(key, options.freshMs, options.maxStaleMs);
|
|
194
|
+
if (hit) {
|
|
195
|
+
options.onOutcome?.(hit.stale ? "stale" : "fresh");
|
|
196
|
+
if (hit.stale) this.refresh(key, () => fetch(true), options.keepAlive);
|
|
197
|
+
return hit.value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const startedAt = Date.now();
|
|
201
|
+
const { value, memoize } = await fetch(false);
|
|
202
|
+
if (memoize) this.store(key, value, startedAt);
|
|
203
|
+
options.onOutcome?.(options.bypass ? "bypass" : "read");
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
clear(): void {
|
|
208
|
+
this.slots.clear();
|
|
209
|
+
this.refreshing.clear();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The tag names a shell key's entry carried when it was last read or written
|
|
215
|
+
* in this isolate, so the next HIT can start their marker reads before the
|
|
216
|
+
* entry names them. A wrong or missing hint costs a wasted read or nothing;
|
|
217
|
+
* the freshness check always uses the entry's own tags.
|
|
218
|
+
*/
|
|
219
|
+
export class TagNameHints {
|
|
220
|
+
private readonly keys = new Map<string, readonly string[]>();
|
|
221
|
+
|
|
222
|
+
constructor(private readonly maxEntries: number = TAG_HINTS_MAX_ENTRIES) {}
|
|
223
|
+
|
|
224
|
+
get(key: string): readonly string[] | undefined {
|
|
225
|
+
return this.keys.get(key);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
remember(key: string, tags: readonly string[] | undefined): void {
|
|
229
|
+
this.keys.delete(key);
|
|
230
|
+
if (!tags || tags.length === 0) return;
|
|
231
|
+
this.keys.set(key, tags);
|
|
232
|
+
if (this.keys.size > this.maxEntries) {
|
|
233
|
+
const oldest = this.keys.keys().next().value as string;
|
|
234
|
+
this.keys.delete(oldest);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
clear(): void {
|
|
239
|
+
this.keys.clear();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The union of a key's remembered tag names and the route's static tags. */
|
|
244
|
+
export function hintedTags(
|
|
245
|
+
remembered: readonly string[] | undefined,
|
|
246
|
+
routeTags: readonly string[] | undefined,
|
|
247
|
+
): string[] {
|
|
248
|
+
if (!routeTags || routeTags.length === 0)
|
|
249
|
+
return remembered ? [...remembered] : [];
|
|
250
|
+
if (!remembered || remembered.length === 0) return [...routeTags];
|
|
251
|
+
return [...new Set([...remembered, ...routeTags])];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The marker row's memo outcome for a read's tags: the most store-bound one
|
|
256
|
+
* (a bypass or store read outranks a stale value, which outranks a fresh one).
|
|
257
|
+
*/
|
|
258
|
+
export function summarizeMarkerOutcomes(
|
|
259
|
+
tags: readonly string[] | undefined,
|
|
260
|
+
outcomes: ReadonlyMap<string, MarkerMemoOutcome>,
|
|
261
|
+
): MarkerMemoOutcome | undefined {
|
|
262
|
+
let summary: MarkerMemoOutcome | undefined;
|
|
263
|
+
for (const tag of tags ?? []) {
|
|
264
|
+
const outcome = outcomes.get(tag);
|
|
265
|
+
if (
|
|
266
|
+
outcome &&
|
|
267
|
+
(!summary || OUTCOME_RANK[outcome] > OUTCOME_RANK[summary])
|
|
268
|
+
) {
|
|
269
|
+
summary = outcome;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return summary;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const OUTCOME_RANK: Record<MarkerMemoOutcome, number> = {
|
|
276
|
+
fresh: 0,
|
|
277
|
+
stale: 1,
|
|
278
|
+
read: 2,
|
|
279
|
+
bypass: 3,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Fill a shell read's marker-row stats (debugPerformance) for the entry's
|
|
284
|
+
* own tags: the memo outcome, and how many of them the read had hinted.
|
|
285
|
+
*/
|
|
286
|
+
export function recordMarkerRow(
|
|
287
|
+
stats: ShellReadStats,
|
|
288
|
+
tags: readonly string[] | undefined,
|
|
289
|
+
outcomes: ReadonlyMap<string, MarkerMemoOutcome> | undefined,
|
|
290
|
+
): void {
|
|
291
|
+
if (!tags || tags.length === 0) return;
|
|
292
|
+
if (outcomes) stats.markerMemo = summarizeMarkerOutcomes(tags, outcomes);
|
|
293
|
+
if (stats.markerHinted) {
|
|
294
|
+
const hinted = stats.markerHinted;
|
|
295
|
+
stats.markerHintHits = tags.filter((tag) => hinted.includes(tag)).length;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* True when this request carries the fresh-reads cookie: a request of the same
|
|
301
|
+
* user after an updateTag()/revalidateTag(), which must not be served from an
|
|
302
|
+
* isolate memo (shell or marker) that could predate the mutation.
|
|
303
|
+
*/
|
|
304
|
+
export function freshReadsRequired(
|
|
305
|
+
ctx: Pick<RequestContext, "_freshReads"> | undefined,
|
|
306
|
+
): boolean {
|
|
307
|
+
return ctx?._freshReads === true;
|
|
308
|
+
}
|
|
@@ -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
|
+
}
|