@rangojs/router 0.0.0-experimental.121 → 0.0.0-experimental.124
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 +7 -2
- package/dist/vite/index.js +47 -6
- package/package.json +61 -21
- package/skills/cache-guide/SKILL.md +8 -6
- package/skills/caching/SKILL.md +148 -1
- package/skills/hooks/SKILL.md +38 -27
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +27 -15
- package/skills/route/SKILL.md +4 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +118 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/use-cache/SKILL.md +9 -7
- package/src/browser/action-fence.ts +37 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +39 -0
- package/src/browser/navigation-store.ts +26 -12
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/rango-state.ts +176 -97
- package/src/browser/react/index.ts +0 -6
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +95 -1
- package/src/cache/cache-runtime.ts +79 -13
- package/src/cache/cache-scope.ts +55 -4
- package/src/cache/cache-tag.ts +135 -0
- package/src/cache/cf/cf-cache-store.ts +2080 -224
- package/src/cache/cf/index.ts +15 -1
- package/src/cache/document-cache.ts +74 -7
- package/src/cache/index.ts +17 -0
- package/src/cache/memory-segment-store.ts +164 -14
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +27 -0
- package/src/client.rsc.tsx +1 -1
- package/src/client.tsx +0 -6
- package/src/component-utils.ts +19 -0
- package/src/handle.ts +29 -9
- package/src/host/testing.ts +43 -14
- package/src/index.rsc.ts +29 -1
- package/src/index.ts +43 -1
- package/src/loader.rsc.ts +24 -3
- package/src/loader.ts +16 -2
- package/src/prerender.ts +24 -3
- package/src/router/basename.ts +14 -0
- package/src/router/match-handlers.ts +62 -20
- package/src/router/prerender-match.ts +6 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/loader-cache.ts +8 -17
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router.ts +36 -7
- package/src/rsc/handler.ts +13 -1
- package/src/rsc/helpers.ts +19 -0
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +8 -1
- package/src/rsc/rsc-rendering.ts +2 -0
- package/src/rsc/types.ts +2 -0
- package/src/runtime-env.ts +18 -0
- package/src/server/cookie-store.ts +52 -1
- package/src/server/request-context.ts +105 -2
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +119 -0
- package/src/testing/internal/context.ts +390 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +80 -0
- package/src/testing/render-handler.ts +360 -0
- package/src/testing/render-route.tsx +594 -0
- package/src/testing/run-loader.ts +474 -0
- package/src/testing/run-middleware.ts +231 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +5 -1
- package/src/types/global-namespace.ts +11 -1
- package/src/types/handler-context.ts +16 -5
- package/src/browser/react/use-client-cache.ts +0 -58
package/src/cache/cache-scope.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
getRequestContext,
|
|
17
17
|
_getRequestContext,
|
|
18
18
|
} from "../server/request-context.js";
|
|
19
|
+
import { recordRequestTags } from "./cache-tag.js";
|
|
20
|
+
import { reportCacheError } from "./cache-error.js";
|
|
19
21
|
import { serializeSegments, deserializeSegments } from "./segment-codec.js";
|
|
20
22
|
import {
|
|
21
23
|
captureHandles,
|
|
@@ -28,7 +30,23 @@ import {
|
|
|
28
30
|
DEFAULT_ROUTE_TTL,
|
|
29
31
|
resolveCacheKey,
|
|
30
32
|
resolveCacheStore,
|
|
33
|
+
resolveTagsOption,
|
|
31
34
|
} from "./cache-policy.js";
|
|
35
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve tags for a cache() boundary from its config (static array or
|
|
39
|
+
* function of ctx). Thin wrapper over the shared resolveTagsOption so the
|
|
40
|
+
* cache() DSL and loader caching resolve tags identically.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export function resolveCacheTags(
|
|
44
|
+
config: PartialCacheOptions | false,
|
|
45
|
+
ctx: RequestContext | undefined,
|
|
46
|
+
): string[] | undefined {
|
|
47
|
+
if (config === false) return undefined;
|
|
48
|
+
return resolveTagsOption(config.tags, ctx, "CacheScope");
|
|
49
|
+
}
|
|
32
50
|
|
|
33
51
|
function debugCacheLog(message: string): void {
|
|
34
52
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -253,8 +271,32 @@ export class CacheScope {
|
|
|
253
271
|
|
|
254
272
|
const { data: cached, shouldRevalidate } = result;
|
|
255
273
|
|
|
256
|
-
// Deserialize segments
|
|
257
|
-
|
|
274
|
+
// Deserialize segments. A failure means the cached segments are corrupt/
|
|
275
|
+
// partial: evict the entry (self-heal - the re-render re-caches under the
|
|
276
|
+
// same key) and report it as corruption, distinct from a transient infra
|
|
277
|
+
// error (handled by the outer catch).
|
|
278
|
+
let segments: ResolvedSegment[];
|
|
279
|
+
try {
|
|
280
|
+
segments = await deserializeSegments(cached.segments);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
reportCacheError(
|
|
283
|
+
error,
|
|
284
|
+
"cache-corrupt",
|
|
285
|
+
`[CacheScope] ${key}: corrupt cached segments, evicting`,
|
|
286
|
+
);
|
|
287
|
+
await store
|
|
288
|
+
.delete(key)
|
|
289
|
+
.catch((e) =>
|
|
290
|
+
reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
|
|
291
|
+
);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// A hit serves content that was tagged at write time, so the document
|
|
296
|
+
// tag union must include this entry's tags for updateTag()/revalidateTag()
|
|
297
|
+
// to invalidate any full-page entry built on top of it. The write path
|
|
298
|
+
// records via cacheRoute (resolveCacheTags); the hit path records here.
|
|
299
|
+
recordRequestTags(cached.tags);
|
|
258
300
|
|
|
259
301
|
// Replay handle data. An empty string means the route pushed no handles —
|
|
260
302
|
// skip the decode entirely (the common case). Otherwise decode the
|
|
@@ -279,7 +321,7 @@ export class CacheScope {
|
|
|
279
321
|
|
|
280
322
|
return { segments, shouldRevalidate };
|
|
281
323
|
} catch (error) {
|
|
282
|
-
|
|
324
|
+
reportCacheError(error, "cache-read", `[CacheScope] lookup ${key}`);
|
|
283
325
|
return null;
|
|
284
326
|
}
|
|
285
327
|
}
|
|
@@ -322,6 +364,10 @@ export class CacheScope {
|
|
|
322
364
|
// Resolve cache key early (while request context is available)
|
|
323
365
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
324
366
|
|
|
367
|
+
// Resolve tags early (while request context is available, before waitUntil)
|
|
368
|
+
const tags = resolveCacheTags(this.config, requestCtx);
|
|
369
|
+
recordRequestTags(tags, requestCtx);
|
|
370
|
+
|
|
325
371
|
// Check if this is a partial request (navigation) vs document request
|
|
326
372
|
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
327
373
|
|
|
@@ -390,6 +436,7 @@ export class CacheScope {
|
|
|
390
436
|
segments: serializedSegments,
|
|
391
437
|
handles: encodedHandles,
|
|
392
438
|
expiresAt: Date.now() + ttl * 1000,
|
|
439
|
+
tags,
|
|
393
440
|
};
|
|
394
441
|
|
|
395
442
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -407,7 +454,11 @@ export class CacheScope {
|
|
|
407
454
|
);
|
|
408
455
|
}
|
|
409
456
|
} catch (error) {
|
|
410
|
-
|
|
457
|
+
reportCacheError(
|
|
458
|
+
error,
|
|
459
|
+
"cache-write",
|
|
460
|
+
`[CacheScope] Failed to cache ${key}`,
|
|
461
|
+
);
|
|
411
462
|
}
|
|
412
463
|
});
|
|
413
464
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Tag API
|
|
3
|
+
*
|
|
4
|
+
* Provides cacheTag() for tagging cached entries at runtime inside "use cache"
|
|
5
|
+
* functions. Tags are scoped via AsyncLocalStorage; calling cacheTag() outside
|
|
6
|
+
* a "use cache" execution throws.
|
|
7
|
+
*
|
|
8
|
+
* The runtime (cache-runtime.ts) wraps "use cache" execution in
|
|
9
|
+
* runWithCacheTagScope(), collects the runtime tags, and merges them with the
|
|
10
|
+
* profile/DSL tags before storing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import {
|
|
15
|
+
_getRequestContext,
|
|
16
|
+
type RequestContext,
|
|
17
|
+
} from "../server/request-context.js";
|
|
18
|
+
|
|
19
|
+
const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a tag for storage.
|
|
23
|
+
*
|
|
24
|
+
* Returns the tag unchanged if usable, or null if it is empty/whitespace-only
|
|
25
|
+
* (dropped consistently in every environment - an empty tag matches nothing).
|
|
26
|
+
*
|
|
27
|
+
* Backend-specific constraints are intentionally NOT enforced here so the tag
|
|
28
|
+
* primitive stays backend-agnostic. In particular, the CFCacheStore
|
|
29
|
+
* encodeURIComponent's tags at serialization time so commas/spaces/non-Latin1
|
|
30
|
+
* characters cannot corrupt the comma-delimited Cloudflare Cache-Tag header or
|
|
31
|
+
* the HTTP marker header (it does not reject them). Keep tags short and
|
|
32
|
+
* low-cardinality: a tag's KV marker key must stay under Cloudflare's 512-byte
|
|
33
|
+
* limit, and a Cache-Tag value under 1024 bytes. The in-memory store has no
|
|
34
|
+
* such limitations.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeTag(tag: string): string | null {
|
|
39
|
+
if (!tag || !tag.trim()) return null;
|
|
40
|
+
return tag;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalize a tag collection: drop empty/whitespace-only tags so the WRITE path
|
|
45
|
+
* matches the invalidate path (updateTag/revalidateTag/cacheTag all normalize).
|
|
46
|
+
* Does not deduplicate - callers that need that wrap with a Set.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeTags(tags: Iterable<string>): string[] {
|
|
51
|
+
const out: string[] = [];
|
|
52
|
+
for (const tag of tags) {
|
|
53
|
+
const normalized = normalizeTag(tag);
|
|
54
|
+
if (normalized !== null) out.push(normalized);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Tag the current "use cache" entry for later invalidation via
|
|
61
|
+
* updateTag() / revalidateTag().
|
|
62
|
+
*
|
|
63
|
+
* Must be called inside a function marked with "use cache".
|
|
64
|
+
* Tags are additive - multiple calls accumulate.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* async function getProduct(ctx) {
|
|
69
|
+
* "use cache";
|
|
70
|
+
* cacheTag(`product:${ctx.params.id}`, "products");
|
|
71
|
+
* return db.getProduct(ctx.params.id);
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function cacheTag(...tags: string[]): void {
|
|
76
|
+
const store = cacheTagStorage.getStore();
|
|
77
|
+
if (!store) {
|
|
78
|
+
throw new Error('cacheTag() must be called inside a "use cache" function.');
|
|
79
|
+
}
|
|
80
|
+
for (const tag of tags) {
|
|
81
|
+
const normalized = normalizeTag(tag);
|
|
82
|
+
if (normalized === null) {
|
|
83
|
+
if (process.env.NODE_ENV !== "production") {
|
|
84
|
+
console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
store.add(normalized);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Record `tags` into the request-scoped tag set (ctx._requestTags), the union of
|
|
94
|
+
* every cache tag resolved while producing the response. The document cache reads
|
|
95
|
+
* this after the render settles so a full-page entry is tagged with everything its
|
|
96
|
+
* content used, making it invalidatable by updateTag()/revalidateTag().
|
|
97
|
+
*
|
|
98
|
+
* Called at the tag-resolution sites: "use cache" stores (cache-runtime, both the
|
|
99
|
+
* miss and read/hit paths), loader cache (cache-policy/loader-cache), and segment
|
|
100
|
+
* cache() (cache-scope). Writes the field directly (not via ctx.set()) so it does
|
|
101
|
+
* not trip the cache-scope side-effect guard, mirroring cacheTag() itself.
|
|
102
|
+
*
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
export function recordRequestTags(
|
|
106
|
+
tags: Iterable<string> | undefined,
|
|
107
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
108
|
+
): void {
|
|
109
|
+
if (!tags) return;
|
|
110
|
+
const set = ctx?._requestTags;
|
|
111
|
+
if (!set) return;
|
|
112
|
+
for (const tag of tags) {
|
|
113
|
+
const normalized = normalizeTag(tag);
|
|
114
|
+
if (normalized !== null) set.add(normalized);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
|
|
120
|
+
* accumulate into the returned Set.
|
|
121
|
+
*
|
|
122
|
+
* The returned Set is the LIVE reference - the caller must await `result`
|
|
123
|
+
* before reading `tags`, because an async cached function may call cacheTag()
|
|
124
|
+
* after an await boundary.
|
|
125
|
+
*
|
|
126
|
+
* @internal Used by cache-runtime.ts to wrap "use cache" execution.
|
|
127
|
+
*/
|
|
128
|
+
export function runWithCacheTagScope<T>(fn: () => T): {
|
|
129
|
+
result: T;
|
|
130
|
+
tags: Set<string>;
|
|
131
|
+
} {
|
|
132
|
+
const tagSet = new Set<string>();
|
|
133
|
+
const result = cacheTagStorage.run(tagSet, fn);
|
|
134
|
+
return { result, tags: tagSet };
|
|
135
|
+
}
|