@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
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
import type { UrlPatterns } from "../urls.js";
|
|
12
12
|
import type { AllUseItems } from "../route-types.js";
|
|
13
13
|
import { extractStaticPrefix } from "../router/pattern-matching.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getParallelSlotEntries,
|
|
16
|
+
RangoContext,
|
|
17
|
+
runWithPrefixes,
|
|
18
|
+
} from "../server/context.js";
|
|
15
19
|
import type { EntryData, TrackedInclude } from "../server/context.js";
|
|
16
20
|
import type { TrailingSlashMode } from "../types.js";
|
|
17
21
|
import { createRouteHelpers } from "../route-definition.js";
|
|
18
22
|
import MapRootLayout from "../server/root-layout.js";
|
|
19
23
|
import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
|
|
24
|
+
import {
|
|
25
|
+
createTransitionWhenError,
|
|
26
|
+
transitionWhenProblem,
|
|
27
|
+
} from "../transition-when-ref.js";
|
|
20
28
|
import {
|
|
21
29
|
isIncludeProvider,
|
|
22
30
|
resolveIncludeModule,
|
|
@@ -59,6 +67,57 @@ export interface GeneratedManifest {
|
|
|
59
67
|
routeSearchSchemas?: Record<string, Record<string, string>>;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Fail discovery on an invalid transition({ when }) anywhere in the entry
|
|
72
|
+
* chains of the evaluated routes: the route itself, its ancestors, their
|
|
73
|
+
* orphan layouts, parallel slots and intercepts. Strict: a plain function is
|
|
74
|
+
* rejected, because discovery evaluates the tree in the react-server graph
|
|
75
|
+
* where a "use client" import is a client reference (transition-when-ref.ts).
|
|
76
|
+
* The error names the first route found through the entry.
|
|
77
|
+
*/
|
|
78
|
+
function validateTransitionWhens(
|
|
79
|
+
manifest: Map<string, EntryData>,
|
|
80
|
+
patterns: Map<string, string>,
|
|
81
|
+
): void {
|
|
82
|
+
const seen = new Set<object>();
|
|
83
|
+
for (const [routeName, routeEntry] of manifest) {
|
|
84
|
+
const route = {
|
|
85
|
+
routeName,
|
|
86
|
+
pattern:
|
|
87
|
+
patterns.get(routeName) ??
|
|
88
|
+
(routeEntry.type === "route" ? routeEntry.pattern : undefined),
|
|
89
|
+
};
|
|
90
|
+
const check = (when: unknown, entryType: string): void => {
|
|
91
|
+
const problem = transitionWhenProblem(when, true);
|
|
92
|
+
if (problem) {
|
|
93
|
+
throw createTransitionWhenError(when, problem, {
|
|
94
|
+
...route,
|
|
95
|
+
entryType,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const visit = (entry: EntryData): void => {
|
|
100
|
+
if (seen.has(entry)) return;
|
|
101
|
+
seen.add(entry);
|
|
102
|
+
check(entry.transition?.when, entry.type);
|
|
103
|
+
for (const orphan of entry.layout) visit(orphan);
|
|
104
|
+
for (const { entry: slot } of getParallelSlotEntries(entry.parallel)) {
|
|
105
|
+
visit(slot);
|
|
106
|
+
}
|
|
107
|
+
for (const intercept of entry.intercept) {
|
|
108
|
+
check(intercept.transition?.when, "intercept");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
for (
|
|
112
|
+
let entry: EntryData | null | undefined = routeEntry;
|
|
113
|
+
entry;
|
|
114
|
+
entry = entry.parent ?? entry.orphanOwner
|
|
115
|
+
) {
|
|
116
|
+
visit(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
62
121
|
/**
|
|
63
122
|
* Build prefix tree node by running the patterns with proper context.
|
|
64
123
|
* Uses a visited set to detect circular includes and prevent infinite recursion.
|
|
@@ -116,6 +175,7 @@ async function buildPrefixTreeNode(
|
|
|
116
175
|
responseTypeRoutes?: Record<string, string>,
|
|
117
176
|
routeSearchSchemas?: Record<string, Record<string, string>>,
|
|
118
177
|
routerId?: string,
|
|
178
|
+
validateTransitionWhen?: boolean,
|
|
119
179
|
): Promise<PrefixTreeNode> {
|
|
120
180
|
// Resolve an async include provider (`() => import("./routes")`) so its routes
|
|
121
181
|
// are walked into the build-time manifest/types/href. Runtime matching still
|
|
@@ -174,6 +234,8 @@ async function buildPrefixTreeNode(
|
|
|
174
234
|
},
|
|
175
235
|
);
|
|
176
236
|
|
|
237
|
+
if (validateTransitionWhen) validateTransitionWhens(manifest, patternsMap);
|
|
238
|
+
|
|
177
239
|
// Collect route names defined in this include (routes have prefixes applied)
|
|
178
240
|
const routes = [...patternsMap.keys()];
|
|
179
241
|
Object.assign(routeManifest, Object.fromEntries(patternsMap));
|
|
@@ -230,6 +292,7 @@ async function buildPrefixTreeNode(
|
|
|
230
292
|
responseTypeRoutes,
|
|
231
293
|
routeSearchSchemas,
|
|
232
294
|
routerId,
|
|
295
|
+
validateTransitionWhen,
|
|
233
296
|
),
|
|
234
297
|
);
|
|
235
298
|
|
|
@@ -313,6 +376,14 @@ export async function generateManifestFull<TEnv>(
|
|
|
313
376
|
* EntryData map built below is local; this is the only seam that surfaces it.
|
|
314
377
|
*/
|
|
315
378
|
collectClientFallbackRef?: (refKey: string) => void;
|
|
379
|
+
/**
|
|
380
|
+
* Fail on an invalid transition({ when }): a non-function, or a plain
|
|
381
|
+
* (server) function where a client reference is required. Only the Vite
|
|
382
|
+
* plugin's discovery passes it: it evaluates the tree in the react-server
|
|
383
|
+
* graph, the one place a "use client" import is told apart from a server
|
|
384
|
+
* function (transition-when-ref.ts).
|
|
385
|
+
*/
|
|
386
|
+
validateTransitionWhen?: boolean;
|
|
316
387
|
},
|
|
317
388
|
): Promise<FullManifest> {
|
|
318
389
|
const routeManifest: Record<string, string> = {};
|
|
@@ -351,6 +422,10 @@ export async function generateManifestFull<TEnv>(
|
|
|
351
422
|
},
|
|
352
423
|
);
|
|
353
424
|
|
|
425
|
+
if (options?.validateTransitionWhen) {
|
|
426
|
+
validateTransitionWhens(manifest, patternsMap);
|
|
427
|
+
}
|
|
428
|
+
|
|
354
429
|
// Surface the "use client" components registered as error/notFound fallbacks
|
|
355
430
|
// (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
|
|
356
431
|
// The boundary may be a handler function and/or wrap the client boundary in
|
|
@@ -413,6 +488,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
413
488
|
responseTypeRoutes,
|
|
414
489
|
routeSearchSchemas,
|
|
415
490
|
options?.routerId,
|
|
491
|
+
options?.validateTransitionWhen,
|
|
416
492
|
),
|
|
417
493
|
);
|
|
418
494
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Background Task Runner
|
|
3
3
|
*
|
|
4
|
-
* Unified helper for scheduling async work via waitUntil
|
|
5
|
-
*
|
|
4
|
+
* Unified helper for scheduling async work via waitUntil, and the two bounded
|
|
5
|
+
* waits the PPR shell capture puts on background work (raceDeadline,
|
|
6
|
+
* settleGrowing). When waitUntil is unavailable, falls back to blocking or
|
|
7
|
+
* skipping.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
interface WaitUntilHost {
|
|
@@ -32,3 +34,54 @@ export function runBackground(
|
|
|
32
34
|
return task();
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Race `work` against an absolute deadline (epoch ms), keeping its value. The
|
|
40
|
+
* losing work is not cancelled (a router match has no abort signal) and its
|
|
41
|
+
* late rejection is swallowed. The timer is unref'd, so a pending deadline
|
|
42
|
+
* never keeps a Node dev process alive.
|
|
43
|
+
*/
|
|
44
|
+
export async function raceDeadline<T>(
|
|
45
|
+
work: Promise<T>,
|
|
46
|
+
deadline: number,
|
|
47
|
+
): Promise<{ done: true; value: T } | { done: false }> {
|
|
48
|
+
work.catch(() => {});
|
|
49
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
const expired = new Promise<{ done: false }>((resolve) => {
|
|
51
|
+
timer = setTimeout(
|
|
52
|
+
() => resolve({ done: false }),
|
|
53
|
+
Math.max(0, deadline - Date.now()),
|
|
54
|
+
);
|
|
55
|
+
(timer as { unref?: () => void }).unref?.();
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
return await Promise.race([
|
|
59
|
+
work.then((value) => ({ done: true as const, value })),
|
|
60
|
+
expired,
|
|
61
|
+
]);
|
|
62
|
+
} finally {
|
|
63
|
+
if (timer) clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wait until every promise in `list` has settled, including ones appended
|
|
69
|
+
* while waiting (a settled write can schedule a nested one: cacheRoute
|
|
70
|
+
* schedules its store.set in a second waitUntil), or until `deadline` (epoch
|
|
71
|
+
* ms). Resolves true when the list drained, false at the deadline. Never
|
|
72
|
+
* mutates `list`: a later call waits for what is still pending.
|
|
73
|
+
*/
|
|
74
|
+
export async function settleGrowing(
|
|
75
|
+
list: readonly Promise<unknown>[],
|
|
76
|
+
deadline: number,
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
let seen = 0;
|
|
79
|
+
while (list.length > seen) {
|
|
80
|
+
if (Date.now() >= deadline) return false;
|
|
81
|
+
const batch = list.slice(seen);
|
|
82
|
+
seen = list.length;
|
|
83
|
+
const settled = await raceDeadline(Promise.allSettled(batch), deadline);
|
|
84
|
+
if (!settled.done) return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
@@ -38,6 +38,53 @@ export interface CacheExecScope {
|
|
|
38
38
|
|
|
39
39
|
const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
|
|
40
40
|
|
|
41
|
+
// Global key: the value must survive HMR re-evaluation of this module, like
|
|
42
|
+
// the other request-scoped ALS instances (server/context.ts RangoContext).
|
|
43
|
+
const IDENTITY_EXEMPT_KEY = Symbol.for("rangojs-router:identity-exempt");
|
|
44
|
+
const identityExemptStorage: AsyncLocalStorage<boolean> = ((globalThis as any)[
|
|
45
|
+
IDENTITY_EXEMPT_KEY
|
|
46
|
+
] ??= new AsyncLocalStorage<boolean>());
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run a callback whose identity reads never reach a shared entry, so
|
|
50
|
+
* guardIdentityRead (server/context.ts) lets them through and records
|
|
51
|
+
* nothing (#976):
|
|
52
|
+
*
|
|
53
|
+
* - a cache's own decisions and metadata: a `key()`, a store `keyGenerator`
|
|
54
|
+
* (cache-policy.ts resolveCacheKey), a `condition()` (cache-scope.ts
|
|
55
|
+
* conditionAllows, loader-cache.ts) and a `tags()` function
|
|
56
|
+
* (cache-policy.ts resolveTagsOption). The value picks the entry, whether
|
|
57
|
+
* one is used, or how it is invalidated; it is never rendered. Reading the
|
|
58
|
+
* request there is the documented way to partition an entry;
|
|
59
|
+
* - the router's `onError` (router/error-handling.ts invokeOnError): it
|
|
60
|
+
* observes, and a render error can reach it inside a capture or a cached
|
|
61
|
+
* scope.
|
|
62
|
+
*
|
|
63
|
+
* Scar: a ppr capture resolves its doc record key, and the record's tags,
|
|
64
|
+
* again under the capture context; once `ctx.request.headers` was guarded, a
|
|
65
|
+
* keyGenerator or tags() reading it refused every capture of the route.
|
|
66
|
+
*
|
|
67
|
+
* The exemption is async-local, so it would follow everything the callback
|
|
68
|
+
* starts. A "use cache" body (runWithCacheExecScope), a loader body
|
|
69
|
+
* (server/context.ts runInsideLoaderBodyScope) and a segment funnel
|
|
70
|
+
* (getContext().runWithStore) end it on entry (endIdentityExempt). Scar: a
|
|
71
|
+
* key() that awaited a "use cache" function reading cookies() stored the
|
|
72
|
+
* first visitor's cookie and served it to the next.
|
|
73
|
+
*/
|
|
74
|
+
export function runIdentityExempt<T>(fn: () => T): T {
|
|
75
|
+
return identityExemptStorage.run(true, fn);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** True inside runIdentityExempt, and not in a scope started from it. */
|
|
79
|
+
export function isInsideIdentityExempt(): boolean {
|
|
80
|
+
return identityExemptStorage.getStore() === true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Run `fn` with the identity exemption ended (see runIdentityExempt). */
|
|
84
|
+
export function endIdentityExempt<T>(fn: () => T): T {
|
|
85
|
+
return isInsideIdentityExempt() ? identityExemptStorage.run(false, fn) : fn();
|
|
86
|
+
}
|
|
87
|
+
|
|
41
88
|
/**
|
|
42
89
|
* Run fn with the "use cache" execution scope active. Continuations spawned
|
|
43
90
|
* from fn's synchronous kickoff inherit the scope; parallel chains do not.
|
|
@@ -48,7 +95,7 @@ export function runWithCacheExecScope<T>(
|
|
|
48
95
|
fn: () => T,
|
|
49
96
|
scope: CacheExecScope = createCacheExecScope(),
|
|
50
97
|
): T {
|
|
51
|
-
return cacheExecStorage.run(scope, fn);
|
|
98
|
+
return endIdentityExempt(() => cacheExecStorage.run(scope, fn));
|
|
52
99
|
}
|
|
53
100
|
|
|
54
101
|
/** A scope nested in the calling chain's current one, if any. */
|
|
@@ -97,21 +97,61 @@ export function cacheKeyBase(
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* The
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* (CacheScope.resolveKeyFrom). One part is returned raw, so a single keyed
|
|
104
|
-
* scope keeps its key byte for byte. Two or more are joined by `|`, each
|
|
105
|
-
* URI-encoded (like partitionShellKey, rsc/shell-capture-constants.ts): an
|
|
106
|
-
* encoded part holds no `|`, so every tuple maps to its own key, of any
|
|
107
|
-
* depth. Raw, `("a|b", "c")` and `("a", "b|c")` named one record. A composed
|
|
108
|
-
* key holds no `:` either, so a single raw result with one (a prefixed
|
|
109
|
-
* `tier:...`, a `doc:` default key) never equals it; an unprefixed raw result
|
|
110
|
-
* can. Shared with the testing helper `shellCacheKey`, which composes a
|
|
111
|
-
* nested partition the same way.
|
|
100
|
+
* The prefix of a `key()` result's part in a record key (#975), and of a
|
|
101
|
+
* loader's own `key()` result after its `loader:<id>:` namespace (#1009,
|
|
102
|
+
* loader-cache.ts loaderKeyFromResult).
|
|
112
103
|
*/
|
|
113
|
-
export
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
export const KEY_PART_PREFIX = "key:";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A cache() record key, or ppr shell partition, from its parts
|
|
108
|
+
* (CacheScope.resolveKeyFrom, CacheScope.resolvePartition): the `key()`
|
|
109
|
+
* results of the scope's cache() chain, outermost first, then its default
|
|
110
|
+
* parts: the keyGenerator results of enclosing scopes on another store
|
|
111
|
+
* (#974) and, for a record key under a scope without its own `key()`, its
|
|
112
|
+
* default key (or its store's keyGenerator result, #970).
|
|
113
|
+
*
|
|
114
|
+
* Key scheme invariant (#975). A `key()` result is often request input,
|
|
115
|
+
* and a record key has no route or scope discriminator (the store reads
|
|
116
|
+
* `store.get(key)`), so a result that equals another record's key writes
|
|
117
|
+
* one route's content under the other's entry. Before #975 a single
|
|
118
|
+
* `key()` result was stored raw: the header value `doc:localhost/pricing`
|
|
119
|
+
* named /pricing's default-keyed record, and `gold|doc%3Alocalhost%2Fpricing`
|
|
120
|
+
* named gold's nested /pricing record (#970). So every part is encoded:
|
|
121
|
+
*
|
|
122
|
+
* - a `key()` part is `key:` plus `encodeURIComponent(result)`: it starts
|
|
123
|
+
* with `key:` and holds no `|` and no other `:`;
|
|
124
|
+
* - a default part is `encodeURIComponent(value)`: it holds no `:` or `|`;
|
|
125
|
+
* an enclosing store's keyGenerator result equal to the default key is an
|
|
126
|
+
* empty part, so each store keeps its position (CacheScope
|
|
127
|
+
* positionalParts);
|
|
128
|
+
* - one `key()` part is the key; two or more parts are joined by `|`,
|
|
129
|
+
* `key()` parts first;
|
|
130
|
+
* - a lone default part stays raw: with no `key()` on the chain the record
|
|
131
|
+
* key is the default key or the store's keyGenerator result, unchanged.
|
|
132
|
+
*
|
|
133
|
+
* Why nothing collides: the router's default keys (`doc:`, `partial:`,
|
|
134
|
+
* `intercept:`, `response:<type>:`) never start with `key:` and hold a `:`.
|
|
135
|
+
* A namespaced `key()` result starts with `key:` and holds no `|`. A
|
|
136
|
+
* composed key holds a `|` and either starts with `key:` or holds no `:`.
|
|
137
|
+
* Among composed keys, splitting on `|` recovers each part, a part's kind
|
|
138
|
+
* (a `key:` prefix, or no `:` at all) and its value. Pinned by the collision
|
|
139
|
+
* probe in src/cache/__tests__/cache-scope-chain.test.ts. A keyGenerator
|
|
140
|
+
* result is the store's own namespace, as before.
|
|
141
|
+
*
|
|
142
|
+
* Shared with the testing helper `shellCacheKey`, so a test builds the
|
|
143
|
+
* production partition.
|
|
144
|
+
*/
|
|
145
|
+
export function composeCacheKeys(
|
|
146
|
+
keyResults: readonly string[],
|
|
147
|
+
defaultParts: readonly string[] = [],
|
|
148
|
+
): string {
|
|
149
|
+
if (keyResults.length === 0 && defaultParts.length === 1) {
|
|
150
|
+
return defaultParts[0];
|
|
151
|
+
}
|
|
152
|
+
const parts = keyResults.map(
|
|
153
|
+
(result) => KEY_PART_PREFIX + encodeURIComponent(result),
|
|
154
|
+
);
|
|
155
|
+
for (const part of defaultParts) parts.push(encodeURIComponent(part));
|
|
156
|
+
return parts.join("|");
|
|
117
157
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { CacheDefaults, SegmentCacheStore } from "./types.js";
|
|
10
10
|
import { _getRequestContext } from "../server/request-context.js";
|
|
11
|
+
import { runIdentityExempt } from "./cache-exec-scope.js";
|
|
11
12
|
import type { RequestContext } from "../server/request-context.js";
|
|
12
13
|
import { normalizeTags } from "./cache-tag.js";
|
|
13
14
|
import { reportCacheError } from "./cache-error.js";
|
|
@@ -127,16 +128,18 @@ export async function resolveCacheKey(
|
|
|
127
128
|
keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
|
|
128
129
|
store: SegmentCacheStore | null,
|
|
129
130
|
defaultKey: string,
|
|
130
|
-
_label: string,
|
|
131
131
|
): Promise<string> {
|
|
132
132
|
const requestCtx = _getRequestContext();
|
|
133
133
|
|
|
134
|
+
// A key reads the request to partition the entry (runIdentityExempt): the
|
|
135
|
+
// identity guards let it through, a ppr capture included.
|
|
134
136
|
if (keyFn && requestCtx) {
|
|
135
|
-
return await keyFn(requestCtx);
|
|
137
|
+
return await runIdentityExempt(() => keyFn(requestCtx));
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
if (store?.keyGenerator && requestCtx) {
|
|
139
|
-
|
|
141
|
+
const keyGenerator = store.keyGenerator;
|
|
142
|
+
return await runIdentityExempt(() => keyGenerator(requestCtx, defaultKey));
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
return defaultKey;
|
|
@@ -157,7 +160,9 @@ export function resolveTagsOption<TEnv>(
|
|
|
157
160
|
return undefined;
|
|
158
161
|
}
|
|
159
162
|
try {
|
|
160
|
-
|
|
163
|
+
// Tags label the entry, never render: identity reads pass
|
|
164
|
+
// (runIdentityExempt), a ppr capture's record write included.
|
|
165
|
+
return normalizeTagList(runIdentityExempt(() => tags(ctx)));
|
|
161
166
|
} catch (error) {
|
|
162
167
|
reportCacheError(
|
|
163
168
|
error,
|