@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
package/src/cache/cache-tag.ts
CHANGED
|
@@ -15,9 +15,221 @@ import {
|
|
|
15
15
|
_getRequestContext,
|
|
16
16
|
type RequestContext,
|
|
17
17
|
} from "../server/request-context.js";
|
|
18
|
+
import {
|
|
19
|
+
getCurrentLoaderBodyId,
|
|
20
|
+
getLoaderBodyTags,
|
|
21
|
+
isInsideLoaderScope,
|
|
22
|
+
type LoaderIdentityRead,
|
|
23
|
+
type LoaderIdentityReadVerb,
|
|
24
|
+
} from "../server/context.js";
|
|
25
|
+
import type { ExecutionStart } from "./tag-invalidation.js";
|
|
18
26
|
|
|
19
27
|
const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Each "use cache" tag scope's enclosing scope when it opened (#980). A tag
|
|
31
|
+
* recorded in a scope goes to every scope above it: an intermediate call can
|
|
32
|
+
* return a value holding a nested call still running, whose tags land after
|
|
33
|
+
* the intermediate call reported its own to its caller. Its caller's encode
|
|
34
|
+
* awaits the same promise, so the tags are in before it reads its own.
|
|
35
|
+
*/
|
|
36
|
+
const scopeParents = new WeakMap<Set<string>, Set<string>>();
|
|
37
|
+
|
|
38
|
+
/** Add `tag` to `scope` and every scope enclosing it. */
|
|
39
|
+
function addToScopes(scope: Set<string> | undefined, tag: string): void {
|
|
40
|
+
for (let s = scope; s; s = scopeParents.get(s)) s.add(tag);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Call `fn` outside any "use cache" tag scope: detached work a cached body
|
|
45
|
+
* schedules (a stale entry's background refresh) records nothing onto the
|
|
46
|
+
* entry being filled.
|
|
47
|
+
*/
|
|
48
|
+
export function outsideCacheTagScope<T>(fn: () => T): T {
|
|
49
|
+
return cacheTagStorage.exit(fn);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tag owners (#957). A route cache() record replays its segments without
|
|
54
|
+
* running their handlers or re-rendering their server components, so the
|
|
55
|
+
* tags that content recorded must ride on the record, or a HIT (and the PPR
|
|
56
|
+
* capture that replays it) loses them.
|
|
57
|
+
*
|
|
58
|
+
* Every tag records onto its owner: the running loader body
|
|
59
|
+
* (getCurrentLoaderBodyId, server/context.ts), else the segment whose handler
|
|
60
|
+
* or record serialization is running (runInSegmentTagScope). No segment scope
|
|
61
|
+
* opens inside a loader body, so this is innermost-wins. A loader's value
|
|
62
|
+
* reaches a record only through the handler (or loader) that reads it, so
|
|
63
|
+
* ctx.use() links the reader to the loader (linkLoaderTags) whoever started
|
|
64
|
+
* it, and getSegmentTags follows those links. A loader nobody reads
|
|
65
|
+
* server-side (useLoader under loading()) stays off every record.
|
|
66
|
+
*
|
|
67
|
+
* The sets are keyed by the request context a tag records on, so a shell
|
|
68
|
+
* capture's derived context (Object.create(reqCtx)) never reads or feeds the
|
|
69
|
+
* foreground's.
|
|
70
|
+
*
|
|
71
|
+
* All of it runs only for a request that can write a record
|
|
72
|
+
* (RequestContext._recordTagOwners, armed by armRecordTagOwners at match
|
|
73
|
+
* time): elsewhere a handler call takes no scope, closure or allocation.
|
|
74
|
+
*/
|
|
75
|
+
const segmentOwnerStorage = new AsyncLocalStorage<string>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Arm the record tag owners for `ctx`: its match resolved a cache scope, so
|
|
79
|
+
* it can write a route cache() record (match-api.ts). A capture's or
|
|
80
|
+
* background re-render's derived context inherits the flag.
|
|
81
|
+
*/
|
|
82
|
+
export function armRecordTagOwners(
|
|
83
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
84
|
+
): void {
|
|
85
|
+
if (ctx) ctx._recordTagOwners = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether `ctx` records tag owners (see armRecordTagOwners). */
|
|
89
|
+
export function recordTagOwnersArmed(
|
|
90
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
91
|
+
): boolean {
|
|
92
|
+
return ctx?._recordTagOwners === true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface RequestTagOwners {
|
|
96
|
+
/** owner key -> tags recorded while it ran */
|
|
97
|
+
tags: Map<string, Set<string>>;
|
|
98
|
+
/** owner key -> loader owner keys whose values it consumed */
|
|
99
|
+
uses: Map<string, Set<string>>;
|
|
100
|
+
}
|
|
101
|
+
const tagOwnersByRequest = new WeakMap<RequestContext, RequestTagOwners>();
|
|
102
|
+
|
|
103
|
+
const segmentOwner = (segmentId: string): string => `s:${segmentId}`;
|
|
104
|
+
const loaderOwner = (loaderId: string): string => `l:${loaderId}`;
|
|
105
|
+
|
|
106
|
+
function ownersFor(ctx: RequestContext): RequestTagOwners {
|
|
107
|
+
let owners = tagOwnersByRequest.get(ctx);
|
|
108
|
+
if (!owners) {
|
|
109
|
+
owners = { tags: new Map(), uses: new Map() };
|
|
110
|
+
tagOwnersByRequest.set(ctx, owners);
|
|
111
|
+
}
|
|
112
|
+
return owners;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setFor(map: Map<string, Set<string>>, key: string): Set<string> {
|
|
116
|
+
let set = map.get(key);
|
|
117
|
+
if (!set) {
|
|
118
|
+
set = new Set();
|
|
119
|
+
map.set(key, set);
|
|
120
|
+
}
|
|
121
|
+
return set;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function currentOwner(): string | undefined {
|
|
125
|
+
const loaderId = getCurrentLoaderBodyId();
|
|
126
|
+
return loaderId !== undefined
|
|
127
|
+
? loaderOwner(loaderId)
|
|
128
|
+
: segmentOwnerStorage.getStore();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Call `fn(arg)` inside segment `segmentId`'s tag scope: tags recorded while
|
|
133
|
+
* it or its async continuations run (outside a loader body) are owned by that
|
|
134
|
+
* segment. Unarmed, it is a plain call.
|
|
135
|
+
*/
|
|
136
|
+
export function runInSegmentTagScope<R, A = undefined>(
|
|
137
|
+
segmentId: string,
|
|
138
|
+
fn: (arg: A) => R,
|
|
139
|
+
arg?: A,
|
|
140
|
+
): R {
|
|
141
|
+
if (!recordTagOwnersArmed()) return fn(arg as A);
|
|
142
|
+
return segmentOwnerStorage.run(segmentOwner(segmentId), fn, arg as A);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Record that the current owner reads loader `loaderId`'s value (ctx.use),
|
|
147
|
+
* so the loader's tags follow the reader onto a record. The DSL funnel
|
|
148
|
+
* starting a loader (resolveLoaderData, inside the DSL loader scope but no
|
|
149
|
+
* loader body) is not a reader.
|
|
150
|
+
*/
|
|
151
|
+
export function linkLoaderTags(
|
|
152
|
+
loaderId: string,
|
|
153
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
154
|
+
): void {
|
|
155
|
+
if (!ctx || !recordTagOwnersArmed(ctx)) return;
|
|
156
|
+
const owner = currentOwner();
|
|
157
|
+
if (owner === undefined) return;
|
|
158
|
+
if (owner.startsWith("s:") && isInsideLoaderScope()) return;
|
|
159
|
+
setFor(ownersFor(ctx).uses, owner).add(loaderOwner(loaderId));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Record `tags` as loader `loaderId`'s own, read outside its body (its
|
|
164
|
+
* cache() config tags, or a loader-cache HIT's stored tags):
|
|
165
|
+
* recordRequestTags with an explicit owner.
|
|
166
|
+
*/
|
|
167
|
+
export function recordLoaderTags(
|
|
168
|
+
loaderId: string,
|
|
169
|
+
tags: Iterable<string> | undefined,
|
|
170
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
171
|
+
): void {
|
|
172
|
+
recordOwnedTags(
|
|
173
|
+
tags,
|
|
174
|
+
ctx,
|
|
175
|
+
recordTagOwnersArmed(ctx) ? loaderOwner(loaderId) : undefined,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The tags segment `segmentId`'s content recorded in `ctx`: its own, plus
|
|
181
|
+
* those of every loader it consumed, transitively.
|
|
182
|
+
*/
|
|
183
|
+
export function getSegmentTags(
|
|
184
|
+
ctx: RequestContext,
|
|
185
|
+
segmentId: string,
|
|
186
|
+
): ReadonlySet<string> {
|
|
187
|
+
const owners = tagOwnersByRequest.get(ctx);
|
|
188
|
+
const out = new Set<string>();
|
|
189
|
+
if (!owners) return out;
|
|
190
|
+
const seen = new Set<string>();
|
|
191
|
+
const pending = [segmentOwner(segmentId)];
|
|
192
|
+
for (let owner = pending.pop(); owner !== undefined; owner = pending.pop()) {
|
|
193
|
+
if (seen.has(owner)) continue;
|
|
194
|
+
seen.add(owner);
|
|
195
|
+
for (const tag of owners.tags.get(owner) ?? []) out.add(tag);
|
|
196
|
+
for (const used of owners.uses.get(owner) ?? []) pending.push(used);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Record `tags` onto each of `segmentIds` (a route cache() HIT replaying
|
|
203
|
+
* those segments): a record written from the replay, such as a shell
|
|
204
|
+
* capture's doc record (match-middleware/cache-store.ts), then carries them
|
|
205
|
+
* as a fresh render's record would. Unarmed, a no-op.
|
|
206
|
+
*/
|
|
207
|
+
export function recordSegmentTags(
|
|
208
|
+
segmentIds: readonly string[],
|
|
209
|
+
tags: Iterable<string> | undefined,
|
|
210
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
211
|
+
): void {
|
|
212
|
+
if (!tags || !ctx || !recordTagOwnersArmed(ctx)) return;
|
|
213
|
+
for (const id of segmentIds) recordOwnedTags(tags, ctx, segmentOwner(id));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The owner of the loaders a shell capture bakes (loader-cache.ts bake
|
|
218
|
+
* lane): their data is shell material that no handler reads, so their tags
|
|
219
|
+
* reach the shell through this owner (shell-capture.ts), not a record.
|
|
220
|
+
*/
|
|
221
|
+
export const SHELL_BAKE_TAG_OWNER: string = "#shell-bake";
|
|
222
|
+
|
|
223
|
+
/** Link loader `loaderId`'s tags to `ownerId` (see SHELL_BAKE_TAG_OWNER). */
|
|
224
|
+
export function linkLoaderTagsTo(
|
|
225
|
+
ownerId: string,
|
|
226
|
+
loaderId: string,
|
|
227
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
228
|
+
): void {
|
|
229
|
+
if (!ctx || !recordTagOwnersArmed(ctx)) return;
|
|
230
|
+
setFor(ownersFor(ctx).uses, segmentOwner(ownerId)).add(loaderOwner(loaderId));
|
|
231
|
+
}
|
|
232
|
+
|
|
21
233
|
export function normalizeTag(tag: string): string | null {
|
|
22
234
|
// Trim and return the canonical (trimmed) form, not the raw tag. Both the
|
|
23
235
|
// write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
|
|
@@ -57,14 +269,15 @@ export function normalizeTags(tags: Iterable<string>): string[] {
|
|
|
57
269
|
* cache" in its tree. This is PPR's DERIVATIVE invalidation: PPR is
|
|
58
270
|
* execution-PRESERVING (everything still runs underneath; only document bytes
|
|
59
271
|
* are shortcut), so its tags ride this existing instrument rather than a
|
|
60
|
-
* first-class ppr key/tag API.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* Inside a cache()
|
|
66
|
-
*
|
|
67
|
-
*
|
|
272
|
+
* first-class ppr key/tag API. Whatever renders into the shell records here,
|
|
273
|
+
* at capture or on the route's cache() record the capture replays. A masked
|
|
274
|
+
* live-lane loader never executes at capture, so its own tags reach a shell
|
|
275
|
+
* only through a handler that reads it (ctx.use).
|
|
276
|
+
*
|
|
277
|
+
* Inside a route cache() boundary the render-callable form also tags that
|
|
278
|
+
* route's cache() record, which re-records it on every HIT (#957). A loader's
|
|
279
|
+
* tags reach the record only when a handler consumes its value (ctx.use);
|
|
280
|
+
* see runInSegmentTagScope / linkLoaderTags. An empty/whitespace-only tag is
|
|
68
281
|
* dropped in both forms (the render-callable form silently, via normalizeTags in
|
|
69
282
|
* recordRequestTags; the scope form with a dev warning).
|
|
70
283
|
*
|
|
@@ -98,7 +311,7 @@ export function cacheTag(...tags: string[]): void {
|
|
|
98
311
|
}
|
|
99
312
|
continue;
|
|
100
313
|
}
|
|
101
|
-
store
|
|
314
|
+
addToScopes(store, normalized);
|
|
102
315
|
}
|
|
103
316
|
return;
|
|
104
317
|
}
|
|
@@ -120,30 +333,273 @@ export function recordRequestTags(
|
|
|
120
333
|
tags: Iterable<string> | undefined,
|
|
121
334
|
ctx: RequestContext | undefined = _getRequestContext(),
|
|
122
335
|
): void {
|
|
336
|
+
recordOwnedTags(
|
|
337
|
+
tags,
|
|
338
|
+
ctx,
|
|
339
|
+
recordTagOwnersArmed(ctx) ? currentOwner() : undefined,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Recorded-tag sets (#964). A loader with its own cache() skips its body on a
|
|
345
|
+
* HIT, so its entry stores what its execution recorded, including the tags of
|
|
346
|
+
* every loader value it read, whoever ran that loader.
|
|
347
|
+
*
|
|
348
|
+
* Each loader execution records into its own set, held on its body scope
|
|
349
|
+
* (runInsideLoaderBodyScope); a loader-cache MISS opens one around its
|
|
350
|
+
* execution and value encodes (captureRecordedTags). The innermost wins. A
|
|
351
|
+
* read of a loader's value links the reader's set to the value's
|
|
352
|
+
* (readValueTags), and the entry write flattens the links
|
|
353
|
+
* (flattenRecordedTags) after its value settled.
|
|
354
|
+
*
|
|
355
|
+
* The per-execution sets and read links run only for a request that resolves
|
|
356
|
+
* a loader-cache binding (armLoaderTagSets); elsewhere a loader execution
|
|
357
|
+
* allocates no set and a read links nothing.
|
|
358
|
+
*
|
|
359
|
+
* The same sets carry the execution's request-identity reads (#972,
|
|
360
|
+
* recordLoaderIdentityRead in server/context.ts): an entry with no declared
|
|
361
|
+
* key refuses an execution whose links reach one (recordedIdentityRead).
|
|
362
|
+
*/
|
|
363
|
+
interface TagCapture {
|
|
364
|
+
into: Set<string>;
|
|
365
|
+
/** The loader body set active when the capture opened. */
|
|
366
|
+
outer: Set<string> | undefined;
|
|
367
|
+
}
|
|
368
|
+
// On globalThis like the loader scopes (server/context.ts): the identity-read
|
|
369
|
+
// recorder is installed there too, so a second evaluated copy of this module
|
|
370
|
+
// (a duplicated package, a dev re-evaluation) must share the capture scope and
|
|
371
|
+
// the recorded reads, or its recorder writes where no fill looks.
|
|
372
|
+
const recordedTagCapture: AsyncLocalStorage<TagCapture> = ((globalThis as any)[
|
|
373
|
+
Symbol.for("rangojs-router:recorded-tag-capture")
|
|
374
|
+
] ??= new AsyncLocalStorage<TagCapture>());
|
|
375
|
+
const tagLinks = new WeakMap<Set<string>, Set<Set<string>>>();
|
|
376
|
+
const loaderValueTags = new WeakMap<object, Set<string>>();
|
|
377
|
+
const identityReads: WeakMap<Set<string>, LoaderIdentityRead> = ((
|
|
378
|
+
globalThis as any
|
|
379
|
+
)[Symbol.for("rangojs-router:identity-reads")] ??= new WeakMap());
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Arm the per-execution loader tag sets for `ctx`: its match resolved a
|
|
383
|
+
* loader bound with its own cache() (match-api.ts, bindsLoaderCache), or such
|
|
384
|
+
* a binding started (loader-cache.ts). A loader-cache MISS captures its own
|
|
385
|
+
* execution and encodes either way.
|
|
386
|
+
*/
|
|
387
|
+
export function armLoaderTagSets(
|
|
388
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
389
|
+
): void {
|
|
390
|
+
if (ctx) ctx._recordLoaderTags = true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Whether `ctx` records per-execution loader tag sets (armLoaderTagSets). */
|
|
394
|
+
export function loaderTagSetsArmed(ctx: RequestContext | undefined): boolean {
|
|
395
|
+
return ctx?._recordLoaderTags === true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function activeTagSet(): Set<string> | undefined {
|
|
399
|
+
const capture = recordedTagCapture.getStore();
|
|
400
|
+
const body = getLoaderBodyTags();
|
|
401
|
+
if (capture && capture.outer === body) return capture.into;
|
|
402
|
+
return body ?? capture?.into;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Call `fn`, adding every tag recorded in its async chain
|
|
407
|
+
* (recordRequestTags, recordLoaderTags) to `into`, until a loader body
|
|
408
|
+
* started inside it takes over with its own set.
|
|
409
|
+
*/
|
|
410
|
+
export function captureRecordedTags<T>(into: Set<string>, fn: () => T): T {
|
|
411
|
+
return recordedTagCapture.run({ into, outer: getLoaderBodyTags() }, fn);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** `into` also answers for every tag `from` records. */
|
|
415
|
+
export function linkRecordedTags(into: Set<string>, from: Set<string>): void {
|
|
416
|
+
if (into === from) return;
|
|
417
|
+
let links = tagLinks.get(into);
|
|
418
|
+
if (!links) tagLinks.set(into, (links = new Set()));
|
|
419
|
+
links.add(from);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Mark a loader's value promise as carrying `tags` (see readValueTags). */
|
|
423
|
+
export function tagLoaderValue(value: object, tags: Set<string>): void {
|
|
424
|
+
loaderValueTags.set(value, tags);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** A loader value's tags also answer for `tags` (tagLoaderValue). */
|
|
428
|
+
export function linkValueTags(value: object, tags: Set<string>): void {
|
|
429
|
+
const own = loaderValueTags.get(value);
|
|
430
|
+
if (own) linkRecordedTags(own, tags);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The current execution reads a loader's value: it takes on its tags. */
|
|
434
|
+
export function readValueTags(value: object): void {
|
|
435
|
+
const tags = loaderValueTags.get(value);
|
|
123
436
|
if (!tags) return;
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
437
|
+
const into = activeTagSet();
|
|
438
|
+
if (into) linkRecordedTags(into, tags);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// recordLoaderIdentityRead (server/context.ts) calls this through the shared
|
|
442
|
+
// key, not an import: a test mocking context.ts would break module init. First
|
|
443
|
+
// read per execution: the fill's error names it.
|
|
444
|
+
(globalThis as Record<symbol, unknown>)[
|
|
445
|
+
Symbol.for("rangojs-router:identity-read-recorder")
|
|
446
|
+
] = (surface: string, verb: LoaderIdentityReadVerb): void => {
|
|
447
|
+
const set = activeTagSet();
|
|
448
|
+
if (set === undefined || identityReads.has(set)) return;
|
|
449
|
+
identityReads.set(set, { surface, verb, bodyId: getCurrentLoaderBodyId() });
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* A loader-cache HIT restores the read its entry's MISS recorded onto the
|
|
454
|
+
* value's set, so an unkeyed reader of the value refuses it as it would on
|
|
455
|
+
* the MISS (loader-cache.ts identity mark).
|
|
456
|
+
*/
|
|
457
|
+
export function markIdentityRead(
|
|
458
|
+
set: Set<string>,
|
|
459
|
+
read: LoaderIdentityRead,
|
|
460
|
+
): void {
|
|
461
|
+
if (!identityReads.has(set)) identityReads.set(set, read);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The identity read `root`'s execution recorded, or one a set it links to
|
|
466
|
+
* recorded, nearest first (breadth-first, so the execution's own read wins).
|
|
467
|
+
*/
|
|
468
|
+
export function recordedIdentityRead(
|
|
469
|
+
root: Set<string>,
|
|
470
|
+
): LoaderIdentityRead | undefined {
|
|
471
|
+
const seen = new Set<Set<string>>();
|
|
472
|
+
const queue = [root];
|
|
473
|
+
for (let i = 0; i < queue.length; i++) {
|
|
474
|
+
const set = queue[i]!;
|
|
475
|
+
if (seen.has(set)) continue;
|
|
476
|
+
seen.add(set);
|
|
477
|
+
const read = identityReads.get(set);
|
|
478
|
+
if (read) return read;
|
|
479
|
+
for (const linked of tagLinks.get(set) ?? []) queue.push(linked);
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** `root` and every set it links to, transitively. */
|
|
485
|
+
function linkedSets(root: Set<string>): Set<Set<string>> {
|
|
486
|
+
const seen = new Set<Set<string>>();
|
|
487
|
+
const pending = [root];
|
|
488
|
+
for (let set = pending.pop(); set !== undefined; set = pending.pop()) {
|
|
489
|
+
if (seen.has(set)) continue;
|
|
490
|
+
seen.add(set);
|
|
491
|
+
for (const linked of tagLinks.get(set) ?? []) pending.push(linked);
|
|
492
|
+
}
|
|
493
|
+
return seen;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** `root`'s tags plus those of every set it links to, transitively. */
|
|
497
|
+
export function flattenRecordedTags(root: Set<string>): Set<string> {
|
|
498
|
+
const out = new Set<string>();
|
|
499
|
+
for (const set of linkedSets(root)) {
|
|
500
|
+
for (const tag of set) out.add(tag);
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Where each execution that records into a set started (#977): a loader
|
|
507
|
+
* execution (createLoaderExecutor), a loader-cache MISS or refresh, a
|
|
508
|
+
* loader-cache binding's value.
|
|
509
|
+
*/
|
|
510
|
+
const tagSetStarts = new WeakMap<Set<string>, ExecutionStart>();
|
|
511
|
+
|
|
512
|
+
/** Record where the execution recording into `set` started. */
|
|
513
|
+
export function markTagSetStart(set: Set<string>, start: ExecutionStart): void {
|
|
514
|
+
tagSetStarts.set(set, start);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* The earliest start among `root` and every set it links to: a value built
|
|
519
|
+
* from another execution's value is as old as the oldest of them. A
|
|
520
|
+
* loader-cache write gates on it (loader-cache.ts), because the loader value
|
|
521
|
+
* it stores can be a run a reader started before the binding executed, and
|
|
522
|
+
* the values that run read can be older still.
|
|
523
|
+
*/
|
|
524
|
+
export function earliestRecordedStart(
|
|
525
|
+
root: Set<string>,
|
|
526
|
+
): ExecutionStart | undefined {
|
|
527
|
+
let earliest: ExecutionStart | undefined;
|
|
528
|
+
for (const set of linkedSets(root)) {
|
|
529
|
+
const start = tagSetStarts.get(set);
|
|
530
|
+
if (!start) continue;
|
|
531
|
+
earliest = earliest
|
|
532
|
+
? {
|
|
533
|
+
seq: Math.min(earliest.seq, start.seq),
|
|
534
|
+
at: Math.min(earliest.at, start.at),
|
|
535
|
+
}
|
|
536
|
+
: start;
|
|
537
|
+
}
|
|
538
|
+
return earliest;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* recordRequestTags, also recording onto `owner`'s set (#957), the active
|
|
543
|
+
* loader tag set (#964), and the enclosing "use cache" execution's tag
|
|
544
|
+
* scope (#980).
|
|
545
|
+
*
|
|
546
|
+
* The last one is how a nested "use cache" call's tags reach the entry that
|
|
547
|
+
* bakes its value: the inner wrapper records its tags (a miss's, a hit's
|
|
548
|
+
* stored ones, a joined execution's) after its own scope closed, so the
|
|
549
|
+
* scope current then is the caller's. Without it an outer entry kept the
|
|
550
|
+
* inner value past updateTag() of an inner tag, and the same-request gate
|
|
551
|
+
* (tag-invalidation.ts invalidatedSince) did not see the outer execution
|
|
552
|
+
* as tagged by it.
|
|
553
|
+
*/
|
|
554
|
+
function recordOwnedTags(
|
|
555
|
+
tags: Iterable<string> | undefined,
|
|
556
|
+
ctx: RequestContext | undefined,
|
|
557
|
+
owner: string | undefined,
|
|
558
|
+
): void {
|
|
559
|
+
if (!tags || !ctx?._requestTags) return;
|
|
560
|
+
const set = ctx._requestTags;
|
|
561
|
+
const captured = activeTagSet();
|
|
562
|
+
const enclosing = cacheTagStorage.getStore();
|
|
563
|
+
let owned: Set<string> | undefined;
|
|
126
564
|
for (const tag of tags) {
|
|
127
565
|
const normalized = normalizeTag(tag);
|
|
128
|
-
if (normalized
|
|
566
|
+
if (normalized === null) continue;
|
|
567
|
+
set.add(normalized);
|
|
568
|
+
captured?.add(normalized);
|
|
569
|
+
addToScopes(enclosing, normalized);
|
|
570
|
+
if (owner !== undefined) {
|
|
571
|
+
owned ??= setFor(ownersFor(ctx).tags, owner);
|
|
572
|
+
owned.add(normalized);
|
|
573
|
+
}
|
|
129
574
|
}
|
|
130
575
|
}
|
|
131
576
|
|
|
132
577
|
/**
|
|
133
|
-
* Run a function within a cache tag scope. Any cacheTag() calls inside `fn
|
|
134
|
-
*
|
|
578
|
+
* Run a function within a cache tag scope. Any cacheTag() calls inside `fn`,
|
|
579
|
+
* and the tags a nested "use cache" call records (recordOwnedTags, #980),
|
|
580
|
+
* accumulate into the returned Set and every scope enclosing it
|
|
581
|
+
* (scopeParents).
|
|
135
582
|
*
|
|
136
583
|
* The returned Set is the LIVE reference - the caller must await `result`
|
|
137
584
|
* before reading `tags`, because an async cached function may call cacheTag()
|
|
138
|
-
* after an await boundary.
|
|
585
|
+
* after an await boundary. Pass `tagSet` to re-enter an execution's scope
|
|
586
|
+
* (its result's Flight encode, #980); it keeps the parent it opened under.
|
|
139
587
|
*
|
|
140
588
|
* @internal Used by cache-runtime.ts to wrap "use cache" execution.
|
|
141
589
|
*/
|
|
142
|
-
export function runWithCacheTagScope<T>(
|
|
590
|
+
export function runWithCacheTagScope<T>(
|
|
591
|
+
fn: () => T,
|
|
592
|
+
tagSet?: Set<string>,
|
|
593
|
+
): {
|
|
143
594
|
result: T;
|
|
144
595
|
tags: Set<string>;
|
|
145
596
|
} {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
597
|
+
let scope = tagSet;
|
|
598
|
+
if (!scope) {
|
|
599
|
+
scope = new Set<string>();
|
|
600
|
+
const parent = cacheTagStorage.getStore();
|
|
601
|
+
if (parent) scopeParents.set(scope, parent);
|
|
602
|
+
}
|
|
603
|
+
const result = cacheTagStorage.run(scope, fn);
|
|
604
|
+
return { result, tags: scope };
|
|
149
605
|
}
|
|
@@ -7,9 +7,28 @@
|
|
|
7
7
|
// (~65k). 8192 turns a per-byte concat loop into O(n/8192) apply calls.
|
|
8
8
|
const FROM_CHARCODE_CHUNK = 8192;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* TC39 Uint8Array base64 methods (workerd, V8 >= 14; not Node 22/24). Same
|
|
12
|
+
* alphabet, padding, and invalid-character throw as btoa/atob, and about 15x
|
|
13
|
+
* faster than the per-byte loop on a 629 KB PPR prelude in workerd (0.05 ms vs
|
|
14
|
+
* 0.75 ms, issue #941). Looked up per call, not captured at import, so a
|
|
15
|
+
* polyfill or test spy installed later is the one that runs.
|
|
16
|
+
*/
|
|
17
|
+
type NativeUint8ArrayBase64 = {
|
|
18
|
+
fromBase64?: (b64: string) => Uint8Array;
|
|
19
|
+
prototype: { toBase64?: (this: Uint8Array) => string };
|
|
20
|
+
};
|
|
21
|
+
const NativeUint8Array = Uint8Array as unknown as NativeUint8ArrayBase64;
|
|
22
|
+
|
|
10
23
|
/** Encode ArrayBuffer to base64 string. */
|
|
11
24
|
export function bufferToBase64(buffer: ArrayBuffer): string {
|
|
12
|
-
|
|
25
|
+
return bytesToBase64(new Uint8Array(buffer));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Encode bytes (a view's own region only) to a base64 string. */
|
|
29
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
30
|
+
const nativeToBase64 = NativeUint8Array.prototype.toBase64;
|
|
31
|
+
if (nativeToBase64) return nativeToBase64.call(bytes);
|
|
13
32
|
// Build the binary (latin1) string in fixed-size chunks instead of one
|
|
14
33
|
// String.fromCharCode per byte. Identical output to the per-byte loop (each
|
|
15
34
|
// byte maps to the same code unit); just far fewer string concatenations for
|
|
@@ -22,12 +41,25 @@ export function bufferToBase64(buffer: ArrayBuffer): string {
|
|
|
22
41
|
return btoa(binary);
|
|
23
42
|
}
|
|
24
43
|
|
|
25
|
-
/** Decode base64 string to
|
|
26
|
-
export function
|
|
44
|
+
/** Decode a base64 string to bytes. Throws on a character outside the alphabet. */
|
|
45
|
+
export function base64ToBytes(base64: string): Uint8Array {
|
|
46
|
+
if (NativeUint8Array.fromBase64) return NativeUint8Array.fromBase64(base64);
|
|
27
47
|
const binary = atob(base64);
|
|
28
48
|
const bytes = new Uint8Array(binary.length);
|
|
29
49
|
for (let i = 0; i < binary.length; i++) {
|
|
30
50
|
bytes[i] = binary.charCodeAt(i);
|
|
31
51
|
}
|
|
32
|
-
return bytes
|
|
52
|
+
return bytes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The byte length `base64` decodes to (padded or not), without decoding. */
|
|
56
|
+
export function base64ByteLength(base64: string): number {
|
|
57
|
+
let length = base64.length;
|
|
58
|
+
while (length > 0 && base64.charCodeAt(length - 1) === 61 /* = */) length--;
|
|
59
|
+
return Math.floor((length * 3) / 4);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Decode base64 string to ArrayBuffer. */
|
|
63
|
+
export function base64ToBuffer(base64: string): ArrayBuffer {
|
|
64
|
+
return base64ToBytes(base64).buffer as ArrayBuffer;
|
|
33
65
|
}
|
|
@@ -132,3 +132,15 @@ export const EDGE_READ_TIMEOUT_MS = 20;
|
|
|
132
132
|
* Override per store via `CFCacheStoreOptions.kvReadTimeoutMs` (<= 0 disables).
|
|
133
133
|
*/
|
|
134
134
|
export const KV_READ_TIMEOUT_MS = 170;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Floor (ms) on the budget for reading a PPR shell's snapshot, the rest of a
|
|
138
|
+
* frame whose head and prelude were already read. A document HIT has sent
|
|
139
|
+
* its prelude by then and cannot render the rest without the snapshot's doc
|
|
140
|
+
* record, so a timed-out read degrades the HIT (the entry is replaced, the
|
|
141
|
+
* page reloads once into a MISS; rsc-rendering.ts degradeUnreplayableShell).
|
|
142
|
+
* That costs far more than waiting, so a slow body read gets at least this
|
|
143
|
+
* long; `kvReadTimeoutMs` above it wins, and `kvReadTimeoutMs <= 0` still
|
|
144
|
+
* disables the bound.
|
|
145
|
+
*/
|
|
146
|
+
export const SHELL_SNAPSHOT_READ_MIN_TIMEOUT_MS = 1000;
|