@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
|
@@ -96,6 +96,9 @@ export class RecordingShellStore<
|
|
|
96
96
|
get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
|
|
97
97
|
return this.inner.keyGenerator;
|
|
98
98
|
}
|
|
99
|
+
get supportsPassiveShellReads(): true | undefined {
|
|
100
|
+
return this.inner.supportsPassiveShellReads;
|
|
101
|
+
}
|
|
99
102
|
|
|
100
103
|
private record(
|
|
101
104
|
family: ShellSnapshotRecord["family"],
|
|
@@ -232,8 +235,9 @@ export class RecordingShellStore<
|
|
|
232
235
|
|
|
233
236
|
async getShell(
|
|
234
237
|
key: string,
|
|
238
|
+
options?: { claimRevalidation?: boolean },
|
|
235
239
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
236
|
-
return this.inner.getShell ? this.inner.getShell(key) : null;
|
|
240
|
+
return this.inner.getShell ? this.inner.getShell(key, options) : null;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
async putShell(
|
|
@@ -242,7 +246,7 @@ export class RecordingShellStore<
|
|
|
242
246
|
ttlSeconds?: number,
|
|
243
247
|
swrSeconds?: number,
|
|
244
248
|
tags?: string[],
|
|
245
|
-
): Promise<void> {
|
|
249
|
+
): Promise<"stored" | "invalidated" | void> {
|
|
246
250
|
return this.inner.putShell?.(key, entry, ttlSeconds, swrSeconds, tags);
|
|
247
251
|
}
|
|
248
252
|
|
|
@@ -360,28 +364,42 @@ export async function buildShellLoaderSeed(
|
|
|
360
364
|
* the snapshot it serves the recorded value AS FRESH (shouldRevalidate: false —
|
|
361
365
|
* a pinned key must NOT kick SWR background revalidation) so the tail's payload
|
|
362
366
|
* matches the frozen prelude. Every other read falls through to the real store
|
|
363
|
-
* (the holes — masked loaders were never recorded — stay live).
|
|
364
|
-
* through
|
|
365
|
-
* family
|
|
367
|
+
* (the holes — masked loaders were never recorded — stay live). Writes pass
|
|
368
|
+
* through so a live hole may legitimately write, except that `segmentsOnly`
|
|
369
|
+
* fully isolates the segment family: misses do not fall through, and writes or
|
|
370
|
+
* deletes stay local to the navigation overlay. The shell family always passes
|
|
371
|
+
* through. With `segmentsOnly`, item and response reads also pass through so a
|
|
372
|
+
* partial navigation cannot freeze captured data or write a partial result into
|
|
373
|
+
* the canonical document namespace.
|
|
366
374
|
*/
|
|
367
375
|
export class SeededShellStore<
|
|
368
376
|
TEnv = unknown,
|
|
369
377
|
> implements SegmentCacheStore<TEnv> {
|
|
370
|
-
private readonly items
|
|
378
|
+
private readonly items: Map<string, ShellSnapshotItemValue> | undefined;
|
|
371
379
|
private readonly segments = new Map<string, CachedEntryData>();
|
|
372
|
-
private readonly responses
|
|
380
|
+
private readonly responses:
|
|
381
|
+
| Map<string, ShellSnapshotResponseValue>
|
|
382
|
+
| undefined;
|
|
383
|
+
private readonly segmentsOnly: boolean;
|
|
373
384
|
|
|
374
385
|
constructor(
|
|
375
386
|
private readonly inner: SegmentCacheStore<TEnv>,
|
|
376
387
|
snapshot: ShellSnapshotRecord[],
|
|
388
|
+
options?: { segmentsOnly?: boolean },
|
|
377
389
|
) {
|
|
390
|
+
this.segmentsOnly = options?.segmentsOnly === true;
|
|
391
|
+
this.items = this.segmentsOnly ? undefined : new Map();
|
|
392
|
+
this.responses = this.segmentsOnly ? undefined : new Map();
|
|
378
393
|
for (const rec of snapshot) {
|
|
379
|
-
if (rec
|
|
380
|
-
|
|
381
|
-
} else if (rec.family === "segment") {
|
|
394
|
+
if (!rec || typeof rec !== "object") continue;
|
|
395
|
+
if (rec.family === "segment") {
|
|
382
396
|
this.segments.set(rec.key, rec.value as CachedEntryData);
|
|
397
|
+
} else if (this.segmentsOnly) {
|
|
398
|
+
continue;
|
|
399
|
+
} else if (rec.family === "item") {
|
|
400
|
+
this.items?.set(rec.key, rec.value as ShellSnapshotItemValue);
|
|
383
401
|
} else if (rec.family === "response") {
|
|
384
|
-
this.responses
|
|
402
|
+
this.responses?.set(rec.key, rec.value as ShellSnapshotResponseValue);
|
|
385
403
|
}
|
|
386
404
|
// "loader" family records are not store reads — serveShellHit seeds them
|
|
387
405
|
// onto the tail context (_shellLoaderSeed) for the resolveLoaderData
|
|
@@ -395,10 +413,14 @@ export class SeededShellStore<
|
|
|
395
413
|
get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
|
|
396
414
|
return this.inner.keyGenerator;
|
|
397
415
|
}
|
|
416
|
+
get supportsPassiveShellReads(): true | undefined {
|
|
417
|
+
return this.inner.supportsPassiveShellReads;
|
|
418
|
+
}
|
|
398
419
|
|
|
399
420
|
async get(key: string): Promise<CacheGetResult | null> {
|
|
400
421
|
const seeded = this.segments.get(key);
|
|
401
422
|
if (seeded) return { data: seeded, shouldRevalidate: false };
|
|
423
|
+
if (this.segmentsOnly) return null;
|
|
402
424
|
return this.inner.get(key);
|
|
403
425
|
}
|
|
404
426
|
|
|
@@ -408,10 +430,17 @@ export class SeededShellStore<
|
|
|
408
430
|
ttl: number,
|
|
409
431
|
swr?: number,
|
|
410
432
|
): Promise<void> {
|
|
433
|
+
if (this.segmentsOnly) {
|
|
434
|
+
this.segments.set(key, data);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
411
437
|
return this.inner.set(key, data, ttl, swr);
|
|
412
438
|
}
|
|
413
439
|
|
|
414
440
|
async delete(key: string): Promise<boolean> {
|
|
441
|
+
if (this.segmentsOnly) {
|
|
442
|
+
return this.segments.delete(key);
|
|
443
|
+
}
|
|
415
444
|
return this.inner.delete(key);
|
|
416
445
|
}
|
|
417
446
|
|
|
@@ -422,7 +451,7 @@ export class SeededShellStore<
|
|
|
422
451
|
async getResponse(
|
|
423
452
|
key: string,
|
|
424
453
|
): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
|
|
425
|
-
const seeded = this.responses
|
|
454
|
+
const seeded = this.responses?.get(key);
|
|
426
455
|
if (seeded) {
|
|
427
456
|
return { response: deserializeResponse(seeded), shouldRevalidate: false };
|
|
428
457
|
}
|
|
@@ -440,7 +469,7 @@ export class SeededShellStore<
|
|
|
440
469
|
}
|
|
441
470
|
|
|
442
471
|
async getItem(key: string): Promise<CacheItemResult | null> {
|
|
443
|
-
const seeded = this.items
|
|
472
|
+
const seeded = this.items?.get(key);
|
|
444
473
|
if (seeded) {
|
|
445
474
|
return {
|
|
446
475
|
value: seeded.value,
|
|
@@ -462,8 +491,9 @@ export class SeededShellStore<
|
|
|
462
491
|
|
|
463
492
|
async getShell(
|
|
464
493
|
key: string,
|
|
494
|
+
options?: { claimRevalidation?: boolean },
|
|
465
495
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
466
|
-
return this.inner.getShell ? this.inner.getShell(key) : null;
|
|
496
|
+
return this.inner.getShell ? this.inner.getShell(key, options) : null;
|
|
467
497
|
}
|
|
468
498
|
|
|
469
499
|
async putShell(
|
|
@@ -472,7 +502,7 @@ export class SeededShellStore<
|
|
|
472
502
|
ttlSeconds?: number,
|
|
473
503
|
swrSeconds?: number,
|
|
474
504
|
tags?: string[],
|
|
475
|
-
): Promise<void> {
|
|
505
|
+
): Promise<"stored" | "invalidated" | void> {
|
|
476
506
|
return this.inner.putShell?.(key, entry, ttlSeconds, swrSeconds, tags);
|
|
477
507
|
}
|
|
478
508
|
|
package/src/cache/types.ts
CHANGED
|
@@ -36,6 +36,13 @@ export interface CacheGetResult {
|
|
|
36
36
|
* @typeParam TEnv - Platform bindings type (e.g., Cloudflare env)
|
|
37
37
|
*/
|
|
38
38
|
export interface SegmentCacheStore<TEnv = unknown> {
|
|
39
|
+
/**
|
|
40
|
+
* The store honors getShell(..., { claimRevalidation: false }) without
|
|
41
|
+
* claiming an SWR lock. Navigation replay requires this opt-in because it
|
|
42
|
+
* cannot recapture an HTML shell after observing a stale entry.
|
|
43
|
+
*/
|
|
44
|
+
readonly supportsPassiveShellReads?: true;
|
|
45
|
+
|
|
39
46
|
/**
|
|
40
47
|
* Default cache options for this store.
|
|
41
48
|
* Used by cache() boundaries when ttl/swr are not explicitly specified.
|
|
@@ -144,9 +151,12 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
144
151
|
*
|
|
145
152
|
* Optional: a store that does not implement the shell family disables the
|
|
146
153
|
* shell-cache middleware (it fails open to the normal HTML render path).
|
|
154
|
+
* A passive read reports a stale entry as `shouldRevalidate: true` without
|
|
155
|
+
* claiming store-specific revalidation ownership.
|
|
147
156
|
*/
|
|
148
157
|
getShell?(
|
|
149
158
|
key: string,
|
|
159
|
+
options?: { claimRevalidation?: boolean },
|
|
150
160
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null>;
|
|
151
161
|
|
|
152
162
|
/**
|
|
@@ -160,6 +170,8 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
160
170
|
* @param swrSeconds - Optional stale-while-revalidate window in seconds
|
|
161
171
|
* @param tags - Optional cache tags for invalidation (participates in
|
|
162
172
|
* invalidateTags via the same tag machinery as the item family)
|
|
173
|
+
* @returns `invalidated` when a generation marker rejected the write,
|
|
174
|
+
* `stored` when acknowledged, or void for stores without acknowledgements.
|
|
163
175
|
*/
|
|
164
176
|
putShell?(
|
|
165
177
|
key: string,
|
|
@@ -167,7 +179,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
167
179
|
ttlSeconds?: number,
|
|
168
180
|
swrSeconds?: number,
|
|
169
181
|
tags?: string[],
|
|
170
|
-
): Promise<void>;
|
|
182
|
+
): Promise<"stored" | "invalidated" | void>;
|
|
171
183
|
|
|
172
184
|
/**
|
|
173
185
|
* Get a cached function result by key.
|
|
@@ -200,10 +212,9 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
200
212
|
/**
|
|
201
213
|
* True when ANY of `tags` was invalidated (invalidateTags/updateTag) at or
|
|
202
214
|
* after `sinceMs` (>= so a same-millisecond invalidation wins, favouring
|
|
203
|
-
* freshness). Consulted by
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* markers against the entry's createdAt rather than by deleting anything.
|
|
215
|
+
* freshness). Consulted by build-shell read-through and runtime shell stores:
|
|
216
|
+
* "was it evicted" is answered by tag markers against the entry's createdAt,
|
|
217
|
+
* including when invalidation races a capture that has not been written yet.
|
|
207
218
|
* Optional: without it, TAGGED build entries are not served (untagged ones
|
|
208
219
|
* are unaffected — they are evictable only by deploy/buildVersion anyway).
|
|
209
220
|
* Fail open to `false` on marker-read errors: a transient store fault must
|
|
@@ -307,7 +318,14 @@ export interface ShellCacheEntry {
|
|
|
307
318
|
* their holes always fill.
|
|
308
319
|
*/
|
|
309
320
|
handlerLiveHoles?: boolean;
|
|
310
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Legacy/fallback marker for a transition({ when }) predicate that was not
|
|
323
|
+
* evaluated by the PPR pre-handler gate. Such an entry stays conservatively
|
|
324
|
+
* ineligible for handler-free replay. New PPR matches evaluate known segment
|
|
325
|
+
* predicates before the pipeline and do not set this marker.
|
|
326
|
+
*/
|
|
327
|
+
transitionWhen?: true;
|
|
328
|
+
/** Capture-generation start time; tag invalidations at or after it win. */
|
|
311
329
|
createdAt: number;
|
|
312
330
|
}
|
|
313
331
|
|
|
@@ -134,9 +134,9 @@ type CacheFamily = "s" | "i" | "r" | "h" | "tm";
|
|
|
134
134
|
* invalidateTags for the build-shell read-through's isTagsInvalidatedSince
|
|
135
135
|
* gate. The platform's expireTag() DELETES tagged entries (no queryable
|
|
136
136
|
* history), so the markers are rango's own record of "tag X was invalidated
|
|
137
|
-
* at T". One year:
|
|
138
|
-
* buildVersion
|
|
139
|
-
*
|
|
137
|
+
* at T". One year: runtime tagged-shell retention is capped to this lifetime,
|
|
138
|
+
* while buildVersion retires build shells on the next deploy. An expired marker
|
|
139
|
+
* therefore cannot resurrect a runtime shell that outlived its invalidation.
|
|
140
140
|
*/
|
|
141
141
|
const TAG_MARKER_TTL_SECONDS = 365 * 24 * 60 * 60;
|
|
142
142
|
|
|
@@ -190,7 +190,7 @@ interface VercelShellEnvelope {
|
|
|
190
190
|
rv: string;
|
|
191
191
|
/** Build version at capture (ShellCacheEntry.buildVersion). */
|
|
192
192
|
bv?: string;
|
|
193
|
-
/**
|
|
193
|
+
/** Capture-generation start time, used by tag marker checks. */
|
|
194
194
|
c: number;
|
|
195
195
|
/** staleAt (ms since epoch). */
|
|
196
196
|
s: number;
|
|
@@ -209,6 +209,8 @@ interface VercelShellEnvelope {
|
|
|
209
209
|
* their holes only a handler re-run can fill.
|
|
210
210
|
*/
|
|
211
211
|
lh?: boolean;
|
|
212
|
+
/** ShellCacheEntry.transitionWhen; conditional transitions must re-run. */
|
|
213
|
+
tw?: true;
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
/** Read-path outcome for the debug sink. */
|
|
@@ -355,6 +357,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
355
357
|
export class VercelCacheStore<
|
|
356
358
|
TEnv = unknown,
|
|
357
359
|
> implements SegmentCacheStore<TEnv> {
|
|
360
|
+
readonly supportsPassiveShellReads: true = true;
|
|
358
361
|
readonly defaults?: CacheDefaults;
|
|
359
362
|
readonly keyGenerator?: (
|
|
360
363
|
ctx: RequestContext<TEnv>,
|
|
@@ -731,6 +734,7 @@ export class VercelCacheStore<
|
|
|
731
734
|
|
|
732
735
|
async getShell(
|
|
733
736
|
key: string,
|
|
737
|
+
options?: { claimRevalidation?: boolean },
|
|
734
738
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
735
739
|
const storeKey = this.toStoreKey(key, "h");
|
|
736
740
|
const started = Date.now();
|
|
@@ -774,14 +778,25 @@ export class VercelCacheStore<
|
|
|
774
778
|
return null;
|
|
775
779
|
}
|
|
776
780
|
|
|
781
|
+
if (
|
|
782
|
+
env.t &&
|
|
783
|
+
env.t.length > 0 &&
|
|
784
|
+
(await this.isTagsInvalidatedSince(env.t, env.c))
|
|
785
|
+
) {
|
|
786
|
+
void this.safeDelete(storeKey);
|
|
787
|
+
this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
777
791
|
const isStale = env.s > 0 && now > env.s;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
792
|
+
let shouldRevalidate = isStale;
|
|
793
|
+
if (isStale && options?.claimRevalidation !== false) {
|
|
794
|
+
shouldRevalidate = await this.claimRevalidation(
|
|
795
|
+
storeKey,
|
|
796
|
+
env.e,
|
|
797
|
+
"[VercelCacheStore] getShell",
|
|
798
|
+
);
|
|
799
|
+
}
|
|
785
800
|
this.emitDebug({
|
|
786
801
|
op: "getShell",
|
|
787
802
|
key,
|
|
@@ -800,6 +815,7 @@ export class VercelCacheStore<
|
|
|
800
815
|
initialTheme: env.i,
|
|
801
816
|
snapshot: env.sn,
|
|
802
817
|
handlerLiveHoles: env.lh,
|
|
818
|
+
transitionWhen: env.tw,
|
|
803
819
|
createdAt: env.c,
|
|
804
820
|
},
|
|
805
821
|
shouldRevalidate,
|
|
@@ -812,10 +828,12 @@ export class VercelCacheStore<
|
|
|
812
828
|
ttlSeconds?: number,
|
|
813
829
|
swrSeconds?: number,
|
|
814
830
|
tags?: string[],
|
|
815
|
-
): Promise<void> {
|
|
831
|
+
): Promise<"stored" | "invalidated" | void> {
|
|
816
832
|
try {
|
|
817
833
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
818
834
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
835
|
+
const totalTtl = ttl + swrWindow;
|
|
836
|
+
const now = Date.now();
|
|
819
837
|
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
820
838
|
// Store the CLAMPED tags (see putResponse/setItem) so dropped tags don't
|
|
821
839
|
// resurface on a hit; write() re-clamps idempotently.
|
|
@@ -823,6 +841,17 @@ export class VercelCacheStore<
|
|
|
823
841
|
tags,
|
|
824
842
|
"[VercelCacheStore] putShell",
|
|
825
843
|
);
|
|
844
|
+
const storeKey = this.toStoreKey(key, "h");
|
|
845
|
+
if (
|
|
846
|
+
safeTags.length > 0 &&
|
|
847
|
+
(await this.isTagsInvalidatedSince(safeTags, entry.createdAt))
|
|
848
|
+
) {
|
|
849
|
+
return "invalidated";
|
|
850
|
+
}
|
|
851
|
+
const retentionTtl =
|
|
852
|
+
safeTags.length > 0
|
|
853
|
+
? Math.min(totalTtl, TAG_MARKER_TTL_SECONDS)
|
|
854
|
+
: totalTtl;
|
|
826
855
|
const env: VercelShellEnvelope = {
|
|
827
856
|
p: entry.prelude,
|
|
828
857
|
po: entry.postponed,
|
|
@@ -830,22 +859,24 @@ export class VercelCacheStore<
|
|
|
830
859
|
bv: entry.buildVersion,
|
|
831
860
|
c: entry.createdAt,
|
|
832
861
|
s: staleAt,
|
|
833
|
-
e: expiresAt,
|
|
862
|
+
e: Math.min(expiresAt, now + retentionTtl * 1000),
|
|
834
863
|
t: safeTags.length > 0 ? safeTags : undefined,
|
|
835
864
|
i: entry.initialTheme,
|
|
836
865
|
sn: entry.snapshot,
|
|
837
866
|
lh: entry.handlerLiveHoles,
|
|
867
|
+
tw: entry.transitionWhen,
|
|
838
868
|
};
|
|
839
869
|
// write() enforces the 2 MB per-item ceiling (withinSizeLimit): an
|
|
840
870
|
// oversized shell prelude is reported and skipped (fail-open to a full
|
|
841
871
|
// render), never silently no-op'd on the platform.
|
|
842
872
|
await this.write(
|
|
843
|
-
|
|
873
|
+
storeKey,
|
|
844
874
|
env,
|
|
845
|
-
|
|
875
|
+
retentionTtl,
|
|
846
876
|
safeTags,
|
|
847
877
|
"[VercelCacheStore] putShell",
|
|
848
878
|
);
|
|
879
|
+
return "stored";
|
|
849
880
|
} catch (error) {
|
|
850
881
|
reportCacheError(error, "cache-write", "[VercelCacheStore] putShell");
|
|
851
882
|
}
|
|
@@ -854,12 +885,10 @@ export class VercelCacheStore<
|
|
|
854
885
|
// --- Tags ---
|
|
855
886
|
|
|
856
887
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* (>= so a same-millisecond invalidation wins). Fails open to `false` on
|
|
862
|
-
* read errors — the same posture as the CF marker check.
|
|
888
|
+
* Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince).
|
|
889
|
+
* expireTag() keeps no queryable history, so invalidateTags() writes "tm"
|
|
890
|
+
* markers for runtime capture races and immutable build shells. Marker >=
|
|
891
|
+
* generation start wins; read errors fail open like the CF marker check.
|
|
863
892
|
*/
|
|
864
893
|
async isTagsInvalidatedSince(
|
|
865
894
|
tags: string[],
|
|
@@ -1163,7 +1192,7 @@ export class VercelCacheStore<
|
|
|
1163
1192
|
|
|
1164
1193
|
private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
1165
1194
|
if (!isRecord(raw)) return null;
|
|
1166
|
-
const { p, po, rv, bv, c, s, e, t, i, sn, lh } = raw;
|
|
1195
|
+
const { p, po, rv, bv, c, s, e, t, i, sn, lh, tw } = raw;
|
|
1167
1196
|
if (typeof p !== "string" || typeof rv !== "string") return null;
|
|
1168
1197
|
if (po !== null && typeof po !== "string") return null;
|
|
1169
1198
|
if (typeof c !== "number") return null;
|
|
@@ -1180,6 +1209,7 @@ export class VercelCacheStore<
|
|
|
1180
1209
|
i: typeof i === "string" ? i : undefined,
|
|
1181
1210
|
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : undefined,
|
|
1182
1211
|
lh: lh === true ? true : undefined,
|
|
1212
|
+
tw: tw === true ? true : undefined,
|
|
1183
1213
|
};
|
|
1184
1214
|
}
|
|
1185
1215
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEV_DISCOVERY_READY_EVENT = "rango:dev-discovery-ready";
|
|
2
|
+
|
|
3
|
+
export const DEV_DISCOVERY_QUERY_EVENT = "rango:dev-discovery-query";
|
|
4
|
+
|
|
5
|
+
export const DEV_DISCOVERY_PROBE_HEADER = "x-rango-dev-discovery-probe";
|
|
6
|
+
|
|
7
|
+
export const DEV_DISCOVERY_EPOCH_HEADER = "x-rango-dev-discovery-epoch";
|
|
8
|
+
|
|
9
|
+
export function isValidDevDiscoveryEpoch(value: unknown): value is number {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
11
|
+
}
|
|
@@ -44,7 +44,7 @@ import { isRouteNotFoundError } from "../errors.js";
|
|
|
44
44
|
import { createReverseFunction } from "../router/handler-context.js";
|
|
45
45
|
import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
|
|
46
46
|
import type { MiddlewareEntry } from "../router/middleware.js";
|
|
47
|
-
import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
47
|
+
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
48
48
|
import {
|
|
49
49
|
resolvePprConfig,
|
|
50
50
|
type ResolvedPprConfig,
|
|
@@ -183,6 +183,9 @@ async function attemptBuildCapture(
|
|
|
183
183
|
stateCookieName: router.resolvedStateCookieName,
|
|
184
184
|
version: opts.buildVersion,
|
|
185
185
|
});
|
|
186
|
+
// Scope registry lookups (root-scope/search-schema) per router during the
|
|
187
|
+
// bake, mirroring rsc/handler.ts on the request path (#762).
|
|
188
|
+
baseCtx._routerId = router.id;
|
|
186
189
|
|
|
187
190
|
// Entry collector: captureAndStoreShell's sink. putShell never fails here,
|
|
188
191
|
// so a "stored" outcome always carries the entry.
|
|
@@ -235,6 +238,9 @@ async function attemptBuildCapture(
|
|
|
235
238
|
getGlobalRouteMap(),
|
|
236
239
|
preview?.routeKey,
|
|
237
240
|
preview?.params ?? {},
|
|
241
|
+
preview?.routeKey
|
|
242
|
+
? isRouteRootScoped(preview.routeKey, router.id)
|
|
243
|
+
: undefined,
|
|
238
244
|
);
|
|
239
245
|
|
|
240
246
|
const runCapture = () =>
|
package/src/prerender/store.ts
CHANGED
|
@@ -54,6 +54,11 @@ declare global {
|
|
|
54
54
|
var __STATIC_MANIFEST:
|
|
55
55
|
| Record<string, () => Promise<{ default: string | StaticEntry }>>
|
|
56
56
|
| undefined;
|
|
57
|
+
// Injected by closeBundle post-processing: lazy loader for the static
|
|
58
|
+
// manifest module (which assigns __STATIC_MANIFEST on evaluation). Resolved
|
|
59
|
+
// by static-store.ts on the first Static() lookup — issue #760.
|
|
60
|
+
// eslint-disable-next-line no-var
|
|
61
|
+
var __loadStaticManifestModule: (() => Promise<unknown>) | undefined;
|
|
57
62
|
// Injected by virtual module in dev mode for on-demand prerender
|
|
58
63
|
// eslint-disable-next-line no-var
|
|
59
64
|
var __PRERENDER_DEV_URL: string | undefined;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getNamePrefix,
|
|
13
13
|
getUrlPrefix,
|
|
14
14
|
requireDslContext,
|
|
15
|
+
stampStaticDefScope,
|
|
15
16
|
type EntryData,
|
|
16
17
|
type EntryPropDatas,
|
|
17
18
|
type EntryPropSegments,
|
|
@@ -560,6 +561,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
560
561
|
if (ctx.namePrefix) {
|
|
561
562
|
(slotHandler as any).$$routePrefix = ctx.namePrefix;
|
|
562
563
|
}
|
|
564
|
+
stampStaticDefScope(slotHandler);
|
|
563
565
|
}
|
|
564
566
|
} else {
|
|
565
567
|
unwrappedSlots[slotName] = slotHandler;
|
|
@@ -1016,8 +1018,11 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1016
1018
|
} satisfies EntryData;
|
|
1017
1019
|
|
|
1018
1020
|
// Capture namespace prefix on static handler for build-time reverse() resolution
|
|
1019
|
-
if (isStatic && handler.$$id
|
|
1020
|
-
(
|
|
1021
|
+
if (isStatic && handler.$$id) {
|
|
1022
|
+
if (ctx.namePrefix) {
|
|
1023
|
+
(handler as any).$$routePrefix = ctx.namePrefix;
|
|
1024
|
+
}
|
|
1025
|
+
stampStaticDefScope(handler);
|
|
1021
1026
|
}
|
|
1022
1027
|
|
|
1023
1028
|
// Merge handler.use defaults with explicit use
|
|
@@ -460,11 +460,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
460
460
|
* startTransition only when the router sets viewTransition: false.
|
|
461
461
|
*
|
|
462
462
|
* Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
|
|
463
|
-
* request. It runs server-side
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
463
|
+
* request. It normally runs server-side after the route handler. On a `ppr`
|
|
464
|
+
* route it is automatically hoisted before route handlers and reevaluated on
|
|
465
|
+
* every replay, so it may read URL/params/action/env and middleware-set context
|
|
466
|
+
* but not handler-set context. Returning false drops this transition for the
|
|
467
|
+
* request. This is distinct from intercept()'s match-time `when` selector.
|
|
468
468
|
*
|
|
469
469
|
* ```typescript
|
|
470
470
|
* // Attach to a single route
|
|
@@ -491,7 +491,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
491
491
|
* @param config - ViewTransition configuration (enter, exit, update, share,
|
|
492
492
|
* default, name), `viewTransition: "auto" | false` to toggle the router
|
|
493
493
|
* boundary (createRouter({ viewTransition }) sets the app-wide default), and
|
|
494
|
-
* `when: (ctx) => boolean` to gate the transition per request
|
|
494
|
+
* `when: (ctx) => boolean` to gate the transition per request
|
|
495
495
|
* @param children - Optional callback returning child routes to wrap
|
|
496
496
|
*/
|
|
497
497
|
transition: {
|