@rangojs/router 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/bin/rango.js +11 -1
- package/dist/types/browser/dev-discovery.d.ts +11 -0
- package/dist/types/browser/navigation-store.d.ts +3 -13
- package/dist/types/browser/prefetch/loader.d.ts +10 -0
- package/dist/types/browser/prefetch/runtime.d.ts +3 -0
- package/dist/types/browser/types.d.ts +4 -16
- package/dist/types/build/generate-manifest.d.ts +6 -0
- package/dist/types/cache/cache-scope.d.ts +7 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
- package/dist/types/cache/document-cache.d.ts +3 -1
- package/dist/types/cache/memory-segment-store.d.ts +6 -6
- package/dist/types/cache/shell-snapshot.d.ts +21 -8
- package/dist/types/cache/types.d.ts +25 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
- package/dist/types/dev-discovery-protocol.d.ts +5 -0
- package/dist/types/prerender/store.d.ts +1 -0
- package/dist/types/route-definition/helpers-types.d.ts +6 -6
- package/dist/types/route-map-builder.d.ts +4 -29
- package/dist/types/router/match-handlers.d.ts +2 -3
- package/dist/types/router/prerender-match.d.ts +4 -1
- package/dist/types/router/router-interfaces.d.ts +3 -1
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
- package/dist/types/router/transition-when.d.ts +13 -0
- package/dist/types/rsc/handler-context.d.ts +1 -0
- package/dist/types/rsc/render-pipeline.d.ts +3 -2
- package/dist/types/rsc/rsc-rendering.d.ts +5 -10
- package/dist/types/rsc/shell-capture.d.ts +4 -8
- package/dist/types/rsc/shell-serve.d.ts +2 -0
- package/dist/types/rsc/transition-gate.d.ts +10 -14
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/server/context.d.ts +16 -0
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/server.d.ts +1 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -2
- package/dist/types/testing/run-transition-when.d.ts +6 -5
- package/dist/types/testing/shell-status.d.ts +23 -3
- package/dist/types/types/route-entry.d.ts +3 -0
- package/dist/types/types/segments.d.ts +27 -22
- package/dist/types/urls/path-helper-types.d.ts +4 -3
- package/dist/types/vite/discovery/state.d.ts +3 -2
- package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
- package/dist/vite/index.js +194 -107
- package/package.json +24 -20
- package/skills/cache-guide/SKILL.md +6 -3
- package/skills/caching/SKILL.md +1 -1
- package/skills/catalog.json +7 -1
- package/skills/deployment-caching/SKILL.md +176 -0
- package/skills/document-cache/SKILL.md +30 -3
- package/skills/ppr/SKILL.md +90 -14
- package/skills/prerender/SKILL.md +15 -8
- package/skills/rango/SKILL.md +20 -17
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/cache-prerender.md +5 -1
- package/skills/vercel/SKILL.md +22 -1
- package/skills/view-transitions/SKILL.md +12 -8
- package/src/browser/dev-discovery.ts +66 -0
- package/src/browser/navigation-client.ts +6 -0
- package/src/browser/navigation-store.ts +4 -271
- package/src/browser/partial-update.ts +7 -14
- package/src/browser/prefetch/cache.ts +1 -1
- package/src/browser/prefetch/loader.ts +110 -0
- package/src/browser/prefetch/runtime.ts +7 -0
- package/src/browser/react/Link.tsx +7 -10
- package/src/browser/react/NavigationProvider.tsx +1 -1
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/rsc-router.tsx +4 -2
- package/src/browser/types.ts +4 -27
- package/src/build/generate-manifest.ts +11 -0
- package/src/cache/cache-scope.ts +23 -7
- package/src/cache/cf/cf-cache-store.ts +63 -39
- package/src/cache/document-cache.ts +4 -2
- package/src/cache/memory-segment-store.ts +17 -6
- package/src/cache/shell-snapshot.ts +45 -15
- package/src/cache/types.ts +24 -6
- package/src/cache/vercel/vercel-cache-store.ts +52 -22
- package/src/dev-discovery-protocol.ts +11 -0
- package/src/prerender/build-shell-capture.ts +7 -1
- package/src/prerender/store.ts +5 -0
- package/src/route-definition/dsl-helpers.ts +7 -2
- package/src/route-definition/helpers-types.ts +6 -6
- package/src/route-map-builder.ts +56 -49
- package/src/router/handler-context.ts +13 -5
- package/src/router/lazy-includes.ts +1 -0
- package/src/router/match-api.ts +8 -4
- package/src/router/match-handlers.ts +44 -6
- package/src/router/match-middleware/cache-lookup.ts +10 -3
- package/src/router/prerender-match.ts +19 -2
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/static-store.ts +36 -10
- package/src/router/segment-resolution/view-transition-default.ts +9 -5
- package/src/router/transition-when.ts +76 -0
- package/src/router.ts +34 -3
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +3 -1
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +4 -11
- package/src/rsc/progressive-enhancement.ts +9 -14
- package/src/rsc/render-pipeline.ts +10 -2
- package/src/rsc/rsc-rendering.ts +319 -147
- package/src/rsc/shell-capture.ts +34 -11
- package/src/rsc/shell-serve.ts +3 -0
- package/src/rsc/transition-gate.ts +37 -40
- package/src/rsc/types.ts +2 -0
- package/src/server/context.ts +28 -0
- package/src/server/request-context.ts +38 -10
- package/src/server.ts +0 -2
- package/src/testing/dispatch.ts +1 -0
- package/src/testing/e2e/index.ts +5 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run-transition-when.ts +42 -9
- package/src/testing/shell-status.ts +92 -3
- package/src/types/route-entry.ts +3 -0
- package/src/types/segments.ts +27 -22
- package/src/urls/include-helper.ts +1 -0
- package/src/urls/path-helper-types.ts +4 -3
- package/src/urls/path-helper.ts +8 -4
- package/src/vite/discovery/bundle-postprocess.ts +10 -7
- package/src/vite/discovery/discover-routers.ts +7 -66
- package/src/vite/discovery/prerender-collection.ts +13 -2
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/discovery/virtual-module-codegen.ts +75 -42
- package/src/vite/plugins/version-injector.ts +0 -1
- package/src/vite/plugins/virtual-entries.ts +11 -1
- package/src/vite/router-discovery.ts +132 -22
- package/src/vite/utils/manifest-utils.ts +1 -4
- package/src/vite/utils/shared-utils.ts +7 -0
|
@@ -32,12 +32,12 @@ export type LinkState =
|
|
|
32
32
|
| LocationStateEntry[]
|
|
33
33
|
| StateOrGetter<Record<string, unknown>>;
|
|
34
34
|
|
|
35
|
-
import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
|
|
36
|
-
import { getAppVersion } from "../app-version.js";
|
|
37
35
|
import {
|
|
38
36
|
observeForPrefetch,
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
prefetchDirect,
|
|
38
|
+
prefetchQueued,
|
|
39
|
+
} from "../prefetch/loader.js";
|
|
40
|
+
import { getAppVersion } from "../app-version.js";
|
|
41
41
|
|
|
42
42
|
// The (hover: none) MediaQueryList, created lazily on first client read and
|
|
43
43
|
// reused across every Link render. matchMedia allocates and registers a live
|
|
@@ -394,7 +394,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
394
394
|
|
|
395
395
|
let cancelled = false;
|
|
396
396
|
let unsubIdle: (() => void) | undefined;
|
|
397
|
-
let
|
|
397
|
+
let stopObserving: (() => void) | undefined;
|
|
398
398
|
|
|
399
399
|
const triggerPrefetch = () => {
|
|
400
400
|
if (cancelled) return;
|
|
@@ -431,8 +431,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
431
431
|
} else if (isViewport) {
|
|
432
432
|
const element = internalRef.current;
|
|
433
433
|
if (!element) return;
|
|
434
|
-
|
|
435
|
-
observeForPrefetch(element, () => {
|
|
434
|
+
stopObserving = observeForPrefetch(element, () => {
|
|
436
435
|
scheduleWhenIdle(triggerPrefetch);
|
|
437
436
|
});
|
|
438
437
|
}
|
|
@@ -440,9 +439,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
440
439
|
return () => {
|
|
441
440
|
cancelled = true;
|
|
442
441
|
unsubIdle?.();
|
|
443
|
-
|
|
444
|
-
unobserveForPrefetch(observedElement);
|
|
445
|
-
}
|
|
442
|
+
stopObserving?.();
|
|
446
443
|
};
|
|
447
444
|
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
448
445
|
|
|
@@ -26,7 +26,7 @@ import type { HandleData } from "../types.js";
|
|
|
26
26
|
import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
27
27
|
import { NonceContext } from "./nonce-context.js";
|
|
28
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
29
|
-
import { cancelAllPrefetches } from "../prefetch/
|
|
29
|
+
import { cancelAllPrefetches } from "../prefetch/loader.js";
|
|
30
30
|
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
31
31
|
import { createAppShellRef, type AppShellRef } from "../app-shell.js";
|
|
32
32
|
import { startConnectionWarmup } from "../connection-warmup.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useContext, useMemo } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
|
-
import { prefetchDirect } from "../prefetch/
|
|
5
|
+
import { prefetchDirect } from "../prefetch/loader.js";
|
|
6
6
|
import { getAppVersion } from "../app-version.js";
|
|
7
7
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
8
8
|
|
|
@@ -26,8 +26,10 @@ import { expandSegmentFragments } from "../segment-fragments.js";
|
|
|
26
26
|
import { initRangoState } from "./rango-state.js";
|
|
27
27
|
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
28
28
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
29
|
+
import {
|
|
30
|
+
setPrefetchConcurrency,
|
|
31
|
+
setPrefetchDecoder,
|
|
32
|
+
} from "./prefetch/loader.js";
|
|
31
33
|
import { setAppVersion } from "./app-version.js";
|
|
32
34
|
import {
|
|
33
35
|
isInterceptSegment,
|
package/src/browser/types.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface RscMetadata {
|
|
|
65
65
|
* Used to detect version mismatches after HMR/deployment.
|
|
66
66
|
*/
|
|
67
67
|
version?: string;
|
|
68
|
+
/** Cloudflare dev worker generation used for stale-document convergence. */
|
|
69
|
+
devDiscoveryEpoch?: number;
|
|
68
70
|
/**
|
|
69
71
|
* TTL in milliseconds for the client-side in-memory prefetch cache.
|
|
70
72
|
* Sent on initial render so the browser can configure its cache duration.
|
|
@@ -430,24 +432,10 @@ export type StateListener = () => void;
|
|
|
430
432
|
/**
|
|
431
433
|
* Navigation store interface
|
|
432
434
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* - SegmentState: Internal segment management for partial updates
|
|
435
|
+
* Owns segment state, history snapshots, and partial-update notifications.
|
|
436
|
+
* EventController owns the public navigation lifecycle exposed by hooks.
|
|
436
437
|
*/
|
|
437
438
|
export interface NavigationStore {
|
|
438
|
-
// Public state (for useNavigation hook)
|
|
439
|
-
getState(): NavigationState;
|
|
440
|
-
setState(partial: Partial<NavigationState>): void;
|
|
441
|
-
subscribe(listener: StateListener): () => void;
|
|
442
|
-
|
|
443
|
-
// Inflight action management
|
|
444
|
-
addInflightAction(action: InflightAction): void;
|
|
445
|
-
removeInflightAction(id: string): void;
|
|
446
|
-
|
|
447
|
-
// Action state (for controlling update behavior during server actions)
|
|
448
|
-
isActionInProgress(): boolean;
|
|
449
|
-
setActionInProgress(value: boolean): void;
|
|
450
|
-
|
|
451
439
|
// Internal segment state (for bridges)
|
|
452
440
|
getSegmentState(): SegmentState;
|
|
453
441
|
setPath(path: string): void;
|
|
@@ -459,8 +447,6 @@ export interface NavigationStore {
|
|
|
459
447
|
setHistoryKey(key: string): void;
|
|
460
448
|
/** Monotonic token of the most recently committed navigation. */
|
|
461
449
|
getNavInstance(): number;
|
|
462
|
-
/** Nav-instance token recorded on a cache entry (undefined if absent). */
|
|
463
|
-
getCacheEntryInstance(historyKey: string): number | undefined;
|
|
464
450
|
cacheSegmentsForHistory(
|
|
465
451
|
historyKey: string,
|
|
466
452
|
segments: ResolvedSegment[],
|
|
@@ -506,7 +492,6 @@ export interface NavigationStore {
|
|
|
506
492
|
stale?: boolean,
|
|
507
493
|
handlesPending?: boolean,
|
|
508
494
|
): void;
|
|
509
|
-
markCacheAsStale(): void;
|
|
510
495
|
markHistoryCacheStale(): void;
|
|
511
496
|
markCacheAsStaleAndBroadcast(): void;
|
|
512
497
|
clearHistoryCache(): void;
|
|
@@ -525,14 +510,6 @@ export interface NavigationStore {
|
|
|
525
510
|
// UI update notifications
|
|
526
511
|
onUpdate(callback: UpdateSubscriber): () => void;
|
|
527
512
|
emitUpdate(update: NavigationUpdate): void;
|
|
528
|
-
|
|
529
|
-
// Action state tracking (for useAction hook)
|
|
530
|
-
getActionState(actionId: string): TrackedActionState;
|
|
531
|
-
setActionState(actionId: string, state: Partial<TrackedActionState>): void;
|
|
532
|
-
subscribeToAction(
|
|
533
|
-
actionId: string,
|
|
534
|
-
listener: ActionStateListener,
|
|
535
|
-
): () => void;
|
|
536
513
|
}
|
|
537
514
|
|
|
538
515
|
// ============================================================================
|
|
@@ -115,6 +115,7 @@ async function buildPrefixTreeNode(
|
|
|
115
115
|
passthroughRoutes?: string[],
|
|
116
116
|
responseTypeRoutes?: Record<string, string>,
|
|
117
117
|
routeSearchSchemas?: Record<string, Record<string, string>>,
|
|
118
|
+
routerId?: string,
|
|
118
119
|
): Promise<PrefixTreeNode> {
|
|
119
120
|
// Resolve an async include provider (`() => import("./routes")`) so its routes
|
|
120
121
|
// are walked into the build-time manifest/types/href. Runtime matching still
|
|
@@ -156,6 +157,7 @@ async function buildPrefixTreeNode(
|
|
|
156
157
|
parent: null,
|
|
157
158
|
counters: {},
|
|
158
159
|
mountIndex,
|
|
160
|
+
...(routerId ? { routerId } : {}),
|
|
159
161
|
trackedIncludes, // Enable nested include tracking
|
|
160
162
|
},
|
|
161
163
|
() => {
|
|
@@ -227,6 +229,7 @@ async function buildPrefixTreeNode(
|
|
|
227
229
|
passthroughRoutes,
|
|
228
230
|
responseTypeRoutes,
|
|
229
231
|
routeSearchSchemas,
|
|
232
|
+
routerId,
|
|
230
233
|
),
|
|
231
234
|
);
|
|
232
235
|
|
|
@@ -296,6 +299,12 @@ export async function generateManifestFull<TEnv>(
|
|
|
296
299
|
mountIndex: number = 0,
|
|
297
300
|
options?: {
|
|
298
301
|
urlPrefix?: string;
|
|
302
|
+
/**
|
|
303
|
+
* Owning router id. Threaded into the evaluation store so path() scopes
|
|
304
|
+
* its search-schema/root-scope registrations per router — same-named
|
|
305
|
+
* routes in different routers must not clobber each other.
|
|
306
|
+
*/
|
|
307
|
+
routerId?: string;
|
|
299
308
|
/**
|
|
300
309
|
* Called once per `"use client"` component registered as an
|
|
301
310
|
* errorBoundary/notFoundBoundary fallback, with its client-reference key
|
|
@@ -328,6 +337,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
328
337
|
parent: null,
|
|
329
338
|
counters: {},
|
|
330
339
|
mountIndex,
|
|
340
|
+
...(options?.routerId ? { routerId: options.routerId } : {}),
|
|
331
341
|
trackedIncludes, // Enable include tracking
|
|
332
342
|
// basename sets the initial URL prefix for all path() registrations
|
|
333
343
|
...(options?.urlPrefix ? { urlPrefix: options.urlPrefix } : {}),
|
|
@@ -402,6 +412,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
402
412
|
passthroughRoutes,
|
|
403
413
|
responseTypeRoutes,
|
|
404
414
|
routeSearchSchemas,
|
|
415
|
+
options?.routerId,
|
|
405
416
|
),
|
|
406
417
|
);
|
|
407
418
|
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -89,6 +89,7 @@ function getDefaultRouteCacheKey(
|
|
|
89
89
|
pathname: string,
|
|
90
90
|
params?: Record<string, string>,
|
|
91
91
|
isIntercept?: boolean,
|
|
92
|
+
prefixOverride?: "doc",
|
|
92
93
|
): string {
|
|
93
94
|
const ctx = getRequestContext();
|
|
94
95
|
const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
|
|
@@ -96,7 +97,9 @@ function getDefaultRouteCacheKey(
|
|
|
96
97
|
const host = ctx?.url.host ?? "localhost";
|
|
97
98
|
|
|
98
99
|
// Intercept navigations get their own cache namespace
|
|
99
|
-
const prefix = isIntercept
|
|
100
|
+
const prefix = isIntercept
|
|
101
|
+
? "intercept"
|
|
102
|
+
: (prefixOverride ?? (isPartial ? "partial" : "doc"));
|
|
100
103
|
|
|
101
104
|
return `${prefix}:${cacheKeyBase(host, pathname, searchParams, params)}`;
|
|
102
105
|
}
|
|
@@ -105,6 +108,8 @@ function getDefaultRouteCacheKey(
|
|
|
105
108
|
// CacheScope
|
|
106
109
|
// ============================================================================
|
|
107
110
|
|
|
111
|
+
const CACHE_HIT_OBSERVERS = new WeakMap<CacheScope, () => void>();
|
|
112
|
+
|
|
108
113
|
/**
|
|
109
114
|
* CacheScope represents a cache boundary in the route tree.
|
|
110
115
|
*
|
|
@@ -131,6 +136,7 @@ export class CacheScope {
|
|
|
131
136
|
constructor(
|
|
132
137
|
config: PartialCacheOptions | false,
|
|
133
138
|
parent: CacheScope | null = null,
|
|
139
|
+
private readonly defaultKeyPrefix?: "doc",
|
|
134
140
|
) {
|
|
135
141
|
this.config = config;
|
|
136
142
|
this.parent = parent;
|
|
@@ -209,7 +215,12 @@ export class CacheScope {
|
|
|
209
215
|
params: Record<string, string>,
|
|
210
216
|
isIntercept?: boolean,
|
|
211
217
|
): Promise<string> {
|
|
212
|
-
const defaultKey = getDefaultRouteCacheKey(
|
|
218
|
+
const defaultKey = getDefaultRouteCacheKey(
|
|
219
|
+
pathname,
|
|
220
|
+
params,
|
|
221
|
+
isIntercept,
|
|
222
|
+
this.defaultKeyPrefix,
|
|
223
|
+
);
|
|
213
224
|
const keyFn = this.config !== false ? this.config.key : undefined;
|
|
214
225
|
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
215
226
|
}
|
|
@@ -339,6 +350,7 @@ export class CacheScope {
|
|
|
339
350
|
);
|
|
340
351
|
}
|
|
341
352
|
|
|
353
|
+
CACHE_HIT_OBSERVERS.get(this)?.();
|
|
342
354
|
return { segments, shouldRevalidate };
|
|
343
355
|
} catch (error) {
|
|
344
356
|
// Covers a store.get() failure AND a throwing consumer key()/keyGenerator
|
|
@@ -529,10 +541,11 @@ export function createCacheScope(
|
|
|
529
541
|
/**
|
|
530
542
|
* Shell fast path: when the route tree derived NO cache scope and the current
|
|
531
543
|
* request context carries the `_shellImplicitCache` marker (a shell capture,
|
|
532
|
-
*
|
|
533
|
-
* scope so withCacheLookup/withCacheStore treat the WHOLE
|
|
534
|
-
* cache() boundary — the shell entry IS a cache() of the
|
|
535
|
-
* loaders as the live carve-outs
|
|
544
|
+
* an eligible HIT tail, or a normal partial navigation replay), substitute an
|
|
545
|
+
* implicit doc-level scope so withCacheLookup/withCacheStore treat the WHOLE
|
|
546
|
+
* matched route as a cache() boundary — the shell entry IS a cache() of the
|
|
547
|
+
* handler layer, with loaders as the live carve-outs
|
|
548
|
+
* (resolveFreshLoadersAndYield).
|
|
536
549
|
*
|
|
537
550
|
* An existing scope — including an explicit cache(false) opt-out — always
|
|
538
551
|
* wins: the consumer's cache() semantics (their ttl/swr/store/condition) are
|
|
@@ -545,8 +558,11 @@ export function resolveShellImplicitCacheScope(
|
|
|
545
558
|
if (scope) return scope;
|
|
546
559
|
const marker = getRequestContext()?._shellImplicitCache;
|
|
547
560
|
if (!marker) return null;
|
|
548
|
-
|
|
561
|
+
const implicitScope = new CacheScope(
|
|
549
562
|
{ ttl: marker.ttl, swr: marker.swr, store: marker.store },
|
|
550
563
|
null,
|
|
564
|
+
marker.keyPrefix,
|
|
551
565
|
);
|
|
566
|
+
if (marker.onHit) CACHE_HIT_OBSERVERS.set(implicitScope, marker.onHit);
|
|
567
|
+
return implicitScope;
|
|
552
568
|
}
|
|
@@ -272,7 +272,7 @@ interface KVShellEnvelope {
|
|
|
272
272
|
rv: string;
|
|
273
273
|
/** Build version captured at prerender time (ShellCacheEntry.buildVersion) */
|
|
274
274
|
bv?: string;
|
|
275
|
-
/**
|
|
275
|
+
/** Capture-generation start time (ms epoch), used by tag marker checks. */
|
|
276
276
|
c: number;
|
|
277
277
|
/** When entry becomes stale (ms epoch) */
|
|
278
278
|
s: number;
|
|
@@ -293,6 +293,8 @@ interface KVShellEnvelope {
|
|
|
293
293
|
* their holes only a handler re-run can fill.
|
|
294
294
|
*/
|
|
295
295
|
lh?: boolean;
|
|
296
|
+
/** ShellCacheEntry.transitionWhen; conditional transitions must re-run. */
|
|
297
|
+
tw?: true;
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
/**
|
|
@@ -323,6 +325,7 @@ interface KVResponseEnvelope {
|
|
|
323
325
|
// ============================================================================
|
|
324
326
|
|
|
325
327
|
export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
328
|
+
readonly supportsPassiveShellReads: true = true;
|
|
326
329
|
readonly defaults?: CacheDefaults;
|
|
327
330
|
readonly keyGenerator?: (
|
|
328
331
|
ctx: RequestContext<TEnv>,
|
|
@@ -1743,7 +1746,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1743
1746
|
// per-colo L1 for them is a deliberate follow-up (see the PPR shell-resume
|
|
1744
1747
|
// design doc). Shell entries live only in KV (the global tier), so the family
|
|
1745
1748
|
// requires a configured KV namespace; without one, getShell/putShell no-op and
|
|
1746
|
-
// the
|
|
1749
|
+
// the integrated PPR serve path fails open to a full HTML render. Tag invalidation
|
|
1747
1750
|
// still applies: shell entries carry tags/taggedAt and are checked against the
|
|
1748
1751
|
// same KV markers isGloballyInvalidated() reads for every other tier.
|
|
1749
1752
|
|
|
@@ -1772,7 +1775,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1772
1775
|
* Get a cached PPR shell entry by key from KV (no L1). Applies the KV read
|
|
1773
1776
|
* budget, corrupt-entry eviction, hard-expiry, and tag invalidation exactly
|
|
1774
1777
|
* like kvGetItem, minus the L1 promote. SWR is a plain staleness flag — KV has
|
|
1775
|
-
* no REVALIDATING herd guard, so the
|
|
1778
|
+
* no REVALIDATING herd guard, so the capture scheduler's module-level
|
|
1776
1779
|
* in-flight set is the recapture stampede guard.
|
|
1777
1780
|
*/
|
|
1778
1781
|
async getShell(
|
|
@@ -1815,6 +1818,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1815
1818
|
initialTheme: envelope.i,
|
|
1816
1819
|
snapshot: envelope.sn,
|
|
1817
1820
|
handlerLiveHoles: envelope.lh,
|
|
1821
|
+
transitionWhen: envelope.tw,
|
|
1818
1822
|
createdAt: envelope.c,
|
|
1819
1823
|
},
|
|
1820
1824
|
shouldRevalidate,
|
|
@@ -1826,8 +1830,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1826
1830
|
}
|
|
1827
1831
|
|
|
1828
1832
|
/**
|
|
1829
|
-
* Store a PPR shell entry in KV with TTL and optional SWR window.
|
|
1830
|
-
*
|
|
1833
|
+
* Store a PPR shell entry in KV with TTL and optional SWR window. The write is
|
|
1834
|
+
* registered with waitUntil and awaited so invalidation rejection can be
|
|
1835
|
+
* acknowledged to the capture scheduler. The tags/taggedAt ride in the envelope
|
|
1831
1836
|
* so isGloballyInvalidated() can invalidate the shell via the shared KV markers.
|
|
1832
1837
|
*/
|
|
1833
1838
|
async putShell(
|
|
@@ -1836,8 +1841,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1836
1841
|
ttlSeconds?: number,
|
|
1837
1842
|
swrSeconds?: number,
|
|
1838
1843
|
tags?: string[],
|
|
1839
|
-
): Promise<void> {
|
|
1840
|
-
// KV-only tier: needs a KV namespace and waitUntil
|
|
1844
|
+
): Promise<"stored" | "invalidated" | void> {
|
|
1845
|
+
// KV-only tier: needs a KV namespace and waitUntil. The same write promise is
|
|
1846
|
+
// registered for isolate lifetime and awaited so invalidation rejection can
|
|
1847
|
+
// be acknowledged to the capture scheduler.
|
|
1841
1848
|
if (!this.kv) {
|
|
1842
1849
|
this.warnShellFamilyInertOnce();
|
|
1843
1850
|
return;
|
|
@@ -1851,9 +1858,15 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1851
1858
|
// letting kv.put reject inside waitUntil (mirrors setItem/kvSetSegment).
|
|
1852
1859
|
if (totalTtl < 60) return;
|
|
1853
1860
|
|
|
1854
|
-
const
|
|
1861
|
+
const retentionTtl =
|
|
1862
|
+
tags && tags.length > 0 && this.tagInvalidationTtl
|
|
1863
|
+
? Math.min(totalTtl, this.tagInvalidationTtl)
|
|
1864
|
+
: totalTtl;
|
|
1865
|
+
const now = Date.now();
|
|
1866
|
+
const staleAt = now + ttl * 1000;
|
|
1867
|
+
const expiresAt = now + retentionTtl * 1000;
|
|
1855
1868
|
const taggedAt =
|
|
1856
|
-
Array.isArray(tags) && tags.length > 0 ?
|
|
1869
|
+
Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
|
|
1857
1870
|
|
|
1858
1871
|
const kvKey = this.toKVKey(`shell:${key}`);
|
|
1859
1872
|
// A key over the KV limit makes kv.put reject deep inside waitUntil; report
|
|
@@ -1871,31 +1884,43 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1871
1884
|
return;
|
|
1872
1885
|
}
|
|
1873
1886
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
(
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1887
|
+
const write = (async (): Promise<"stored" | "invalidated" | void> => {
|
|
1888
|
+
try {
|
|
1889
|
+
if (
|
|
1890
|
+
tags &&
|
|
1891
|
+
tags.length > 0 &&
|
|
1892
|
+
(await this.isGloballyInvalidated(tags, entry.createdAt))
|
|
1893
|
+
) {
|
|
1894
|
+
return "invalidated";
|
|
1895
|
+
}
|
|
1896
|
+
const envelope: KVShellEnvelope = {
|
|
1897
|
+
p: entry.prelude,
|
|
1898
|
+
po: entry.postponed,
|
|
1899
|
+
rv: entry.reactVersion,
|
|
1900
|
+
bv: entry.buildVersion,
|
|
1901
|
+
c: entry.createdAt,
|
|
1902
|
+
s: staleAt,
|
|
1903
|
+
e: expiresAt,
|
|
1904
|
+
t: tags,
|
|
1905
|
+
ta: taggedAt,
|
|
1906
|
+
i: entry.initialTheme,
|
|
1907
|
+
sn: entry.snapshot,
|
|
1908
|
+
lh: entry.handlerLiveHoles,
|
|
1909
|
+
tw: entry.transitionWhen,
|
|
1910
|
+
};
|
|
1911
|
+
await this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
1912
|
+
expirationTtl: retentionTtl,
|
|
1913
|
+
});
|
|
1914
|
+
return "stored";
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
|
|
1917
|
+
return undefined;
|
|
1918
|
+
}
|
|
1919
|
+
})();
|
|
1920
|
+
this.waitUntil(async () => {
|
|
1921
|
+
await write;
|
|
1922
|
+
});
|
|
1923
|
+
return await write;
|
|
1899
1924
|
} catch (error) {
|
|
1900
1925
|
reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
|
|
1901
1926
|
}
|
|
@@ -2610,11 +2635,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2610
2635
|
* eager purge still fires for the whole batch first (it is additive).
|
|
2611
2636
|
*/
|
|
2612
2637
|
/**
|
|
2613
|
-
*
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
2617
|
-
* envelope check (identical semantics: marker >= since, fail open).
|
|
2638
|
+
* Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
|
|
2639
|
+
* SAME KV markers used by runtime envelopes also evict immutable build shells
|
|
2640
|
+
* and captures whose write races updateTag(). Thin public wrapper over the
|
|
2641
|
+
* private envelope check (marker >= since, fail open).
|
|
2618
2642
|
*/
|
|
2619
2643
|
async isTagsInvalidatedSince(
|
|
2620
2644
|
tags: string[],
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Document-Level Cache Middleware
|
|
3
3
|
*
|
|
4
|
-
* Caches full HTTP responses
|
|
4
|
+
* Caches full HTTP responses in the configured app store based on Cache-Control
|
|
5
|
+
* headers. A deployment CDN may independently consume the same shared-cache
|
|
6
|
+
* directives; this middleware itself runs inside the worker/function.
|
|
5
7
|
* Routes opt-in to caching by setting s-maxage or stale-while-revalidate headers.
|
|
6
8
|
*
|
|
7
9
|
* Flow:
|
|
@@ -58,7 +60,7 @@ function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
|
58
60
|
|
|
59
61
|
// RFC 7234: in a SHARED cache, `private` and `no-store` forbid storage and
|
|
60
62
|
// MUST win over `s-maxage` even though `private, s-maxage` is contradictory.
|
|
61
|
-
// The document cache is a shared
|
|
63
|
+
// The document cache is a shared app store, so refuse both regardless of any
|
|
62
64
|
// s-maxage / stale-while-revalidate also present. Match standalone directive
|
|
63
65
|
// tokens (start/end, whitespace, comma, semicolon, or `=` bounded), not a
|
|
64
66
|
// substring, so a value containing "private" cannot false-veto.
|
|
@@ -188,6 +188,7 @@ const DEFAULT_MAX_ENTRIES = 1000;
|
|
|
188
188
|
export class MemorySegmentCacheStore<
|
|
189
189
|
TEnv = unknown,
|
|
190
190
|
> implements SegmentCacheStore<TEnv> {
|
|
191
|
+
readonly supportsPassiveShellReads: true = true;
|
|
191
192
|
private cache: Map<string, CachedEntryData>;
|
|
192
193
|
private responseCache: Map<string, CachedResponseEntry>;
|
|
193
194
|
private itemCache: Map<string, CachedItemEntry>;
|
|
@@ -197,11 +198,9 @@ export class MemorySegmentCacheStore<
|
|
|
197
198
|
/** prefixed cache key -> set of tags (reverse index for O(tags) unregister) */
|
|
198
199
|
private keyTags: Map<string, Set<string>>;
|
|
199
200
|
/**
|
|
200
|
-
* tag -> epoch ms of its latest invalidateTags() call.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* these markers against the entry's build-time createdAt. Per-isolate,
|
|
204
|
-
* like every other map here — matching this store's tag semantics.
|
|
201
|
+
* tag -> epoch ms of its latest invalidateTags() call. Runtime capture races
|
|
202
|
+
* and immutable build shells are gated by comparing these markers with the
|
|
203
|
+
* shell generation start. Per-isolate, like every other map here.
|
|
205
204
|
*/
|
|
206
205
|
private tagInvalidatedAt: Map<string, number>;
|
|
207
206
|
readonly defaults?: CacheDefaults;
|
|
@@ -459,7 +458,14 @@ export class MemorySegmentCacheStore<
|
|
|
459
458
|
ttlSeconds?: number,
|
|
460
459
|
swrSeconds?: number,
|
|
461
460
|
tags?: string[],
|
|
462
|
-
): Promise<
|
|
461
|
+
): Promise<"stored" | "invalidated"> {
|
|
462
|
+
if (
|
|
463
|
+
tags &&
|
|
464
|
+
tags.length > 0 &&
|
|
465
|
+
this.tagsInvalidatedSince(tags, entry.createdAt)
|
|
466
|
+
) {
|
|
467
|
+
return "invalidated";
|
|
468
|
+
}
|
|
463
469
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
464
470
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
465
471
|
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
@@ -470,12 +476,17 @@ export class MemorySegmentCacheStore<
|
|
|
470
476
|
if (tags && tags.length > 0) {
|
|
471
477
|
this.registerTags(tags, prefixedKey);
|
|
472
478
|
}
|
|
479
|
+
return "stored";
|
|
473
480
|
}
|
|
474
481
|
|
|
475
482
|
async isTagsInvalidatedSince(
|
|
476
483
|
tags: string[],
|
|
477
484
|
sinceMs: number,
|
|
478
485
|
): Promise<boolean> {
|
|
486
|
+
return this.tagsInvalidatedSince(tags, sinceMs);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private tagsInvalidatedSince(tags: string[], sinceMs: number): boolean {
|
|
479
490
|
for (const tag of tags) {
|
|
480
491
|
const at = this.tagInvalidatedAt.get(tag);
|
|
481
492
|
// >= so a same-millisecond invalidation wins (freshness over staleness),
|