@rangojs/router 0.20.0 → 0.21.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/bin/rango.js +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1537 -444
- package/package.json +1 -2
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +31 -5
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
|
@@ -502,13 +502,23 @@ export interface RangoOptions<TEnv = any> {
|
|
|
502
502
|
nonce?: NonceProvider<TEnv>;
|
|
503
503
|
|
|
504
504
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
505
|
+
* Version override. When set, this exact value is used for both of the
|
|
506
|
+
* router's versions: it keys the router's cached data (segments, `"use
|
|
507
|
+
* cache"` values, loader data) and its stored HTML (PPR shells,
|
|
508
|
+
* document-cache responses), is sent in payload metadata, and is what the
|
|
509
|
+
* browser's `_rsc_v` is compared with (a mismatch reloads the tab).
|
|
510
|
+
*
|
|
511
|
+
* Leave it unset for the default: rango computes two versions per router at
|
|
512
|
+
* build time, a data version from the router's server code and a document
|
|
513
|
+
* version that also covers the client assets. The same code builds to the
|
|
514
|
+
* same versions, so a deploy that does not change this router keeps its
|
|
515
|
+
* cache. In dev both are a stamp bumped on every RSC module edit.
|
|
516
|
+
*
|
|
517
|
+
* Set it to clear this router's cache on demand (change the value), or to
|
|
518
|
+
* tie the cache to your own release id (a per-deploy value clears the cache
|
|
519
|
+
* on every deploy).
|
|
520
|
+
*
|
|
521
|
+
* @default the router's build versions
|
|
512
522
|
*/
|
|
513
523
|
version?: string;
|
|
514
524
|
|
|
@@ -50,19 +50,29 @@ import {
|
|
|
50
50
|
type LoaderIdentityRead,
|
|
51
51
|
} from "../../server/context.js";
|
|
52
52
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
53
|
-
import type {
|
|
53
|
+
import type {
|
|
54
|
+
HandleStore,
|
|
55
|
+
RecordAuthority,
|
|
56
|
+
} from "../../server/handle-store.js";
|
|
54
57
|
import type { CacheItemResult } from "../../cache/types.js";
|
|
55
58
|
import {
|
|
56
59
|
startHandleCapture,
|
|
57
60
|
type HandleCapture,
|
|
58
61
|
} from "../../cache/handle-capture.js";
|
|
59
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
appendHandles,
|
|
64
|
+
encodeHandles,
|
|
65
|
+
decodeHandles,
|
|
66
|
+
type OwnedPushDelivery,
|
|
67
|
+
} from "../../cache/handle-snapshot.js";
|
|
68
|
+
import type { ShellLoaderSeedEntry } from "../../cache/shell-snapshot.js";
|
|
60
69
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
61
70
|
import { runIdentityExempt } from "../../cache/cache-exec-scope.js";
|
|
62
71
|
import {
|
|
63
72
|
getRequestContext,
|
|
64
73
|
_getRequestContext,
|
|
65
74
|
runWithRequestContext,
|
|
75
|
+
type RequestContext,
|
|
66
76
|
} from "../../server/request-context.js";
|
|
67
77
|
import { observePhase, PHASES } from "../instrument.js";
|
|
68
78
|
import {
|
|
@@ -256,26 +266,6 @@ function chainLoaderEntries(entry: EntryData): readonly LoaderEntry[] {
|
|
|
256
266
|
return loaders;
|
|
257
267
|
}
|
|
258
268
|
|
|
259
|
-
/**
|
|
260
|
-
* The ids of the loaders a request over the matched chain `entries` runs on
|
|
261
|
-
* the live lane (no `ssr: false`), on a chain entry, its orphan layouts or
|
|
262
|
-
* its parallel slots. A document HIT tail restores the record's pushes of
|
|
263
|
-
* every other owner as authoritative (withCacheLookup, HandleStore
|
|
264
|
-
* pushRestored); a live-lane loader is a hole, so its live run replaces its
|
|
265
|
-
* recorded pushes instead. Memoized per entry.
|
|
266
|
-
*/
|
|
267
|
-
export function liveLaneLoaderIds(
|
|
268
|
-
entries: readonly EntryData[],
|
|
269
|
-
): ReadonlySet<string> {
|
|
270
|
-
const ids = new Set<string>();
|
|
271
|
-
for (const entry of entries) {
|
|
272
|
-
for (const l of chainLoaderEntries(entry)) {
|
|
273
|
-
if (!l.bake) ids.add(l.loader.$$id);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return ids;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
269
|
/**
|
|
280
270
|
* The lane of each loader the matched chain `entries` registers: "live" (no
|
|
281
271
|
* `ssr: false`) or "bake". A loader registered on both is "live". A PPR
|
|
@@ -295,6 +285,75 @@ export function routeLoaderLanes(
|
|
|
295
285
|
return lanes;
|
|
296
286
|
}
|
|
297
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The pins this request serves loaders from, by loader id: the seed a
|
|
290
|
+
* document HIT tail (serveShellHit) or a navigation replay whose doc record
|
|
291
|
+
* hit (matchPartialWithPprReplay) armed from the shell entry's loader
|
|
292
|
+
* records. A capture serves none: its bake-lane loaders execute.
|
|
293
|
+
*
|
|
294
|
+
* The one answer to "is this loader served from the shell record on this
|
|
295
|
+
* request", for its value (resolveLoaderData) and for the record's copies of
|
|
296
|
+
* its handle pushes (loaderPins): both look the loader up here and nowhere
|
|
297
|
+
* else. A condition that makes pins, or one loader's pin, not apply belongs
|
|
298
|
+
* here or in the seed's decode (shell-snapshot.ts buildShellLoaderSeed),
|
|
299
|
+
* never after a lookup, or the two disagree: issues #1001 and #1003 were the
|
|
300
|
+
* pushes decided from the request's type while the value was decided from
|
|
301
|
+
* the pin.
|
|
302
|
+
*/
|
|
303
|
+
function servedPins(
|
|
304
|
+
reqCtx: RequestContext<any> | undefined,
|
|
305
|
+
): ReadonlyMap<string, ShellLoaderSeedEntry> | undefined {
|
|
306
|
+
return isShellCaptureActive(reqCtx) ? undefined : reqCtx?._shellLoaderSeed;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* What a record restored on a request over the matched chain `entries` is to
|
|
311
|
+
* each loader's handle pushes (HandleStore RecordAuthority), from the pins
|
|
312
|
+
* the request serves (servedPins). Call it when the record hits: the pins are
|
|
313
|
+
* read here, once, because a navigation replay arms its seed for the match
|
|
314
|
+
* only and the store asks while loader bodies run.
|
|
315
|
+
*
|
|
316
|
+
* Each side applies its own lane to the pin. The value is per registration
|
|
317
|
+
* (resolveLoaderData: an `ssr: false` registration is served its pin). The
|
|
318
|
+
* pushes are per loader: one the route also registers without `ssr: false`
|
|
319
|
+
* is a hole whatever its other registration's pin says, because the capture
|
|
320
|
+
* credited its pushes to the live registration's run (routeLoaderLanes).
|
|
321
|
+
*
|
|
322
|
+
* A dependency the route does not register has no pin of its own, and the
|
|
323
|
+
* record does not name the loader that ran it at capture: its copies stand
|
|
324
|
+
* only while every `ssr: false` loader is pinned (exact when the pins are
|
|
325
|
+
* all there or all gone).
|
|
326
|
+
*/
|
|
327
|
+
export function loaderPins(
|
|
328
|
+
entries: readonly EntryData[],
|
|
329
|
+
reqCtx: RequestContext<any> | undefined,
|
|
330
|
+
): OwnedPushDelivery {
|
|
331
|
+
const pins = servedPins(reqCtx);
|
|
332
|
+
let lanes: ReadonlyMap<string, "live" | "bake"> | undefined;
|
|
333
|
+
let dependency: RecordAuthority | undefined;
|
|
334
|
+
return (loaderId) => {
|
|
335
|
+
lanes ??= routeLoaderLanes(entries);
|
|
336
|
+
const lane = lanes.get(loaderId);
|
|
337
|
+
if (lane === "live") return "hole";
|
|
338
|
+
if (lane === "bake") {
|
|
339
|
+
const pin = pins?.get(loaderId);
|
|
340
|
+
return pin ? (pin.complete ? "pin" : "copies") : "hole";
|
|
341
|
+
}
|
|
342
|
+
if (dependency === undefined) {
|
|
343
|
+
let bakeLane = 0;
|
|
344
|
+
let pinned = 0;
|
|
345
|
+
for (const [id, registered] of lanes) {
|
|
346
|
+
if (registered !== "bake") continue;
|
|
347
|
+
bakeLane++;
|
|
348
|
+
if (pins?.has(id)) pinned++;
|
|
349
|
+
}
|
|
350
|
+
dependency =
|
|
351
|
+
bakeLane > 0 && pinned === bakeLane ? "copies" : "placeholders";
|
|
352
|
+
}
|
|
353
|
+
return dependency;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
298
357
|
function getLoaderStore(
|
|
299
358
|
loaderEntry: LoaderEntry,
|
|
300
359
|
): import("../../cache/types.js").SegmentCacheStore | null {
|
|
@@ -323,11 +382,12 @@ function getLoaderStore(
|
|
|
323
382
|
* replay; and the store reads the body scope to tell a live push from a
|
|
324
383
|
* replayed one.
|
|
325
384
|
* A PPR shell capture records the replayed pushes under this loader (its
|
|
326
|
-
* push funnel reads the pushReplayed owner, shell-capture.ts), so a
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
385
|
+
* push funnel reads the pushReplayed owner, shell-capture.ts), so a replay
|
|
386
|
+
* of that shell that serves this loader from its pin restores them owned
|
|
387
|
+
* (HandleStore.pushRestored), on a document HIT and on a navigation alike.
|
|
388
|
+
* This replay still runs there: the store drops its settled values, which
|
|
389
|
+
* the record already restored, and keeps the thenable ones the record could
|
|
390
|
+
* not keep (a deferred push, the reason the loader's pin carries `runs`).
|
|
331
391
|
*/
|
|
332
392
|
async function replayLoaderHandles(
|
|
333
393
|
encoded: string | undefined,
|
|
@@ -338,34 +398,7 @@ async function replayLoaderHandles(
|
|
|
338
398
|
): Promise<void> {
|
|
339
399
|
const recorded = encoded ? await decodeHandles(encoded) : {};
|
|
340
400
|
if (!recorded) return;
|
|
341
|
-
|
|
342
|
-
const claims = (owner: string): boolean => {
|
|
343
|
-
let deliver = delivers.get(owner);
|
|
344
|
-
if (deliver === undefined) {
|
|
345
|
-
deliver = claim ? claim(owner) : true;
|
|
346
|
-
delivers.set(owner, deliver);
|
|
347
|
-
}
|
|
348
|
-
return deliver;
|
|
349
|
-
};
|
|
350
|
-
const replay = (): void => {
|
|
351
|
-
for (const key in recorded) {
|
|
352
|
-
const owner = key.slice(key.indexOf(":") + 1);
|
|
353
|
-
if (!claims(owner)) continue;
|
|
354
|
-
for (const [handleName, values] of Object.entries(recorded[key])) {
|
|
355
|
-
for (const value of values) {
|
|
356
|
-
handleStore.pushReplayed(handleName, segmentId, value, owner);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
// A hole's entry, pushes or none, takes the place of the shell copies of
|
|
362
|
-
// its pushes (HandleStore.redeliverReplays). Not once the hole delivered
|
|
363
|
-
// in this request: a run a reader started first replaces them itself.
|
|
364
|
-
if (claims(cachedLoaderId)) {
|
|
365
|
-
handleStore.redeliverReplays(cachedLoaderId, replay);
|
|
366
|
-
} else {
|
|
367
|
-
replay();
|
|
368
|
-
}
|
|
401
|
+
appendHandles(recorded, handleStore, segmentId, claim, cachedLoaderId);
|
|
369
402
|
}
|
|
370
403
|
|
|
371
404
|
/**
|
|
@@ -492,10 +525,11 @@ export function resolveLoaderData<TEnv>(
|
|
|
492
525
|
return createMaskedLoaderPromise();
|
|
493
526
|
}
|
|
494
527
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
528
|
+
// `bake`: the pin is the `ssr: false` registration's. The caller's key is
|
|
529
|
+
// passed for an unflagged loader of an entry without loading() too.
|
|
530
|
+
if (bakeSegmentKey && loaderEntry.bake) {
|
|
531
|
+
const recorded = servedPins(reqCtx)?.get(loaderEntry.loader.$$id);
|
|
532
|
+
if (recorded) {
|
|
499
533
|
if (!recorded.holes) {
|
|
500
534
|
// Pin-first (hole-free record): the pinned container is what the
|
|
501
535
|
// payload serves (recorded paths win wholesale), resolved
|
|
@@ -800,7 +834,8 @@ function executeLoaderData<TEnv>(
|
|
|
800
834
|
});
|
|
801
835
|
|
|
802
836
|
// An entry without handles (none recorded, or dropped by an encode
|
|
803
|
-
// timeout) replays none, and still
|
|
837
|
+
// timeout) replays none, and still takes the place of the loader's
|
|
838
|
+
// placeholders (appendHandles): a claimed entry is the loader's source.
|
|
804
839
|
if (cachedRead && handleStore && owningSegmentId) {
|
|
805
840
|
await replayLoaderHandles(
|
|
806
841
|
hitHandles,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-router cache versions: the shapes shared by the build (which computes
|
|
3
|
+
* them, vite/discovery/build-versions.ts) and the runtime (which reads them,
|
|
4
|
+
* server/build-version-table.ts). No imports, so the Vite plugin bundle can
|
|
5
|
+
* use it without resolving the `@rangojs/router:version` virtual module.
|
|
6
|
+
*
|
|
7
|
+
* Design: docs/design/per-app-cache-version.md.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The two versions one `createRouter()` serves with.
|
|
12
|
+
*
|
|
13
|
+
* - `data` keys cached RSC data: segment entries, `"use cache"` values and
|
|
14
|
+
* loader data. It follows the router's server code only.
|
|
15
|
+
* - `document` keys stored HTML (PPR shells, document-cache responses), is the
|
|
16
|
+
* value the browser echoes as `_rsc_v`, and stamps `ShellCacheEntry.buildVersion`.
|
|
17
|
+
* It follows the server code and the client asset URLs.
|
|
18
|
+
*/
|
|
19
|
+
export interface RouterVersions {
|
|
20
|
+
readonly data: string;
|
|
21
|
+
readonly document: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The build's table as the `@rangojs/router:version` module exports it:
|
|
26
|
+
* router id to `[data, document]`. {@link DEFAULT_ROUTER_VERSIONS_KEY} holds
|
|
27
|
+
* the whole-build pair, used for a router the build could not attribute.
|
|
28
|
+
*/
|
|
29
|
+
export type RouterVersionsTable = Readonly<
|
|
30
|
+
Record<string, readonly [data: string, document: string]>
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
/** Table key of the whole-build versions. Not a legal `$$id` (8 hex chars). */
|
|
34
|
+
export const DEFAULT_ROUTER_VERSIONS_KEY = "*";
|
|
35
|
+
|
|
36
|
+
/** One value for both versions: a consumer-set `version`, or the dev stamp. */
|
|
37
|
+
export function uniformVersions(version: string): RouterVersions {
|
|
38
|
+
return { data: version, document: version };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The versions a router serves with under `table`: a consumer-set `version`
|
|
43
|
+
* for both; else the router's own entry; else the whole-build pair, for a
|
|
44
|
+
* router the table does not list. `undefined` when there is no table (dev,
|
|
45
|
+
* tests) or it has neither entry. One function for the request handler and
|
|
46
|
+
* the build's shell capture, which must stamp what the handler will check.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveVersionsFrom(
|
|
49
|
+
table: RouterVersionsTable | undefined,
|
|
50
|
+
routerId: string | undefined,
|
|
51
|
+
override: string | undefined,
|
|
52
|
+
): RouterVersions | undefined {
|
|
53
|
+
if (override !== undefined) return uniformVersions(override);
|
|
54
|
+
if (!table) return undefined;
|
|
55
|
+
const pair =
|
|
56
|
+
(routerId !== undefined && Object.hasOwn(table, routerId)
|
|
57
|
+
? table[routerId]
|
|
58
|
+
: undefined) ?? table[DEFAULT_ROUTER_VERSIONS_KEY];
|
|
59
|
+
return pair ? { data: pair[0], document: pair[1] } : undefined;
|
|
60
|
+
}
|
package/src/rsc/full-payload.ts
CHANGED
|
@@ -52,8 +52,12 @@ export function buildFullPayload(
|
|
|
52
52
|
rootLayout: ctx.router.rootLayout,
|
|
53
53
|
// Full render: resolve deferred handle values server-side so SSR markup and
|
|
54
54
|
// the first sync useHandle read see resolved values. Partial payloads
|
|
55
|
-
// (rsc-rendering.ts) keep streaming (handleStore.stream()).
|
|
56
|
-
|
|
55
|
+
// (rsc-rendering.ts) keep streaming (handleStore.stream()). A shell
|
|
56
|
+
// capture renders from what its record keeps (resolvedHandleStream).
|
|
57
|
+
handles: resolvedHandleStream(
|
|
58
|
+
handleStore,
|
|
59
|
+
reqCtx._shellCaptureRun === true,
|
|
60
|
+
),
|
|
57
61
|
// Post-handler-barrier pushes (streaming loader ctx.handle() writes)
|
|
58
62
|
// stream here; the client applies them after hydration. Instantly
|
|
59
63
|
// complete when the loader lane is idle — including PPR shell capture,
|
|
@@ -14,6 +14,11 @@ import type { SSRStreamMode } from "../router/router-options.js";
|
|
|
14
14
|
|
|
15
15
|
export interface HandlerContext<TEnv = unknown> {
|
|
16
16
|
router: RangoInternal<TEnv, any>;
|
|
17
|
+
/**
|
|
18
|
+
* The router's DOCUMENT version (router-versions.ts): payload metadata, the
|
|
19
|
+
* `_rsc_v` reload check, and PPR shell stamping and gating. The data version
|
|
20
|
+
* is not here; cache stores read it from the request context.
|
|
21
|
+
*/
|
|
17
22
|
version: string;
|
|
18
23
|
devDiscoveryEpoch?: number;
|
|
19
24
|
renderToReadableStream: RSCDependencies["renderToReadableStream"];
|
package/src/rsc/handler.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
2
|
-
/// <reference path="../vite/plugins/version.d.ts" />
|
|
3
2
|
/**
|
|
4
3
|
* RSC Request Handler
|
|
5
4
|
*
|
|
@@ -47,7 +46,7 @@ import {
|
|
|
47
46
|
type ResponseRouteMatch,
|
|
48
47
|
} from "./response-route-handler.js";
|
|
49
48
|
import { nonce as nonceToken, resolveProviderNonce } from "./nonce.js";
|
|
50
|
-
import {
|
|
49
|
+
import { resolveRouterVersions } from "../server/build-version-table.js";
|
|
51
50
|
import type { ErrorPhase } from "../types.js";
|
|
52
51
|
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
53
52
|
import {
|
|
@@ -169,7 +168,18 @@ export function createRSCHandler<
|
|
|
169
168
|
TEnv = unknown,
|
|
170
169
|
TRoutes extends Record<string, string> = Record<string, string>,
|
|
171
170
|
>(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
|
|
172
|
-
const { router,
|
|
171
|
+
const { router, nonce: nonceProvider } = options;
|
|
172
|
+
// A consumer-set version (this option, else createRouter({ version })) is
|
|
173
|
+
// used for both; otherwise the router's own pair from the build.
|
|
174
|
+
const versions = resolveRouterVersions(
|
|
175
|
+
router.id,
|
|
176
|
+
options.version ?? router.version,
|
|
177
|
+
);
|
|
178
|
+
// Everything the handler itself versions is tied to the HTML a tab holds:
|
|
179
|
+
// payload metadata (what the browser echoes as `_rsc_v`), the reload check,
|
|
180
|
+
// shell stamping and gating, the rango state value. The data version is only
|
|
181
|
+
// a cache-key prefix, read by the stores from the request context.
|
|
182
|
+
const version = versions.document;
|
|
173
183
|
|
|
174
184
|
// Handler-owned registry of explicit per-scope stores from cache({ store }).
|
|
175
185
|
// Lives in the closure so it is scoped per handler (multi-router deployments
|
|
@@ -572,7 +582,7 @@ export function createRSCHandler<
|
|
|
572
582
|
executionContext: executionCtx,
|
|
573
583
|
themeConfig: router.themeConfig,
|
|
574
584
|
stateCookieName: router.resolvedStateCookieName,
|
|
575
|
-
|
|
585
|
+
versions,
|
|
576
586
|
});
|
|
577
587
|
if (unmarkedRequest) requestContext._shellForcedMiss = true;
|
|
578
588
|
// Gate on the SAME enabled-semantics withTimeout uses (isTimeoutEnabled):
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -1336,7 +1336,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1336
1336
|
// null and the seeded record could not have been consulted, which used to
|
|
1337
1337
|
// read as a misleading `snapshot-miss` AFTER two wasted getShell reads.
|
|
1338
1338
|
// Same predicate as resolveNavigation, decided before any store I/O.
|
|
1339
|
-
if (!getNavigationContextHeader(request)) {
|
|
1339
|
+
if (!getNavigationContextHeader(request, url.origin)) {
|
|
1340
1340
|
return runMatch({ outcome: "BYPASS", reason: "no-navigation-context" });
|
|
1341
1341
|
}
|
|
1342
1342
|
// Fragment passthrough (#700), extended to partial replay. Navigation
|
|
@@ -1745,6 +1745,17 @@ function serveShellHit(
|
|
|
1745
1745
|
// close over that same context or a streaming tail inherits the base
|
|
1746
1746
|
// context's premature non-streaming handle snapshot.
|
|
1747
1747
|
wireRenderBarrier(tailCtx, handleStore);
|
|
1748
|
+
// The tail's render barrier resolves once its record is replayed and
|
|
1749
|
+
// before a loader runs (cache-lookup.ts withCacheLookup, yieldFromStore).
|
|
1750
|
+
// The store then holds what the prelude was rendered from, so that is
|
|
1751
|
+
// the snapshot the document hydrates with; what this request's loaders
|
|
1752
|
+
// do to it afterwards reaches the client after hydration
|
|
1753
|
+
// (HandleStore.freezeDocumentSnapshot, issue #1035).
|
|
1754
|
+
const resolveRenderBarrier = tailCtx._resolveRenderBarrier;
|
|
1755
|
+
tailCtx._resolveRenderBarrier = (segments) => {
|
|
1756
|
+
handleStore.freezeDocumentSnapshot();
|
|
1757
|
+
resolveRenderBarrier(segments);
|
|
1758
|
+
};
|
|
1748
1759
|
return tailCtx;
|
|
1749
1760
|
};
|
|
1750
1761
|
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* served through the same serveShellHit as a captured one.
|
|
12
12
|
*
|
|
13
13
|
* Lifecycle:
|
|
14
|
-
* - No expiry until the next deploy
|
|
15
|
-
*
|
|
14
|
+
* - No expiry until the next deploy: every build ships its own manifest, and
|
|
15
|
+
* its entries carry that build's document version for the router, which is
|
|
16
|
+
* what the buildVersion gate compares.
|
|
16
17
|
* - `ppr.ttl` drives STALENESS ONLY: past createdAt + ttl the entry still
|
|
17
18
|
* serves, but a runtime recapture is scheduled — SWR is the UPGRADE path
|
|
18
19
|
* from build entry to fresher runtime entry, not the bootstrap path. The
|
|
@@ -90,9 +91,10 @@ function loadManifest(): Promise<ShellManifestModule | null> {
|
|
|
90
91
|
* Only the per-request gates (tag markers, staleness) stay outside the memo;
|
|
91
92
|
* the document HIT decodes the prelude once per serve (openShellDocument),
|
|
92
93
|
* partial replay never does. `null` memoizes a failed verdict —
|
|
93
|
-
* deterministically invalid.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
94
|
+
* deterministically invalid. Keyed by the asking router's document version as
|
|
95
|
+
* well as the spec: the manifest key is pathname-only (shell-manifest-key.ts),
|
|
96
|
+
* so under a host router two routers with different versions can ask about one
|
|
97
|
+
* record.
|
|
96
98
|
*/
|
|
97
99
|
const validatedSpecs = new Map<string, ValidatedBuildShellEntry | null>();
|
|
98
100
|
|
|
@@ -110,7 +112,8 @@ async function validatedManifestRecord(
|
|
|
110
112
|
spec: string,
|
|
111
113
|
buildVersion: string,
|
|
112
114
|
): Promise<ValidatedBuildShellEntry | undefined> {
|
|
113
|
-
|
|
115
|
+
const memoKey = `${buildVersion}\u0000${spec}`;
|
|
116
|
+
let verdict = validatedSpecs.get(memoKey);
|
|
114
117
|
if (verdict === undefined) {
|
|
115
118
|
const record = (await mod.loadShellAsset(spec)).default;
|
|
116
119
|
verdict =
|
|
@@ -118,7 +121,7 @@ async function validatedManifestRecord(
|
|
|
118
121
|
hasIntactShellPayload(record.entry)
|
|
119
122
|
? (record as ValidatedBuildShellEntry)
|
|
120
123
|
: null;
|
|
121
|
-
validatedSpecs.set(
|
|
124
|
+
validatedSpecs.set(memoKey, verdict);
|
|
122
125
|
}
|
|
123
126
|
return verdict ?? undefined;
|
|
124
127
|
}
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -1961,8 +1961,8 @@ export function deriveShellCaptureContext(
|
|
|
1961
1961
|
// (settleCaptureRecord) can wait for its promises, top-level and nested,
|
|
1962
1962
|
// before the doc record encodes the handles a HIT replays. A route
|
|
1963
1963
|
// cache() record's restore (handle-snapshot.ts restoreHandles) lands
|
|
1964
|
-
// here too: a value it restores
|
|
1965
|
-
//
|
|
1964
|
+
// here too: a value it restores as a placeholder keeps that loader as
|
|
1965
|
+
// owner, so a HIT restores it as that loader's (pushRestored).
|
|
1966
1966
|
// - DSL-LOADER pushes. Only bake-lane { ssr: false } loaders and the
|
|
1967
1967
|
// loaders they await via ctx.use execute at capture (live loaders are
|
|
1968
1968
|
// masked), their pushes are in the captured HTML (<head> title/meta,
|
|
@@ -1970,28 +1970,33 @@ export function deriveShellCaptureContext(
|
|
|
1970
1970
|
// loader (loader-cache.ts), so the stored record carries them: each
|
|
1971
1971
|
// settled, thenable-free push from a loader body keeps that loader as
|
|
1972
1972
|
// `owner` (HandleStore.push), the record stores it
|
|
1973
|
-
// (CachedEntryData.handleOwners), and
|
|
1974
|
-
//
|
|
1975
|
-
//
|
|
1976
|
-
//
|
|
1977
|
-
//
|
|
1978
|
-
//
|
|
1973
|
+
// (CachedEntryData.handleOwners), and a replay that serves the loader
|
|
1974
|
+
// from its pin restores it through pushRestored (restoreHandles). Those
|
|
1975
|
+
// copies stand: a promise-carrying bake-lane loader that runs on the
|
|
1976
|
+
// replay reads the store, so its settled pushes (and those replayed
|
|
1977
|
+
// inside its body) are dropped and only its thenable ones are added. A
|
|
1978
|
+
// loader the replay does not serve from a pin is a hole: one the route
|
|
1979
|
+
// also runs on the live lane, or one whose pin the entry lost. Its run
|
|
1980
|
+
// replaces its copies (pushPlaceholder, #936), those of the
|
|
1979
1981
|
// dependencies it awaits included (they are credited to it). Nested
|
|
1980
1982
|
// thenables stay masked (the promise shape is the liveness
|
|
1981
1983
|
// declaration, mask-nested.ts). A deferred (thenable) push,
|
|
1982
1984
|
// one with masked nested promises, or one outside a loader body (a
|
|
1983
1985
|
// loader-cache replay names its loader: pushReplayed below) stays
|
|
1984
1986
|
// tagged (HandleStore.push loaderPush) so the
|
|
1985
|
-
// handle encode cannot stall on a never-resolving mask
|
|
1986
|
-
//
|
|
1987
|
-
//
|
|
1988
|
-
//
|
|
1987
|
+
// handle encode cannot stall on a never-resolving mask. A tagged push
|
|
1988
|
+
// is out of the record (captureHandles) and out of the capture's own
|
|
1989
|
+
// render (resolvedHandleStream `recordedOnly`, issue #1035): the
|
|
1990
|
+
// prelude is rendered from what the record keeps, which is what a HIT
|
|
1991
|
+
// hydrates with. The loader records then carry `runs: 1` so a HIT runs
|
|
1992
|
+
// those bodies for them, and what they push reaches the client after
|
|
1993
|
+
// hydration (HandleStore.freezeDocumentSnapshot).
|
|
1989
1994
|
const handlerPushSettles: Promise<void>[] = [];
|
|
1990
1995
|
// A replay (a loader-cache HIT, loader-cache.ts replayLoaderHandles, or a
|
|
1991
1996
|
// route cache() record's owned values, restoreHandles) re-pushes a loader's
|
|
1992
|
-
// recorded values through pushReplayed, which
|
|
1993
|
-
// loader body: name that loader for the funnel so the
|
|
1994
|
-
// under it too.
|
|
1997
|
+
// recorded values through pushReplayed or pushPlaceholder, which call
|
|
1998
|
+
// push() outside any loader body: name that loader for the funnel so the
|
|
1999
|
+
// record keeps them under it too.
|
|
1995
2000
|
let replayOwner: string | undefined;
|
|
1996
2001
|
// The live-lane loader whose body a push is made in, at any depth, unless
|
|
1997
2002
|
// a bake-lane loader's body is nearer (createMatchContextForFull sets the
|
|
@@ -2011,15 +2016,17 @@ export function deriveShellCaptureContext(
|
|
|
2011
2016
|
: undefined)
|
|
2012
2017
|
);
|
|
2013
2018
|
};
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2019
|
+
for (const copy of ["pushReplayed", "pushPlaceholder"] as const) {
|
|
2020
|
+
const raw = freshHandleStore[copy].bind(freshHandleStore);
|
|
2021
|
+
freshHandleStore[copy] = (handleName, segmentId, value, loaderId) => {
|
|
2022
|
+
replayOwner = loaderId;
|
|
2023
|
+
try {
|
|
2024
|
+
raw(handleName, segmentId, value, loaderId);
|
|
2025
|
+
} finally {
|
|
2026
|
+
replayOwner = undefined;
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2023
2030
|
const rawCapturePush = freshHandleStore.push.bind(freshHandleStore);
|
|
2024
2031
|
freshHandleStore.push = (
|
|
2025
2032
|
handleName: string,
|
|
@@ -2043,8 +2050,11 @@ export function deriveShellCaptureContext(
|
|
|
2043
2050
|
let owner: string | undefined;
|
|
2044
2051
|
if (isThenable(value)) {
|
|
2045
2052
|
// Deferred (thenable) loader pushes always keep the tag — the
|
|
2046
|
-
// carve-out below is for settled values only.
|
|
2047
|
-
|
|
2053
|
+
// carve-out below is for settled values only. Promise.resolve first: a
|
|
2054
|
+
// loader cache() entry replays a deferred push as the thenable Flight
|
|
2055
|
+
// decoded it to, whose then() returns nothing, and the slot was
|
|
2056
|
+
// `undefined` (the empty element of issue #1035).
|
|
2057
|
+
masked = Promise.resolve(value).then(mask);
|
|
2048
2058
|
} else {
|
|
2049
2059
|
masked = mask(value);
|
|
2050
2060
|
if (!maskedNestedThenable) {
|
package/src/rsc/types.ts
CHANGED
|
@@ -357,13 +357,11 @@ export interface CreateRSCHandlerOptions<
|
|
|
357
357
|
cache?: HandlerCacheConfig | ((env: TEnv) => HandlerCacheConfig);
|
|
358
358
|
|
|
359
359
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
360
|
+
* Version override for this handler. Same meaning as
|
|
361
|
+
* `createRouter({ version })`, which it takes precedence over: this exact
|
|
362
|
+
* value is used for both of the router's versions.
|
|
362
363
|
*
|
|
363
|
-
*
|
|
364
|
-
* Only set this if you need a custom versioning strategy.
|
|
365
|
-
*
|
|
366
|
-
* @default VERSION from rsc-router:version
|
|
364
|
+
* @default createRouter({ version }), else the router's build versions
|
|
367
365
|
*/
|
|
368
366
|
version?: string;
|
|
369
367
|
|