@rangojs/router 0.1.0 → 0.1.1

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.
Files changed (89) hide show
  1. package/dist/bin/rango.js +11 -1
  2. package/dist/types/browser/dev-discovery.d.ts +11 -0
  3. package/dist/types/browser/navigation-store.d.ts +3 -13
  4. package/dist/types/browser/prefetch/loader.d.ts +10 -0
  5. package/dist/types/browser/prefetch/runtime.d.ts +3 -0
  6. package/dist/types/browser/types.d.ts +4 -16
  7. package/dist/types/build/generate-manifest.d.ts +6 -0
  8. package/dist/types/cache/cache-scope.d.ts +7 -5
  9. package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
  10. package/dist/types/cache/memory-segment-store.d.ts +6 -6
  11. package/dist/types/cache/shell-snapshot.d.ts +21 -8
  12. package/dist/types/cache/types.d.ts +24 -7
  13. package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
  14. package/dist/types/dev-discovery-protocol.d.ts +5 -0
  15. package/dist/types/prerender/store.d.ts +1 -0
  16. package/dist/types/route-map-builder.d.ts +4 -29
  17. package/dist/types/router/prerender-match.d.ts +4 -1
  18. package/dist/types/router/router-interfaces.d.ts +3 -1
  19. package/dist/types/rsc/handler-context.d.ts +1 -0
  20. package/dist/types/rsc/render-pipeline.d.ts +3 -2
  21. package/dist/types/rsc/rsc-rendering.d.ts +5 -10
  22. package/dist/types/rsc/shell-capture.d.ts +4 -8
  23. package/dist/types/rsc/types.d.ts +2 -0
  24. package/dist/types/server/context.d.ts +16 -0
  25. package/dist/types/server/request-context.d.ts +18 -8
  26. package/dist/types/server.d.ts +1 -1
  27. package/dist/types/types/route-entry.d.ts +3 -0
  28. package/dist/types/vite/discovery/state.d.ts +3 -2
  29. package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
  30. package/dist/vite/index.js +194 -107
  31. package/package.json +7 -2
  32. package/skills/caching/SKILL.md +1 -1
  33. package/skills/ppr/SKILL.md +48 -4
  34. package/src/browser/dev-discovery.ts +66 -0
  35. package/src/browser/navigation-client.ts +6 -0
  36. package/src/browser/navigation-store.ts +4 -271
  37. package/src/browser/partial-update.ts +7 -14
  38. package/src/browser/prefetch/cache.ts +1 -1
  39. package/src/browser/prefetch/loader.ts +110 -0
  40. package/src/browser/prefetch/runtime.ts +7 -0
  41. package/src/browser/react/Link.tsx +7 -10
  42. package/src/browser/react/NavigationProvider.tsx +1 -1
  43. package/src/browser/react/use-router.ts +1 -1
  44. package/src/browser/rsc-router.tsx +4 -2
  45. package/src/browser/types.ts +4 -27
  46. package/src/build/generate-manifest.ts +11 -0
  47. package/src/cache/cache-scope.ts +17 -6
  48. package/src/cache/cf/cf-cache-store.ts +63 -39
  49. package/src/cache/memory-segment-store.ts +17 -6
  50. package/src/cache/shell-snapshot.ts +45 -15
  51. package/src/cache/types.ts +23 -6
  52. package/src/cache/vercel/vercel-cache-store.ts +52 -22
  53. package/src/dev-discovery-protocol.ts +11 -0
  54. package/src/prerender/build-shell-capture.ts +7 -1
  55. package/src/prerender/store.ts +5 -0
  56. package/src/route-definition/dsl-helpers.ts +7 -2
  57. package/src/route-map-builder.ts +56 -49
  58. package/src/router/handler-context.ts +13 -5
  59. package/src/router/lazy-includes.ts +1 -0
  60. package/src/router/match-api.ts +8 -4
  61. package/src/router/prerender-match.ts +19 -2
  62. package/src/router/router-interfaces.ts +4 -0
  63. package/src/router/segment-resolution/static-store.ts +36 -10
  64. package/src/router.ts +34 -3
  65. package/src/rsc/handler-context.ts +1 -0
  66. package/src/rsc/handler.ts +3 -1
  67. package/src/rsc/loader-fetch.ts +2 -2
  68. package/src/rsc/manifest-init.ts +4 -11
  69. package/src/rsc/render-pipeline.ts +10 -2
  70. package/src/rsc/rsc-rendering.ts +207 -146
  71. package/src/rsc/shell-capture.ts +34 -11
  72. package/src/rsc/types.ts +2 -0
  73. package/src/server/context.ts +28 -0
  74. package/src/server/request-context.ts +32 -10
  75. package/src/server.ts +0 -2
  76. package/src/testing/dispatch.ts +1 -0
  77. package/src/types/route-entry.ts +3 -0
  78. package/src/urls/include-helper.ts +1 -0
  79. package/src/urls/path-helper.ts +8 -4
  80. package/src/vite/discovery/bundle-postprocess.ts +10 -7
  81. package/src/vite/discovery/discover-routers.ts +7 -66
  82. package/src/vite/discovery/prerender-collection.ts +13 -2
  83. package/src/vite/discovery/state.ts +4 -4
  84. package/src/vite/discovery/virtual-module-codegen.ts +75 -42
  85. package/src/vite/plugins/version-injector.ts +0 -1
  86. package/src/vite/plugins/virtual-entries.ts +11 -1
  87. package/src/vite/router-discovery.ts +132 -22
  88. package/src/vite/utils/manifest-utils.ts +1 -4
  89. package/src/vite/utils/shared-utils.ts +7 -0
package/dist/bin/rango.js CHANGED
@@ -1424,7 +1424,17 @@ async function initializeApp() {
1424
1424
  // context, including strictMode (default true) from createRouter. StrictMode
1425
1425
  // is the default; createRouter({ strictMode: false }) ships the opt-out in the
1426
1426
  // payload metadata. StrictMode emits no DOM, so toggling never changes markup.
1427
- const { strictMode } = await initBrowserApp({ rscStream, deps });
1427
+ const { strictMode, initialPayload } = await initBrowserApp({ rscStream, deps });
1428
+
1429
+ if (import.meta.hot) {
1430
+ const { startDevDiscoveryHandshake } = await import(
1431
+ "@rangojs/router/internal/browser/dev-discovery"
1432
+ );
1433
+ startDevDiscoveryHandshake(
1434
+ initialPayload.metadata?.devDiscoveryEpoch,
1435
+ import.meta.hot
1436
+ );
1437
+ }
1428
1438
 
1429
1439
  const app = createElement(Rango);
1430
1440
  hydrateRoot(
@@ -0,0 +1,11 @@
1
+ import { DEV_DISCOVERY_QUERY_EVENT, DEV_DISCOVERY_READY_EVENT } from "../dev-discovery-protocol.js";
2
+ export interface DevDiscoveryHot {
3
+ on(event: typeof DEV_DISCOVERY_READY_EVENT | "vite:ws:connect", listener: (payload: unknown) => void): void;
4
+ send(event: typeof DEV_DISCOVERY_QUERY_EVENT, payload: {
5
+ epoch: number;
6
+ }): void;
7
+ }
8
+ export interface DevDiscoveryHandshakeOptions {
9
+ reload?: () => void;
10
+ }
11
+ export declare function startDevDiscoveryHandshake(documentEpoch: unknown, hot: DevDiscoveryHot, options?: DevDiscoveryHandshakeOptions): void;
@@ -61,11 +61,10 @@ export interface NavigationStoreConfig {
61
61
  onCrossTabRefresh?: () => void;
62
62
  }
63
63
  /**
64
- * Create a navigation store for managing browser-side navigation state
64
+ * Create a navigation store for browser-side segment and history state.
65
65
  *
66
- * The store manages two types of state:
67
- * - NavigationState: Public state exposed via useNavigation hook
68
- * - SegmentState: Internal segment management for partial RSC updates
66
+ * The public navigation lifecycle lives in EventController; this store owns
67
+ * segment reconciliation, history snapshots, and cross-tab cache invalidation.
69
68
  *
70
69
  * @param config - Initial configuration
71
70
  * @returns NavigationStore instance
@@ -77,15 +76,6 @@ export interface NavigationStoreConfig {
77
76
  * initialSegmentIds: [],
78
77
  * });
79
78
  *
80
- * // Subscribe to state changes (for useNavigation hook)
81
- * const unsubscribe = store.subscribe(() => {
82
- * const state = store.getState();
83
- * console.log('Navigation state:', state);
84
- * });
85
- *
86
- * // Update state
87
- * store.setState({ state: 'loading' });
88
- *
89
79
  * // Subscribe to UI updates (for re-rendering)
90
80
  * store.onUpdate((update) => {
91
81
  * console.log('New root:', update.root);
@@ -0,0 +1,10 @@
1
+ import type { RscPayload } from "../types.js";
2
+ type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
3
+ export declare function setPrefetchDecoder(fn: PrefetchDecoder): void;
4
+ export declare function setPrefetchConcurrency(value: number): void;
5
+ export declare function prefetchDirect(url: string, segmentIds: string[], version?: string, routerId?: string, prefetchKey?: ":source"): void;
6
+ export declare function prefetchQueued(url: string, segmentIds: string[], version?: string, routerId?: string, prefetchKey?: ":source"): void;
7
+ export declare function observeForPrefetch(element: Element, callback: () => void): () => void;
8
+ export declare function cancelAllPrefetches(keepUrl?: string | null): void;
9
+ export declare function abortAllPrefetches(): void;
10
+ export {};
@@ -0,0 +1,3 @@
1
+ export { prefetchDirect, prefetchQueued, setPrefetchDecoder } from "./fetch.js";
2
+ export { abortAllPrefetches, cancelAllPrefetches, setPrefetchConcurrency, } from "./queue.js";
3
+ export { observeForPrefetch, unobserveForPrefetch } from "./observer.js";
@@ -60,6 +60,8 @@ export interface RscMetadata {
60
60
  * Used to detect version mismatches after HMR/deployment.
61
61
  */
62
62
  version?: string;
63
+ /** Cloudflare dev worker generation used for stale-document convergence. */
64
+ devDiscoveryEpoch?: number;
63
65
  /**
64
66
  * TTL in milliseconds for the client-side in-memory prefetch cache.
65
67
  * Sent on initial render so the browser can configure its cache duration.
@@ -370,18 +372,10 @@ export type StateListener = () => void;
370
372
  /**
371
373
  * Navigation store interface
372
374
  *
373
- * Manages both:
374
- * - NavigationState: Public state exposed via useNavigation hook
375
- * - SegmentState: Internal segment management for partial updates
375
+ * Owns segment state, history snapshots, and partial-update notifications.
376
+ * EventController owns the public navigation lifecycle exposed by hooks.
376
377
  */
377
378
  export interface NavigationStore {
378
- getState(): NavigationState;
379
- setState(partial: Partial<NavigationState>): void;
380
- subscribe(listener: StateListener): () => void;
381
- addInflightAction(action: InflightAction): void;
382
- removeInflightAction(id: string): void;
383
- isActionInProgress(): boolean;
384
- setActionInProgress(value: boolean): void;
385
379
  getSegmentState(): SegmentState;
386
380
  setPath(path: string): void;
387
381
  setCurrentUrl(url: string): void;
@@ -390,8 +384,6 @@ export interface NavigationStore {
390
384
  setHistoryKey(key: string): void;
391
385
  /** Monotonic token of the most recently committed navigation. */
392
386
  getNavInstance(): number;
393
- /** Nav-instance token recorded on a cache entry (undefined if absent). */
394
- getCacheEntryInstance(historyKey: string): number | undefined;
395
387
  cacheSegmentsForHistory(historyKey: string, segments: ResolvedSegment[], handleData?: HandleData): void;
396
388
  getCachedSegments(historyKey: string): {
397
389
  segments: ResolvedSegment[];
@@ -420,7 +412,6 @@ export interface NavigationStore {
420
412
  * probe and write into one historyCache scan for the per-yield streaming path.
421
413
  */
422
414
  updateCacheHandleDataIfOwned(historyKey: string, handleData: HandleData, ownerInstance: number, stale?: boolean, handlesPending?: boolean): void;
423
- markCacheAsStale(): void;
424
415
  markHistoryCacheStale(): void;
425
416
  markCacheAsStaleAndBroadcast(): void;
426
417
  clearHistoryCache(): void;
@@ -431,9 +422,6 @@ export interface NavigationStore {
431
422
  setRouterId?(id: string): void;
432
423
  onUpdate(callback: UpdateSubscriber): () => void;
433
424
  emitUpdate(update: NavigationUpdate): void;
434
- getActionState(actionId: string): TrackedActionState;
435
- setActionState(actionId: string, state: Partial<TrackedActionState>): void;
436
- subscribeToAction(actionId: string, listener: ActionStateListener): () => void;
437
425
  }
438
426
  /**
439
427
  * Options for partial navigation fetch
@@ -79,6 +79,12 @@ export declare function generateManifest<TEnv>(urlpatterns: UrlPatterns<TEnv, an
79
79
  */
80
80
  export declare function generateManifestFull<TEnv>(urlpatterns: UrlPatterns<TEnv, any>, mountIndex?: number, options?: {
81
81
  urlPrefix?: string;
82
+ /**
83
+ * Owning router id. Threaded into the evaluation store so path() scopes
84
+ * its search-schema/root-scope registrations per router — same-named
85
+ * routes in different routers must not clobber each other.
86
+ */
87
+ routerId?: string;
82
88
  /**
83
89
  * Called once per `"use client"` component registered as an
84
90
  * errorBoundary/notFoundBoundary fallback, with its client-reference key
@@ -30,11 +30,12 @@ export declare function resolveCacheTags(config: PartialCacheOptions | false, ct
30
30
  * 4. Hardcoded fallback (60 seconds)
31
31
  */
32
32
  export declare class CacheScope {
33
+ private readonly defaultKeyPrefix?;
33
34
  readonly config: PartialCacheOptions | false;
34
35
  readonly parent: CacheScope | null;
35
36
  /** Explicit store from cache() options, if specified */
36
37
  private readonly explicitStore;
37
- constructor(config: PartialCacheOptions | false, parent?: CacheScope | null);
38
+ constructor(config: PartialCacheOptions | false, parent?: CacheScope | null, defaultKeyPrefix?: "doc" | undefined);
38
39
  /**
39
40
  * Whether caching is enabled for this scope
40
41
  */
@@ -121,10 +122,11 @@ export declare function createCacheScope(config: {
121
122
  /**
122
123
  * Shell fast path: when the route tree derived NO cache scope and the current
123
124
  * request context carries the `_shellImplicitCache` marker (a shell capture,
124
- * or a HIT tail serving an eligible entry), substitute an implicit doc-level
125
- * scope so withCacheLookup/withCacheStore treat the WHOLE matched route as a
126
- * cache() boundary — the shell entry IS a cache() of the handler layer, with
127
- * loaders as the live carve-outs (resolveFreshLoadersAndYield).
125
+ * an eligible HIT tail, or a normal partial navigation replay), substitute an
126
+ * implicit doc-level scope so withCacheLookup/withCacheStore treat the WHOLE
127
+ * matched route as a cache() boundary — the shell entry IS a cache() of the
128
+ * handler layer, with loaders as the live carve-outs
129
+ * (resolveFreshLoadersAndYield).
128
130
  *
129
131
  * An existing scope — including an explicit cache(false) opt-out — always
130
132
  * wins: the consumer's cache() semantics (their ttl/swr/store/condition) are
@@ -29,6 +29,7 @@ export { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_TAGS_HEADER, CACHE_TA
29
29
  import type { KVNamespace, CFCacheReadDebugEvent, CFCacheDebug, CFCacheStoreOptions } from "./cf-cache-types.js";
30
30
  export type { KVNamespace, CFCacheReadDebugEvent, CFCacheDebug, CFCacheStoreOptions, };
31
31
  export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
32
+ readonly supportsPassiveShellReads: true;
32
33
  readonly defaults?: CacheDefaults;
33
34
  readonly keyGenerator?: (ctx: RequestContext<TEnv>, defaultKey: string) => string | Promise<string>;
34
35
  private readonly namespace?;
@@ -265,7 +266,7 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
265
266
  * Get a cached PPR shell entry by key from KV (no L1). Applies the KV read
266
267
  * budget, corrupt-entry eviction, hard-expiry, and tag invalidation exactly
267
268
  * like kvGetItem, minus the L1 promote. SWR is a plain staleness flag — KV has
268
- * no REVALIDATING herd guard, so the shell-cache middleware's module-level
269
+ * no REVALIDATING herd guard, so the capture scheduler's module-level
269
270
  * in-flight set is the recapture stampede guard.
270
271
  */
271
272
  getShell(key: string): Promise<{
@@ -273,11 +274,12 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
273
274
  shouldRevalidate?: boolean;
274
275
  } | null>;
275
276
  /**
276
- * Store a PPR shell entry in KV with TTL and optional SWR window. Non-blocking
277
- * (waitUntil) like the other KV writes. The tags/taggedAt ride in the envelope
277
+ * Store a PPR shell entry in KV with TTL and optional SWR window. The write is
278
+ * registered with waitUntil and awaited so invalidation rejection can be
279
+ * acknowledged to the capture scheduler. The tags/taggedAt ride in the envelope
278
280
  * so isGloballyInvalidated() can invalidate the shell via the shared KV markers.
279
281
  */
280
- putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<void>;
282
+ putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
281
283
  /**
282
284
  * Convert string key to Request object for CF Cache API.
283
285
  * Includes version in URL if specified (for cache invalidation on code changes).
@@ -537,11 +539,10 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
537
539
  * eager purge still fires for the whole batch first (it is additive).
538
540
  */
539
541
  /**
540
- * Build-shell read-through gate (SegmentCacheStore.isTagsInvalidatedSince):
541
- * a baked shell entry is immutable in the build manifest, so eviction is
542
- * answered by the SAME KV tag markers updateTag() writes, compared against
543
- * the entry's build-time createdAt. Thin public wrapper over the private
544
- * envelope check (identical semantics: marker >= since, fail open).
542
+ * Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
543
+ * SAME KV markers used by runtime envelopes also evict immutable build shells
544
+ * and captures whose write races updateTag(). Thin public wrapper over the
545
+ * private envelope check (marker >= since, fail open).
545
546
  */
546
547
  isTagsInvalidatedSince(tags: string[], sinceMs: number): Promise<boolean>;
547
548
  invalidateTags(tags: string[]): Promise<void>;
@@ -106,6 +106,7 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
106
106
  * ```
107
107
  */
108
108
  export declare class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
109
+ readonly supportsPassiveShellReads: true;
109
110
  private cache;
110
111
  private responseCache;
111
112
  private itemCache;
@@ -115,11 +116,9 @@ export declare class MemorySegmentCacheStore<TEnv = unknown> implements SegmentC
115
116
  /** prefixed cache key -> set of tags (reverse index for O(tags) unregister) */
116
117
  private keyTags;
117
118
  /**
118
- * tag -> epoch ms of its latest invalidateTags() call. The build-shell
119
- * read-through's isTagsInvalidatedSince gate: baked shell entries are
120
- * immutable in the build manifest, so eviction is answered by comparing
121
- * these markers against the entry's build-time createdAt. Per-isolate,
122
- * like every other map here — matching this store's tag semantics.
119
+ * tag -> epoch ms of its latest invalidateTags() call. Runtime capture races
120
+ * and immutable build shells are gated by comparing these markers with the
121
+ * shell generation start. Per-isolate, like every other map here.
123
122
  */
124
123
  private tagInvalidatedAt;
125
124
  readonly defaults?: CacheDefaults;
@@ -150,8 +149,9 @@ export declare class MemorySegmentCacheStore<TEnv = unknown> implements SegmentC
150
149
  entry: ShellCacheEntry;
151
150
  shouldRevalidate?: boolean;
152
151
  } | null>;
153
- putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<void>;
152
+ putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated">;
154
153
  isTagsInvalidatedSince(tags: string[], sinceMs: number): Promise<boolean>;
154
+ private tagsInvalidatedSince;
155
155
  invalidateTags(tags: string[]): Promise<void>;
156
156
  private registerTags;
157
157
  private unregisterTags;
@@ -47,6 +47,7 @@ export declare class RecordingShellStore<TEnv = unknown> implements SegmentCache
47
47
  constructor(inner: SegmentCacheStore<TEnv>);
48
48
  get defaults(): SegmentCacheStore<TEnv>["defaults"];
49
49
  get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"];
50
+ get supportsPassiveShellReads(): true | undefined;
50
51
  private record;
51
52
  /** Track a deferred cache-write promise so the capture can await it pre-drain. */
52
53
  trackWrite(p: Promise<unknown>): void;
@@ -83,11 +84,13 @@ export declare class RecordingShellStore<TEnv = unknown> implements SegmentCache
83
84
  putResponse(key: string, response: Response, ttl: number, swr?: number, tags?: string[]): Promise<void>;
84
85
  getItem(key: string): Promise<CacheItemResult | null>;
85
86
  setItem(key: string, value: string, options?: CacheItemOptions): Promise<void>;
86
- getShell(key: string): Promise<{
87
+ getShell(key: string, options?: {
88
+ claimRevalidation?: boolean;
89
+ }): Promise<{
87
90
  entry: ShellCacheEntry;
88
91
  shouldRevalidate?: boolean;
89
92
  } | null>;
90
- putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<void>;
93
+ putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
91
94
  invalidateTags(tags: string[]): Promise<void>;
92
95
  }
93
96
  /** True iff `store` is a RecordingShellStore (duck-typed across module copies). */
@@ -141,18 +144,26 @@ export declare function buildShellLoaderSeed(snapshot: ShellSnapshotRecord[]): P
141
144
  * the snapshot it serves the recorded value AS FRESH (shouldRevalidate: false —
142
145
  * a pinned key must NOT kick SWR background revalidation) so the tail's payload
143
146
  * matches the frozen prelude. Every other read falls through to the real store
144
- * (the holes — masked loaders were never recorded — stay live). ALL writes pass
145
- * through unchanged: a live hole's loader may legitimately write. The shell
146
- * family always passes through.
147
+ * (the holes — masked loaders were never recorded — stay live). Writes pass
148
+ * through so a live hole may legitimately write, except that `segmentsOnly`
149
+ * fully isolates the segment family: misses do not fall through, and writes or
150
+ * deletes stay local to the navigation overlay. The shell family always passes
151
+ * through. With `segmentsOnly`, item and response reads also pass through so a
152
+ * partial navigation cannot freeze captured data or write a partial result into
153
+ * the canonical document namespace.
147
154
  */
148
155
  export declare class SeededShellStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
149
156
  private readonly inner;
150
157
  private readonly items;
151
158
  private readonly segments;
152
159
  private readonly responses;
153
- constructor(inner: SegmentCacheStore<TEnv>, snapshot: ShellSnapshotRecord[]);
160
+ private readonly segmentsOnly;
161
+ constructor(inner: SegmentCacheStore<TEnv>, snapshot: ShellSnapshotRecord[], options?: {
162
+ segmentsOnly?: boolean;
163
+ });
154
164
  get defaults(): SegmentCacheStore<TEnv>["defaults"];
155
165
  get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"];
166
+ get supportsPassiveShellReads(): true | undefined;
156
167
  get(key: string): Promise<CacheGetResult | null>;
157
168
  set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
158
169
  delete(key: string): Promise<boolean>;
@@ -164,10 +175,12 @@ export declare class SeededShellStore<TEnv = unknown> implements SegmentCacheSto
164
175
  putResponse(key: string, response: Response, ttl: number, swr?: number, tags?: string[]): Promise<void>;
165
176
  getItem(key: string): Promise<CacheItemResult | null>;
166
177
  setItem(key: string, value: string, options?: CacheItemOptions): Promise<void>;
167
- getShell(key: string): Promise<{
178
+ getShell(key: string, options?: {
179
+ claimRevalidation?: boolean;
180
+ }): Promise<{
168
181
  entry: ShellCacheEntry;
169
182
  shouldRevalidate?: boolean;
170
183
  } | null>;
171
- putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<void>;
184
+ putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
172
185
  invalidateTags(tags: string[]): Promise<void>;
173
186
  }
@@ -33,6 +33,12 @@ export interface CacheGetResult {
33
33
  * @typeParam TEnv - Platform bindings type (e.g., Cloudflare env)
34
34
  */
35
35
  export interface SegmentCacheStore<TEnv = unknown> {
36
+ /**
37
+ * The store honors getShell(..., { claimRevalidation: false }) without
38
+ * claiming an SWR lock. Navigation replay requires this opt-in because it
39
+ * cannot recapture an HTML shell after observing a stale entry.
40
+ */
41
+ readonly supportsPassiveShellReads?: true;
36
42
  /**
37
43
  * Default cache options for this store.
38
44
  * Used by cache() boundaries when ttl/swr are not explicitly specified.
@@ -120,8 +126,12 @@ export interface SegmentCacheStore<TEnv = unknown> {
120
126
  *
121
127
  * Optional: a store that does not implement the shell family disables the
122
128
  * shell-cache middleware (it fails open to the normal HTML render path).
129
+ * A passive read reports a stale entry as `shouldRevalidate: true` without
130
+ * claiming store-specific revalidation ownership.
123
131
  */
124
- getShell?(key: string): Promise<{
132
+ getShell?(key: string, options?: {
133
+ claimRevalidation?: boolean;
134
+ }): Promise<{
125
135
  entry: ShellCacheEntry;
126
136
  shouldRevalidate?: boolean;
127
137
  } | null>;
@@ -136,8 +146,10 @@ export interface SegmentCacheStore<TEnv = unknown> {
136
146
  * @param swrSeconds - Optional stale-while-revalidate window in seconds
137
147
  * @param tags - Optional cache tags for invalidation (participates in
138
148
  * invalidateTags via the same tag machinery as the item family)
149
+ * @returns `invalidated` when a generation marker rejected the write,
150
+ * `stored` when acknowledged, or void for stores without acknowledgements.
139
151
  */
140
- putShell?(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<void>;
152
+ putShell?(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
141
153
  /**
142
154
  * Get a cached function result by key.
143
155
  * Returns the serialized value, optional handle data, and staleness flag.
@@ -162,10 +174,9 @@ export interface SegmentCacheStore<TEnv = unknown> {
162
174
  /**
163
175
  * True when ANY of `tags` was invalidated (invalidateTags/updateTag) at or
164
176
  * after `sinceMs` (>= so a same-millisecond invalidation wins, favouring
165
- * freshness). Consulted by the build-time shell read-through
166
- * (rsc/shell-build-manifest.ts): a baked shell entry is immutable in the
167
- * build manifest, so "was it evicted" is answered by the store's tag
168
- * markers against the entry's createdAt rather than by deleting anything.
177
+ * freshness). Consulted by build-shell read-through and runtime shell stores:
178
+ * "was it evicted" is answered by tag markers against the entry's createdAt,
179
+ * including when invalidation races a capture that has not been written yet.
169
180
  * Optional: without it, TAGGED build entries are not served (untagged ones
170
181
  * are unaffected — they are evictable only by deploy/buildVersion anyway).
171
182
  * Fail open to `false` on marker-read errors: a transient store fault must
@@ -267,7 +278,13 @@ export interface ShellCacheEntry {
267
278
  * their holes always fill.
268
279
  */
269
280
  handlerLiveHoles?: boolean;
270
- /** Epoch ms when the shell was captured. */
281
+ /**
282
+ * The capture encountered transition({ when }). Its effective hold policy is
283
+ * request-dependent, so handler-free document and navigation replay must
284
+ * re-run resolution to collect and evaluate the predicate.
285
+ */
286
+ transitionWhen?: true;
287
+ /** Capture-generation start time; tag invalidations at or after it win. */
271
288
  createdAt: number;
272
289
  }
273
290
  /**
@@ -184,6 +184,7 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
184
184
  * ```
185
185
  */
186
186
  export declare class VercelCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
187
+ readonly supportsPassiveShellReads: true;
187
188
  readonly defaults?: CacheDefaults;
188
189
  readonly keyGenerator?: (ctx: RequestContext<TEnv>, defaultKey: string) => string | Promise<string>;
189
190
  private readonly cache;
@@ -203,18 +204,18 @@ export declare class VercelCacheStore<TEnv = unknown> implements SegmentCacheSto
203
204
  putResponse(key: string, response: Response, ttl: number, swr?: number, tags?: string[]): Promise<void>;
204
205
  getItem(key: string): Promise<CacheItemResult | null>;
205
206
  setItem(key: string, value: string, options?: CacheItemOptions): Promise<void>;
206
- getShell(key: string): Promise<{
207
+ getShell(key: string, options?: {
208
+ claimRevalidation?: boolean;
209
+ }): Promise<{
207
210
  entry: ShellCacheEntry;
208
211
  shouldRevalidate?: boolean;
209
212
  } | null>;
210
- putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<void>;
213
+ putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
211
214
  /**
212
- * Build-shell read-through gate (SegmentCacheStore.isTagsInvalidatedSince).
213
- * The platform's expireTag() DELETES tagged entries and keeps no queryable
214
- * history, so invalidateTags() below writes its own "tm" marker entries and
215
- * this compares them against the baked entry's build-time createdAt
216
- * (>= so a same-millisecond invalidation wins). Fails open to `false` on
217
- * read errors — the same posture as the CF marker check.
215
+ * Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince).
216
+ * expireTag() keeps no queryable history, so invalidateTags() writes "tm"
217
+ * markers for runtime capture races and immutable build shells. Marker >=
218
+ * generation start wins; read errors fail open like the CF marker check.
218
219
  */
219
220
  isTagsInvalidatedSince(tags: string[], sinceMs: number): Promise<boolean>;
220
221
  invalidateTags(tags: string[]): Promise<void>;
@@ -0,0 +1,5 @@
1
+ export declare const DEV_DISCOVERY_READY_EVENT = "rango:dev-discovery-ready";
2
+ export declare const DEV_DISCOVERY_QUERY_EVENT = "rango:dev-discovery-query";
3
+ export declare const DEV_DISCOVERY_PROBE_HEADER = "x-rango-dev-discovery-probe";
4
+ export declare const DEV_DISCOVERY_EPOCH_HEADER = "x-rango-dev-discovery-epoch";
5
+ export declare function isValidDevDiscoveryEpoch(value: unknown): value is number;
@@ -37,6 +37,7 @@ declare global {
37
37
  var __STATIC_MANIFEST: Record<string, () => Promise<{
38
38
  default: string | StaticEntry;
39
39
  }>> | undefined;
40
+ var __loadStaticManifestModule: (() => Promise<unknown>) | undefined;
40
41
  var __PRERENDER_DEV_URL: string | undefined;
41
42
  }
42
43
  /**
@@ -7,10 +7,6 @@
7
7
  *
8
8
  * See docs/manifests.md for the full data flow.
9
9
  */
10
- declare let cachedPrecomputedEntries: Array<{
11
- staticPrefix: string;
12
- routes: Record<string, string>;
13
- }> | null;
14
10
  /**
15
11
  * Register routes into the global route map.
16
12
  * Routes are merged with any existing registered routes.
@@ -63,26 +59,6 @@ export declare function hasCachedManifest(): boolean;
63
59
  * Clear the cached manifest (for testing)
64
60
  */
65
61
  export declare function clearCachedManifest(): void;
66
- /**
67
- * Set pre-computed route entries from build-time data.
68
- *
69
- * Each entry corresponds to a leaf node in the prefix tree (no nested includes).
70
- * evaluateLazyEntry() checks these before running the handler, avoiding the
71
- * 5-50ms cost of handler evaluation for route matching on the first request.
72
- *
73
- * @param entries - Array of { staticPrefix, routes } from build-time prefix tree leaves
74
- */
75
- export declare function setPrecomputedEntries(entries: Array<{
76
- staticPrefix: string;
77
- routes: Record<string, string>;
78
- }> | null): void;
79
- /**
80
- * Get pre-computed route entries (if available)
81
- */
82
- export declare function getPrecomputedEntries(): typeof cachedPrecomputedEntries;
83
- declare let cachedRouteTrie: import("./build/route-trie.js").TrieNode | null;
84
- export declare function setRouteTrie(trie: typeof cachedRouteTrie): void;
85
- export declare function getRouteTrie(): typeof cachedRouteTrie;
86
62
  /**
87
63
  * Clear all cached route data (global and per-router).
88
64
  * Called during HMR when route definitions change so the handler rebuilds
@@ -115,13 +91,12 @@ export declare function waitForManifestReady(): Promise<void> | null;
115
91
  * Register whether a route is at root scope.
116
92
  * Called by path() during route evaluation.
117
93
  */
118
- export declare function registerRouteRootScope(routeName: string, rootScoped: boolean): void;
94
+ export declare function registerRouteRootScope(routeName: string, rootScoped: boolean, routerId?: string): void;
119
95
  /**
120
96
  * Check if a route is at root scope.
121
97
  * Returns undefined if the route has not been registered (e.g. in unit tests).
122
98
  */
123
- export declare function isRouteRootScoped(routeName: string): boolean | undefined;
99
+ export declare function isRouteRootScoped(routeName: string, routerId?: string): boolean | undefined;
124
100
  import type { SearchSchema } from "./search-params.js";
125
- export declare function registerSearchSchema(routeName: string, schema: SearchSchema): void;
126
- export declare function getSearchSchema(routeName: string): SearchSchema | undefined;
127
- export {};
101
+ export declare function registerSearchSchema(routeName: string, schema: SearchSchema, routerId?: string): void;
102
+ export declare function getSearchSchema(routeName: string, routerId?: string): SearchSchema | undefined;
@@ -6,6 +6,9 @@ import type { HandlerContext, ResolvedSegment } from "../types";
6
6
  import type { SerializedSegmentData } from "../cache/types.js";
7
7
  import type { RouteMatchResult } from "./pattern-matching.js";
8
8
  export interface PrerenderMatchDeps<TEnv = any> {
9
+ /** Owning router id; scopes bake-time root-scope/search-schema lookups the
10
+ * same way reqCtx._routerId does at runtime (issue #762). */
11
+ routerId?: string;
9
12
  findMatch: (pathname: string) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
10
13
  buildRouterContext: () => RouterContext<TEnv>;
11
14
  mergedRouteMap: Record<string, string>;
@@ -44,7 +47,7 @@ devMode?: boolean): Promise<{
44
47
  * the component. Returns the encoded Flight string (or null on failure).
45
48
  * Used by the Vite plugin to collect static segment data at build time.
46
49
  */
47
- export declare function renderStaticSegment<TEnv = any>(handler: Function, handlerId: string, mergedRouteMap: Record<string, string>, routeName?: string, buildEnv?: TEnv, devMode?: boolean): Promise<{
50
+ export declare function renderStaticSegment<TEnv = any>(handler: Function, handlerId: string, mergedRouteMap: Record<string, string>, routeName?: string, buildEnv?: TEnv, devMode?: boolean, routerId?: string, rootScoped?: boolean): Promise<{
48
51
  encoded: string;
49
52
  handles: string;
50
53
  } | null>;
@@ -333,6 +333,8 @@ export interface RangoInternal<TEnv = any, TRoutes extends Record<string, unknow
333
333
  readonly __sourceFile?: string;
334
334
  /** @internal basename for runtime manifest generation */
335
335
  readonly __basename?: string;
336
+ /** @internal Cloudflare dev worker generation captured at construction. */
337
+ readonly __devDiscoveryEpoch?: number;
336
338
  /**
337
339
  * @internal Router-level error/notFound fallbacks (`createRouter` options),
338
340
  * exposed for the build-time clientChunks discovery so a `"use client"`
@@ -364,7 +366,7 @@ export interface RangoInternal<TEnv = any, TRoutes extends Record<string, unknow
364
366
  * Render a single Static handler at build time.
365
367
  * Returns the RSC-serialized component string and handle data, or null on failure.
366
368
  */
367
- renderStaticSegment(handler: Function, handlerId: string, routeName?: string, buildEnv?: any, devMode?: boolean): Promise<{
369
+ renderStaticSegment(handler: Function, handlerId: string, routeName?: string, buildEnv?: any, devMode?: boolean, rootScoped?: boolean): Promise<{
368
370
  encoded: string;
369
371
  handles: string;
370
372
  } | null>;
@@ -13,6 +13,7 @@ import type { SSRStreamMode } from "../router/router-options.js";
13
13
  export interface HandlerContext<TEnv = unknown> {
14
14
  router: RangoInternal<TEnv, any>;
15
15
  version: string;
16
+ devDiscoveryEpoch?: number;
16
17
  renderToReadableStream: RSCDependencies["renderToReadableStream"];
17
18
  decodeReply: RSCDependencies["decodeReply"];
18
19
  createTemporaryReferenceSet: RSCDependencies["createTemporaryReferenceSet"];
@@ -39,8 +39,9 @@ export interface RscRenderStageTracking {
39
39
  /** Whole-render span receiving low-cardinality current/error phase attrs. */
40
40
  span?: TraceSpan;
41
41
  }
42
+ type RscRenderContext<TEnv> = Pick<HandlerContext<TEnv>, "renderToReadableStream" | "callOnError" | "devDiscoveryEpoch">;
42
43
  export interface RscRenderInput<TEnv> {
43
- ctx: Pick<HandlerContext<TEnv>, "renderToReadableStream" | "callOnError">;
44
+ ctx: RscRenderContext<TEnv>;
44
45
  request: Request;
45
46
  url: URL;
46
47
  env: TEnv;
@@ -51,7 +52,7 @@ export interface RscRenderInput<TEnv> {
51
52
  tracking?: RscRenderStageTracking;
52
53
  }
53
54
  export interface RscFlightStageInput<TEnv> {
54
- ctx: Pick<HandlerContext<TEnv>, "renderToReadableStream" | "callOnError">;
55
+ ctx: RscRenderContext<TEnv>;
55
56
  request: Request;
56
57
  url: URL;
57
58
  env: TEnv;
@@ -9,15 +9,10 @@ import { getRequestContext } from "../server/request-context.js";
9
9
  import type { HandlerContext } from "./handler-context.js";
10
10
  export declare function handleRscRendering<TEnv>(ctx: HandlerContext<TEnv>, request: Request, env: TEnv, url: URL, isPartial: boolean, handleStore: ReturnType<typeof getRequestContext>["_handleStore"], nonce: string | undefined): Promise<Response>;
11
11
  /**
12
- * Neutralize the shell-HIT degradation redirect target.
13
- *
14
- * The inline `location.replace` emitted by serveShellHit when a shell HIT lands
15
- * on a URL whose route became redirecting mid-TTL is a document-native redirect
16
- * exit that BYPASSES the 3xx chokepoint (guardOutgoingRedirect acts only on 3xx
17
- * + Location responses, never a committed 200 body). So it reuses the ONE
18
- * same-origin resolver directly: a cross-origin/unparseable/unsafe target
19
- * neutralizes to the same safe same-origin landing as redirect-guard.ts
20
- * (basename root, or "/" when unset) rather than navigating the user off-host.
21
- * A safe same-origin/relative target passes through as its normalized href.
12
+ * Neutralize the shell-HIT degradation redirect target. The inline
13
+ * `location.replace` in a committed 200 body bypasses the 3xx chokepoint
14
+ * (guardOutgoingRedirect only sees 3xx + Location), so this reuses the same
15
+ * same-origin resolver directly: unsafe targets neutralize to the
16
+ * redirect-guard.ts landing instead of navigating the user off-host.
22
17
  */
23
18
  export declare function resolveShellHitRedirectTarget(rawTarget: string, requestOrigin: string, basename: string | undefined): string;