@rangojs/router 0.18.0 → 0.19.1
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 +37 -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 +48 -5
- 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 +126 -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 +962 -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 +67 -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 +54 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +202 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +209 -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 +42 -5
- 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 +152 -26
- 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 +263 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +134 -87
- 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
|
@@ -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
|
+
}
|