@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- 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 -45
- 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-router.ts +2 -1
- 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 +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- 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 +48 -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 +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- 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 +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- 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 +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- 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 +34 -0
- package/src/redirect-origin.ts +100 -0
- 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 +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- 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 +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- 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 -22
- 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-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -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 +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- 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/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- 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 +4 -14
- package/src/types/handler-context.ts +28 -9
- 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 +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- 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/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- 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 +2 -108
- package/src/vite/router-discovery.ts +9 -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/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type HandleData,
|
|
41
41
|
} from "./handle-store.js";
|
|
42
42
|
import { isHandle } from "../handle.js";
|
|
43
|
+
import { withDefer } from "../defer.js";
|
|
43
44
|
import { track, type MetricsStore } from "./context.js";
|
|
44
45
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
45
46
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
@@ -476,6 +477,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
|
|
|
476
477
|
export function setRequestContextParams(
|
|
477
478
|
params: Record<string, string>,
|
|
478
479
|
routeName?: string,
|
|
480
|
+
routeMap?: Record<string, string>,
|
|
479
481
|
): void {
|
|
480
482
|
const ctx = requestContextStorage.getStore();
|
|
481
483
|
if (ctx) {
|
|
@@ -488,9 +490,13 @@ export function setRequestContextParams(
|
|
|
488
490
|
: undefined
|
|
489
491
|
) as DefaultRouteName | undefined;
|
|
490
492
|
}
|
|
491
|
-
// Update reverse with scoped resolution now that route is known
|
|
493
|
+
// Update reverse with scoped resolution now that route is known. Production
|
|
494
|
+
// omits routeMap and uses the global map (routes are registered globally);
|
|
495
|
+
// the testing primitives (renderToFlightString/renderServerTree) pass a
|
|
496
|
+
// scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
|
|
497
|
+
// registered last.
|
|
492
498
|
ctx.reverse = createReverseFunction(
|
|
493
|
-
getGlobalRouteMap(),
|
|
499
|
+
routeMap ?? getGlobalRouteMap(),
|
|
494
500
|
routeName,
|
|
495
501
|
params,
|
|
496
502
|
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
@@ -594,12 +600,9 @@ export function createRequestContext<TEnv>(
|
|
|
594
600
|
version: stateVersion,
|
|
595
601
|
} = options;
|
|
596
602
|
const cookieHeader = request.headers.get("Cookie");
|
|
597
|
-
// One Set-Cookie per request no matter how many invalidateClientCache() calls.
|
|
598
603
|
let rangoStateRotated = false;
|
|
599
604
|
let parsedCookies: Record<string, string> | null = null;
|
|
600
605
|
|
|
601
|
-
// Create stub response for collecting headers/cookies.
|
|
602
|
-
// All cookie/header mutations go here; cookie reads derive from it.
|
|
603
606
|
let stubResponse = initialResponse
|
|
604
607
|
? new Response(null, {
|
|
605
608
|
status: initialResponse.status,
|
|
@@ -608,11 +611,9 @@ export function createRequestContext<TEnv>(
|
|
|
608
611
|
})
|
|
609
612
|
: new Response(null, { status: 200 });
|
|
610
613
|
|
|
611
|
-
// Create handle store and loader memoization for this request
|
|
612
614
|
const handleStore = createHandleStore();
|
|
613
615
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
614
616
|
|
|
615
|
-
// Lazy parse cookies from the original Cookie header
|
|
616
617
|
const getParsedCookies = (): Record<string, string> => {
|
|
617
618
|
if (!parsedCookies) {
|
|
618
619
|
parsedCookies = parseCookiesFromHeader(cookieHeader);
|
|
@@ -620,7 +621,6 @@ export function createRequestContext<TEnv>(
|
|
|
620
621
|
return parsedCookies;
|
|
621
622
|
};
|
|
622
623
|
|
|
623
|
-
// Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
|
|
624
624
|
let responseCookieCache: Map<string, string | null> | null = null;
|
|
625
625
|
const getResponseCookies = (): Map<string, string | null> => {
|
|
626
626
|
if (!responseCookieCache) {
|
|
@@ -632,8 +632,6 @@ export function createRequestContext<TEnv>(
|
|
|
632
632
|
responseCookieCache = null;
|
|
633
633
|
};
|
|
634
634
|
|
|
635
|
-
// Guard: throw if a response-level side effect is called inside a cache() scope.
|
|
636
|
-
// Uses ALS to detect the scope (set during segment resolution).
|
|
637
635
|
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
638
636
|
if (isInsideCacheScope()) {
|
|
639
637
|
throw new Error(
|
|
@@ -644,8 +642,7 @@ export function createRequestContext<TEnv>(
|
|
|
644
642
|
}
|
|
645
643
|
}
|
|
646
644
|
|
|
647
|
-
//
|
|
648
|
-
// The stub IS the source of truth for same-request mutations.
|
|
645
|
+
// Response stub Set-Cookie wins, then original header (source of truth for mutations).
|
|
649
646
|
const effectiveCookie = (name: string): string | undefined => {
|
|
650
647
|
const mutations = getResponseCookies();
|
|
651
648
|
if (mutations.has(name)) {
|
|
@@ -655,14 +652,11 @@ export function createRequestContext<TEnv>(
|
|
|
655
652
|
return getParsedCookies()[name];
|
|
656
653
|
};
|
|
657
654
|
|
|
658
|
-
// Theme helpers (only used when themeConfig is provided)
|
|
659
655
|
const getTheme = (): Theme | undefined => {
|
|
660
656
|
if (!themeConfig) return undefined;
|
|
661
657
|
|
|
662
|
-
// Use overlay-aware read so setTheme() in the same request is reflected
|
|
663
658
|
const stored = effectiveCookie(themeConfig.storageKey);
|
|
664
659
|
if (stored) {
|
|
665
|
-
// Validate stored value
|
|
666
660
|
if (stored === "system" && themeConfig.enableSystem) {
|
|
667
661
|
return "system";
|
|
668
662
|
}
|
|
@@ -676,7 +670,6 @@ export function createRequestContext<TEnv>(
|
|
|
676
670
|
const setTheme = (theme: Theme): void => {
|
|
677
671
|
if (!themeConfig) return;
|
|
678
672
|
|
|
679
|
-
// Validate theme value
|
|
680
673
|
if (theme !== "system" && !themeConfig.themes.includes(theme)) {
|
|
681
674
|
console.warn(
|
|
682
675
|
`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
|
|
@@ -684,7 +677,6 @@ export function createRequestContext<TEnv>(
|
|
|
684
677
|
return;
|
|
685
678
|
}
|
|
686
679
|
|
|
687
|
-
// Write to stub — effectiveCookie() will pick it up on next read
|
|
688
680
|
stubResponse.headers.append(
|
|
689
681
|
"Set-Cookie",
|
|
690
682
|
serializeCookieValue(themeConfig.storageKey, theme, {
|
|
@@ -696,10 +688,8 @@ export function createRequestContext<TEnv>(
|
|
|
696
688
|
invalidateResponseCookieCache();
|
|
697
689
|
};
|
|
698
690
|
|
|
699
|
-
// Strip internal _rsc* params so userland sees a clean URL.
|
|
700
691
|
const cleanUrl = stripInternalParams(url);
|
|
701
692
|
|
|
702
|
-
// Build the context object first (without use), then add use
|
|
703
693
|
const ctx: RequestContext<TEnv> = {
|
|
704
694
|
env,
|
|
705
695
|
request,
|
|
@@ -840,7 +830,6 @@ export function createRequestContext<TEnv>(
|
|
|
840
830
|
});
|
|
841
831
|
},
|
|
842
832
|
|
|
843
|
-
// Placeholder - will be replaced below
|
|
844
833
|
use: null as any,
|
|
845
834
|
|
|
846
835
|
method: request.method,
|
|
@@ -869,7 +858,6 @@ export function createRequestContext<TEnv>(
|
|
|
869
858
|
this._onResponseCallbacks.push(callback);
|
|
870
859
|
},
|
|
871
860
|
|
|
872
|
-
// Theme properties (only set when themeConfig is provided)
|
|
873
861
|
get theme() {
|
|
874
862
|
return themeConfig ? getTheme() : undefined;
|
|
875
863
|
},
|
|
@@ -893,19 +881,17 @@ export function createRequestContext<TEnv>(
|
|
|
893
881
|
_reportedErrors: new WeakSet<object>(),
|
|
894
882
|
_metricsStore: undefined,
|
|
895
883
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
_resolveRenderBarrier: null as any, // set below
|
|
884
|
+
_renderBarrier: null as any,
|
|
885
|
+
_resolveRenderBarrier: null as any,
|
|
899
886
|
_renderBarrierSegmentOrder: undefined,
|
|
900
887
|
|
|
901
888
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
902
889
|
};
|
|
903
890
|
|
|
904
|
-
// Lazy
|
|
905
|
-
// calls rendered(). Requests that don't use rendered() pay zero cost.
|
|
891
|
+
// Lazy allocation: only create Promise when a loader calls rendered().
|
|
906
892
|
let barrierResolved = false;
|
|
907
893
|
let resolveBarrier: (() => void) | undefined;
|
|
908
|
-
ctx._renderBarrier = null as any;
|
|
894
|
+
ctx._renderBarrier = null as any;
|
|
909
895
|
ctx._resolveRenderBarrier = (
|
|
910
896
|
segments: Array<{ type: string; id: string }>,
|
|
911
897
|
) => {
|
|
@@ -916,9 +902,6 @@ export function createRequestContext<TEnv>(
|
|
|
916
902
|
.map((s) => s.id);
|
|
917
903
|
ctx._renderBarrierSegmentOrder = segOrder;
|
|
918
904
|
|
|
919
|
-
// Closing the guard window means no handler can still form a deadlock cycle
|
|
920
|
-
// with a rendered() loader: drop the dependency-tracking state and mark it
|
|
921
|
-
// closed. WHEN this runs is the only streaming/non-streaming difference.
|
|
922
905
|
const closeGuard = () => {
|
|
923
906
|
ctx._renderBarrierWaiters = undefined;
|
|
924
907
|
ctx._handlerLoaderDeps = undefined;
|
|
@@ -926,20 +909,8 @@ export function createRequestContext<TEnv>(
|
|
|
926
909
|
};
|
|
927
910
|
|
|
928
911
|
if (ctx._treeHasStreaming) {
|
|
929
|
-
// Streaming: rendered() keeps waiting on handleStore.settled past this
|
|
930
|
-
// point, and loading() handlers are still in flight. The eager snapshot
|
|
931
|
-
// here would be incomplete, so leave it unset — rendered() builds and
|
|
932
|
-
// caches the complete one after settled. Keep the guard window OPEN so a
|
|
933
|
-
// handler that resumes and awaits a still-waiting rendered() loader is
|
|
934
|
-
// still caught; close it once settled (every tracked handler has finished
|
|
935
|
-
// then, so none can await a loader anymore). settled resolves after
|
|
936
|
-
// rendered() seals; if no loader used rendered(), nothing seals and the
|
|
937
|
-
// (empty) guard state is simply GC'd at request end.
|
|
938
912
|
handleStore.settled.then(closeGuard);
|
|
939
913
|
} else {
|
|
940
|
-
// Non-streaming: all handlers have settled by now. Build and cache the
|
|
941
|
-
// snapshot so loader ctx.use(handle) calls don't rebuild it, and close the
|
|
942
|
-
// guard window immediately.
|
|
943
914
|
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
944
915
|
handleStore,
|
|
945
916
|
segOrder,
|
|
@@ -950,9 +921,6 @@ export function createRequestContext<TEnv>(
|
|
|
950
921
|
};
|
|
951
922
|
Object.defineProperty(ctx, "_renderBarrier", {
|
|
952
923
|
get() {
|
|
953
|
-
// Barrier already resolved (cache/prerender hit) or first lazy access.
|
|
954
|
-
// Either way, replace the getter with a concrete value to avoid
|
|
955
|
-
// repeated Promise.resolve() allocations on subsequent reads.
|
|
956
924
|
const p = barrierResolved
|
|
957
925
|
? Promise.resolve()
|
|
958
926
|
: new Promise<void>((resolve) => {
|
|
@@ -968,24 +936,16 @@ export function createRequestContext<TEnv>(
|
|
|
968
936
|
configurable: true,
|
|
969
937
|
});
|
|
970
938
|
|
|
971
|
-
// Now create use() with access to ctx
|
|
972
939
|
ctx.use = createUseFunction({
|
|
973
940
|
handleStore,
|
|
974
941
|
loaderPromises,
|
|
975
942
|
getContext: () => ctx,
|
|
976
943
|
});
|
|
977
944
|
|
|
978
|
-
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
979
945
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
980
946
|
return ctx;
|
|
981
947
|
}
|
|
982
948
|
|
|
983
|
-
/**
|
|
984
|
-
* Parse Set-Cookie headers from a response into effective cookie state.
|
|
985
|
-
* Returns a map of cookie name -> value (string) or name -> null (deleted).
|
|
986
|
-
* Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
|
|
987
|
-
* Max-Age=0 is treated as a delete.
|
|
988
|
-
*/
|
|
989
949
|
const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
|
|
990
950
|
|
|
991
951
|
function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
@@ -993,7 +953,6 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
993
953
|
const setCookies = response.headers.getSetCookie();
|
|
994
954
|
|
|
995
955
|
for (const header of setCookies) {
|
|
996
|
-
// First segment before ';' is the name=value pair
|
|
997
956
|
const semiIdx = header.indexOf(";");
|
|
998
957
|
const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
|
|
999
958
|
const eqIdx = pair.indexOf("=");
|
|
@@ -1005,11 +964,9 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
1005
964
|
name = decodeURIComponent(pair.substring(0, eqIdx).trim());
|
|
1006
965
|
value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
|
|
1007
966
|
} catch {
|
|
1008
|
-
// Malformed encoding — skip this entry
|
|
1009
967
|
continue;
|
|
1010
968
|
}
|
|
1011
969
|
|
|
1012
|
-
// Max-Age=0 means the cookie is being deleted
|
|
1013
970
|
const isDeleted = MAX_AGE_ZERO_RE.test(header);
|
|
1014
971
|
result.set(name, isDeleted ? null : value);
|
|
1015
972
|
}
|
|
@@ -1017,10 +974,10 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
1017
974
|
return result;
|
|
1018
975
|
}
|
|
1019
976
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
function parseCookiesFromHeader(
|
|
977
|
+
// Exported for unit tests; the canonical cookie parse/serialize lives here
|
|
978
|
+
// (a duplicate copy in middleware-cookies.ts was removed). Not part of the
|
|
979
|
+
// public export surface.
|
|
980
|
+
export function parseCookiesFromHeader(
|
|
1024
981
|
cookieHeader: string | null,
|
|
1025
982
|
): Record<string, string> {
|
|
1026
983
|
if (!cookieHeader) return {};
|
|
@@ -1035,7 +992,7 @@ function parseCookiesFromHeader(
|
|
|
1035
992
|
try {
|
|
1036
993
|
cookies[name] = decodeURIComponent(raw);
|
|
1037
994
|
} catch {
|
|
1038
|
-
// Malformed percent-
|
|
995
|
+
// Malformed percent-encoding: fall back to raw value
|
|
1039
996
|
cookies[name] = raw;
|
|
1040
997
|
}
|
|
1041
998
|
}
|
|
@@ -1044,10 +1001,7 @@ function parseCookiesFromHeader(
|
|
|
1044
1001
|
return cookies;
|
|
1045
1002
|
}
|
|
1046
1003
|
|
|
1047
|
-
|
|
1048
|
-
* Serialize a cookie for Set-Cookie header
|
|
1049
|
-
*/
|
|
1050
|
-
function serializeCookieValue(
|
|
1004
|
+
export function serializeCookieValue(
|
|
1051
1005
|
name: string,
|
|
1052
1006
|
value: string,
|
|
1053
1007
|
options: CookieOptions = {},
|
|
@@ -1074,20 +1028,12 @@ export interface CreateUseFunctionOptions<TEnv> {
|
|
|
1074
1028
|
getContext: () => RequestContext<TEnv>;
|
|
1075
1029
|
}
|
|
1076
1030
|
|
|
1077
|
-
/**
|
|
1078
|
-
* Create the use() function for loader and handle composition.
|
|
1079
|
-
*
|
|
1080
|
-
* This is the unified implementation used by both RequestContext and HandlerContext.
|
|
1081
|
-
* - For loaders: executes and memoizes loader functions
|
|
1082
|
-
* - For handles: returns a push function to add handle data
|
|
1083
|
-
*/
|
|
1084
1031
|
export function createUseFunction<TEnv>(
|
|
1085
1032
|
options: CreateUseFunctionOptions<TEnv>,
|
|
1086
1033
|
): RequestContext["use"] {
|
|
1087
1034
|
const { handleStore, loaderPromises, getContext } = options;
|
|
1088
1035
|
|
|
1089
1036
|
return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
1090
|
-
// Handle case: return a push function
|
|
1091
1037
|
if (isHandle(item)) {
|
|
1092
1038
|
const handle = item;
|
|
1093
1039
|
const ctx = getContext();
|
|
@@ -1100,30 +1046,24 @@ export function createUseFunction<TEnv>(
|
|
|
1100
1046
|
);
|
|
1101
1047
|
}
|
|
1102
1048
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
// Push directly - promises will be serialized by RSC and streamed
|
|
1114
|
-
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
1115
|
-
};
|
|
1049
|
+
return withDefer(
|
|
1050
|
+
(dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
|
|
1051
|
+
const valueOrPromise =
|
|
1052
|
+
typeof dataOrFn === "function"
|
|
1053
|
+
? (dataOrFn as () => Promise<unknown>)()
|
|
1054
|
+
: dataOrFn;
|
|
1055
|
+
|
|
1056
|
+
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
1116
1059
|
}
|
|
1117
1060
|
|
|
1118
|
-
// Loader case
|
|
1119
1061
|
const loader = item as LoaderDefinition<any, any>;
|
|
1120
1062
|
|
|
1121
|
-
// Return cached promise if already started
|
|
1122
1063
|
if (loaderPromises.has(loader.$$id)) {
|
|
1123
1064
|
return loaderPromises.get(loader.$$id);
|
|
1124
1065
|
}
|
|
1125
1066
|
|
|
1126
|
-
// Get loader function - either from loader object or fetchable registry
|
|
1127
1067
|
let loaderFn = loader.fn;
|
|
1128
1068
|
if (!loaderFn) {
|
|
1129
1069
|
const fetchable = getFetchableLoader(loader.$$id);
|
|
@@ -1140,7 +1080,6 @@ export function createUseFunction<TEnv>(
|
|
|
1140
1080
|
|
|
1141
1081
|
const ctx = getContext();
|
|
1142
1082
|
|
|
1143
|
-
// Create loader context with recursive use() support
|
|
1144
1083
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
1145
1084
|
params: ctx.params,
|
|
1146
1085
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
@@ -1157,7 +1096,6 @@ export function createUseFunction<TEnv>(
|
|
|
1157
1096
|
use: (<TDep, TDepParams = any>(
|
|
1158
1097
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
1159
1098
|
): Promise<TDep> => {
|
|
1160
|
-
// Recursive call - will start dep loader if not already started
|
|
1161
1099
|
return ctx.use(dep);
|
|
1162
1100
|
}) as LoaderContext["use"],
|
|
1163
1101
|
method: "GET",
|
|
@@ -1181,7 +1119,6 @@ export function createUseFunction<TEnv>(
|
|
|
1181
1119
|
doneLoader();
|
|
1182
1120
|
});
|
|
1183
1121
|
|
|
1184
|
-
// Memoize for subsequent calls
|
|
1185
1122
|
loaderPromises.set(loader.$$id, promise);
|
|
1186
1123
|
|
|
1187
1124
|
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
|
@@ -37,8 +37,6 @@ import type { UseItems, HandlerUseItem } from "./route-types.js";
|
|
|
37
37
|
import { isCachedFunction } from "./cache/taint.js";
|
|
38
38
|
import { isUnderTestRunner } from "./runtime-env.js";
|
|
39
39
|
|
|
40
|
-
// -- Types ------------------------------------------------------------------
|
|
41
|
-
|
|
42
40
|
export interface StaticHandlerOptions {
|
|
43
41
|
/**
|
|
44
42
|
* Keep handler in server bundle for live fallback (default: false).
|
|
@@ -62,11 +60,8 @@ export interface StaticHandlerDefinition<
|
|
|
62
60
|
use?: () => UseItems<HandlerUseItem>;
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
// Process-stable fallback id counter (mirrors createHandle / createLoader /
|
|
68
|
-
// Prerender). Only assigned in a bare unit test where the Vite plugin did not
|
|
69
|
-
// inject an id; never fires in a real build (the plugin always injects).
|
|
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.
|
|
70
65
|
let runtimeStaticIdCounter = 0;
|
|
71
66
|
|
|
72
67
|
export function Static<TParams extends Record<string, any> = {}>(
|
|
@@ -75,8 +70,6 @@ export function Static<TParams extends Record<string, any> = {}>(
|
|
|
75
70
|
__injectedId?: string,
|
|
76
71
|
): StaticHandlerDefinition<TParams>;
|
|
77
72
|
|
|
78
|
-
// -- Implementation ---------------------------------------------------------
|
|
79
|
-
|
|
80
73
|
export function Static<TParams extends Record<string, any>>(
|
|
81
74
|
handler: Function,
|
|
82
75
|
optionsOrId?: StaticHandlerOptions | string,
|
|
@@ -100,25 +93,12 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
100
93
|
id = maybeId ?? "";
|
|
101
94
|
}
|
|
102
95
|
|
|
103
|
-
// Throw unless under a test runner. The plugin always injects $$id for a
|
|
104
|
-
// supported `export const` Static on every build, so a missing id means either
|
|
105
|
-
// no plugin (a bare test — fall back below) or an UNSUPPORTED shape the plugin
|
|
106
|
-
// silently skipped (dev OR a real build — fail loud; a synthetic id would
|
|
107
|
-
// degrade to a silent static/prerender miss). The message is already small (no
|
|
108
|
-
// stack-parsing diagnostic), so it ships as-is. isUnderTestRunner() is
|
|
109
|
-
// runtime-safe — never a bare `process.env` access.
|
|
110
96
|
if (!id && !isUnderTestRunner()) {
|
|
111
97
|
throw new Error(
|
|
112
98
|
"[rango] Static: missing $$id. Use `export const X = Static(...)` and " +
|
|
113
99
|
"ensure the exposeInternalIds Vite plugin is configured.",
|
|
114
100
|
);
|
|
115
101
|
}
|
|
116
|
-
// Under vitest with no plugin id: assign a process-stable runtime id so a
|
|
117
|
-
// whole-app router with Static() routes constructs in a bare test. Never
|
|
118
|
-
// reached in a real build (the throw above fires there); staticHandlerId is
|
|
119
|
-
// read only during RSC serving (never in dispatch / assertGeneratedRoutesMatch),
|
|
120
|
-
// and the build static manifest keys on the plugin id. Mirrors createHandle /
|
|
121
|
-
// createLoader / Prerender.
|
|
122
102
|
if (!id) {
|
|
123
103
|
id = `__rango_runtime_static_${runtimeStaticIdCounter++}`;
|
|
124
104
|
}
|
|
@@ -131,11 +111,6 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
131
111
|
};
|
|
132
112
|
}
|
|
133
113
|
|
|
134
|
-
// -- Type guard -------------------------------------------------------------
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Type guard to check if a value is a StaticHandlerDefinition.
|
|
138
|
-
*/
|
|
139
114
|
export function isStaticHandler(
|
|
140
115
|
value: unknown,
|
|
141
116
|
): value is StaticHandlerDefinition {
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
* 2. Telemetry path — `createCacheSink` returns a `{ sink, events }` pair the
|
|
13
13
|
* consumer wires via `createRouter({ telemetry: sink })`. This has ZERO
|
|
14
14
|
* production surface: no header, just structured `cache.decision` events
|
|
15
|
-
* (which carry the same coarse `segments` cache signal).
|
|
15
|
+
* (which carry the same coarse `segments` cache signal). Assert with
|
|
16
|
+
* `assertCacheDecision(events, routeKey, expected)` (the one-call counterpart
|
|
17
|
+
* of `assertCacheStatus`) or filter raw via `filterCacheDecisions`.
|
|
18
|
+
*
|
|
19
|
+
* Both paths report the SAME coarse route-level signal — pick by TRANSPORT, not
|
|
20
|
+
* by meaning: the header is the only signal a black-box Playwright `Response`
|
|
21
|
+
* carries (needs the debug gate ON); the sink is the only zero-production-surface
|
|
22
|
+
* option and the only one exposing per-segment `shouldRevalidate`.
|
|
16
23
|
*
|
|
17
24
|
* v1 cache status is COARSE (route-level): the router reports a single entry
|
|
18
25
|
* keyed by the route key (the route NAME), not per individual segment.
|
|
@@ -37,18 +44,6 @@ export type ExpectedCacheStatus = CacheSegmentStatus;
|
|
|
37
44
|
/** A target carrying response headers (a Response or a `{ headers }` object). */
|
|
38
45
|
export type CacheStatusTarget = Response | { headers: Headers };
|
|
39
46
|
|
|
40
|
-
/**
|
|
41
|
-
* Parse an `X-Rango-Cache` header value into a `{ routeKey: status }` map.
|
|
42
|
-
*
|
|
43
|
-
* Header format: `<routeKey>=<status>, <routeKey2>=<status2>`. The key is the
|
|
44
|
-
* route NAME (ctx.routeKey, e.g. `product.detail`), NOT the URL pattern —
|
|
45
|
-
* see assertCacheStatus. Whitespace around entries and the `=` is tolerated.
|
|
46
|
-
* Entries without a status are ignored.
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* parseCacheHeader("product.detail=hit, shop.layout=stale")
|
|
50
|
-
* // => { "product.detail": "hit", "shop.layout": "stale" }
|
|
51
|
-
*/
|
|
52
47
|
export function parseCacheHeader(
|
|
53
48
|
headerValue: string | null | undefined,
|
|
54
49
|
): Record<string, string> {
|
|
@@ -71,25 +66,6 @@ function getHeaders(target: CacheStatusTarget): Headers {
|
|
|
71
66
|
return target.headers;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
/**
|
|
75
|
-
* Assert that the `X-Rango-Cache` header reports `expected` status for the
|
|
76
|
-
* given route. Throws a descriptive error when the header is missing (gate
|
|
77
|
-
* off), the route is absent, or the status differs.
|
|
78
|
-
*
|
|
79
|
-
* `routeKey` is the route NAME (e.g. `product.detail`), the same id the header
|
|
80
|
-
* carries — NOT the URL pattern (`/products/:id`). The signal is built from
|
|
81
|
-
* ctx.routeKey (telemetry.ts), so a pattern-shaped key never matches.
|
|
82
|
-
*
|
|
83
|
-
* The header is produced by the RSC render pipeline, so get the Response from
|
|
84
|
-
* the router's real fetch path (`router.fetch(...)`), with the debug cache
|
|
85
|
-
* signal gate enabled (`debugCacheSignal: true` or `RANGO_TEST_SIGNALS=1`).
|
|
86
|
-
* NOTE: `dispatch()` is the non-RSC primitive and never emits this header.
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* // debugCacheSignal must be enabled on the router under test.
|
|
90
|
-
* const res = await router.fetch(new Request("https://app/products/42"));
|
|
91
|
-
* assertCacheStatus(res, "product.detail", "hit");
|
|
92
|
-
*/
|
|
93
69
|
export function assertCacheStatus(
|
|
94
70
|
target: CacheStatusTarget,
|
|
95
71
|
segment: string,
|
|
@@ -131,19 +107,6 @@ export interface CacheSink {
|
|
|
131
107
|
events: TelemetryEvent[];
|
|
132
108
|
}
|
|
133
109
|
|
|
134
|
-
/**
|
|
135
|
-
* Create a capturing telemetry sink for asserting on `cache.decision` events.
|
|
136
|
-
*
|
|
137
|
-
* This is the ZERO-production-surface path: no response header is emitted, the
|
|
138
|
-
* consumer just inspects the captured events.
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* const { sink, events } = createCacheSink();
|
|
142
|
-
* const router = createRouter({ telemetry: sink, ... });
|
|
143
|
-
* // ...send a request through the router's RSC fetch path...
|
|
144
|
-
* const decisions = filterCacheDecisions(events);
|
|
145
|
-
* expect(decisions[0].segments?.[0].cacheStatus).toBe("hit");
|
|
146
|
-
*/
|
|
147
110
|
export function createCacheSink(): CacheSink {
|
|
148
111
|
const events: TelemetryEvent[] = [];
|
|
149
112
|
const sink: TelemetrySink = {
|
|
@@ -154,9 +117,6 @@ export function createCacheSink(): CacheSink {
|
|
|
154
117
|
return { sink, events };
|
|
155
118
|
}
|
|
156
119
|
|
|
157
|
-
/**
|
|
158
|
-
* Filter captured telemetry events down to `cache.decision` events.
|
|
159
|
-
*/
|
|
160
120
|
export function filterCacheDecisions(
|
|
161
121
|
events: readonly TelemetryEvent[],
|
|
162
122
|
): CacheDecisionEvent[] {
|
|
@@ -164,3 +124,39 @@ export function filterCacheDecisions(
|
|
|
164
124
|
(e): e is CacheDecisionEvent => e.type === "cache.decision",
|
|
165
125
|
);
|
|
166
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Telemetry-path counterpart of {@link assertCacheStatus}: assert a captured
|
|
130
|
+
* `cache.decision` event reported `expected` for the segment keyed by `routeKey`
|
|
131
|
+
* (the route NAME, the same coarse key the header path uses). Throws an
|
|
132
|
+
* actionable error when no matching segment was captured, or on a mismatch.
|
|
133
|
+
*
|
|
134
|
+
* Pairs with {@link createCacheSink}: wire `createRouter({ telemetry: sink })`,
|
|
135
|
+
* drive an RSC request, then assert against the recorded `events`. This is the
|
|
136
|
+
* zero-production-surface path (no header to enable). NOTE: `events` accumulates
|
|
137
|
+
* across requests, so the FIRST matching segment wins — slice or recreate the
|
|
138
|
+
* sink between requests for the same `routeKey`.
|
|
139
|
+
*/
|
|
140
|
+
export function assertCacheDecision(
|
|
141
|
+
events: readonly TelemetryEvent[],
|
|
142
|
+
routeKey: string,
|
|
143
|
+
expected: ExpectedCacheStatus,
|
|
144
|
+
): void {
|
|
145
|
+
const segments = filterCacheDecisions(events).flatMap(
|
|
146
|
+
(d) => d.segments ?? [],
|
|
147
|
+
);
|
|
148
|
+
const seg = segments.find((s) => s.id === routeKey);
|
|
149
|
+
if (seg === undefined) {
|
|
150
|
+
const known = segments.map((s) => s.id);
|
|
151
|
+
throw new Error(
|
|
152
|
+
`assertCacheDecision: no cache.decision segment for routeKey "${routeKey}". ` +
|
|
153
|
+
`Seen: ${known.length > 0 ? known.join(", ") : "(none)"}. Wire ` +
|
|
154
|
+
`createRouter({ telemetry: createCacheSink().sink }) and drive an RSC request.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (seg.cacheStatus !== expected) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`assertCacheDecision: routeKey "${routeKey}" expected "${expected}" but got "${seg.cacheStatus}".`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -17,27 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
import { getCollectFn, type Handle } from "../handle.js";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Run a handle's collect function on per-segment pushed values.
|
|
22
|
-
*
|
|
23
|
-
* @param handle - The handle whose collect to run.
|
|
24
|
-
* @param segments - Per-segment pushed values: each entry is the array of values
|
|
25
|
-
* one route segment pushed for this handle, in parent -> child order. Empty
|
|
26
|
-
* per-segment arrays are dropped before the collect runs, matching production
|
|
27
|
-
* collectHandleData (a segment that pushed nothing is not passed through).
|
|
28
|
-
* @returns The accumulated value the handle's collect produces.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```ts
|
|
32
|
-
* // Default flatten
|
|
33
|
-
* collectHandle(Breadcrumbs, [[{ label: "Home", href: "/" }], [{ label: "P", href: "/p" }]]);
|
|
34
|
-
* // -> [{ label: "Home", href: "/" }, { label: "P", href: "/p" }]
|
|
35
|
-
*
|
|
36
|
-
* // Custom "last wins"
|
|
37
|
-
* const PageTitle = createHandle<string, string>((s) => s.flat().at(-1) ?? "");
|
|
38
|
-
* collectHandle(PageTitle, [["Home"], ["Product"]]); // -> "Product"
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
20
|
export function collectHandle<TData, TAccumulated>(
|
|
42
21
|
handle: Handle<TData, TAccumulated>,
|
|
43
22
|
segments: ReadonlyArray<ReadonlyArray<TData>>,
|
|
@@ -55,9 +34,7 @@ export function collectHandle<TData, TAccumulated>(
|
|
|
55
34
|
return segments.flat() as unknown as TAccumulated;
|
|
56
35
|
}
|
|
57
36
|
|
|
58
|
-
//
|
|
59
|
-
// nothing (empty arrays) are dropped before the collect runs, so a collect
|
|
60
|
-
// that inspects segment count or indices sees the same input as at runtime.
|
|
37
|
+
// Drop empty arrays matching production behavior (segment count/indices).
|
|
61
38
|
const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
|
|
62
39
|
return collectFn(nonEmpty);
|
|
63
40
|
}
|