@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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 +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- 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 +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -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/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- 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 +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +27 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +58 -3
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -21
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -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 +127 -0
- package/src/testing/e2e/matchers.ts +35 -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 +97 -0
- package/src/testing/flight-normalize.ts +11 -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 +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +311 -0
- package/src/testing/render-route.tsx +504 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +16 -13
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/use-cache-transform.ts +0 -36
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +1 -108
- package/src/vite/router-discovery.ts +2 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
14
|
import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
15
15
|
import type { CookieOptions } from "../router/middleware.js";
|
|
16
|
+
import {
|
|
17
|
+
KEEP_CACHE_HEADER,
|
|
18
|
+
getRawCookieValue,
|
|
19
|
+
mintStateValue,
|
|
20
|
+
serializeStateCookie,
|
|
21
|
+
} from "../browser/cookie-name.js";
|
|
16
22
|
import type { LoaderDefinition, LoaderContext } from "../types.js";
|
|
17
23
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
18
24
|
import type {
|
|
@@ -103,6 +109,10 @@ export interface RequestContext<
|
|
|
103
109
|
setStatus(status: number): void;
|
|
104
110
|
/** @internal Set status bypassing cache-exec guard (for framework error handling) */
|
|
105
111
|
_setStatus(status: number): void;
|
|
112
|
+
/** @internal Rotate the rango state cookie (server seat of invalidateClientCache). */
|
|
113
|
+
_rotateStateCookie(): void;
|
|
114
|
+
/** @internal Set the keepClientCache() directive header on the response. */
|
|
115
|
+
_setKeepCacheDirective(): void;
|
|
106
116
|
|
|
107
117
|
/**
|
|
108
118
|
* Access loader data or push handle data.
|
|
@@ -360,6 +370,15 @@ export interface RequestContext<
|
|
|
360
370
|
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
361
371
|
*/
|
|
362
372
|
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @internal Coarse route-level cache signal for the X-Rango-Cache debug
|
|
376
|
+
* header. Populated by match/matchPartial only when the debug cache signal
|
|
377
|
+
* gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
|
|
378
|
+
* the response-finalization path (createResponseWithMergedHeaders). Undefined
|
|
379
|
+
* when the gate is off, so no header is emitted.
|
|
380
|
+
*/
|
|
381
|
+
_cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
|
|
363
382
|
}
|
|
364
383
|
|
|
365
384
|
/**
|
|
@@ -401,8 +420,11 @@ export type PublicRequestContext<
|
|
|
401
420
|
| "_metricsStore"
|
|
402
421
|
| "_basename"
|
|
403
422
|
| "_setStatus"
|
|
423
|
+
| "_rotateStateCookie"
|
|
424
|
+
| "_setKeepCacheDirective"
|
|
404
425
|
| "_variables"
|
|
405
426
|
| "_classifiedRoute"
|
|
427
|
+
| "_cacheSignal"
|
|
406
428
|
| "res"
|
|
407
429
|
>;
|
|
408
430
|
|
|
@@ -540,6 +562,10 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
540
562
|
executionContext?: ExecutionContext;
|
|
541
563
|
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
542
564
|
themeConfig?: ResolvedThemeConfig | null;
|
|
565
|
+
/** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
|
|
566
|
+
stateCookieName?: string;
|
|
567
|
+
/** Build version, used as the prefix of a server-rotated rango state value. */
|
|
568
|
+
version?: string;
|
|
543
569
|
}
|
|
544
570
|
|
|
545
571
|
/**
|
|
@@ -564,12 +590,13 @@ export function createRequestContext<TEnv>(
|
|
|
564
590
|
cacheProfiles,
|
|
565
591
|
executionContext,
|
|
566
592
|
themeConfig,
|
|
593
|
+
stateCookieName,
|
|
594
|
+
version: stateVersion,
|
|
567
595
|
} = options;
|
|
568
596
|
const cookieHeader = request.headers.get("Cookie");
|
|
597
|
+
let rangoStateRotated = false;
|
|
569
598
|
let parsedCookies: Record<string, string> | null = null;
|
|
570
599
|
|
|
571
|
-
// Create stub response for collecting headers/cookies.
|
|
572
|
-
// All cookie/header mutations go here; cookie reads derive from it.
|
|
573
600
|
let stubResponse = initialResponse
|
|
574
601
|
? new Response(null, {
|
|
575
602
|
status: initialResponse.status,
|
|
@@ -578,11 +605,9 @@ export function createRequestContext<TEnv>(
|
|
|
578
605
|
})
|
|
579
606
|
: new Response(null, { status: 200 });
|
|
580
607
|
|
|
581
|
-
// Create handle store and loader memoization for this request
|
|
582
608
|
const handleStore = createHandleStore();
|
|
583
609
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
584
610
|
|
|
585
|
-
// Lazy parse cookies from the original Cookie header
|
|
586
611
|
const getParsedCookies = (): Record<string, string> => {
|
|
587
612
|
if (!parsedCookies) {
|
|
588
613
|
parsedCookies = parseCookiesFromHeader(cookieHeader);
|
|
@@ -590,7 +615,6 @@ export function createRequestContext<TEnv>(
|
|
|
590
615
|
return parsedCookies;
|
|
591
616
|
};
|
|
592
617
|
|
|
593
|
-
// Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
|
|
594
618
|
let responseCookieCache: Map<string, string | null> | null = null;
|
|
595
619
|
const getResponseCookies = (): Map<string, string | null> => {
|
|
596
620
|
if (!responseCookieCache) {
|
|
@@ -602,8 +626,6 @@ export function createRequestContext<TEnv>(
|
|
|
602
626
|
responseCookieCache = null;
|
|
603
627
|
};
|
|
604
628
|
|
|
605
|
-
// Guard: throw if a response-level side effect is called inside a cache() scope.
|
|
606
|
-
// Uses ALS to detect the scope (set during segment resolution).
|
|
607
629
|
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
608
630
|
if (isInsideCacheScope()) {
|
|
609
631
|
throw new Error(
|
|
@@ -614,8 +636,7 @@ export function createRequestContext<TEnv>(
|
|
|
614
636
|
}
|
|
615
637
|
}
|
|
616
638
|
|
|
617
|
-
//
|
|
618
|
-
// The stub IS the source of truth for same-request mutations.
|
|
639
|
+
// Response stub Set-Cookie wins, then original header (source of truth for mutations).
|
|
619
640
|
const effectiveCookie = (name: string): string | undefined => {
|
|
620
641
|
const mutations = getResponseCookies();
|
|
621
642
|
if (mutations.has(name)) {
|
|
@@ -625,14 +646,11 @@ export function createRequestContext<TEnv>(
|
|
|
625
646
|
return getParsedCookies()[name];
|
|
626
647
|
};
|
|
627
648
|
|
|
628
|
-
// Theme helpers (only used when themeConfig is provided)
|
|
629
649
|
const getTheme = (): Theme | undefined => {
|
|
630
650
|
if (!themeConfig) return undefined;
|
|
631
651
|
|
|
632
|
-
// Use overlay-aware read so setTheme() in the same request is reflected
|
|
633
652
|
const stored = effectiveCookie(themeConfig.storageKey);
|
|
634
653
|
if (stored) {
|
|
635
|
-
// Validate stored value
|
|
636
654
|
if (stored === "system" && themeConfig.enableSystem) {
|
|
637
655
|
return "system";
|
|
638
656
|
}
|
|
@@ -646,7 +664,6 @@ export function createRequestContext<TEnv>(
|
|
|
646
664
|
const setTheme = (theme: Theme): void => {
|
|
647
665
|
if (!themeConfig) return;
|
|
648
666
|
|
|
649
|
-
// Validate theme value
|
|
650
667
|
if (theme !== "system" && !themeConfig.themes.includes(theme)) {
|
|
651
668
|
console.warn(
|
|
652
669
|
`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
|
|
@@ -654,7 +671,6 @@ export function createRequestContext<TEnv>(
|
|
|
654
671
|
return;
|
|
655
672
|
}
|
|
656
673
|
|
|
657
|
-
// Write to stub — effectiveCookie() will pick it up on next read
|
|
658
674
|
stubResponse.headers.append(
|
|
659
675
|
"Set-Cookie",
|
|
660
676
|
serializeCookieValue(themeConfig.storageKey, theme, {
|
|
@@ -666,10 +682,8 @@ export function createRequestContext<TEnv>(
|
|
|
666
682
|
invalidateResponseCookieCache();
|
|
667
683
|
};
|
|
668
684
|
|
|
669
|
-
// Strip internal _rsc* params so userland sees a clean URL.
|
|
670
685
|
const cleanUrl = stripInternalParams(url);
|
|
671
686
|
|
|
672
|
-
// Build the context object first (without use), then add use
|
|
673
687
|
const ctx: RequestContext<TEnv> = {
|
|
674
688
|
env,
|
|
675
689
|
request,
|
|
@@ -755,6 +769,45 @@ export function createRequestContext<TEnv>(
|
|
|
755
769
|
stubResponse.headers.set(name, value);
|
|
756
770
|
},
|
|
757
771
|
|
|
772
|
+
// Rotate the rango state cookie for the responding client (the server seat
|
|
773
|
+
// of invalidateClientCache). Writes ONE Set-Cookie per request with the
|
|
774
|
+
// value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
|
|
775
|
+
// serializer), not the URL-encoded form serializeCookieValue would produce.
|
|
776
|
+
// The timestamp is strictly greater than the client's current one (inbound
|
|
777
|
+
// X-Rango-State), so a same-millisecond server rotation still differs from
|
|
778
|
+
// the client value and the divergence observer fires.
|
|
779
|
+
_rotateStateCookie(): void {
|
|
780
|
+
if (rangoStateRotated) return;
|
|
781
|
+
rangoStateRotated = true;
|
|
782
|
+
if (!stateCookieName) return;
|
|
783
|
+
// The client's current value, for the monotonic guard: prefer the
|
|
784
|
+
// X-Rango-State header (router navigation/prefetch fetches send it), but
|
|
785
|
+
// fall back to the request's rango state cookie — action POSTs / plain
|
|
786
|
+
// app fetch()s carry no router header yet DO send the cookie. Without the
|
|
787
|
+
// fallback, prevTs stays 0 and a same-ms mint can equal the client value,
|
|
788
|
+
// leaving the divergence observer silent. `|| null` so an empty header
|
|
789
|
+
// ('' from proxy normalization) falls through instead of short-circuiting.
|
|
790
|
+
// getRawCookieValue reads the cookie undecoded (the wire value
|
|
791
|
+
// decodeStateValue decodes exactly once) AND is the same parser the client
|
|
792
|
+
// mirror uses, so both seats read the same jar entry.
|
|
793
|
+
const prevRaw =
|
|
794
|
+
(request.headers.get("x-rango-state") || null) ??
|
|
795
|
+
getRawCookieValue(cookieHeader, stateCookieName);
|
|
796
|
+
const value = mintStateValue(stateVersion ?? "0", prevRaw);
|
|
797
|
+
stubResponse.headers.append(
|
|
798
|
+
"Set-Cookie",
|
|
799
|
+
serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
|
|
800
|
+
);
|
|
801
|
+
invalidateResponseCookieCache();
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
// Set the keepClientCache() directive header. The action bridge reads it on
|
|
805
|
+
// the response and suppresses its automatic invalidation. `.set` makes this
|
|
806
|
+
// idempotent (one header regardless of call count).
|
|
807
|
+
_setKeepCacheDirective(): void {
|
|
808
|
+
stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
|
|
809
|
+
},
|
|
810
|
+
|
|
758
811
|
setStatus(status: number): void {
|
|
759
812
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
760
813
|
assertNotInsideCacheScopeALS("setStatus");
|
|
@@ -771,7 +824,6 @@ export function createRequestContext<TEnv>(
|
|
|
771
824
|
});
|
|
772
825
|
},
|
|
773
826
|
|
|
774
|
-
// Placeholder - will be replaced below
|
|
775
827
|
use: null as any,
|
|
776
828
|
|
|
777
829
|
method: request.method,
|
|
@@ -800,7 +852,6 @@ export function createRequestContext<TEnv>(
|
|
|
800
852
|
this._onResponseCallbacks.push(callback);
|
|
801
853
|
},
|
|
802
854
|
|
|
803
|
-
// Theme properties (only set when themeConfig is provided)
|
|
804
855
|
get theme() {
|
|
805
856
|
return themeConfig ? getTheme() : undefined;
|
|
806
857
|
},
|
|
@@ -824,19 +875,17 @@ export function createRequestContext<TEnv>(
|
|
|
824
875
|
_reportedErrors: new WeakSet<object>(),
|
|
825
876
|
_metricsStore: undefined,
|
|
826
877
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
_resolveRenderBarrier: null as any, // set below
|
|
878
|
+
_renderBarrier: null as any,
|
|
879
|
+
_resolveRenderBarrier: null as any,
|
|
830
880
|
_renderBarrierSegmentOrder: undefined,
|
|
831
881
|
|
|
832
882
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
833
883
|
};
|
|
834
884
|
|
|
835
|
-
// Lazy
|
|
836
|
-
// calls rendered(). Requests that don't use rendered() pay zero cost.
|
|
885
|
+
// Lazy allocation: only create Promise when a loader calls rendered().
|
|
837
886
|
let barrierResolved = false;
|
|
838
887
|
let resolveBarrier: (() => void) | undefined;
|
|
839
|
-
ctx._renderBarrier = null as any;
|
|
888
|
+
ctx._renderBarrier = null as any;
|
|
840
889
|
ctx._resolveRenderBarrier = (
|
|
841
890
|
segments: Array<{ type: string; id: string }>,
|
|
842
891
|
) => {
|
|
@@ -847,9 +896,6 @@ export function createRequestContext<TEnv>(
|
|
|
847
896
|
.map((s) => s.id);
|
|
848
897
|
ctx._renderBarrierSegmentOrder = segOrder;
|
|
849
898
|
|
|
850
|
-
// Closing the guard window means no handler can still form a deadlock cycle
|
|
851
|
-
// with a rendered() loader: drop the dependency-tracking state and mark it
|
|
852
|
-
// closed. WHEN this runs is the only streaming/non-streaming difference.
|
|
853
899
|
const closeGuard = () => {
|
|
854
900
|
ctx._renderBarrierWaiters = undefined;
|
|
855
901
|
ctx._handlerLoaderDeps = undefined;
|
|
@@ -857,20 +903,8 @@ export function createRequestContext<TEnv>(
|
|
|
857
903
|
};
|
|
858
904
|
|
|
859
905
|
if (ctx._treeHasStreaming) {
|
|
860
|
-
// Streaming: rendered() keeps waiting on handleStore.settled past this
|
|
861
|
-
// point, and loading() handlers are still in flight. The eager snapshot
|
|
862
|
-
// here would be incomplete, so leave it unset — rendered() builds and
|
|
863
|
-
// caches the complete one after settled. Keep the guard window OPEN so a
|
|
864
|
-
// handler that resumes and awaits a still-waiting rendered() loader is
|
|
865
|
-
// still caught; close it once settled (every tracked handler has finished
|
|
866
|
-
// then, so none can await a loader anymore). settled resolves after
|
|
867
|
-
// rendered() seals; if no loader used rendered(), nothing seals and the
|
|
868
|
-
// (empty) guard state is simply GC'd at request end.
|
|
869
906
|
handleStore.settled.then(closeGuard);
|
|
870
907
|
} else {
|
|
871
|
-
// Non-streaming: all handlers have settled by now. Build and cache the
|
|
872
|
-
// snapshot so loader ctx.use(handle) calls don't rebuild it, and close the
|
|
873
|
-
// guard window immediately.
|
|
874
908
|
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
875
909
|
handleStore,
|
|
876
910
|
segOrder,
|
|
@@ -881,9 +915,6 @@ export function createRequestContext<TEnv>(
|
|
|
881
915
|
};
|
|
882
916
|
Object.defineProperty(ctx, "_renderBarrier", {
|
|
883
917
|
get() {
|
|
884
|
-
// Barrier already resolved (cache/prerender hit) or first lazy access.
|
|
885
|
-
// Either way, replace the getter with a concrete value to avoid
|
|
886
|
-
// repeated Promise.resolve() allocations on subsequent reads.
|
|
887
918
|
const p = barrierResolved
|
|
888
919
|
? Promise.resolve()
|
|
889
920
|
: new Promise<void>((resolve) => {
|
|
@@ -899,24 +930,16 @@ export function createRequestContext<TEnv>(
|
|
|
899
930
|
configurable: true,
|
|
900
931
|
});
|
|
901
932
|
|
|
902
|
-
// Now create use() with access to ctx
|
|
903
933
|
ctx.use = createUseFunction({
|
|
904
934
|
handleStore,
|
|
905
935
|
loaderPromises,
|
|
906
936
|
getContext: () => ctx,
|
|
907
937
|
});
|
|
908
938
|
|
|
909
|
-
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
910
939
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
911
940
|
return ctx;
|
|
912
941
|
}
|
|
913
942
|
|
|
914
|
-
/**
|
|
915
|
-
* Parse Set-Cookie headers from a response into effective cookie state.
|
|
916
|
-
* Returns a map of cookie name -> value (string) or name -> null (deleted).
|
|
917
|
-
* Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
|
|
918
|
-
* Max-Age=0 is treated as a delete.
|
|
919
|
-
*/
|
|
920
943
|
const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
|
|
921
944
|
|
|
922
945
|
function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
@@ -924,7 +947,6 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
924
947
|
const setCookies = response.headers.getSetCookie();
|
|
925
948
|
|
|
926
949
|
for (const header of setCookies) {
|
|
927
|
-
// First segment before ';' is the name=value pair
|
|
928
950
|
const semiIdx = header.indexOf(";");
|
|
929
951
|
const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
|
|
930
952
|
const eqIdx = pair.indexOf("=");
|
|
@@ -936,11 +958,9 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
936
958
|
name = decodeURIComponent(pair.substring(0, eqIdx).trim());
|
|
937
959
|
value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
|
|
938
960
|
} catch {
|
|
939
|
-
// Malformed encoding — skip this entry
|
|
940
961
|
continue;
|
|
941
962
|
}
|
|
942
963
|
|
|
943
|
-
// Max-Age=0 means the cookie is being deleted
|
|
944
964
|
const isDeleted = MAX_AGE_ZERO_RE.test(header);
|
|
945
965
|
result.set(name, isDeleted ? null : value);
|
|
946
966
|
}
|
|
@@ -948,9 +968,6 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
948
968
|
return result;
|
|
949
969
|
}
|
|
950
970
|
|
|
951
|
-
/**
|
|
952
|
-
* Parse cookies from Cookie header
|
|
953
|
-
*/
|
|
954
971
|
function parseCookiesFromHeader(
|
|
955
972
|
cookieHeader: string | null,
|
|
956
973
|
): Record<string, string> {
|
|
@@ -966,7 +983,7 @@ function parseCookiesFromHeader(
|
|
|
966
983
|
try {
|
|
967
984
|
cookies[name] = decodeURIComponent(raw);
|
|
968
985
|
} catch {
|
|
969
|
-
// Malformed percent-
|
|
986
|
+
// Malformed percent-encoding: fall back to raw value
|
|
970
987
|
cookies[name] = raw;
|
|
971
988
|
}
|
|
972
989
|
}
|
|
@@ -975,9 +992,6 @@ function parseCookiesFromHeader(
|
|
|
975
992
|
return cookies;
|
|
976
993
|
}
|
|
977
994
|
|
|
978
|
-
/**
|
|
979
|
-
* Serialize a cookie for Set-Cookie header
|
|
980
|
-
*/
|
|
981
995
|
function serializeCookieValue(
|
|
982
996
|
name: string,
|
|
983
997
|
value: string,
|
|
@@ -1005,20 +1019,12 @@ export interface CreateUseFunctionOptions<TEnv> {
|
|
|
1005
1019
|
getContext: () => RequestContext<TEnv>;
|
|
1006
1020
|
}
|
|
1007
1021
|
|
|
1008
|
-
/**
|
|
1009
|
-
* Create the use() function for loader and handle composition.
|
|
1010
|
-
*
|
|
1011
|
-
* This is the unified implementation used by both RequestContext and HandlerContext.
|
|
1012
|
-
* - For loaders: executes and memoizes loader functions
|
|
1013
|
-
* - For handles: returns a push function to add handle data
|
|
1014
|
-
*/
|
|
1015
1022
|
export function createUseFunction<TEnv>(
|
|
1016
1023
|
options: CreateUseFunctionOptions<TEnv>,
|
|
1017
1024
|
): RequestContext["use"] {
|
|
1018
1025
|
const { handleStore, loaderPromises, getContext } = options;
|
|
1019
1026
|
|
|
1020
1027
|
return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
1021
|
-
// Handle case: return a push function
|
|
1022
1028
|
if (isHandle(item)) {
|
|
1023
1029
|
const handle = item;
|
|
1024
1030
|
const ctx = getContext();
|
|
@@ -1031,30 +1037,24 @@ export function createUseFunction<TEnv>(
|
|
|
1031
1037
|
);
|
|
1032
1038
|
}
|
|
1033
1039
|
|
|
1034
|
-
// Return a push function bound to this handle and segment
|
|
1035
1040
|
return (
|
|
1036
1041
|
dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
|
|
1037
1042
|
) => {
|
|
1038
|
-
// If it's a function, call it immediately to get the promise
|
|
1039
1043
|
const valueOrPromise =
|
|
1040
1044
|
typeof dataOrFn === "function"
|
|
1041
1045
|
? (dataOrFn as () => Promise<unknown>)()
|
|
1042
1046
|
: dataOrFn;
|
|
1043
1047
|
|
|
1044
|
-
// Push directly - promises will be serialized by RSC and streamed
|
|
1045
1048
|
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
1046
1049
|
};
|
|
1047
1050
|
}
|
|
1048
1051
|
|
|
1049
|
-
// Loader case
|
|
1050
1052
|
const loader = item as LoaderDefinition<any, any>;
|
|
1051
1053
|
|
|
1052
|
-
// Return cached promise if already started
|
|
1053
1054
|
if (loaderPromises.has(loader.$$id)) {
|
|
1054
1055
|
return loaderPromises.get(loader.$$id);
|
|
1055
1056
|
}
|
|
1056
1057
|
|
|
1057
|
-
// Get loader function - either from loader object or fetchable registry
|
|
1058
1058
|
let loaderFn = loader.fn;
|
|
1059
1059
|
if (!loaderFn) {
|
|
1060
1060
|
const fetchable = getFetchableLoader(loader.$$id);
|
|
@@ -1071,7 +1071,6 @@ export function createUseFunction<TEnv>(
|
|
|
1071
1071
|
|
|
1072
1072
|
const ctx = getContext();
|
|
1073
1073
|
|
|
1074
|
-
// Create loader context with recursive use() support
|
|
1075
1074
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
1076
1075
|
params: ctx.params,
|
|
1077
1076
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
@@ -1088,7 +1087,6 @@ export function createUseFunction<TEnv>(
|
|
|
1088
1087
|
use: (<TDep, TDepParams = any>(
|
|
1089
1088
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
1090
1089
|
): Promise<TDep> => {
|
|
1091
|
-
// Recursive call - will start dep loader if not already started
|
|
1092
1090
|
return ctx.use(dep);
|
|
1093
1091
|
}) as LoaderContext["use"],
|
|
1094
1092
|
method: "GET",
|
|
@@ -1112,7 +1110,6 @@ export function createUseFunction<TEnv>(
|
|
|
1112
1110
|
doneLoader();
|
|
1113
1111
|
});
|
|
1114
1112
|
|
|
1115
|
-
// Memoize for subsequent calls
|
|
1116
1113
|
loaderPromises.set(loader.$$id, promise);
|
|
1117
1114
|
|
|
1118
1115
|
return promise;
|
package/src/ssr/index.tsx
CHANGED
|
@@ -71,7 +71,7 @@ export interface SSRRenderOptions {
|
|
|
71
71
|
*/
|
|
72
72
|
export interface SSRDependencies<TEnv = unknown> {
|
|
73
73
|
/**
|
|
74
|
-
* createFromReadableStream from @
|
|
74
|
+
* createFromReadableStream from @rangojs/router/internal/deps/ssr
|
|
75
75
|
*/
|
|
76
76
|
createFromReadableStream: <T>(
|
|
77
77
|
stream: ReadableStream<Uint8Array>,
|
|
@@ -86,7 +86,7 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
86
86
|
) => Promise<ReactDOMReadableStream>;
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* injectRSCPayload from
|
|
89
|
+
* injectRSCPayload from @rangojs/router/internal/deps/html-stream-server
|
|
90
90
|
*/
|
|
91
91
|
injectRSCPayload: (
|
|
92
92
|
rscStream: ReadableStream<Uint8Array>,
|
|
@@ -218,10 +218,10 @@ function createSsrEventController(opts: {
|
|
|
218
218
|
*
|
|
219
219
|
* @example
|
|
220
220
|
* ```tsx
|
|
221
|
-
* import { createSSRHandler } from "
|
|
222
|
-
* import { createFromReadableStream } from "@
|
|
221
|
+
* import { createSSRHandler } from "@rangojs/router/ssr";
|
|
222
|
+
* import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
|
|
223
223
|
* import { renderToReadableStream } from "react-dom/server.edge";
|
|
224
|
-
* import { injectRSCPayload } from "
|
|
224
|
+
* import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
225
225
|
*
|
|
226
226
|
* export const renderHTML = createSSRHandler({
|
|
227
227
|
* createFromReadableStream,
|
|
@@ -263,6 +263,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
263
263
|
let payload: Promise<RscPayload> | undefined;
|
|
264
264
|
let handlesPromise: Promise<HandleData> | undefined;
|
|
265
265
|
let ssrContextValue: NavigationStoreContextValue | undefined;
|
|
266
|
+
let rootPromise: Promise<React.ReactNode> | undefined;
|
|
266
267
|
function SsrRoot() {
|
|
267
268
|
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
268
269
|
const resolved = React.use(payload);
|
|
@@ -296,17 +297,16 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
296
297
|
};
|
|
297
298
|
|
|
298
299
|
// Build content tree from segments.
|
|
299
|
-
// Order must match NavigationProvider: NavigationStoreContext > ThemeProvider > content
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
// Order must match NavigationProvider: NavigationStoreContext > NonceContext > ThemeProvider > content
|
|
301
|
+
// Memoize like payload/handles above: renderSegments is async, so
|
|
302
|
+
// React.use() on a fresh promise suspends and replays SsrRoot, which
|
|
303
|
+
// would re-run the entire segment-tree build on every initial render.
|
|
304
|
+
rootPromise ??= Promise.resolve(
|
|
305
|
+
renderSegments(resolved.metadata?.segments ?? [], {
|
|
303
306
|
rootLayout: resolved.metadata?.rootLayout,
|
|
304
|
-
},
|
|
307
|
+
}),
|
|
305
308
|
);
|
|
306
|
-
let content: React.ReactNode =
|
|
307
|
-
reconstructedRoot instanceof Promise
|
|
308
|
-
? React.use(reconstructedRoot)
|
|
309
|
-
: reconstructedRoot;
|
|
309
|
+
let content: React.ReactNode = React.use(rootPromise);
|
|
310
310
|
|
|
311
311
|
// Wrap content with ThemeProvider if theme is enabled
|
|
312
312
|
if (themeConfig) {
|
package/src/static-handler.ts
CHANGED
|
@@ -35,8 +35,7 @@ import type { Handler } from "./types.js";
|
|
|
35
35
|
import type { StaticBuildContext } from "./prerender.js";
|
|
36
36
|
import type { UseItems, HandlerUseItem } from "./route-types.js";
|
|
37
37
|
import { isCachedFunction } from "./cache/taint.js";
|
|
38
|
-
|
|
39
|
-
// -- Types ------------------------------------------------------------------
|
|
38
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
40
39
|
|
|
41
40
|
export interface StaticHandlerOptions {
|
|
42
41
|
/**
|
|
@@ -61,7 +60,9 @@ export interface StaticHandlerDefinition<
|
|
|
61
60
|
use?: () => UseItems<HandlerUseItem>;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
//
|
|
63
|
+
// Process-stable fallback id counter (mirrors createHandle/createLoader/Prerender).
|
|
64
|
+
// Only assigned in bare unit tests where the Vite plugin did not inject an id.
|
|
65
|
+
let runtimeStaticIdCounter = 0;
|
|
65
66
|
|
|
66
67
|
export function Static<TParams extends Record<string, any> = {}>(
|
|
67
68
|
handler: (ctx: StaticBuildContext) => ReactNode | Promise<ReactNode>,
|
|
@@ -69,8 +70,6 @@ export function Static<TParams extends Record<string, any> = {}>(
|
|
|
69
70
|
__injectedId?: string,
|
|
70
71
|
): StaticHandlerDefinition<TParams>;
|
|
71
72
|
|
|
72
|
-
// -- Implementation ---------------------------------------------------------
|
|
73
|
-
|
|
74
73
|
export function Static<TParams extends Record<string, any>>(
|
|
75
74
|
handler: Function,
|
|
76
75
|
optionsOrId?: StaticHandlerOptions | string,
|
|
@@ -94,12 +93,15 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
94
93
|
id = maybeId ?? "";
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
if (!id) {
|
|
96
|
+
if (!id && !isUnderTestRunner()) {
|
|
98
97
|
throw new Error(
|
|
99
|
-
"[rango] Static: missing $$id. " +
|
|
100
|
-
"
|
|
98
|
+
"[rango] Static: missing $$id. Use `export const X = Static(...)` and " +
|
|
99
|
+
"ensure the exposeInternalIds Vite plugin is configured.",
|
|
101
100
|
);
|
|
102
101
|
}
|
|
102
|
+
if (!id) {
|
|
103
|
+
id = `__rango_runtime_static_${runtimeStaticIdCounter++}`;
|
|
104
|
+
}
|
|
103
105
|
|
|
104
106
|
return {
|
|
105
107
|
__brand: "staticHandler" as const,
|
|
@@ -109,11 +111,6 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
// -- Type guard -------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Type guard to check if a value is a StaticHandlerDefinition.
|
|
116
|
-
*/
|
|
117
114
|
export function isStaticHandler(
|
|
118
115
|
value: unknown,
|
|
119
116
|
): value is StaticHandlerDefinition {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-status testing primitives for @rangojs/router consumers.
|
|
3
|
+
*
|
|
4
|
+
* Two complementary paths, both DEVELOPMENT/TEST ONLY:
|
|
5
|
+
*
|
|
6
|
+
* 1. Header path — `parseCacheHeader` / `assertCacheStatus` read the
|
|
7
|
+
* `X-Rango-Cache` response header. The header is emitted only when the
|
|
8
|
+
* router's debug cache signal gate is on (the `debugCacheSignal` option or
|
|
9
|
+
* `RANGO_TEST_SIGNALS=1`). With the gate off there is no header and these
|
|
10
|
+
* helpers throw a clear "header missing" error.
|
|
11
|
+
*
|
|
12
|
+
* 2. Telemetry path — `createCacheSink` returns a `{ sink, events }` pair the
|
|
13
|
+
* consumer wires via `createRouter({ telemetry: sink })`. This has ZERO
|
|
14
|
+
* production surface: no header, just structured `cache.decision` events
|
|
15
|
+
* (which carry the same coarse `segments` cache signal).
|
|
16
|
+
*
|
|
17
|
+
* v1 cache status is COARSE (route-level): the router reports a single entry
|
|
18
|
+
* keyed by the route key (the route NAME), not per individual segment.
|
|
19
|
+
*
|
|
20
|
+
* Import path: from a Vitest unit/integration test use `@rangojs/router/testing`;
|
|
21
|
+
* from a Playwright e2e use `@rangojs/router/testing/e2e` (the barrel pulls a
|
|
22
|
+
* build-only virtual that does not resolve in a plain Playwright runner).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
CacheDecisionEvent,
|
|
27
|
+
CacheSegmentStatus,
|
|
28
|
+
TelemetryEvent,
|
|
29
|
+
TelemetrySink,
|
|
30
|
+
} from "../router/telemetry.js";
|
|
31
|
+
|
|
32
|
+
const CACHE_HEADER = "X-Rango-Cache";
|
|
33
|
+
|
|
34
|
+
/** Expected cache status passed to assertCacheStatus. */
|
|
35
|
+
export type ExpectedCacheStatus = CacheSegmentStatus;
|
|
36
|
+
|
|
37
|
+
/** A target carrying response headers (a Response or a `{ headers }` object). */
|
|
38
|
+
export type CacheStatusTarget = Response | { headers: Headers };
|
|
39
|
+
|
|
40
|
+
export function parseCacheHeader(
|
|
41
|
+
headerValue: string | null | undefined,
|
|
42
|
+
): Record<string, string> {
|
|
43
|
+
const result: Record<string, string> = {};
|
|
44
|
+
if (!headerValue) return result;
|
|
45
|
+
for (const rawEntry of headerValue.split(",")) {
|
|
46
|
+
const entry = rawEntry.trim();
|
|
47
|
+
if (entry.length === 0) continue;
|
|
48
|
+
const eq = entry.indexOf("=");
|
|
49
|
+
if (eq === -1) continue;
|
|
50
|
+
const id = entry.slice(0, eq).trim();
|
|
51
|
+
const status = entry.slice(eq + 1).trim();
|
|
52
|
+
if (id.length === 0 || status.length === 0) continue;
|
|
53
|
+
result[id] = status;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getHeaders(target: CacheStatusTarget): Headers {
|
|
59
|
+
return target.headers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertCacheStatus(
|
|
63
|
+
target: CacheStatusTarget,
|
|
64
|
+
segment: string,
|
|
65
|
+
expected: ExpectedCacheStatus,
|
|
66
|
+
): void {
|
|
67
|
+
const headerValue = getHeaders(target).get(CACHE_HEADER);
|
|
68
|
+
if (headerValue === null) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`assertCacheStatus: response has no ${CACHE_HEADER} header. ` +
|
|
71
|
+
`Enable the debug cache signal via createRouter({ debugCacheSignal: true }) ` +
|
|
72
|
+
`or RANGO_TEST_SIGNALS=1.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const map = parseCacheHeader(headerValue);
|
|
76
|
+
const actual = map[segment];
|
|
77
|
+
if (actual === undefined) {
|
|
78
|
+
const known = Object.keys(map);
|
|
79
|
+
throw new Error(
|
|
80
|
+
`assertCacheStatus: segment "${segment}" not found in ${CACHE_HEADER} ` +
|
|
81
|
+
`("${headerValue}"). Known segments: ${
|
|
82
|
+
known.length > 0 ? known.join(", ") : "(none)"
|
|
83
|
+
}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (actual !== expected) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`assertCacheStatus: segment "${segment}" expected "${expected}" but got "${actual}".`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A telemetry sink paired with the array it records events into.
|
|
95
|
+
*/
|
|
96
|
+
export interface CacheSink {
|
|
97
|
+
/** Wire into `createRouter({ telemetry: sink })`. */
|
|
98
|
+
sink: TelemetrySink;
|
|
99
|
+
/** All telemetry events captured so far, in emit order. */
|
|
100
|
+
events: TelemetryEvent[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createCacheSink(): CacheSink {
|
|
104
|
+
const events: TelemetryEvent[] = [];
|
|
105
|
+
const sink: TelemetrySink = {
|
|
106
|
+
emit(event: TelemetryEvent): void {
|
|
107
|
+
events.push(event);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
return { sink, events };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function filterCacheDecisions(
|
|
114
|
+
events: readonly TelemetryEvent[],
|
|
115
|
+
): CacheDecisionEvent[] {
|
|
116
|
+
return events.filter(
|
|
117
|
+
(e): e is CacheDecisionEvent => e.type === "cache.decision",
|
|
118
|
+
);
|
|
119
|
+
}
|