@rangojs/router 0.5.0 → 0.5.2
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 +5 -1
- package/dist/types/browser/event-controller.d.ts +6 -0
- package/dist/types/browser/prefetch/cache.d.ts +31 -2
- package/dist/types/cache/cf/cf-cache-constants.d.ts +8 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +15 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +1 -1
- package/dist/types/cache/cf/cf-kv-utils.d.ts +21 -0
- package/dist/types/handles/is-thenable.d.ts +2 -4
- package/dist/types/route-definition/helpers-types.d.ts +5 -4
- package/dist/types/rsc/helpers.d.ts +3 -0
- package/dist/types/rsc/render-pipeline.d.ts +9 -0
- package/dist/types/rsc/routine-plan.d.ts +124 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +17 -0
- package/dist/types/server/request-context.d.ts +4 -1
- package/dist/types/static-handler.d.ts +15 -1
- package/dist/types/urls/path-helper-types.d.ts +6 -7
- package/dist/types/vite/encryption-key.d.ts +2 -0
- package/dist/types/vite/plugins/expose-internal-ids.d.ts +10 -0
- package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
- package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
- package/dist/types/vite/utils/shared-utils.d.ts +12 -0
- package/dist/vite/index.js +154 -57
- package/package.json +3 -3
- package/skills/caching/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/response-routes/SKILL.md +4 -2
- package/skills/typesafety/generated-files-and-cli.md +16 -9
- package/skills/typesafety/route-types.md +5 -1
- package/skills/use-cache/SKILL.md +47 -0
- package/src/browser/event-controller.ts +40 -15
- package/src/browser/partial-update.ts +26 -11
- package/src/browser/prefetch/cache.ts +53 -9
- package/src/browser/prefetch/fetch.ts +83 -2
- package/src/browser/react/NavigationProvider.tsx +7 -0
- package/src/cache/cache-runtime.ts +89 -15
- package/src/cache/cf/cf-cache-constants.ts +8 -1
- package/src/cache/cf/cf-cache-store.ts +41 -64
- package/src/cache/cf/cf-cache-types.ts +1 -1
- package/src/cache/cf/cf-kv-utils.ts +38 -0
- package/src/cache/segment-codec.ts +21 -5
- package/src/handles/is-thenable.ts +2 -4
- package/src/route-definition/helpers-types.ts +5 -3
- package/src/router/match-middleware/cache-lookup.ts +24 -15
- package/src/rsc/handler.ts +26 -5
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/progressive-enhancement.ts +247 -70
- package/src/rsc/render-pipeline.ts +68 -24
- package/src/rsc/routine-plan.ts +359 -0
- package/src/rsc/rsc-rendering.ts +555 -302
- package/src/rsc/server-action.ts +180 -71
- package/src/rsc/shell-capture-constants.ts +18 -0
- package/src/rsc/shell-capture.ts +92 -21
- package/src/server/request-context.ts +6 -0
- package/src/ssr/ssr-root.tsx +1 -0
- package/src/static-handler.ts +18 -2
- package/src/urls/path-helper-types.ts +9 -10
- package/src/vite/encryption-key.ts +29 -0
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-internal-ids.ts +46 -0
- package/src/vite/plugins/server-ref-hashing.ts +74 -0
- package/src/vite/plugins/server-reference-pattern.ts +10 -0
- package/src/vite/rango.ts +9 -0
- package/src/vite/router-discovery.ts +21 -2
- package/src/vite/utils/shared-utils.ts +12 -7
|
@@ -57,10 +57,13 @@ import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
|
57
57
|
import type { CacheErrorCategory } from "../cache-error.js";
|
|
58
58
|
import { bufferToBase64, base64ToBuffer } from "./cf-base64.js";
|
|
59
59
|
import {
|
|
60
|
+
KV_KEY_PRESERVED_PREFIX_BYTES,
|
|
60
61
|
KV_MAX_KEY_BYTES,
|
|
61
62
|
KV_MIN_EXPIRATION_TTL,
|
|
62
63
|
kvKeyByteLength,
|
|
64
|
+
kvKeyDigest,
|
|
63
65
|
remainingCacheControl,
|
|
66
|
+
truncateToBytes,
|
|
64
67
|
} from "./cf-kv-utils.js";
|
|
65
68
|
import {
|
|
66
69
|
TAG_MARKER_CACHE_PREFIX,
|
|
@@ -1325,7 +1328,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1325
1328
|
|
|
1326
1329
|
// L2: delete from KV
|
|
1327
1330
|
if (this.kv && this.waitUntil) {
|
|
1328
|
-
const kvKey = this.toKVKey(key);
|
|
1331
|
+
const kvKey = await this.toKVKey(key);
|
|
1329
1332
|
this.waitUntil(() =>
|
|
1330
1333
|
reportingAsync(
|
|
1331
1334
|
() => this.kv!.delete(kvKey),
|
|
@@ -1559,7 +1562,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1559
1562
|
|
|
1560
1563
|
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
1561
1564
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
1562
|
-
const kvKey = this.toDocKVKey(key);
|
|
1565
|
+
const kvKey = await this.toDocKVKey(key);
|
|
1563
1566
|
// Finding #3: never persist a per-client signal in the KV envelope.
|
|
1564
1567
|
const headersArray: [string, string][] = [];
|
|
1565
1568
|
response.headers.forEach((v, k) => {
|
|
@@ -1848,7 +1851,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1848
1851
|
// JSON.stringify(KVItemEnvelope); field names differ from L1 so we assemble
|
|
1849
1852
|
// from the pre-escaped value/handles pieces rather than re-stringifying.
|
|
1850
1853
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
1851
|
-
const kvKey = this.toKVKey(`fn:${key}`);
|
|
1854
|
+
const kvKey = await this.toKVKey(`fn:${key}`);
|
|
1852
1855
|
const expiresAt = staleAt + swrWindow * 1000;
|
|
1853
1856
|
let envelopeJson = `{"v":${valueJson}`;
|
|
1854
1857
|
if (handlesJson !== undefined) envelopeJson += `,"h":${handlesJson}`;
|
|
@@ -2073,20 +2076,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2073
2076
|
const taggedAt =
|
|
2074
2077
|
Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
|
|
2075
2078
|
|
|
2076
|
-
const kvKey = this.toKVKey(`shell:${key}`);
|
|
2077
|
-
|
|
2078
|
-
const kvKeyBytes = kvKeyByteLength(kvKey);
|
|
2079
|
-
if (kvKeyBytes > KV_MAX_KEY_BYTES) {
|
|
2080
|
-
reportCacheError(
|
|
2081
|
-
new Error(
|
|
2082
|
-
`shell cache key produces a ${kvKeyBytes}-byte KV key, over the ` +
|
|
2083
|
-
`${KV_MAX_KEY_BYTES}-byte limit; the shell was not persisted to KV (L2).`,
|
|
2084
|
-
),
|
|
2085
|
-
"cache-write",
|
|
2086
|
-
"[CFCacheStore] putShell",
|
|
2087
|
-
);
|
|
2088
|
-
writeKv = false;
|
|
2089
|
-
}
|
|
2079
|
+
const kvKey = await this.toKVKey(`shell:${key}`);
|
|
2080
|
+
const writeKv = retentionTtl >= KV_MIN_EXPIRATION_TTL;
|
|
2090
2081
|
|
|
2091
2082
|
const write = (async (): Promise<"stored" | "invalidated" | void> => {
|
|
2092
2083
|
if (
|
|
@@ -2219,7 +2210,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2219
2210
|
if (!this.kv) return null;
|
|
2220
2211
|
try {
|
|
2221
2212
|
const readStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2222
|
-
const kvKey = this.toKVKey(`shell:${key}`);
|
|
2213
|
+
const kvKey = await this.toKVKey(`shell:${key}`);
|
|
2223
2214
|
const { value: envelope, timedOut } =
|
|
2224
2215
|
await this.kvGetOrEvict<CFShellEnvelope>(
|
|
2225
2216
|
kvKey,
|
|
@@ -2330,11 +2321,27 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2330
2321
|
/**
|
|
2331
2322
|
* Convert string key to KV key string.
|
|
2332
2323
|
* Uses same version prefix as Cache API for consistent invalidation.
|
|
2324
|
+
*
|
|
2325
|
+
* Single chokepoint for EVERY KV family (segments, items, shells, documents
|
|
2326
|
+
* via toDocKVKey, tag markers via tagMarkerKey): a composed key over
|
|
2327
|
+
* Cloudflare KV's 512-byte limit is normalized to a preserved readable
|
|
2328
|
+
* prefix plus a SHA-256-derived 128-bit digest of the FULL key. Without
|
|
2329
|
+
* this, kv.put/get reject with `414 ... exceeds key length limit of 512`
|
|
2330
|
+
* and the entry silently never reaches L2 (observed in production for
|
|
2331
|
+
* "use cache" items whose serialized args — e.g. a CMS query object — blow
|
|
2332
|
+
* the cap; autobarn pilot). Keys are opaque storage identifiers, so
|
|
2333
|
+
* normalization is semantics-preserving as long as distinct logical keys
|
|
2334
|
+
* stay distinct: colliding requires an identical 400-byte prefix AND a
|
|
2335
|
+
* 128-bit SHA-256 collision. Deterministic, so every family's read, write,
|
|
2336
|
+
* and delete paths agree on the stored key.
|
|
2333
2337
|
* @internal
|
|
2334
2338
|
*/
|
|
2335
|
-
private toKVKey(key: string): string {
|
|
2339
|
+
private async toKVKey(key: string): Promise<string> {
|
|
2336
2340
|
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
2337
|
-
|
|
2341
|
+
const composed = `${versionPath}${key}`;
|
|
2342
|
+
if (kvKeyByteLength(composed) <= KV_MAX_KEY_BYTES) return composed;
|
|
2343
|
+
const prefix = truncateToBytes(composed, KV_KEY_PRESERVED_PREFIX_BYTES);
|
|
2344
|
+
return `${prefix}~${await kvKeyDigest(composed)}`;
|
|
2338
2345
|
}
|
|
2339
2346
|
|
|
2340
2347
|
/**
|
|
@@ -2364,7 +2371,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2364
2371
|
* document tier is the one with a request-host context here.
|
|
2365
2372
|
* @internal
|
|
2366
2373
|
*/
|
|
2367
|
-
private toDocKVKey(key: string): string {
|
|
2374
|
+
private toDocKVKey(key: string): Promise<string> {
|
|
2368
2375
|
return this.toKVKey(`h/${this.docKVHost()}/doc:${key}`);
|
|
2369
2376
|
}
|
|
2370
2377
|
|
|
@@ -2480,7 +2487,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2480
2487
|
// ============================================================================
|
|
2481
2488
|
|
|
2482
2489
|
/** KV key for a tag's invalidation marker. */
|
|
2483
|
-
private tagMarkerKey(tag: string): string {
|
|
2490
|
+
private tagMarkerKey(tag: string): Promise<string> {
|
|
2484
2491
|
return this.toKVKey(`${TAG_MARKER_PREFIX}${tag}`);
|
|
2485
2492
|
}
|
|
2486
2493
|
|
|
@@ -2805,7 +2812,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2805
2812
|
// (reported cache-read), which also fails open. Either way one slow tag
|
|
2806
2813
|
// never amplifies into a per-segment stall.
|
|
2807
2814
|
const { value: raw, timedOut } = await this.readWithTimeout<string | null>(
|
|
2808
|
-
() => this.kv!.get(this.tagMarkerKey(tag), { type: "text" }),
|
|
2815
|
+
async () => this.kv!.get(await this.tagMarkerKey(tag), { type: "text" }),
|
|
2809
2816
|
this.kvReadTimeoutMs,
|
|
2810
2817
|
"tag marker KV read",
|
|
2811
2818
|
);
|
|
@@ -3012,7 +3019,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3012
3019
|
*
|
|
3013
3020
|
* Durable-write integrity: the in-memory write-through (memo + L1) for a tag
|
|
3014
3021
|
* runs ONLY after that tag's KV marker write is confirmed. If any KV write
|
|
3015
|
-
* fails (transient error
|
|
3022
|
+
* fails (transient error; over-limit keys are normalized by toKVKey rather
|
|
3023
|
+
* than rejected), this rejects with the
|
|
3016
3024
|
* failed tags so an awaiting updateTag() surfaces the failure instead of
|
|
3017
3025
|
* silently reporting success while other requests/colos serve stale data. The
|
|
3018
3026
|
* eager purge still fires for the whole batch first (it is additive).
|
|
@@ -3065,18 +3073,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3065
3073
|
}
|
|
3066
3074
|
await Promise.all(
|
|
3067
3075
|
tags.map(async (tag) => {
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
errors.push(
|
|
3073
|
-
new Error(
|
|
3074
|
-
`tag "${tag}" produces a ${markerKeyBytes}-byte KV ` +
|
|
3075
|
-
`marker key, over the ${KV_MAX_KEY_BYTES}-byte limit`,
|
|
3076
|
-
),
|
|
3077
|
-
);
|
|
3078
|
-
return;
|
|
3079
|
-
}
|
|
3076
|
+
// An over-limit tag no longer rejects: tagMarkerKey normalizes
|
|
3077
|
+
// through toKVKey, and the marker read path derives the key the same
|
|
3078
|
+
// way, so oversized tags invalidate correctly instead of erroring.
|
|
3079
|
+
const markerKey = await this.tagMarkerKey(tag);
|
|
3080
3080
|
try {
|
|
3081
3081
|
await this.kv!.put(markerKey, String(invalidatedAt), {
|
|
3082
3082
|
...(this.tagInvalidationTtl
|
|
@@ -3200,7 +3200,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3200
3200
|
if (!this.kv) return null;
|
|
3201
3201
|
|
|
3202
3202
|
try {
|
|
3203
|
-
const kvKey = this.toKVKey(key);
|
|
3203
|
+
const kvKey = await this.toKVKey(key);
|
|
3204
3204
|
const { value: envelope, timedOut } =
|
|
3205
3205
|
await this.kvGetOrEvict<KVSegmentEnvelope>(
|
|
3206
3206
|
kvKey,
|
|
@@ -3294,29 +3294,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3294
3294
|
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
3295
3295
|
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
3296
3296
|
|
|
3297
|
-
const kvKey = this.toKVKey(key);
|
|
3298
|
-
|
|
3299
|
-
// Reject an oversized data-segment KV key the same way tag-marker keys are
|
|
3300
|
-
// rejected in invalidateTags(). A key over KV_MAX_KEY_BYTES makes kv.put()
|
|
3301
|
-
// fail, so the segment silently never lands in L2 (KV) and every cold-colo
|
|
3302
|
-
// or TTL-expired read re-renders instead of serving stale. Segment keys can
|
|
3303
|
-
// grow with user-controlled inputs (e.g. a route's search params), so report
|
|
3304
|
-
// a clear, actionable error and skip the doomed write rather than letting it
|
|
3305
|
-
// reject deep inside waitUntil as an opaque cache-write failure.
|
|
3306
|
-
const kvKeyBytes = kvKeyByteLength(kvKey);
|
|
3307
|
-
if (kvKeyBytes > KV_MAX_KEY_BYTES) {
|
|
3308
|
-
reportCacheError(
|
|
3309
|
-
new Error(
|
|
3310
|
-
`cache segment key produces a ${kvKeyBytes}-byte KV key, over the ` +
|
|
3311
|
-
`${KV_MAX_KEY_BYTES}-byte limit; the segment was not persisted to KV (L2). ` +
|
|
3312
|
-
`Reduce the cache-key inputs (e.g. large search params on this route).`,
|
|
3313
|
-
),
|
|
3314
|
-
"cache-write",
|
|
3315
|
-
"[CFCacheStore] kvSetSegment",
|
|
3316
|
-
);
|
|
3317
|
-
return;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
3297
|
const expiresAt = staleAt + swrWindow * 1000;
|
|
3321
3298
|
// Same wire shape as JSON.stringify({ d, s, e }) — dataJson is already
|
|
3322
3299
|
// valid JSON for CachedEntryData, so embedding it avoids re-walking the tree.
|
|
@@ -3324,8 +3301,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3324
3301
|
|
|
3325
3302
|
this.waitUntil(() =>
|
|
3326
3303
|
reportingAsync(
|
|
3327
|
-
() =>
|
|
3328
|
-
this.kv!.put(
|
|
3304
|
+
async () =>
|
|
3305
|
+
this.kv!.put(await this.toKVKey(key), envelopeJson, {
|
|
3329
3306
|
expirationTtl: totalTtl,
|
|
3330
3307
|
}),
|
|
3331
3308
|
"cache-write",
|
|
@@ -3386,7 +3363,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3386
3363
|
if (!this.kv) return null;
|
|
3387
3364
|
|
|
3388
3365
|
try {
|
|
3389
|
-
const kvKey = this.toKVKey(`fn:${key}`);
|
|
3366
|
+
const kvKey = await this.toKVKey(`fn:${key}`);
|
|
3390
3367
|
const { value: envelope, timedOut } =
|
|
3391
3368
|
await this.kvGetOrEvict<KVItemEnvelope>(
|
|
3392
3369
|
kvKey,
|
|
@@ -3508,7 +3485,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3508
3485
|
if (!this.kv) return null;
|
|
3509
3486
|
|
|
3510
3487
|
try {
|
|
3511
|
-
const kvKey = this.toDocKVKey(key);
|
|
3488
|
+
const kvKey = await this.toDocKVKey(key);
|
|
3512
3489
|
// The document path is debug-silent (op is only get/getItem): a KV-read
|
|
3513
3490
|
// timeout here is bounded for resilience parity (kvGetOrEvict applies the
|
|
3514
3491
|
// budget) but emits no kv-timeout event, so its absence from the debug
|
|
@@ -333,7 +333,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
333
333
|
* cannot stall the request; the read then falls through to its normal miss
|
|
334
334
|
* path (L2/KV or render).
|
|
335
335
|
*
|
|
336
|
-
* Defaults to {@link EDGE_LOOKUP_TIMEOUT_MS} (
|
|
336
|
+
* Defaults to {@link EDGE_LOOKUP_TIMEOUT_MS} (25). Set to 0 (or any value
|
|
337
337
|
* <= 0) to disable the budget and always await `match`.
|
|
338
338
|
*/
|
|
339
339
|
edgeLookupTimeoutMs?: number;
|
|
@@ -26,6 +26,44 @@ export function kvKeyByteLength(key: string): number {
|
|
|
26
26
|
return kvKeyEncoder.encode(key).length;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Bytes of a composed key preserved verbatim when an over-limit key is
|
|
31
|
+
* normalized (toKVKey). 400 leaves room for the `~` separator and the 32-hex
|
|
32
|
+
* digest inside KV_MAX_KEY_BYTES while keeping the version prefix and most of
|
|
33
|
+
* the logical key readable (and prefix-listable) in the KV dashboard.
|
|
34
|
+
*/
|
|
35
|
+
export const KV_KEY_PRESERVED_PREFIX_BYTES = 400;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Truncate to at most `maxBytes` of UTF-8. A multibyte sequence split at the
|
|
39
|
+
* boundary decodes to U+FFFD, which is stripped — the result is deterministic
|
|
40
|
+
* for a given input, which is all key normalization needs.
|
|
41
|
+
*/
|
|
42
|
+
export function truncateToBytes(value: string, maxBytes: number): string {
|
|
43
|
+
const bytes = kvKeyEncoder.encode(value);
|
|
44
|
+
if (bytes.length <= maxBytes) return value;
|
|
45
|
+
return new TextDecoder()
|
|
46
|
+
.decode(bytes.subarray(0, maxBytes))
|
|
47
|
+
.replace(/�+$/, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 128-bit hex digest (SHA-256 truncated) of a composed KV key. WebCrypto is
|
|
52
|
+
* available on workerd and Node alike; SHA-256 (not a fast non-crypto hash)
|
|
53
|
+
* because cache keys embed user-influenced serialized args — an engineered
|
|
54
|
+
* collision between two normalized keys would serve one entry's payload for
|
|
55
|
+
* the other's.
|
|
56
|
+
*/
|
|
57
|
+
export async function kvKeyDigest(value: string): Promise<string> {
|
|
58
|
+
const digest = await crypto.subtle.digest(
|
|
59
|
+
"SHA-256",
|
|
60
|
+
kvKeyEncoder.encode(value),
|
|
61
|
+
);
|
|
62
|
+
return Array.from(new Uint8Array(digest, 0, 16), (b) =>
|
|
63
|
+
b.toString(16).padStart(2, "0"),
|
|
64
|
+
).join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
/**
|
|
30
68
|
* Compute the Cache-Control directive for a stale-path REVALIDATING re-put from
|
|
31
69
|
* the entry's stored hard-expiry deadline (CACHE_EXPIRES_AT_HEADER). Returns the
|
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
} from "@vitejs/plugin-rsc/rsc";
|
|
19
19
|
import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
|
|
20
20
|
|
|
21
|
+
// Preserve embedded server references on a cache/prerender HIT so they
|
|
22
|
+
// re-serialize to the client instead of resolving to a raw function React
|
|
23
|
+
// refuses to pass to a Client Component. Shared across the deserialize sites so
|
|
24
|
+
// the decode policy has a single owner.
|
|
25
|
+
const PRESERVE_SERVER_REFS = { preserveServerReferences: true } as const;
|
|
26
|
+
|
|
21
27
|
/**
|
|
22
28
|
* Convert a ReadableStream to a string.
|
|
23
29
|
*/
|
|
@@ -81,7 +87,11 @@ export async function rscDeserialize<T>(
|
|
|
81
87
|
|
|
82
88
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
83
89
|
const stream = stringToStream(encoded);
|
|
84
|
-
return createFromReadableStream<T>(
|
|
90
|
+
return createFromReadableStream<T>(
|
|
91
|
+
stream,
|
|
92
|
+
{ temporaryReferences },
|
|
93
|
+
PRESERVE_SERVER_REFS,
|
|
94
|
+
);
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
/**
|
|
@@ -116,7 +126,11 @@ export async function serializeResult(value: unknown): Promise<string | null> {
|
|
|
116
126
|
export async function deserializeResult<T>(encoded: string): Promise<T> {
|
|
117
127
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
118
128
|
const stream = stringToStream(encoded);
|
|
119
|
-
return createFromReadableStream<T>(
|
|
129
|
+
return createFromReadableStream<T>(
|
|
130
|
+
stream,
|
|
131
|
+
{ temporaryReferences },
|
|
132
|
+
PRESERVE_SERVER_REFS,
|
|
133
|
+
);
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
/**
|
|
@@ -271,9 +285,11 @@ export async function deserializeSegments(
|
|
|
271
285
|
|
|
272
286
|
const [component, layout, loaderData, loaderDataPromise, loadingData] =
|
|
273
287
|
await Promise.all([
|
|
274
|
-
createFromReadableStream(
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
createFromReadableStream(
|
|
289
|
+
stringToStream(item.encoded),
|
|
290
|
+
{ temporaryReferences },
|
|
291
|
+
PRESERVE_SERVER_REFS,
|
|
292
|
+
),
|
|
277
293
|
rscDeserialize(item.encodedLayout),
|
|
278
294
|
rscDeserialize(item.encodedLoaderData),
|
|
279
295
|
rscDeserialize(item.encodedLoaderDataPromise),
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* handle entries are deferred (`Promise`) values to await before any consumer
|
|
5
|
-
* sees them, and which pass through verbatim.
|
|
2
|
+
* Shared thenable predicate for framework orchestration. It decides which values
|
|
3
|
+
* participate in promise lifecycles and which pass through synchronously.
|
|
6
4
|
*
|
|
7
5
|
* Requires a CALLABLE `then` (`typeof obj.then === "function"`), not merely a
|
|
8
6
|
* `"then" in obj` membership check, so a non-promise carrying a `then` field
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
TransitionItem,
|
|
34
34
|
UseItems,
|
|
35
35
|
} from "../route-types.js";
|
|
36
|
+
import type { StaticHandlerRef } from "../static-handler.js";
|
|
36
37
|
|
|
37
38
|
// Re-export route item types for backward compatibility
|
|
38
39
|
export type {
|
|
@@ -140,8 +141,8 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
140
141
|
* },
|
|
141
142
|
* })
|
|
142
143
|
* ```
|
|
143
|
-
* @param slots - Object with slot names (prefixed with @) mapped to handlers
|
|
144
|
-
* or `{ handler, use? }`
|
|
144
|
+
* @param slots - Object with slot names (prefixed with @) mapped to handlers,
|
|
145
|
+
* Static() definitions, or `{ handler, use? }` descriptors.
|
|
145
146
|
* @param use - Optional callback for loaders, loading, revalidate, etc.
|
|
146
147
|
* Items here apply to every slot in the call (broadcast).
|
|
147
148
|
* For per-slot single-assignment items, use the slot descriptor's
|
|
@@ -157,8 +158,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
157
158
|
`@${string}`,
|
|
158
159
|
| Handler<any, any, TEnv>
|
|
159
160
|
| ReactNode
|
|
161
|
+
| StaticHandlerRef
|
|
160
162
|
| {
|
|
161
|
-
handler: Handler<any, any, TEnv> | ReactNode;
|
|
163
|
+
handler: Handler<any, any, TEnv> | ReactNode | StaticHandlerRef;
|
|
162
164
|
use?: () => UseItems<ParallelUseItem>;
|
|
163
165
|
}
|
|
164
166
|
>,
|
|
@@ -450,21 +450,30 @@ export function withCacheLookup<TEnv>(
|
|
|
450
450
|
resolveLoadersOnly,
|
|
451
451
|
} = getRouterContext<TEnv>();
|
|
452
452
|
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
)
|
|
467
|
-
|
|
453
|
+
if (prerenderStoreShortCircuits(ctx.matched.pr, ctx.request)) {
|
|
454
|
+
// Actions normally re-render fresh and skip the prerender store. But a pure
|
|
455
|
+
// Prerender route's handler is evicted at build, so there is no fresh
|
|
456
|
+
// handler to run on an action re-render -- without the fallback the
|
|
457
|
+
// re-render falls through to the evicted handler and throws "No prerender
|
|
458
|
+
// data found". Serve the prerendered entry instead (the action ran already;
|
|
459
|
+
// its result is applied client-side via useActionState). Passthrough routes
|
|
460
|
+
// keep a liveHandler, so they still re-render fresh on actions.
|
|
461
|
+
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
462
|
+
(entry) => entry.type === "route" && entry.isPassthrough === true,
|
|
463
|
+
);
|
|
464
|
+
if (!ctx.isAction || !isPassthroughPrerenderRoute) {
|
|
465
|
+
await ensurePrerenderDeps();
|
|
466
|
+
if (prerenderStoreInstance) {
|
|
467
|
+
const served = yield* tryPrerenderLookup(
|
|
468
|
+
ctx,
|
|
469
|
+
state,
|
|
470
|
+
pipelineStart,
|
|
471
|
+
pipelineReqCtx,
|
|
472
|
+
resolveLoadersOnly,
|
|
473
|
+
resolveLoadersOnlyWithRevalidation,
|
|
474
|
+
);
|
|
475
|
+
if (served) return;
|
|
476
|
+
}
|
|
468
477
|
}
|
|
469
478
|
}
|
|
470
479
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
type RequestPlan,
|
|
111
111
|
type ExecutableRequestPlan,
|
|
112
112
|
} from "../router/request-classification.js";
|
|
113
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
113
114
|
|
|
114
115
|
/**
|
|
115
116
|
* Create an RSC request handler.
|
|
@@ -247,7 +248,8 @@ export function createRSCHandler<
|
|
|
247
248
|
actionId?: string,
|
|
248
249
|
): Promise<Response> {
|
|
249
250
|
const timeoutError = new RouterTimeoutError(phase, durationMs);
|
|
250
|
-
const
|
|
251
|
+
const requestContext = _getRequestContext<TEnv>();
|
|
252
|
+
const cursor = requestContext?._renderForeground;
|
|
251
253
|
const render: RenderTimeoutContext | undefined =
|
|
252
254
|
phase === "render-start" && cursor
|
|
253
255
|
? {
|
|
@@ -261,11 +263,29 @@ export function createRSCHandler<
|
|
|
261
263
|
}),
|
|
262
264
|
}
|
|
263
265
|
: undefined;
|
|
266
|
+
const trace =
|
|
267
|
+
phase === "render-start" ? requestContext?._activeRoutine : undefined;
|
|
268
|
+
const activeEntries = trace?.active() ?? [];
|
|
269
|
+
const activeStep = activeEntries.at(-1);
|
|
270
|
+
const activeAt = performance.now();
|
|
271
|
+
const routineSnapshot =
|
|
272
|
+
trace && activeStep
|
|
273
|
+
? {
|
|
274
|
+
name: trace.name,
|
|
275
|
+
path: activeEntries.map((entry) => entry.name),
|
|
276
|
+
durationMs: activeAt - activeStep.startedAt,
|
|
277
|
+
}
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
280
|
+
if (INTERNAL_RANGO_DEBUG && trace && activeStep) {
|
|
281
|
+
console.log(
|
|
282
|
+
`[routine] TIMEOUT ${request.method} ${url.pathname} (${trace.name})\n${trace.formatActive(activeEntries, activeAt)}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
264
285
|
|
|
265
|
-
// Each surface gets its OWN shallow
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// other two observe.
|
|
286
|
+
// Each surface gets its OWN shallow render snapshot. A consumer that mutates
|
|
287
|
+
// its copy must not corrupt what the other two observe. The internal-debug
|
|
288
|
+
// routine snapshot is bounded metadata sent only to onError.
|
|
269
289
|
callOnError(timeoutError, phase === "action" ? "action" : "handler", {
|
|
270
290
|
request,
|
|
271
291
|
url,
|
|
@@ -278,6 +298,7 @@ export function createRSCHandler<
|
|
|
278
298
|
phase,
|
|
279
299
|
durationMs,
|
|
280
300
|
...(render && { render: { ...render } }),
|
|
301
|
+
...(routineSnapshot && { routine: routineSnapshot }),
|
|
281
302
|
},
|
|
282
303
|
});
|
|
283
304
|
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
_getRequestContext,
|
|
9
9
|
getLocationState,
|
|
10
|
+
setRequestContextParams,
|
|
10
11
|
} from "../server/request-context.js";
|
|
11
12
|
import type { RequestContext } from "../server/request-context.js";
|
|
12
13
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
24
25
|
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
25
26
|
import type { RscPayload } from "./types.js";
|
|
27
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
@@ -381,3 +383,14 @@ export function finalizeResponse(response: Response): Response {
|
|
|
381
383
|
if (!ctx) return response;
|
|
382
384
|
return drainOnResponseCallbacks(ctx, response);
|
|
383
385
|
}
|
|
386
|
+
|
|
387
|
+
/** Match and stamp params/routeName on the request context as one operation. */
|
|
388
|
+
export async function matchAndRecordParams<TEnv>(
|
|
389
|
+
ctx: HandlerContext<TEnv>,
|
|
390
|
+
request: Request,
|
|
391
|
+
env: TEnv,
|
|
392
|
+
): Promise<Awaited<ReturnType<HandlerContext<TEnv>["router"]["match"]>>> {
|
|
393
|
+
const match = await ctx.router.match(request, { env });
|
|
394
|
+
setRequestContextParams(match.params, match.routeName);
|
|
395
|
+
return match;
|
|
396
|
+
}
|