@rangojs/router 0.0.0-experimental.135 → 0.0.0-experimental.136
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/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/browser/prefetch/cache.ts +17 -7
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/rsc-router.tsx +11 -4
- package/src/browser/types.ts +10 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/router-interfaces.ts +14 -0
- package/src/router/router-options.ts +30 -0
- package/src/router.ts +16 -0
- package/src/rsc/handler.ts +2 -0
- package/src/rsc/rsc-rendering.ts +4 -0
- package/src/rsc/types.ts +4 -0
package/dist/vite/index.js
CHANGED
|
@@ -2297,7 +2297,7 @@ import { resolve } from "node:path";
|
|
|
2297
2297
|
// package.json
|
|
2298
2298
|
var package_default = {
|
|
2299
2299
|
name: "@rangojs/router",
|
|
2300
|
-
version: "0.0.0-experimental.
|
|
2300
|
+
version: "0.0.0-experimental.136",
|
|
2301
2301
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2302
2302
|
keywords: [
|
|
2303
2303
|
"react",
|
package/package.json
CHANGED
|
@@ -77,13 +77,24 @@ export interface DecodedPrefetch {
|
|
|
77
77
|
|
|
78
78
|
let cacheTTL = 300_000;
|
|
79
79
|
|
|
80
|
+
// Max stored entries before FIFO eviction. Mirrors DEFAULT_PREFETCH_CACHE_SIZE
|
|
81
|
+
// (router/prefetch-limits.ts); kept as a local literal so the client bundle
|
|
82
|
+
// doesn't pull in router-layer code, matching the cacheTTL default above.
|
|
83
|
+
// Overridden at startup by initPrefetchCache from server metadata.
|
|
84
|
+
let maxPrefetchCacheSize = 100;
|
|
85
|
+
|
|
80
86
|
/**
|
|
81
|
-
* Initialize the prefetch cache with the configured TTL.
|
|
82
|
-
* Called once at app startup with the
|
|
83
|
-
*
|
|
87
|
+
* Initialize the prefetch cache with the configured TTL and max size.
|
|
88
|
+
* Called once at app startup with the values from server metadata. Each
|
|
89
|
+
* argument is applied only when provided, so a caller can set just the TTL.
|
|
90
|
+
* A TTL of 0 disables the in-memory cache and all prefetching. A size below 1
|
|
91
|
+
* is ignored (the default is kept) — disabling prefetch is the TTL's job.
|
|
84
92
|
*/
|
|
85
|
-
export function initPrefetchCache(ttlMs
|
|
86
|
-
cacheTTL = ttlMs;
|
|
93
|
+
export function initPrefetchCache(ttlMs?: number, maxSize?: number): void {
|
|
94
|
+
if (ttlMs !== undefined) cacheTTL = ttlMs;
|
|
95
|
+
if (maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 1) {
|
|
96
|
+
maxPrefetchCacheSize = Math.floor(maxSize);
|
|
97
|
+
}
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
/**
|
|
@@ -93,7 +104,6 @@ export function initPrefetchCache(ttlMs: number): void {
|
|
|
93
104
|
export function isPrefetchCacheDisabled(): boolean {
|
|
94
105
|
return cacheTTL <= 0;
|
|
95
106
|
}
|
|
96
|
-
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
97
107
|
|
|
98
108
|
interface PrefetchCacheEntry {
|
|
99
109
|
entry: DecodedPrefetch;
|
|
@@ -268,7 +278,7 @@ export function storePrefetch(
|
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
// FIFO eviction if at capacity
|
|
271
|
-
if (cache.size >=
|
|
281
|
+
if (cache.size >= maxPrefetchCacheSize) {
|
|
272
282
|
const oldest = cache.keys().next().value;
|
|
273
283
|
if (oldest) cache.delete(oldest);
|
|
274
284
|
}
|
|
@@ -16,9 +16,24 @@
|
|
|
16
16
|
|
|
17
17
|
import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Max prefetches executing at once. Mirrors DEFAULT_PREFETCH_CONCURRENCY
|
|
20
|
+
// (router/prefetch-limits.ts); kept as a local literal so the client bundle
|
|
21
|
+
// doesn't pull in router-layer code. Overridden at startup by
|
|
22
|
+
// setPrefetchConcurrency from server metadata.
|
|
23
|
+
let maxConcurrent = 2;
|
|
20
24
|
const IMAGE_WAIT_TIMEOUT = 2000;
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Set the max number of concurrently-executing speculative prefetches.
|
|
28
|
+
* Called once at app startup with the value from server metadata. A value
|
|
29
|
+
* below 1 (or non-finite) is ignored, keeping the default.
|
|
30
|
+
*/
|
|
31
|
+
export function setPrefetchConcurrency(n: number): void {
|
|
32
|
+
if (Number.isFinite(n) && n >= 1) {
|
|
33
|
+
maxConcurrent = Math.floor(n);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
let active = 0;
|
|
23
38
|
const queue: Array<{
|
|
24
39
|
key: string;
|
|
@@ -42,7 +57,7 @@ function startExecution(
|
|
|
42
57
|
abortControllers.delete(key);
|
|
43
58
|
// Only decrement if this key wasn't already cleared by cancelAllPrefetches.
|
|
44
59
|
// Without this guard, cancelled tasks' .finally() would underflow active
|
|
45
|
-
// below zero, breaking the
|
|
60
|
+
// below zero, breaking the maxConcurrent guarantee.
|
|
46
61
|
if (executing.delete(key)) {
|
|
47
62
|
active--;
|
|
48
63
|
}
|
|
@@ -63,7 +78,7 @@ function startExecution(
|
|
|
63
78
|
*/
|
|
64
79
|
function scheduleDrain(): void {
|
|
65
80
|
if (drainScheduled) return;
|
|
66
|
-
if (active >=
|
|
81
|
+
if (active >= maxConcurrent || queue.length === 0) return;
|
|
67
82
|
drainScheduled = true;
|
|
68
83
|
const gen = drainGeneration;
|
|
69
84
|
waitForIdle()
|
|
@@ -83,7 +98,7 @@ function scheduleDrain(): void {
|
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
function drain(): void {
|
|
86
|
-
while (active <
|
|
101
|
+
while (active < maxConcurrent && queue.length > 0) {
|
|
87
102
|
const item = queue.shift()!;
|
|
88
103
|
queued.delete(item.key);
|
|
89
104
|
startExecution(item.key, item.execute);
|
|
@@ -25,6 +25,7 @@ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
|
25
25
|
import { initRangoState } from "./rango-state.js";
|
|
26
26
|
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
27
27
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchConcurrency } from "./prefetch/queue.js";
|
|
28
29
|
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
29
30
|
import { setAppVersion } from "./app-version.js";
|
|
30
31
|
import {
|
|
@@ -245,11 +246,17 @@ export async function initBrowserApp(
|
|
|
245
246
|
initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
|
|
246
247
|
setAppVersion(version);
|
|
247
248
|
|
|
248
|
-
// Initialize the in-memory prefetch cache TTL
|
|
249
|
-
// A
|
|
249
|
+
// Initialize the in-memory prefetch cache (TTL + max size) and the prefetch
|
|
250
|
+
// queue concurrency from server config. A TTL of 0 disables the cache;
|
|
251
|
+
// undefined values fall back to the module defaults.
|
|
250
252
|
const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
+
const prefetchCacheSize = initialPayload.metadata?.prefetchCacheSize;
|
|
254
|
+
if (prefetchCacheTTL !== undefined || prefetchCacheSize !== undefined) {
|
|
255
|
+
initPrefetchCache(prefetchCacheTTL, prefetchCacheSize);
|
|
256
|
+
}
|
|
257
|
+
const prefetchConcurrency = initialPayload.metadata?.prefetchConcurrency;
|
|
258
|
+
if (prefetchConcurrency !== undefined) {
|
|
259
|
+
setPrefetchConcurrency(prefetchConcurrency);
|
|
253
260
|
}
|
|
254
261
|
|
|
255
262
|
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
package/src/browser/types.ts
CHANGED
|
@@ -70,6 +70,16 @@ export interface RscMetadata {
|
|
|
70
70
|
* Sent on initial render so the browser can configure its cache duration.
|
|
71
71
|
*/
|
|
72
72
|
prefetchCacheTTL?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Max entries in the client-side in-memory prefetch cache (FIFO eviction).
|
|
75
|
+
* Sent on initial render so the browser can configure its cache capacity.
|
|
76
|
+
*/
|
|
77
|
+
prefetchCacheSize?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Max concurrent speculative prefetch requests on the client.
|
|
80
|
+
* Sent on initial render so the browser can configure its prefetch queue.
|
|
81
|
+
*/
|
|
82
|
+
prefetchConcurrency?: number;
|
|
73
83
|
/**
|
|
74
84
|
* Server-resolved rango state cookie name (`{prefix}_{routerId}`). The client
|
|
75
85
|
* reads it verbatim and binds the rango state cookie to it; composition
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the client-side prefetch limits — the in-memory prefetch cache size
|
|
3
|
+
* and the speculative-prefetch queue concurrency — once, at router init. Both
|
|
4
|
+
* are positive-integer counts shipped to the browser in payload metadata; the
|
|
5
|
+
* browser entry feeds them to the prefetch cache (`initPrefetchCache`) and the
|
|
6
|
+
* prefetch queue (`setPrefetchConcurrency`) at startup.
|
|
7
|
+
*
|
|
8
|
+
* Policy note: this is a SEPARATE policy from resolvePrefetchCacheTTL
|
|
9
|
+
* (prefetch-cache-ttl.ts). These are counts, not durations, so the disable
|
|
10
|
+
* paths differ: a TTL of `false`/0 intentionally turns prefetching off, but a
|
|
11
|
+
* cache size or concurrency of 0 would silently break prefetching while leaving
|
|
12
|
+
* it nominally enabled — never the intent of passing a number. So any value
|
|
13
|
+
* that is not a finite integer >= 1 (0, negative, fractional below 1, NaN,
|
|
14
|
+
* Infinity, undefined) falls back to the default. Finite values >= 1 are
|
|
15
|
+
* floored. Do not unify this guard with the TTL guard.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_PREFETCH_CACHE_SIZE = 100;
|
|
19
|
+
export const DEFAULT_PREFETCH_CONCURRENCY = 2;
|
|
20
|
+
|
|
21
|
+
export function resolvePrefetchCacheSize(raw: number | undefined): number {
|
|
22
|
+
return resolvePositiveCount(raw, DEFAULT_PREFETCH_CACHE_SIZE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvePrefetchConcurrency(raw: number | undefined): number {
|
|
26
|
+
return resolvePositiveCount(raw, DEFAULT_PREFETCH_CONCURRENCY);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolvePositiveCount(
|
|
30
|
+
raw: number | undefined,
|
|
31
|
+
fallback: number,
|
|
32
|
+
): number {
|
|
33
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1) {
|
|
34
|
+
return Math.floor(raw);
|
|
35
|
+
}
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
@@ -301,6 +301,20 @@ export interface RangoInternal<
|
|
|
301
301
|
*/
|
|
302
302
|
readonly prefetchCacheTTL: number;
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Maximum number of decoded prefetch payloads the client keeps in its
|
|
306
|
+
* in-memory prefetch cache (FIFO eviction at capacity). Shipped to the
|
|
307
|
+
* client in payload metadata. Derived from prefetchCacheSize.
|
|
308
|
+
*/
|
|
309
|
+
readonly prefetchCacheSize: number;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Maximum number of speculative prefetch requests the client runs
|
|
313
|
+
* concurrently. Shipped to the client in payload metadata. Derived from
|
|
314
|
+
* prefetchConcurrency.
|
|
315
|
+
*/
|
|
316
|
+
readonly prefetchConcurrency: number;
|
|
317
|
+
|
|
304
318
|
/**
|
|
305
319
|
* Resolved rango state cookie name (`{prefix}_{routerId}`), composed once at
|
|
306
320
|
* router init and shipped to the client in payload metadata. The server-side
|
|
@@ -497,6 +497,36 @@ export interface RangoOptions<TEnv = any> {
|
|
|
497
497
|
*/
|
|
498
498
|
prefetchCacheTTL?: number | false;
|
|
499
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Maximum number of decoded prefetch payloads the client keeps in its
|
|
502
|
+
* in-memory prefetch cache. When the cache is full the oldest entry is
|
|
503
|
+
* evicted (FIFO) to make room for a new prefetch.
|
|
504
|
+
*
|
|
505
|
+
* Each entry retains a fully decoded RSC payload (and the route's client
|
|
506
|
+
* chunks pulled in while decoding), so this is the lever on client-side
|
|
507
|
+
* prefetch memory: a higher value warms more routes at the cost of more
|
|
508
|
+
* retained payloads. Staleness is bounded separately by `prefetchCacheTTL`;
|
|
509
|
+
* this bounds the entry COUNT.
|
|
510
|
+
*
|
|
511
|
+
* Values below 1 (or non-finite) fall back to the default. To turn
|
|
512
|
+
* prefetching off entirely, set `prefetchCacheTTL: false` instead.
|
|
513
|
+
*
|
|
514
|
+
* @default 100
|
|
515
|
+
*/
|
|
516
|
+
prefetchCacheSize?: number;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Maximum number of speculative prefetch requests (viewport/render strategy)
|
|
520
|
+
* the client runs concurrently. Hover prefetches bypass this queue and fire
|
|
521
|
+
* immediately; this caps only the background, idle-gated queue so prefetches
|
|
522
|
+
* never saturate the browser's connection pool.
|
|
523
|
+
*
|
|
524
|
+
* Values below 1 (or non-finite) fall back to the default.
|
|
525
|
+
*
|
|
526
|
+
* @default 2
|
|
527
|
+
*/
|
|
528
|
+
prefetchConcurrency?: number;
|
|
529
|
+
|
|
500
530
|
/**
|
|
501
531
|
* Prefix for the rango state cookie name. The resolved name is
|
|
502
532
|
* `{prefix}_{routerId}`; the prefix is sanitized to cookie-name-safe
|
package/src/router.ts
CHANGED
|
@@ -111,6 +111,10 @@ import {
|
|
|
111
111
|
} from "./router/prerender-match.js";
|
|
112
112
|
import { resolveStateCookieName } from "./router/state-cookie-name.js";
|
|
113
113
|
import { resolvePrefetchCacheTTL } from "./router/prefetch-cache-ttl.js";
|
|
114
|
+
import {
|
|
115
|
+
resolvePrefetchCacheSize,
|
|
116
|
+
resolvePrefetchConcurrency,
|
|
117
|
+
} from "./router/prefetch-limits.js";
|
|
114
118
|
|
|
115
119
|
// Re-export public types and values from extracted modules
|
|
116
120
|
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
|
|
@@ -150,6 +154,8 @@ export function createRouter<TEnv = any>(
|
|
|
150
154
|
nonce,
|
|
151
155
|
version,
|
|
152
156
|
prefetchCacheTTL: prefetchCacheTTLOption,
|
|
157
|
+
prefetchCacheSize: prefetchCacheSizeOption,
|
|
158
|
+
prefetchConcurrency: prefetchConcurrencyOption,
|
|
153
159
|
stateCookiePrefix: stateCookiePrefixOption,
|
|
154
160
|
warmup: warmupOption,
|
|
155
161
|
allowDebugManifest: allowDebugManifestOption = false,
|
|
@@ -242,6 +248,14 @@ export function createRouter<TEnv = any>(
|
|
|
242
248
|
const prefetchCacheControl: string | false =
|
|
243
249
|
resolvedPrefetchCacheTTL.cacheControl;
|
|
244
250
|
|
|
251
|
+
// Resolve client-side prefetch limits (in-memory cache size and queue
|
|
252
|
+
// concurrency). Both are positive-integer counts; sub-1/non-finite inputs
|
|
253
|
+
// fall back to the defaults. Shipped to the client in payload metadata.
|
|
254
|
+
const prefetchCacheSize = resolvePrefetchCacheSize(prefetchCacheSizeOption);
|
|
255
|
+
const prefetchConcurrency = resolvePrefetchConcurrency(
|
|
256
|
+
prefetchConcurrencyOption,
|
|
257
|
+
);
|
|
258
|
+
|
|
245
259
|
// Resolve warmup enabled flag (default: true)
|
|
246
260
|
const warmupEnabled = warmupOption !== false;
|
|
247
261
|
|
|
@@ -968,6 +982,8 @@ export function createRouter<TEnv = any>(
|
|
|
968
982
|
// Expose prefetch cache settings
|
|
969
983
|
prefetchCacheControl,
|
|
970
984
|
prefetchCacheTTL,
|
|
985
|
+
prefetchCacheSize,
|
|
986
|
+
prefetchConcurrency,
|
|
971
987
|
|
|
972
988
|
// Expose the resolved rango state cookie name for the server-side writer
|
|
973
989
|
// (invalidateClientCache) and for shipping to the client in metadata.
|
package/src/rsc/handler.ts
CHANGED
|
@@ -1134,6 +1134,8 @@ export function createRSCHandler<
|
|
|
1134
1134
|
handles: handleStore.stream(),
|
|
1135
1135
|
version,
|
|
1136
1136
|
prefetchCacheTTL: router.prefetchCacheTTL,
|
|
1137
|
+
prefetchCacheSize: router.prefetchCacheSize,
|
|
1138
|
+
prefetchConcurrency: router.prefetchConcurrency,
|
|
1137
1139
|
stateCookieName: router.resolvedStateCookieName,
|
|
1138
1140
|
themeConfig: router.themeConfig,
|
|
1139
1141
|
warmupEnabled: router.warmupEnabled,
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -81,6 +81,8 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
81
81
|
handles: handleStore.stream(),
|
|
82
82
|
version: ctx.version,
|
|
83
83
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
84
|
+
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
|
85
|
+
prefetchConcurrency: ctx.router.prefetchConcurrency,
|
|
84
86
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
85
87
|
themeConfig: ctx.router.themeConfig,
|
|
86
88
|
// Carry warmupEnabled on the initial full-render payload so the client
|
|
@@ -141,6 +143,8 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
141
143
|
handles: handleStore.stream(),
|
|
142
144
|
version: ctx.version,
|
|
143
145
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
146
|
+
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
|
147
|
+
prefetchConcurrency: ctx.router.prefetchConcurrency,
|
|
144
148
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
145
149
|
},
|
|
146
150
|
};
|
package/src/rsc/types.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface RscPayload {
|
|
|
43
43
|
version?: string;
|
|
44
44
|
/** TTL in milliseconds for the client-side in-memory prefetch cache */
|
|
45
45
|
prefetchCacheTTL?: number;
|
|
46
|
+
/** Max entries in the client-side in-memory prefetch cache (FIFO eviction) */
|
|
47
|
+
prefetchCacheSize?: number;
|
|
48
|
+
/** Max concurrent speculative prefetch requests on the client */
|
|
49
|
+
prefetchConcurrency?: number;
|
|
46
50
|
/** Server-resolved rango state cookie name; the client reads it verbatim. */
|
|
47
51
|
stateCookieName?: string;
|
|
48
52
|
/** Theme configuration for FOUC prevention */
|