@rangojs/router 0.0.0-experimental.139 → 0.0.0-experimental.140
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 +27 -2
- package/dist/vite/index.js +147 -30
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +1 -0
- package/skills/caching/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +15 -0
- package/skills/migrate-react-router/SKILL.md +15 -2
- package/skills/ppr/SKILL.md +426 -0
- package/skills/rango/SKILL.md +28 -25
- package/skills/route/SKILL.md +43 -0
- package/src/build/route-trie.ts +35 -7
- package/src/cache/cf/cf-cache-store.ts +155 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/memory-segment-store.ts +57 -1
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/types.ts +58 -0
- package/src/cache/vercel/vercel-cache-store.ts +159 -5
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +17 -0
- package/src/router/middleware.ts +14 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/segment-resolution/fresh.ts +12 -1
- package/src/router/segment-resolution/loader-cache.ts +14 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/rsc-rendering.ts +105 -51
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/types.ts +26 -0
- package/src/server/cookie-store.ts +45 -0
- package/src/server/live.ts +130 -0
- package/src/server/request-context.ts +49 -0
- package/src/ssr/index.tsx +377 -180
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/render-route.tsx +7 -9
- package/src/types/route-config.ts +19 -7
- package/src/urls/type-extraction.ts +43 -18
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/plugins/virtual-entries.ts +27 -2
- package/src/vite/router-discovery.ts +69 -15
- package/src/vite/utils/prerender-utils.ts +17 -4
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
CacheGetResult,
|
|
35
35
|
CacheItemResult,
|
|
36
36
|
CacheItemOptions,
|
|
37
|
+
ShellCacheEntry,
|
|
37
38
|
} from "../types.js";
|
|
38
39
|
import {
|
|
39
40
|
_getRequestContext,
|
|
@@ -189,6 +190,29 @@ interface KVItemEnvelope {
|
|
|
189
190
|
ta?: number;
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
/**
|
|
194
|
+
* KV envelope for PPR shell cache entries.
|
|
195
|
+
* @internal
|
|
196
|
+
*/
|
|
197
|
+
interface KVShellEnvelope {
|
|
198
|
+
/** base64-encoded prelude bytes */
|
|
199
|
+
p: string;
|
|
200
|
+
/** postponed state JSON, or null (DATA variant — no holes) */
|
|
201
|
+
po: string | null;
|
|
202
|
+
/** React.version captured at prerender time */
|
|
203
|
+
rv: string;
|
|
204
|
+
/** createdAt (ms epoch) */
|
|
205
|
+
c: number;
|
|
206
|
+
/** When entry becomes stale (ms epoch) */
|
|
207
|
+
s: number;
|
|
208
|
+
/** When entry hard-expires (ms epoch) */
|
|
209
|
+
e: number;
|
|
210
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
211
|
+
t?: string[];
|
|
212
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
213
|
+
ta?: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
192
216
|
/**
|
|
193
217
|
* KV envelope for document cache entries.
|
|
194
218
|
* @internal
|
|
@@ -1574,6 +1598,137 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1574
1598
|
}
|
|
1575
1599
|
}
|
|
1576
1600
|
|
|
1601
|
+
// ============================================================================
|
|
1602
|
+
// Shell Cache Methods (PPR shell resume) — KV-only in v1
|
|
1603
|
+
// ============================================================================
|
|
1604
|
+
//
|
|
1605
|
+
// Unlike the segment/item/document tiers, the shell family has NO Cache-API L1
|
|
1606
|
+
// tier: the prelude bytes + postponed blob are large and version-coupled, and a
|
|
1607
|
+
// per-colo L1 for them is a deliberate follow-up (see the PPR shell-resume
|
|
1608
|
+
// design doc). Shell entries live only in KV (the global tier), so the family
|
|
1609
|
+
// requires a configured KV namespace; without one, getShell/putShell no-op and
|
|
1610
|
+
// the shell-cache middleware fails open to a full HTML render. Tag invalidation
|
|
1611
|
+
// still applies: shell entries carry tags/taggedAt and are checked against the
|
|
1612
|
+
// same KV markers isGloballyInvalidated() reads for every other tier.
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* Get a cached PPR shell entry by key from KV (no L1). Applies the KV read
|
|
1616
|
+
* budget, corrupt-entry eviction, hard-expiry, and tag invalidation exactly
|
|
1617
|
+
* like kvGetItem, minus the L1 promote. SWR is a plain staleness flag — KV has
|
|
1618
|
+
* no REVALIDATING herd guard, so the shell-cache middleware's module-level
|
|
1619
|
+
* in-flight set is the recapture stampede guard.
|
|
1620
|
+
*/
|
|
1621
|
+
async getShell(
|
|
1622
|
+
key: string,
|
|
1623
|
+
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
1624
|
+
if (!this.kv) return null;
|
|
1625
|
+
try {
|
|
1626
|
+
const kvKey = this.toKVKey(`shell:${key}`);
|
|
1627
|
+
const { value: envelope, timedOut } =
|
|
1628
|
+
await this.kvGetOrEvict<KVShellEnvelope>(
|
|
1629
|
+
kvKey,
|
|
1630
|
+
(e) =>
|
|
1631
|
+
typeof e.p === "string" &&
|
|
1632
|
+
(e.po === null || typeof e.po === "string") &&
|
|
1633
|
+
typeof e.rv === "string" &&
|
|
1634
|
+
typeof e.e === "number" &&
|
|
1635
|
+
typeof e.s === "number",
|
|
1636
|
+
"getShell",
|
|
1637
|
+
);
|
|
1638
|
+
// A timeout, a missing key, or an already-evicted corrupt entry is a miss.
|
|
1639
|
+
if (timedOut || !envelope) return null;
|
|
1640
|
+
|
|
1641
|
+
const now = Date.now();
|
|
1642
|
+
if (now > envelope.e) return null;
|
|
1643
|
+
|
|
1644
|
+
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const shouldRevalidate = envelope.s > 0 && now > envelope.s;
|
|
1649
|
+
return {
|
|
1650
|
+
entry: {
|
|
1651
|
+
prelude: envelope.p,
|
|
1652
|
+
postponed: envelope.po,
|
|
1653
|
+
reactVersion: envelope.rv,
|
|
1654
|
+
createdAt: envelope.c,
|
|
1655
|
+
},
|
|
1656
|
+
shouldRevalidate,
|
|
1657
|
+
};
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Store a PPR shell entry in KV with TTL and optional SWR window. Non-blocking
|
|
1666
|
+
* (waitUntil) like the other KV writes. The tags/taggedAt ride in the envelope
|
|
1667
|
+
* so isGloballyInvalidated() can invalidate the shell via the shared KV markers.
|
|
1668
|
+
*/
|
|
1669
|
+
async putShell(
|
|
1670
|
+
key: string,
|
|
1671
|
+
entry: ShellCacheEntry,
|
|
1672
|
+
ttlSeconds?: number,
|
|
1673
|
+
swrSeconds?: number,
|
|
1674
|
+
tags?: string[],
|
|
1675
|
+
): Promise<void> {
|
|
1676
|
+
// KV-only tier: needs a KV namespace and waitUntil (writes are non-blocking).
|
|
1677
|
+
if (!this.kv || !this.waitUntil) return;
|
|
1678
|
+
try {
|
|
1679
|
+
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
1680
|
+
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
1681
|
+
const totalTtl = ttl + swrWindow;
|
|
1682
|
+
// KV requires expirationTtl >= 60s; skip a shorter-lived shell rather than
|
|
1683
|
+
// letting kv.put reject inside waitUntil (mirrors setItem/kvSetSegment).
|
|
1684
|
+
if (totalTtl < 60) return;
|
|
1685
|
+
|
|
1686
|
+
const staleAt = Date.now() + ttl * 1000;
|
|
1687
|
+
const taggedAt =
|
|
1688
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1689
|
+
|
|
1690
|
+
const kvKey = this.toKVKey(`shell:${key}`);
|
|
1691
|
+
// A key over the KV limit makes kv.put reject deep inside waitUntil; report
|
|
1692
|
+
// and skip the doomed write (mirrors kvSetSegment).
|
|
1693
|
+
const kvKeyBytes = kvKeyByteLength(kvKey);
|
|
1694
|
+
if (kvKeyBytes > KV_MAX_KEY_BYTES) {
|
|
1695
|
+
reportCacheError(
|
|
1696
|
+
new Error(
|
|
1697
|
+
`shell cache key produces a ${kvKeyBytes}-byte KV key, over the ` +
|
|
1698
|
+
`${KV_MAX_KEY_BYTES}-byte limit; the shell was not persisted.`,
|
|
1699
|
+
),
|
|
1700
|
+
"cache-write",
|
|
1701
|
+
"[CFCacheStore] putShell",
|
|
1702
|
+
);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
this.waitUntil(() =>
|
|
1707
|
+
reportingAsync(
|
|
1708
|
+
() => {
|
|
1709
|
+
const envelope: KVShellEnvelope = {
|
|
1710
|
+
p: entry.prelude,
|
|
1711
|
+
po: entry.postponed,
|
|
1712
|
+
rv: entry.reactVersion,
|
|
1713
|
+
c: entry.createdAt,
|
|
1714
|
+
s: staleAt,
|
|
1715
|
+
e: staleAt + swrWindow * 1000,
|
|
1716
|
+
t: tags,
|
|
1717
|
+
ta: taggedAt,
|
|
1718
|
+
};
|
|
1719
|
+
return this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
1720
|
+
expirationTtl: totalTtl,
|
|
1721
|
+
});
|
|
1722
|
+
},
|
|
1723
|
+
"cache-write",
|
|
1724
|
+
"[CFCacheStore] putShell",
|
|
1725
|
+
),
|
|
1726
|
+
);
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1577
1732
|
// ============================================================================
|
|
1578
1733
|
// Key Helpers
|
|
1579
1734
|
// ============================================================================
|
package/src/cache/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type {
|
|
|
4
4
|
CacheGetResult,
|
|
5
5
|
CacheItemResult,
|
|
6
6
|
CacheItemOptions,
|
|
7
|
+
ShellCacheEntry,
|
|
7
8
|
SerializedSegmentData,
|
|
8
9
|
SegmentHandleData,
|
|
9
10
|
} from "./types.js";
|
|
@@ -43,4 +44,9 @@ export {
|
|
|
43
44
|
type DocumentCacheOptions,
|
|
44
45
|
} from "./document-cache.js";
|
|
45
46
|
|
|
47
|
+
export {
|
|
48
|
+
createShellCacheMiddleware,
|
|
49
|
+
type ShellCacheOptions,
|
|
50
|
+
} from "./shell-cache.js";
|
|
51
|
+
|
|
46
52
|
export type { CacheErrorCategory } from "./cache-error.js";
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
CacheGetResult,
|
|
13
13
|
CacheItemResult,
|
|
14
14
|
CacheItemOptions,
|
|
15
|
+
ShellCacheEntry,
|
|
15
16
|
} from "./types.js";
|
|
16
17
|
import type { RequestContext } from "../server/request-context.js";
|
|
17
18
|
import { isPerClientSignalHeader } from "../browser/cookie-name.js";
|
|
@@ -26,6 +27,7 @@ import { reportCacheError } from "./cache-error.js";
|
|
|
26
27
|
const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
|
|
27
28
|
const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
|
|
28
29
|
const ITEM_CACHE_REGISTRY_KEY = "__rsc_router_item_cache_registry__";
|
|
30
|
+
const SHELL_CACHE_REGISTRY_KEY = "__rsc_router_shell_cache_registry__";
|
|
29
31
|
const TAG_INDEX_REGISTRY_KEY = "__rsc_router_tag_index_registry__";
|
|
30
32
|
const KEY_TAGS_REGISTRY_KEY = "__rsc_router_key_tags_registry__";
|
|
31
33
|
|
|
@@ -65,6 +67,13 @@ interface CachedItemEntry {
|
|
|
65
67
|
tags?: string[];
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
interface CachedShellEntry {
|
|
71
|
+
entry: ShellCacheEntry;
|
|
72
|
+
expiresAt: number;
|
|
73
|
+
staleAt: number;
|
|
74
|
+
tags?: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
/**
|
|
69
78
|
* Options for MemorySegmentCacheStore
|
|
70
79
|
*/
|
|
@@ -157,7 +166,8 @@ export class MemorySegmentCacheStore<
|
|
|
157
166
|
private cache: Map<string, CachedEntryData>;
|
|
158
167
|
private responseCache: Map<string, CachedResponseEntry>;
|
|
159
168
|
private itemCache: Map<string, CachedItemEntry>;
|
|
160
|
-
|
|
169
|
+
private shellCache: Map<string, CachedShellEntry>;
|
|
170
|
+
/** tag -> set of prefixed cache keys (seg:key, res:key, item:key, shell:key) */
|
|
161
171
|
private tagIndex: Map<string, Set<string>>;
|
|
162
172
|
/** prefixed cache key -> set of tags (reverse index for O(tags) unregister) */
|
|
163
173
|
private keyTags: Map<string, Set<string>>;
|
|
@@ -181,6 +191,10 @@ export class MemorySegmentCacheStore<
|
|
|
181
191
|
ITEM_CACHE_REGISTRY_KEY,
|
|
182
192
|
options.name,
|
|
183
193
|
);
|
|
194
|
+
this.shellCache = getNamedMap<CachedShellEntry>(
|
|
195
|
+
SHELL_CACHE_REGISTRY_KEY,
|
|
196
|
+
options.name,
|
|
197
|
+
);
|
|
184
198
|
this.tagIndex = getNamedMap<Set<string>>(
|
|
185
199
|
TAG_INDEX_REGISTRY_KEY,
|
|
186
200
|
options.name,
|
|
@@ -193,6 +207,7 @@ export class MemorySegmentCacheStore<
|
|
|
193
207
|
this.cache = new Map<string, CachedEntryData>();
|
|
194
208
|
this.responseCache = new Map<string, CachedResponseEntry>();
|
|
195
209
|
this.itemCache = new Map<string, CachedItemEntry>();
|
|
210
|
+
this.shellCache = new Map<string, CachedShellEntry>();
|
|
196
211
|
this.tagIndex = new Map<string, Set<string>>();
|
|
197
212
|
this.keyTags = new Map<string, Set<string>>();
|
|
198
213
|
}
|
|
@@ -245,6 +260,7 @@ export class MemorySegmentCacheStore<
|
|
|
245
260
|
this.cache.clear();
|
|
246
261
|
this.responseCache.clear();
|
|
247
262
|
this.itemCache.clear();
|
|
263
|
+
this.shellCache.clear();
|
|
248
264
|
this.tagIndex.clear();
|
|
249
265
|
this.keyTags.clear();
|
|
250
266
|
}
|
|
@@ -353,6 +369,43 @@ export class MemorySegmentCacheStore<
|
|
|
353
369
|
}
|
|
354
370
|
}
|
|
355
371
|
|
|
372
|
+
async getShell(
|
|
373
|
+
key: string,
|
|
374
|
+
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
375
|
+
const cached = this.shellCache.get(key);
|
|
376
|
+
if (!cached) return null;
|
|
377
|
+
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
if (now > cached.expiresAt) {
|
|
380
|
+
this.unregisterTags(`shell:${key}`);
|
|
381
|
+
this.shellCache.delete(key);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// SWR mirrors the item family: stale within the swr window still serves, and
|
|
386
|
+
// signals shouldRevalidate so the middleware schedules a background recapture.
|
|
387
|
+
const shouldRevalidate = cached.staleAt > 0 && now > cached.staleAt;
|
|
388
|
+
return { entry: cached.entry, shouldRevalidate };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async putShell(
|
|
392
|
+
key: string,
|
|
393
|
+
entry: ShellCacheEntry,
|
|
394
|
+
ttlSeconds?: number,
|
|
395
|
+
swrSeconds?: number,
|
|
396
|
+
tags?: string[],
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
399
|
+
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
400
|
+
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
401
|
+
const prefixedKey = `shell:${key}`;
|
|
402
|
+
this.unregisterTags(prefixedKey);
|
|
403
|
+
this.shellCache.set(key, { entry, expiresAt, staleAt, tags });
|
|
404
|
+
if (tags && tags.length > 0) {
|
|
405
|
+
this.registerTags(tags, prefixedKey);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
356
409
|
async invalidateTags(tags: string[]): Promise<void> {
|
|
357
410
|
for (const tag of tags) {
|
|
358
411
|
const keys = this.tagIndex.get(tag);
|
|
@@ -371,6 +424,8 @@ export class MemorySegmentCacheStore<
|
|
|
371
424
|
this.responseCache.delete(rawKey);
|
|
372
425
|
} else if (prefix === "item") {
|
|
373
426
|
this.itemCache.delete(rawKey);
|
|
427
|
+
} else if (prefix === "shell") {
|
|
428
|
+
this.shellCache.delete(rawKey);
|
|
374
429
|
}
|
|
375
430
|
|
|
376
431
|
this.unregisterTags(prefixedKey);
|
|
@@ -421,6 +476,7 @@ export class MemorySegmentCacheStore<
|
|
|
421
476
|
delete (globalThis as any)[CACHE_REGISTRY_KEY];
|
|
422
477
|
delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
|
|
423
478
|
delete (globalThis as any)[ITEM_CACHE_REGISTRY_KEY];
|
|
479
|
+
delete (globalThis as any)[SHELL_CACHE_REGISTRY_KEY];
|
|
424
480
|
delete (globalThis as any)[TAG_INDEX_REGISTRY_KEY];
|
|
425
481
|
delete (globalThis as any)[KEY_TAGS_REGISTRY_KEY];
|
|
426
482
|
}
|