@rangojs/router 0.17.0 → 0.18.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/history-state.d.ts +16 -0
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +98 -14
- package/dist/types/cache/cache-tag.d.ts +92 -7
- 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 +185 -44
- 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 +34 -0
- package/dist/types/cache/handle-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- 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 +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -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 +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- package/src/cache/cache-tag.ts +375 -11
- 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 +990 -348
- 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 +143 -39
- package/src/cache/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
package/src/cache/cache-tag.ts
CHANGED
|
@@ -15,9 +15,197 @@ 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";
|
|
18
25
|
|
|
19
26
|
const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Tag owners (#957). A route cache() record replays its segments without
|
|
30
|
+
* running their handlers or re-rendering their server components, so the
|
|
31
|
+
* tags that content recorded must ride on the record, or a HIT (and the PPR
|
|
32
|
+
* capture that replays it) loses them.
|
|
33
|
+
*
|
|
34
|
+
* Every tag records onto its owner: the running loader body
|
|
35
|
+
* (getCurrentLoaderBodyId, server/context.ts), else the segment whose handler
|
|
36
|
+
* or record serialization is running (runInSegmentTagScope). No segment scope
|
|
37
|
+
* opens inside a loader body, so this is innermost-wins. A loader's value
|
|
38
|
+
* reaches a record only through the handler (or loader) that reads it, so
|
|
39
|
+
* ctx.use() links the reader to the loader (linkLoaderTags) whoever started
|
|
40
|
+
* it, and getSegmentTags follows those links. A loader nobody reads
|
|
41
|
+
* server-side (useLoader under loading()) stays off every record.
|
|
42
|
+
*
|
|
43
|
+
* The sets are keyed by the request context a tag records on, so a shell
|
|
44
|
+
* capture's derived context (Object.create(reqCtx)) never reads or feeds the
|
|
45
|
+
* foreground's.
|
|
46
|
+
*
|
|
47
|
+
* All of it runs only for a request that can write a record
|
|
48
|
+
* (RequestContext._recordTagOwners, armed by armRecordTagOwners at match
|
|
49
|
+
* time): elsewhere a handler call takes no scope, closure or allocation.
|
|
50
|
+
*/
|
|
51
|
+
const segmentOwnerStorage = new AsyncLocalStorage<string>();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Arm the record tag owners for `ctx`: its match resolved a cache scope, so
|
|
55
|
+
* it can write a route cache() record (match-api.ts). A capture's or
|
|
56
|
+
* background re-render's derived context inherits the flag.
|
|
57
|
+
*/
|
|
58
|
+
export function armRecordTagOwners(
|
|
59
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
60
|
+
): void {
|
|
61
|
+
if (ctx) ctx._recordTagOwners = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether `ctx` records tag owners (see armRecordTagOwners). */
|
|
65
|
+
export function recordTagOwnersArmed(
|
|
66
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
67
|
+
): boolean {
|
|
68
|
+
return ctx?._recordTagOwners === true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface RequestTagOwners {
|
|
72
|
+
/** owner key -> tags recorded while it ran */
|
|
73
|
+
tags: Map<string, Set<string>>;
|
|
74
|
+
/** owner key -> loader owner keys whose values it consumed */
|
|
75
|
+
uses: Map<string, Set<string>>;
|
|
76
|
+
}
|
|
77
|
+
const tagOwnersByRequest = new WeakMap<RequestContext, RequestTagOwners>();
|
|
78
|
+
|
|
79
|
+
const segmentOwner = (segmentId: string): string => `s:${segmentId}`;
|
|
80
|
+
const loaderOwner = (loaderId: string): string => `l:${loaderId}`;
|
|
81
|
+
|
|
82
|
+
function ownersFor(ctx: RequestContext): RequestTagOwners {
|
|
83
|
+
let owners = tagOwnersByRequest.get(ctx);
|
|
84
|
+
if (!owners) {
|
|
85
|
+
owners = { tags: new Map(), uses: new Map() };
|
|
86
|
+
tagOwnersByRequest.set(ctx, owners);
|
|
87
|
+
}
|
|
88
|
+
return owners;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setFor(map: Map<string, Set<string>>, key: string): Set<string> {
|
|
92
|
+
let set = map.get(key);
|
|
93
|
+
if (!set) {
|
|
94
|
+
set = new Set();
|
|
95
|
+
map.set(key, set);
|
|
96
|
+
}
|
|
97
|
+
return set;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function currentOwner(): string | undefined {
|
|
101
|
+
const loaderId = getCurrentLoaderBodyId();
|
|
102
|
+
return loaderId !== undefined
|
|
103
|
+
? loaderOwner(loaderId)
|
|
104
|
+
: segmentOwnerStorage.getStore();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Call `fn(arg)` inside segment `segmentId`'s tag scope: tags recorded while
|
|
109
|
+
* it or its async continuations run (outside a loader body) are owned by that
|
|
110
|
+
* segment. Unarmed, it is a plain call.
|
|
111
|
+
*/
|
|
112
|
+
export function runInSegmentTagScope<R, A = undefined>(
|
|
113
|
+
segmentId: string,
|
|
114
|
+
fn: (arg: A) => R,
|
|
115
|
+
arg?: A,
|
|
116
|
+
): R {
|
|
117
|
+
if (!recordTagOwnersArmed()) return fn(arg as A);
|
|
118
|
+
return segmentOwnerStorage.run(segmentOwner(segmentId), fn, arg as A);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Record that the current owner reads loader `loaderId`'s value (ctx.use),
|
|
123
|
+
* so the loader's tags follow the reader onto a record. The DSL funnel
|
|
124
|
+
* starting a loader (resolveLoaderData, inside the DSL loader scope but no
|
|
125
|
+
* loader body) is not a reader.
|
|
126
|
+
*/
|
|
127
|
+
export function linkLoaderTags(
|
|
128
|
+
loaderId: string,
|
|
129
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
130
|
+
): void {
|
|
131
|
+
if (!ctx || !recordTagOwnersArmed(ctx)) return;
|
|
132
|
+
const owner = currentOwner();
|
|
133
|
+
if (owner === undefined) return;
|
|
134
|
+
if (owner.startsWith("s:") && isInsideLoaderScope()) return;
|
|
135
|
+
setFor(ownersFor(ctx).uses, owner).add(loaderOwner(loaderId));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Record `tags` as loader `loaderId`'s own, read outside its body (its
|
|
140
|
+
* cache() config tags, or a loader-cache HIT's stored tags):
|
|
141
|
+
* recordRequestTags with an explicit owner.
|
|
142
|
+
*/
|
|
143
|
+
export function recordLoaderTags(
|
|
144
|
+
loaderId: string,
|
|
145
|
+
tags: Iterable<string> | undefined,
|
|
146
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
147
|
+
): void {
|
|
148
|
+
recordOwnedTags(
|
|
149
|
+
tags,
|
|
150
|
+
ctx,
|
|
151
|
+
recordTagOwnersArmed(ctx) ? loaderOwner(loaderId) : undefined,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The tags segment `segmentId`'s content recorded in `ctx`: its own, plus
|
|
157
|
+
* those of every loader it consumed, transitively.
|
|
158
|
+
*/
|
|
159
|
+
export function getSegmentTags(
|
|
160
|
+
ctx: RequestContext,
|
|
161
|
+
segmentId: string,
|
|
162
|
+
): ReadonlySet<string> {
|
|
163
|
+
const owners = tagOwnersByRequest.get(ctx);
|
|
164
|
+
const out = new Set<string>();
|
|
165
|
+
if (!owners) return out;
|
|
166
|
+
const seen = new Set<string>();
|
|
167
|
+
const pending = [segmentOwner(segmentId)];
|
|
168
|
+
for (let owner = pending.pop(); owner !== undefined; owner = pending.pop()) {
|
|
169
|
+
if (seen.has(owner)) continue;
|
|
170
|
+
seen.add(owner);
|
|
171
|
+
for (const tag of owners.tags.get(owner) ?? []) out.add(tag);
|
|
172
|
+
for (const used of owners.uses.get(owner) ?? []) pending.push(used);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Record `tags` onto each of `segmentIds` (a route cache() HIT replaying
|
|
179
|
+
* those segments): a record written from the replay, such as a shell
|
|
180
|
+
* capture's doc record (match-middleware/cache-store.ts), then carries them
|
|
181
|
+
* as a fresh render's record would. Unarmed, a no-op.
|
|
182
|
+
*/
|
|
183
|
+
export function recordSegmentTags(
|
|
184
|
+
segmentIds: readonly string[],
|
|
185
|
+
tags: Iterable<string> | undefined,
|
|
186
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
187
|
+
): void {
|
|
188
|
+
if (!tags || !ctx || !recordTagOwnersArmed(ctx)) return;
|
|
189
|
+
for (const id of segmentIds) recordOwnedTags(tags, ctx, segmentOwner(id));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The owner of the loaders a shell capture bakes (loader-cache.ts bake
|
|
194
|
+
* lane): their data is shell material that no handler reads, so their tags
|
|
195
|
+
* reach the shell through this owner (shell-capture.ts), not a record.
|
|
196
|
+
*/
|
|
197
|
+
export const SHELL_BAKE_TAG_OWNER: string = "#shell-bake";
|
|
198
|
+
|
|
199
|
+
/** Link loader `loaderId`'s tags to `ownerId` (see SHELL_BAKE_TAG_OWNER). */
|
|
200
|
+
export function linkLoaderTagsTo(
|
|
201
|
+
ownerId: string,
|
|
202
|
+
loaderId: string,
|
|
203
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
204
|
+
): void {
|
|
205
|
+
if (!ctx || !recordTagOwnersArmed(ctx)) return;
|
|
206
|
+
setFor(ownersFor(ctx).uses, segmentOwner(ownerId)).add(loaderOwner(loaderId));
|
|
207
|
+
}
|
|
208
|
+
|
|
21
209
|
export function normalizeTag(tag: string): string | null {
|
|
22
210
|
// Trim and return the canonical (trimmed) form, not the raw tag. Both the
|
|
23
211
|
// write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
|
|
@@ -57,14 +245,15 @@ export function normalizeTags(tags: Iterable<string>): string[] {
|
|
|
57
245
|
* cache" in its tree. This is PPR's DERIVATIVE invalidation: PPR is
|
|
58
246
|
* execution-PRESERVING (everything still runs underneath; only document bytes
|
|
59
247
|
* 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
|
-
*
|
|
248
|
+
* first-class ppr key/tag API. Whatever renders into the shell records here,
|
|
249
|
+
* at capture or on the route's cache() record the capture replays. A masked
|
|
250
|
+
* live-lane loader never executes at capture, so its own tags reach a shell
|
|
251
|
+
* only through a handler that reads it (ctx.use).
|
|
252
|
+
*
|
|
253
|
+
* Inside a route cache() boundary the render-callable form also tags that
|
|
254
|
+
* route's cache() record, which re-records it on every HIT (#957). A loader's
|
|
255
|
+
* tags reach the record only when a handler consumes its value (ctx.use);
|
|
256
|
+
* see runInSegmentTagScope / linkLoaderTags. An empty/whitespace-only tag is
|
|
68
257
|
* dropped in both forms (the render-callable form silently, via normalizeTags in
|
|
69
258
|
* recordRequestTags; the scope form with a dev warning).
|
|
70
259
|
*
|
|
@@ -120,12 +309,187 @@ export function recordRequestTags(
|
|
|
120
309
|
tags: Iterable<string> | undefined,
|
|
121
310
|
ctx: RequestContext | undefined = _getRequestContext(),
|
|
122
311
|
): void {
|
|
312
|
+
recordOwnedTags(
|
|
313
|
+
tags,
|
|
314
|
+
ctx,
|
|
315
|
+
recordTagOwnersArmed(ctx) ? currentOwner() : undefined,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Recorded-tag sets (#964). A loader with its own cache() skips its body on a
|
|
321
|
+
* HIT, so its entry stores what its execution recorded, including the tags of
|
|
322
|
+
* every loader value it read, whoever ran that loader.
|
|
323
|
+
*
|
|
324
|
+
* Each loader execution records into its own set, held on its body scope
|
|
325
|
+
* (runInsideLoaderBodyScope); a loader-cache MISS opens one around its
|
|
326
|
+
* execution and value encodes (captureRecordedTags). The innermost wins. A
|
|
327
|
+
* read of a loader's value links the reader's set to the value's
|
|
328
|
+
* (readValueTags), and the entry write flattens the links
|
|
329
|
+
* (flattenRecordedTags) after its value settled.
|
|
330
|
+
*
|
|
331
|
+
* The per-execution sets and read links run only for a request that resolves
|
|
332
|
+
* a loader-cache binding (armLoaderTagSets); elsewhere a loader execution
|
|
333
|
+
* allocates no set and a read links nothing.
|
|
334
|
+
*
|
|
335
|
+
* The same sets carry the execution's request-identity reads (#972,
|
|
336
|
+
* recordLoaderIdentityRead in server/context.ts): an entry with no declared
|
|
337
|
+
* key refuses an execution whose links reach one (recordedIdentityRead).
|
|
338
|
+
*/
|
|
339
|
+
interface TagCapture {
|
|
340
|
+
into: Set<string>;
|
|
341
|
+
/** The loader body set active when the capture opened. */
|
|
342
|
+
outer: Set<string> | undefined;
|
|
343
|
+
}
|
|
344
|
+
// On globalThis like the loader scopes (server/context.ts): the identity-read
|
|
345
|
+
// recorder is installed there too, so a second evaluated copy of this module
|
|
346
|
+
// (a duplicated package, a dev re-evaluation) must share the capture scope and
|
|
347
|
+
// the recorded reads, or its recorder writes where no fill looks.
|
|
348
|
+
const recordedTagCapture: AsyncLocalStorage<TagCapture> = ((globalThis as any)[
|
|
349
|
+
Symbol.for("rangojs-router:recorded-tag-capture")
|
|
350
|
+
] ??= new AsyncLocalStorage<TagCapture>());
|
|
351
|
+
const tagLinks = new WeakMap<Set<string>, Set<Set<string>>>();
|
|
352
|
+
const loaderValueTags = new WeakMap<object, Set<string>>();
|
|
353
|
+
const identityReads: WeakMap<Set<string>, LoaderIdentityRead> = ((
|
|
354
|
+
globalThis as any
|
|
355
|
+
)[Symbol.for("rangojs-router:identity-reads")] ??= new WeakMap());
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Arm the per-execution loader tag sets for `ctx`: its match resolved a
|
|
359
|
+
* loader bound with its own cache() (match-api.ts, bindsLoaderCache), or such
|
|
360
|
+
* a binding started (loader-cache.ts). A loader-cache MISS captures its own
|
|
361
|
+
* execution and encodes either way.
|
|
362
|
+
*/
|
|
363
|
+
export function armLoaderTagSets(
|
|
364
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
365
|
+
): void {
|
|
366
|
+
if (ctx) ctx._recordLoaderTags = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Whether `ctx` records per-execution loader tag sets (armLoaderTagSets). */
|
|
370
|
+
export function loaderTagSetsArmed(ctx: RequestContext | undefined): boolean {
|
|
371
|
+
return ctx?._recordLoaderTags === true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function activeTagSet(): Set<string> | undefined {
|
|
375
|
+
const capture = recordedTagCapture.getStore();
|
|
376
|
+
const body = getLoaderBodyTags();
|
|
377
|
+
if (capture && capture.outer === body) return capture.into;
|
|
378
|
+
return body ?? capture?.into;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Call `fn`, adding every tag recorded in its async chain
|
|
383
|
+
* (recordRequestTags, recordLoaderTags) to `into`, until a loader body
|
|
384
|
+
* started inside it takes over with its own set.
|
|
385
|
+
*/
|
|
386
|
+
export function captureRecordedTags<T>(into: Set<string>, fn: () => T): T {
|
|
387
|
+
return recordedTagCapture.run({ into, outer: getLoaderBodyTags() }, fn);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** `into` also answers for every tag `from` records. */
|
|
391
|
+
export function linkRecordedTags(into: Set<string>, from: Set<string>): void {
|
|
392
|
+
if (into === from) return;
|
|
393
|
+
let links = tagLinks.get(into);
|
|
394
|
+
if (!links) tagLinks.set(into, (links = new Set()));
|
|
395
|
+
links.add(from);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Mark a loader's value promise as carrying `tags` (see readValueTags). */
|
|
399
|
+
export function tagLoaderValue(value: object, tags: Set<string>): void {
|
|
400
|
+
loaderValueTags.set(value, tags);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** A loader value's tags also answer for `tags` (tagLoaderValue). */
|
|
404
|
+
export function linkValueTags(value: object, tags: Set<string>): void {
|
|
405
|
+
const own = loaderValueTags.get(value);
|
|
406
|
+
if (own) linkRecordedTags(own, tags);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The current execution reads a loader's value: it takes on its tags. */
|
|
410
|
+
export function readValueTags(value: object): void {
|
|
411
|
+
const tags = loaderValueTags.get(value);
|
|
123
412
|
if (!tags) return;
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
413
|
+
const into = activeTagSet();
|
|
414
|
+
if (into) linkRecordedTags(into, tags);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// recordLoaderIdentityRead (server/context.ts) calls this through the shared
|
|
418
|
+
// key, not an import: a test mocking context.ts would break module init. First
|
|
419
|
+
// read per execution: the fill's error names it.
|
|
420
|
+
(globalThis as Record<symbol, unknown>)[
|
|
421
|
+
Symbol.for("rangojs-router:identity-read-recorder")
|
|
422
|
+
] = (surface: string, verb: LoaderIdentityReadVerb): void => {
|
|
423
|
+
const set = activeTagSet();
|
|
424
|
+
if (set === undefined || identityReads.has(set)) return;
|
|
425
|
+
identityReads.set(set, { surface, verb, bodyId: getCurrentLoaderBodyId() });
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* A loader-cache HIT restores the read its entry's MISS recorded onto the
|
|
430
|
+
* value's set, so an unkeyed reader of the value refuses it as it would on
|
|
431
|
+
* the MISS (loader-cache.ts identity mark).
|
|
432
|
+
*/
|
|
433
|
+
export function markIdentityRead(
|
|
434
|
+
set: Set<string>,
|
|
435
|
+
read: LoaderIdentityRead,
|
|
436
|
+
): void {
|
|
437
|
+
if (!identityReads.has(set)) identityReads.set(set, read);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The identity read `root`'s execution recorded, or one a set it links to
|
|
442
|
+
* recorded, nearest first (breadth-first, so the execution's own read wins).
|
|
443
|
+
*/
|
|
444
|
+
export function recordedIdentityRead(
|
|
445
|
+
root: Set<string>,
|
|
446
|
+
): LoaderIdentityRead | undefined {
|
|
447
|
+
const seen = new Set<Set<string>>();
|
|
448
|
+
const queue = [root];
|
|
449
|
+
for (let i = 0; i < queue.length; i++) {
|
|
450
|
+
const set = queue[i]!;
|
|
451
|
+
if (seen.has(set)) continue;
|
|
452
|
+
seen.add(set);
|
|
453
|
+
const read = identityReads.get(set);
|
|
454
|
+
if (read) return read;
|
|
455
|
+
for (const linked of tagLinks.get(set) ?? []) queue.push(linked);
|
|
456
|
+
}
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** `root`'s tags plus those of every set it links to, transitively. */
|
|
461
|
+
export function flattenRecordedTags(root: Set<string>): Set<string> {
|
|
462
|
+
const out = new Set<string>();
|
|
463
|
+
const seen = new Set<Set<string>>();
|
|
464
|
+
const pending = [root];
|
|
465
|
+
for (let set = pending.pop(); set !== undefined; set = pending.pop()) {
|
|
466
|
+
if (seen.has(set)) continue;
|
|
467
|
+
seen.add(set);
|
|
468
|
+
for (const tag of set) out.add(tag);
|
|
469
|
+
for (const linked of tagLinks.get(set) ?? []) pending.push(linked);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** recordRequestTags, also recording onto `owner`'s set (#957). */
|
|
475
|
+
function recordOwnedTags(
|
|
476
|
+
tags: Iterable<string> | undefined,
|
|
477
|
+
ctx: RequestContext | undefined,
|
|
478
|
+
owner: string | undefined,
|
|
479
|
+
): void {
|
|
480
|
+
if (!tags || !ctx?._requestTags) return;
|
|
481
|
+
const set = ctx._requestTags;
|
|
482
|
+
const captured = activeTagSet();
|
|
483
|
+
let owned: Set<string> | undefined;
|
|
126
484
|
for (const tag of tags) {
|
|
127
485
|
const normalized = normalizeTag(tag);
|
|
128
|
-
if (normalized
|
|
486
|
+
if (normalized === null) continue;
|
|
487
|
+
set.add(normalized);
|
|
488
|
+
captured?.add(normalized);
|
|
489
|
+
if (owner !== undefined) {
|
|
490
|
+
owned ??= setFor(ownersFor(ctx).tags, owner);
|
|
491
|
+
owned.add(normalized);
|
|
492
|
+
}
|
|
129
493
|
}
|
|
130
494
|
}
|
|
131
495
|
|
|
@@ -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;
|