@rangojs/router 0.27.0 → 0.28.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 +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/event-controller.d.ts +2 -1
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/segment-reconciler.d.ts +6 -4
- package/dist/types/browser/types.d.ts +2 -1
- package/dist/types/browser/wire-protocol.d.ts +25 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/tag-invalidation.d.ts +10 -0
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +4 -4
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +4 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +10 -0
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +24 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +9 -1
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +34 -0
- package/dist/vite/index.js +3809 -3710
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +17 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +8 -1
- package/skills/testing/response-routes.md +6 -4
- package/skills/use-cache/SKILL.md +46 -8
- package/src/__internal.ts +7 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +5 -3
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +14 -13
- package/src/browser/navigation-client.ts +18 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +66 -53
- package/src/browser/server-action-bridge.ts +50 -57
- package/src/browser/types.ts +2 -1
- package/src/browser/wire-protocol.ts +41 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +110 -50
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +34 -13
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +21 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +4 -4
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +77 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +4 -5
- package/src/router/match-context.ts +20 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +41 -53
- package/src/router/match-middleware/cache-lookup.ts +675 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +4 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/loader-cache.ts +14 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +137 -0
- package/src/router/segment-resolution/revalidation.ts +897 -776
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +10 -0
- package/src/router.ts +12 -13
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +757 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +212 -169
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +26 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +223 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/serve-shell-request.ts +23 -36
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +9 -1
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +35 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -445,6 +445,85 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
445
445
|
return typeof value === "object" && value !== null;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
function asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
|
|
449
|
+
if (!isRecord(raw)) return null;
|
|
450
|
+
const { d, s, e, ta } = raw;
|
|
451
|
+
if (!isRecord(d) || !Array.isArray((d as Record<string, unknown>).segments)) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
455
|
+
return {
|
|
456
|
+
d: d as unknown as CachedEntryData,
|
|
457
|
+
s,
|
|
458
|
+
e,
|
|
459
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
|
|
464
|
+
if (!isRecord(raw)) return null;
|
|
465
|
+
const { v, h, s, e, t, ta } = raw;
|
|
466
|
+
if (typeof v !== "string") return null;
|
|
467
|
+
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
468
|
+
return {
|
|
469
|
+
v,
|
|
470
|
+
h: typeof h === "string" ? h : undefined,
|
|
471
|
+
s,
|
|
472
|
+
e,
|
|
473
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
474
|
+
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
479
|
+
if (!isRecord(raw)) return null;
|
|
480
|
+
const { p, po, rv, bv, c, s, e, t, i, lp, sn, dk, pr, no } = raw;
|
|
481
|
+
if (typeof rv !== "string" || typeof bv !== "string") return null;
|
|
482
|
+
// Document half: required unless navigationOnly (`no`), which stores
|
|
483
|
+
// neither field. Tolerate a legacy navigationOnly envelope that still
|
|
484
|
+
// carries them.
|
|
485
|
+
if (p === undefined ? no !== true : typeof p !== "string") return null;
|
|
486
|
+
if (po === undefined ? no !== true : po !== null && typeof po !== "string") {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
if (typeof c !== "number") return null;
|
|
490
|
+
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
491
|
+
return {
|
|
492
|
+
...(typeof p === "string" ? { p } : {}),
|
|
493
|
+
...(po !== undefined ? { po: po as string | null } : {}),
|
|
494
|
+
rv,
|
|
495
|
+
bv,
|
|
496
|
+
c,
|
|
497
|
+
s,
|
|
498
|
+
e,
|
|
499
|
+
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
500
|
+
i: typeof i === "string" ? i : undefined,
|
|
501
|
+
lp: typeof lp === "string" ? lp : undefined,
|
|
502
|
+
// A non-array field reads as no records.
|
|
503
|
+
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : [],
|
|
504
|
+
dk: typeof dk === "string" ? dk : undefined,
|
|
505
|
+
pr: typeof pr === "string" ? pr : undefined,
|
|
506
|
+
no: no === true ? true : undefined,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
|
|
511
|
+
if (!isRecord(raw)) return null;
|
|
512
|
+
const { b, st, hd, s, e, t, ta } = raw;
|
|
513
|
+
if (typeof b !== "string" || typeof st !== "number") return null;
|
|
514
|
+
if (!Array.isArray(hd)) return null;
|
|
515
|
+
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
516
|
+
return {
|
|
517
|
+
b,
|
|
518
|
+
st,
|
|
519
|
+
hd: hd as [string, string][],
|
|
520
|
+
s,
|
|
521
|
+
e,
|
|
522
|
+
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
523
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
448
527
|
/**
|
|
449
528
|
* The envelope's `ta` stamp, on tagged entries only: the producing execution's
|
|
450
529
|
* start when the writer passed it, else write time (#1068). Only the request
|
|
@@ -456,6 +535,83 @@ function tagStamp(tags: string[], startedAt?: number): { ta?: number } {
|
|
|
456
535
|
return ta === undefined ? {} : { ta };
|
|
457
536
|
}
|
|
458
537
|
|
|
538
|
+
/**
|
|
539
|
+
* A family read of VercelCacheStore (`get`, `getItem`, `getResponse`,
|
|
540
|
+
* through readRecord): its keys, its envelope check, the tags the request
|
|
541
|
+
* mask reads and the hit it serves.
|
|
542
|
+
*/
|
|
543
|
+
interface VercelReadFamily<
|
|
544
|
+
E extends { s: number; e: number; ta?: number },
|
|
545
|
+
V extends { shouldRevalidate: boolean },
|
|
546
|
+
Err extends CacheReadError | null,
|
|
547
|
+
> {
|
|
548
|
+
op: "get" | "getItem" | "getResponse";
|
|
549
|
+
family: "s" | "i" | "r";
|
|
550
|
+
/** Names the envelope in the corrupt-entry report. */
|
|
551
|
+
name: "segment" | "item" | "response";
|
|
552
|
+
readError: Err;
|
|
553
|
+
decode(raw: unknown): E | null;
|
|
554
|
+
/** The tags the request mask checks, with the envelope's `ta`. */
|
|
555
|
+
tags(env: E): string[] | undefined;
|
|
556
|
+
/**
|
|
557
|
+
* The hit, with `shouldRevalidate: false` until the stale claim sets it;
|
|
558
|
+
* throws when the envelope does not rebuild.
|
|
559
|
+
*/
|
|
560
|
+
hit(env: E): V;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const SEGMENT_READ: VercelReadFamily<
|
|
564
|
+
VercelSegmentEnvelope,
|
|
565
|
+
CacheGetResult,
|
|
566
|
+
CacheReadError
|
|
567
|
+
> = {
|
|
568
|
+
op: "get",
|
|
569
|
+
family: "s",
|
|
570
|
+
name: "segment",
|
|
571
|
+
// Distinct from a miss so the PPR replay composition renders uncached
|
|
572
|
+
// instead of substituting the seeded doc record.
|
|
573
|
+
readError: CACHE_READ_ERROR,
|
|
574
|
+
decode: asSegmentEnvelope,
|
|
575
|
+
tags: (env) => env.d.tags,
|
|
576
|
+
hit: (env) => ({ data: env.d, shouldRevalidate: false }),
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const ITEM_READ: VercelReadFamily<VercelItemEnvelope, CacheItemResult, null> = {
|
|
580
|
+
op: "getItem",
|
|
581
|
+
family: "i",
|
|
582
|
+
name: "item",
|
|
583
|
+
readError: null,
|
|
584
|
+
decode: asItemEnvelope,
|
|
585
|
+
tags: (env) => env.t,
|
|
586
|
+
hit: (env) => ({
|
|
587
|
+
value: env.v,
|
|
588
|
+
handles: env.h,
|
|
589
|
+
shouldRevalidate: false,
|
|
590
|
+
tags: env.t,
|
|
591
|
+
}),
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const RESPONSE_READ: VercelReadFamily<
|
|
595
|
+
VercelResponseEnvelope,
|
|
596
|
+
{ response: Response; shouldRevalidate: boolean },
|
|
597
|
+
null
|
|
598
|
+
> = {
|
|
599
|
+
op: "getResponse",
|
|
600
|
+
family: "r",
|
|
601
|
+
name: "response",
|
|
602
|
+
readError: null,
|
|
603
|
+
decode: asResponseEnvelope,
|
|
604
|
+
tags: (env) => env.t,
|
|
605
|
+
// Invalid base64, bad header entries or an out-of-range status throw.
|
|
606
|
+
hit: (env) => ({
|
|
607
|
+
response: new Response(base64ToBuffer(env.b), {
|
|
608
|
+
status: env.st,
|
|
609
|
+
headers: new Headers(env.hd),
|
|
610
|
+
}),
|
|
611
|
+
shouldRevalidate: false,
|
|
612
|
+
}),
|
|
613
|
+
};
|
|
614
|
+
|
|
459
615
|
/**
|
|
460
616
|
* Vercel Runtime Cache-backed segment cache store.
|
|
461
617
|
*
|
|
@@ -562,68 +718,8 @@ export class VercelCacheStore<
|
|
|
562
718
|
|
|
563
719
|
// --- Segment family (get/set/delete) ---
|
|
564
720
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const started = Date.now();
|
|
568
|
-
let raw: unknown;
|
|
569
|
-
try {
|
|
570
|
-
raw = await this.cache.get(storeKey);
|
|
571
|
-
} catch (error) {
|
|
572
|
-
reportCacheError(error, "cache-read", "[VercelCacheStore] get");
|
|
573
|
-
this.emitDebug({ op: "get", key, outcome: "error" });
|
|
574
|
-
// Distinct from a miss so the PPR replay composition renders uncached
|
|
575
|
-
// instead of substituting the seeded doc record (CACHE_READ_ERROR).
|
|
576
|
-
return CACHE_READ_ERROR;
|
|
577
|
-
}
|
|
578
|
-
const readMs = Date.now() - started;
|
|
579
|
-
|
|
580
|
-
if (raw == null) {
|
|
581
|
-
this.emitDebug({ op: "get", key, outcome: "miss", readMs });
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
const env = this.asSegmentEnvelope(this.decodeRaw(raw));
|
|
585
|
-
if (!env) {
|
|
586
|
-
reportCacheError(
|
|
587
|
-
new Error("malformed segment envelope"),
|
|
588
|
-
"cache-corrupt",
|
|
589
|
-
"[VercelCacheStore] get",
|
|
590
|
-
);
|
|
591
|
-
void this.safeDelete(storeKey);
|
|
592
|
-
this.emitDebug({ op: "get", key, outcome: "corrupt", readMs });
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const now = Date.now();
|
|
597
|
-
if (now > env.e) {
|
|
598
|
-
void this.safeDelete(storeKey);
|
|
599
|
-
this.emitDebug({
|
|
600
|
-
op: "get",
|
|
601
|
-
key,
|
|
602
|
-
outcome: "expired",
|
|
603
|
-
staleAt: env.s,
|
|
604
|
-
expiresAt: env.e,
|
|
605
|
-
readMs,
|
|
606
|
-
});
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
if (this.maskedMiss("get", key, { t: env.d.tags, ta: env.ta }, readMs)) {
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const isStale = env.s > 0 && now > env.s;
|
|
614
|
-
const shouldRevalidate = isStale
|
|
615
|
-
? await this.claimRevalidation(storeKey, env.e, "[VercelCacheStore] get")
|
|
616
|
-
: false;
|
|
617
|
-
this.emitDebug({
|
|
618
|
-
op: "get",
|
|
619
|
-
key,
|
|
620
|
-
outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
|
|
621
|
-
shouldRevalidate,
|
|
622
|
-
staleAt: env.s,
|
|
623
|
-
expiresAt: env.e,
|
|
624
|
-
readMs,
|
|
625
|
-
});
|
|
626
|
-
return { data: env.d, shouldRevalidate };
|
|
721
|
+
get(key: string): Promise<CacheGetResult | null | CacheReadError> {
|
|
722
|
+
return this.readRecord(key, SEGMENT_READ);
|
|
627
723
|
}
|
|
628
724
|
|
|
629
725
|
async set(
|
|
@@ -677,92 +773,10 @@ export class VercelCacheStore<
|
|
|
677
773
|
|
|
678
774
|
// --- Response family (getResponse/putResponse) ---
|
|
679
775
|
|
|
680
|
-
|
|
776
|
+
getResponse(
|
|
681
777
|
key: string,
|
|
682
778
|
): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
|
|
683
|
-
|
|
684
|
-
const started = Date.now();
|
|
685
|
-
let raw: unknown;
|
|
686
|
-
try {
|
|
687
|
-
raw = await this.cache.get(storeKey);
|
|
688
|
-
} catch (error) {
|
|
689
|
-
reportCacheError(error, "cache-read", "[VercelCacheStore] getResponse");
|
|
690
|
-
this.emitDebug({ op: "getResponse", key, outcome: "error" });
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
const readMs = Date.now() - started;
|
|
694
|
-
|
|
695
|
-
if (raw == null) {
|
|
696
|
-
this.emitDebug({ op: "getResponse", key, outcome: "miss", readMs });
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
const env = this.asResponseEnvelope(this.decodeRaw(raw));
|
|
700
|
-
if (!env) {
|
|
701
|
-
reportCacheError(
|
|
702
|
-
new Error("malformed response envelope"),
|
|
703
|
-
"cache-corrupt",
|
|
704
|
-
"[VercelCacheStore] getResponse",
|
|
705
|
-
);
|
|
706
|
-
void this.safeDelete(storeKey);
|
|
707
|
-
this.emitDebug({ op: "getResponse", key, outcome: "corrupt", readMs });
|
|
708
|
-
return null;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const now = Date.now();
|
|
712
|
-
if (now > env.e) {
|
|
713
|
-
void this.safeDelete(storeKey);
|
|
714
|
-
this.emitDebug({
|
|
715
|
-
op: "getResponse",
|
|
716
|
-
key,
|
|
717
|
-
outcome: "expired",
|
|
718
|
-
staleAt: env.s,
|
|
719
|
-
expiresAt: env.e,
|
|
720
|
-
readMs,
|
|
721
|
-
});
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
if (this.maskedMiss("getResponse", key, env, readMs)) return null;
|
|
725
|
-
|
|
726
|
-
// Reconstruct the Response BEFORE claiming revalidation. A corrupt body
|
|
727
|
-
// (e.g. invalid base64 from base64ToBuffer, or bad header entries) would
|
|
728
|
-
// otherwise throw out of getResponse — breaking the fail-open contract — and
|
|
729
|
-
// a stale read would have written a lock for an entry it then evicts. Treat a
|
|
730
|
-
// reconstruction failure as a corrupt entry: report, evict, and miss.
|
|
731
|
-
let response: Response;
|
|
732
|
-
try {
|
|
733
|
-
response = new Response(base64ToBuffer(env.b), {
|
|
734
|
-
status: env.st,
|
|
735
|
-
headers: new Headers(env.hd),
|
|
736
|
-
});
|
|
737
|
-
} catch (error) {
|
|
738
|
-
reportCacheError(
|
|
739
|
-
error,
|
|
740
|
-
"cache-corrupt",
|
|
741
|
-
"[VercelCacheStore] getResponse",
|
|
742
|
-
);
|
|
743
|
-
void this.safeDelete(storeKey);
|
|
744
|
-
this.emitDebug({ op: "getResponse", key, outcome: "corrupt", readMs });
|
|
745
|
-
return null;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const isStale = env.s > 0 && now > env.s;
|
|
749
|
-
const shouldRevalidate = isStale
|
|
750
|
-
? await this.claimRevalidation(
|
|
751
|
-
storeKey,
|
|
752
|
-
env.e,
|
|
753
|
-
"[VercelCacheStore] getResponse",
|
|
754
|
-
)
|
|
755
|
-
: false;
|
|
756
|
-
this.emitDebug({
|
|
757
|
-
op: "getResponse",
|
|
758
|
-
key,
|
|
759
|
-
outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
|
|
760
|
-
shouldRevalidate,
|
|
761
|
-
staleAt: env.s,
|
|
762
|
-
expiresAt: env.e,
|
|
763
|
-
readMs,
|
|
764
|
-
});
|
|
765
|
-
return { response, shouldRevalidate };
|
|
779
|
+
return this.readRecord(key, RESPONSE_READ);
|
|
766
780
|
}
|
|
767
781
|
|
|
768
782
|
async putResponse(
|
|
@@ -813,73 +827,8 @@ export class VercelCacheStore<
|
|
|
813
827
|
|
|
814
828
|
// --- Item family ("use cache" - getItem/setItem) ---
|
|
815
829
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const started = Date.now();
|
|
819
|
-
let raw: unknown;
|
|
820
|
-
try {
|
|
821
|
-
raw = await this.cache.get(storeKey);
|
|
822
|
-
} catch (error) {
|
|
823
|
-
reportCacheError(error, "cache-read", "[VercelCacheStore] getItem");
|
|
824
|
-
this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
827
|
-
const readMs = Date.now() - started;
|
|
828
|
-
|
|
829
|
-
if (raw == null) {
|
|
830
|
-
this.emitDebug({ op: "getItem", key, outcome: "miss", readMs });
|
|
831
|
-
return null;
|
|
832
|
-
}
|
|
833
|
-
const env = this.asItemEnvelope(this.decodeRaw(raw));
|
|
834
|
-
if (!env) {
|
|
835
|
-
reportCacheError(
|
|
836
|
-
new Error("malformed item envelope"),
|
|
837
|
-
"cache-corrupt",
|
|
838
|
-
"[VercelCacheStore] getItem",
|
|
839
|
-
);
|
|
840
|
-
void this.safeDelete(storeKey);
|
|
841
|
-
this.emitDebug({ op: "getItem", key, outcome: "corrupt", readMs });
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const now = Date.now();
|
|
846
|
-
if (now > env.e) {
|
|
847
|
-
void this.safeDelete(storeKey);
|
|
848
|
-
this.emitDebug({
|
|
849
|
-
op: "getItem",
|
|
850
|
-
key,
|
|
851
|
-
outcome: "expired",
|
|
852
|
-
staleAt: env.s,
|
|
853
|
-
expiresAt: env.e,
|
|
854
|
-
readMs,
|
|
855
|
-
});
|
|
856
|
-
return null;
|
|
857
|
-
}
|
|
858
|
-
if (this.maskedMiss("getItem", key, env, readMs)) return null;
|
|
859
|
-
|
|
860
|
-
const isStale = env.s > 0 && now > env.s;
|
|
861
|
-
const shouldRevalidate = isStale
|
|
862
|
-
? await this.claimRevalidation(
|
|
863
|
-
storeKey,
|
|
864
|
-
env.e,
|
|
865
|
-
"[VercelCacheStore] getItem",
|
|
866
|
-
)
|
|
867
|
-
: false;
|
|
868
|
-
this.emitDebug({
|
|
869
|
-
op: "getItem",
|
|
870
|
-
key,
|
|
871
|
-
outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
|
|
872
|
-
shouldRevalidate,
|
|
873
|
-
staleAt: env.s,
|
|
874
|
-
expiresAt: env.e,
|
|
875
|
-
readMs,
|
|
876
|
-
});
|
|
877
|
-
return {
|
|
878
|
-
value: env.v,
|
|
879
|
-
handles: env.h,
|
|
880
|
-
shouldRevalidate,
|
|
881
|
-
tags: env.t,
|
|
882
|
-
};
|
|
830
|
+
getItem(key: string): Promise<CacheItemResult | null> {
|
|
831
|
+
return this.readRecord(key, ITEM_READ);
|
|
883
832
|
}
|
|
884
833
|
|
|
885
834
|
async setItem(
|
|
@@ -1071,7 +1020,7 @@ export class VercelCacheStore<
|
|
|
1071
1020
|
this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
|
|
1072
1021
|
return null;
|
|
1073
1022
|
}
|
|
1074
|
-
const env =
|
|
1023
|
+
const env = asShellEnvelope(this.decodeRaw(raw));
|
|
1075
1024
|
if (stats) {
|
|
1076
1025
|
const readAt = performance.now();
|
|
1077
1026
|
stats.matchMs = readAt - readStartedAt;
|
|
@@ -1479,20 +1428,88 @@ export class VercelCacheStore<
|
|
|
1479
1428
|
return maskedForRequest(_getRequestContext(), this, tags, taggedAt ?? 0);
|
|
1480
1429
|
}
|
|
1481
1430
|
|
|
1482
|
-
/** A data read's mask check: reports the miss and returns true when masked. */
|
|
1483
|
-
private maskedMiss(
|
|
1484
|
-
op: "get" | "getItem" | "getResponse",
|
|
1485
|
-
key: string,
|
|
1486
|
-
env: { t?: string[]; ta?: number },
|
|
1487
|
-
readMs: number,
|
|
1488
|
-
): boolean {
|
|
1489
|
-
if (!this.maskedForRequest(env.t, env.ta)) return false;
|
|
1490
|
-
this.emitDebug({ op, key, outcome: "miss", readMs });
|
|
1491
|
-
return true;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
1431
|
// --- Internals ---
|
|
1495
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* The read of a data family (get, getItem, getResponse): a failed read is
|
|
1435
|
+
* the family's `readError`; a missing, corrupt, expired or masked entry is a
|
|
1436
|
+
* miss, and a corrupt or expired one is evicted. The hit is built before the
|
|
1437
|
+
* stale claim, so an entry that does not rebuild is evicted as corrupt with
|
|
1438
|
+
* no revalidation lock written for it.
|
|
1439
|
+
*/
|
|
1440
|
+
private async readRecord<
|
|
1441
|
+
E extends { s: number; e: number; ta?: number },
|
|
1442
|
+
V extends { shouldRevalidate: boolean },
|
|
1443
|
+
Err extends CacheReadError | null,
|
|
1444
|
+
>(key: string, f: VercelReadFamily<E, V, Err>): Promise<V | null | Err> {
|
|
1445
|
+
const { op } = f;
|
|
1446
|
+
const storeKey = this.toStoreKey(key, f.family);
|
|
1447
|
+
const label = `[VercelCacheStore] ${op}`;
|
|
1448
|
+
const started = Date.now();
|
|
1449
|
+
let raw: unknown;
|
|
1450
|
+
try {
|
|
1451
|
+
raw = await this.cache.get(storeKey);
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
reportCacheError(error, "cache-read", label);
|
|
1454
|
+
this.emitDebug({ op, key, outcome: "error" });
|
|
1455
|
+
return f.readError;
|
|
1456
|
+
}
|
|
1457
|
+
const readMs = Date.now() - started;
|
|
1458
|
+
|
|
1459
|
+
if (raw == null) {
|
|
1460
|
+
this.emitDebug({ op, key, outcome: "miss", readMs });
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
const corrupt = (error: unknown): null => {
|
|
1464
|
+
reportCacheError(error, "cache-corrupt", label);
|
|
1465
|
+
void this.safeDelete(storeKey);
|
|
1466
|
+
this.emitDebug({ op, key, outcome: "corrupt", readMs });
|
|
1467
|
+
return null;
|
|
1468
|
+
};
|
|
1469
|
+
const env = f.decode(this.decodeRaw(raw));
|
|
1470
|
+
if (!env) return corrupt(new Error(`malformed ${f.name} envelope`));
|
|
1471
|
+
|
|
1472
|
+
const now = Date.now();
|
|
1473
|
+
if (now > env.e) {
|
|
1474
|
+
void this.safeDelete(storeKey);
|
|
1475
|
+
this.emitDebug({
|
|
1476
|
+
op,
|
|
1477
|
+
key,
|
|
1478
|
+
outcome: "expired",
|
|
1479
|
+
staleAt: env.s,
|
|
1480
|
+
expiresAt: env.e,
|
|
1481
|
+
readMs,
|
|
1482
|
+
});
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.maskedForRequest(f.tags(env), env.ta)) {
|
|
1486
|
+
this.emitDebug({ op, key, outcome: "miss", readMs });
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
let hit: V;
|
|
1490
|
+
try {
|
|
1491
|
+
hit = f.hit(env);
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
return corrupt(error);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const isStale = env.s > 0 && now > env.s;
|
|
1497
|
+
const shouldRevalidate = isStale
|
|
1498
|
+
? await this.claimRevalidation(storeKey, env.e, label)
|
|
1499
|
+
: false;
|
|
1500
|
+
this.emitDebug({
|
|
1501
|
+
op,
|
|
1502
|
+
key,
|
|
1503
|
+
outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
|
|
1504
|
+
shouldRevalidate,
|
|
1505
|
+
staleAt: env.s,
|
|
1506
|
+
expiresAt: env.e,
|
|
1507
|
+
readMs,
|
|
1508
|
+
});
|
|
1509
|
+
hit.shouldRevalidate = shouldRevalidate;
|
|
1510
|
+
return hit;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1496
1513
|
// The `rg:{family}:` prefix keeps the three value tiers in disjoint
|
|
1497
1514
|
// keyspaces, but the isolation is only as strong as the cache's
|
|
1498
1515
|
// keyHashFunction: djb2 (getCache's default) folds this to 32 bits, so at a
|
|
@@ -1695,88 +1712,4 @@ export class VercelCacheStore<
|
|
|
1695
1712
|
// The debug sink must never break the cache path.
|
|
1696
1713
|
}
|
|
1697
1714
|
}
|
|
1698
|
-
|
|
1699
|
-
private asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
|
|
1700
|
-
if (!isRecord(raw)) return null;
|
|
1701
|
-
const { d, s, e, ta } = raw;
|
|
1702
|
-
if (
|
|
1703
|
-
!isRecord(d) ||
|
|
1704
|
-
!Array.isArray((d as Record<string, unknown>).segments)
|
|
1705
|
-
) {
|
|
1706
|
-
return null;
|
|
1707
|
-
}
|
|
1708
|
-
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1709
|
-
return {
|
|
1710
|
-
d: d as unknown as CachedEntryData,
|
|
1711
|
-
s,
|
|
1712
|
-
e,
|
|
1713
|
-
ta: typeof ta === "number" ? ta : undefined,
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
private asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
|
|
1718
|
-
if (!isRecord(raw)) return null;
|
|
1719
|
-
const { v, h, s, e, t, ta } = raw;
|
|
1720
|
-
if (typeof v !== "string") return null;
|
|
1721
|
-
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1722
|
-
return {
|
|
1723
|
-
v,
|
|
1724
|
-
h: typeof h === "string" ? h : undefined,
|
|
1725
|
-
s,
|
|
1726
|
-
e,
|
|
1727
|
-
ta: typeof ta === "number" ? ta : undefined,
|
|
1728
|
-
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
1733
|
-
if (!isRecord(raw)) return null;
|
|
1734
|
-
const { p, po, rv, bv, c, s, e, t, i, lp, sn, dk, pr, no } = raw;
|
|
1735
|
-
if (typeof rv !== "string" || typeof bv !== "string") return null;
|
|
1736
|
-
// Document half: required unless navigationOnly (`no`), which stores
|
|
1737
|
-
// neither field. Tolerate a legacy navigationOnly envelope that still
|
|
1738
|
-
// carries them.
|
|
1739
|
-
if (p === undefined ? no !== true : typeof p !== "string") return null;
|
|
1740
|
-
if (
|
|
1741
|
-
po === undefined ? no !== true : po !== null && typeof po !== "string"
|
|
1742
|
-
) {
|
|
1743
|
-
return null;
|
|
1744
|
-
}
|
|
1745
|
-
if (typeof c !== "number") return null;
|
|
1746
|
-
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1747
|
-
return {
|
|
1748
|
-
...(typeof p === "string" ? { p } : {}),
|
|
1749
|
-
...(po !== undefined ? { po: po as string | null } : {}),
|
|
1750
|
-
rv,
|
|
1751
|
-
bv,
|
|
1752
|
-
c,
|
|
1753
|
-
s,
|
|
1754
|
-
e,
|
|
1755
|
-
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1756
|
-
i: typeof i === "string" ? i : undefined,
|
|
1757
|
-
lp: typeof lp === "string" ? lp : undefined,
|
|
1758
|
-
// A non-array field reads as no records.
|
|
1759
|
-
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : [],
|
|
1760
|
-
dk: typeof dk === "string" ? dk : undefined,
|
|
1761
|
-
pr: typeof pr === "string" ? pr : undefined,
|
|
1762
|
-
no: no === true ? true : undefined,
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
private asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
|
|
1767
|
-
if (!isRecord(raw)) return null;
|
|
1768
|
-
const { b, st, hd, s, e, t, ta } = raw;
|
|
1769
|
-
if (typeof b !== "string" || typeof st !== "number") return null;
|
|
1770
|
-
if (!Array.isArray(hd)) return null;
|
|
1771
|
-
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1772
|
-
return {
|
|
1773
|
-
b,
|
|
1774
|
-
st,
|
|
1775
|
-
hd: hd as [string, string][],
|
|
1776
|
-
s,
|
|
1777
|
-
e,
|
|
1778
|
-
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1779
|
-
ta: typeof ta === "number" ? ta : undefined,
|
|
1780
|
-
};
|
|
1781
|
-
}
|
|
1782
1715
|
}
|
package/src/handles/pushed-by.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { HandleData } from "../server/handle-store.js";
|
|
|
7
7
|
*
|
|
8
8
|
* A segment's handle bucket holds its handler's pushes and those of the
|
|
9
9
|
* loaders registered on it: a loader pushes under its owning segment's id
|
|
10
|
-
* (
|
|
10
|
+
* (revalidation.ts pins `_currentSegmentId`). A navigation or an
|
|
11
11
|
* action response decides each of them on its own: the handler re-runs and a
|
|
12
12
|
* loader whose revalidate() answers false keeps its data on the client. The
|
|
13
13
|
* response carries only what re-ran, so replacing the whole bucket with it
|
package/src/prefetch-markers.ts
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The wire markers of prefetches
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* like segment-fragments.ts: the browser must not import the server modules
|
|
8
|
-
* that use them.
|
|
2
|
+
* The wire markers of prefetches: shared by the browser and the server
|
|
3
|
+
* (router/segment-resolution/prefetch-deferral.ts requestKind,
|
|
4
|
+
* rsc/prefetch-render-error.ts, rsc/rsc-rendering.ts). Dependency free, like
|
|
5
|
+
* segment-fragments.ts: the browser must not import the server modules that
|
|
6
|
+
* use them.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
12
10
|
export const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
13
11
|
|
|
14
|
-
/** Query param of a fill request (browser/navigation-client.ts). */
|
|
15
|
-
export const FILL_PARAM = "_rsc_fill";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Query param of a stale request (browser/navigation-client.ts): `revalidate()`
|
|
19
|
-
* sees `stale: true` (router/navigation-snapshot.ts).
|
|
20
|
-
*/
|
|
21
|
-
export const STALE_PARAM = "_rsc_stale";
|
|
22
|
-
|
|
23
12
|
/**
|
|
24
13
|
* The last row of a prefetch whose render reported an error
|
|
25
14
|
* (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
|