@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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.
Files changed (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Shared Helpers for Segment Resolution
3
+ *
4
+ * Common utilities used by both fresh and revalidation resolution paths:
5
+ * - Handler result processing (Response vs ReactNode)
6
+ * - Static handler interception (build-time pre-rendered components)
7
+ * - Layout handler resolution with static fallback
8
+ * - Error boundary segment creation
9
+ */
10
+
11
+ import type { ReactNode } from "react";
12
+ import { DataNotFoundError } from "../../errors";
13
+ import {
14
+ createErrorInfo,
15
+ createErrorSegment,
16
+ createNotFoundInfo,
17
+ createNotFoundSegment,
18
+ } from "../error-handling.js";
19
+ import { getRequestContext } from "../../server/request-context.js";
20
+ import { DefaultErrorFallback } from "../../default-error-boundary.js";
21
+ import type { EntryData } from "../../server/context";
22
+ import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
23
+ import type { SegmentResolutionDeps } from "../types.js";
24
+ import { debugLog } from "../logging.js";
25
+ import { tryStaticLookup } from "./static-store.js";
26
+ import type { TelemetrySink } from "../telemetry.js";
27
+ import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Handler result processing
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Handle Response returns from handlers.
35
+ * When a handler returns a Response (e.g., redirect), throw it to trigger
36
+ * the short-circuit mechanism. Otherwise return the ReactNode.
37
+ */
38
+ export function handleHandlerResult(
39
+ result: ReactNode | Response | Promise<ReactNode> | Promise<Response>,
40
+ ): ReactNode {
41
+ if (result instanceof Response) {
42
+ throw result;
43
+ }
44
+ if (result instanceof Promise) {
45
+ return result.then((resolved) => {
46
+ if (resolved instanceof Response) {
47
+ throw resolved;
48
+ }
49
+ return resolved;
50
+ }) as ReactNode;
51
+ }
52
+ return result;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Static handler interception
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Try to resolve a component from the build-time static store.
61
+ * Returns undefined synchronously when the entry is not a static prerender,
62
+ * avoiding unnecessary promise wrapping on the hot path.
63
+ */
64
+ export function tryStaticHandler(
65
+ entry: EntryData,
66
+ segmentId: string,
67
+ ): Promise<ReactNode | undefined> | undefined {
68
+ const entryAny = entry as any;
69
+ if (entryAny.isStaticPrerender && entryAny.staticHandlerId) {
70
+ return tryStaticLookup(entryAny.staticHandlerId, segmentId);
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ /**
76
+ * Try to resolve a parallel slot component from the build-time static store.
77
+ * Returns undefined synchronously when no static handler ID exists for the slot.
78
+ */
79
+ export function tryStaticSlot(
80
+ parallelEntry: EntryData,
81
+ slot: string,
82
+ segmentId: string,
83
+ ): Promise<ReactNode | undefined> | undefined {
84
+ const slotStaticId = (parallelEntry as any).staticHandlerIds?.[slot];
85
+ if (slotStaticId) {
86
+ return tryStaticLookup(slotStaticId, segmentId);
87
+ }
88
+ return undefined;
89
+ }
90
+
91
+ /**
92
+ * Resolve a layout or cache entry's handler component.
93
+ * Checks build-time static store first, then invokes the handler.
94
+ */
95
+ export async function resolveLayoutComponent<TEnv>(
96
+ entry: EntryData,
97
+ context: HandlerContext<any, TEnv>,
98
+ ): Promise<ReactNode> {
99
+ const component = await tryStaticHandler(entry, entry.shortCode);
100
+ if (component !== undefined) return component;
101
+ return typeof entry.handler === "function"
102
+ ? handleHandlerResult(await entry.handler(context))
103
+ : (entry.handler as ReactNode);
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Error boundary segment creation
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * Context for error reporting in segment resolution.
112
+ * When provided, callOnError is invoked with this context.
113
+ */
114
+ export interface ErrorReportContext {
115
+ request?: Request;
116
+ url?: URL;
117
+ routeKey?: string;
118
+ env?: any;
119
+ isPartial?: boolean;
120
+ requestStartTime?: number;
121
+ telemetry?: TelemetrySink;
122
+ }
123
+
124
+ /**
125
+ * Handle a caught error during segment resolution by creating an
126
+ * error or not-found segment with the nearest boundary.
127
+ *
128
+ * Called by resolveWithErrorBoundary to produce error/notFound segments.
129
+ */
130
+ export function catchSegmentError<TEnv>(
131
+ error: unknown,
132
+ entry: EntryData,
133
+ params: Record<string, string>,
134
+ deps: SegmentResolutionDeps<TEnv>,
135
+ report?: ErrorReportContext,
136
+ pathname?: string,
137
+ ): ResolvedSegment {
138
+ const reportError = (
139
+ handledByBoundary: boolean,
140
+ metadata?: Record<string, unknown>,
141
+ ) => {
142
+ if (!report) return;
143
+ deps.callOnError(error, "handler", {
144
+ request: report.request as Request,
145
+ url: report.url,
146
+ routeKey: report.routeKey,
147
+ params,
148
+ segmentId: entry.shortCode,
149
+ segmentType: entry.type as any,
150
+ env: report.env,
151
+ isPartial: report.isPartial,
152
+ handledByBoundary,
153
+ metadata,
154
+ requestStartTime: report.requestStartTime,
155
+ });
156
+ if (report.telemetry) {
157
+ const errorObj =
158
+ error instanceof Error ? error : new Error(String(error));
159
+ safeEmit(resolveSink(report.telemetry), {
160
+ type: "handler.error",
161
+ timestamp: performance.now(),
162
+ requestId: report.request ? getRequestId(report.request) : undefined,
163
+ segmentId: entry.shortCode,
164
+ segmentType: entry.type,
165
+ error: errorObj,
166
+ handledByBoundary,
167
+ pathname,
168
+ routeKey: report.routeKey,
169
+ params,
170
+ });
171
+ }
172
+ };
173
+
174
+ const setResponseStatus = (status: number) => {
175
+ const reqCtx = getRequestContext();
176
+ if (reqCtx) {
177
+ reqCtx.setStatus(status);
178
+ }
179
+ };
180
+
181
+ if (error instanceof DataNotFoundError) {
182
+ const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+
184
+ if (notFoundFallback) {
185
+ const notFoundInfo = createNotFoundInfo(
186
+ error,
187
+ entry.shortCode,
188
+ entry.type,
189
+ pathname,
190
+ );
191
+
192
+ reportError(true, {
193
+ notFound: true,
194
+ message: notFoundInfo.message,
195
+ });
196
+
197
+ debugLog("segment", "notFound boundary handled error", {
198
+ segmentId: entry.shortCode,
199
+ message: notFoundInfo.message,
200
+ });
201
+
202
+ setResponseStatus(404);
203
+
204
+ return createNotFoundSegment(
205
+ notFoundInfo,
206
+ notFoundFallback,
207
+ entry,
208
+ params,
209
+ );
210
+ }
211
+ }
212
+
213
+ const fallback = deps.findNearestErrorBoundary(entry);
214
+ const segmentType: ErrorInfo["segmentType"] = entry.type;
215
+ const errorInfo = createErrorInfo(error, entry.shortCode, segmentType);
216
+ const effectiveFallback = fallback ?? DefaultErrorFallback;
217
+
218
+ reportError(!!effectiveFallback);
219
+
220
+ debugLog("segment", "error boundary handled error", {
221
+ segmentId: entry.shortCode,
222
+ boundary: fallback ? "custom" : "default",
223
+ message: errorInfo.message,
224
+ });
225
+
226
+ setResponseStatus(500);
227
+
228
+ return createErrorSegment(errorInfo, effectiveFallback, entry, params);
229
+ }
230
+
231
+ /**
232
+ * Generic error boundary wrapper for segment resolution.
233
+ * Catches non-Response errors and produces an error/notFound segment
234
+ * via catchSegmentError. Response throws (e.g. redirects) are re-thrown.
235
+ *
236
+ * The caller provides a `wrapError` callback to shape the error segment
237
+ * into the expected return type (e.g. ResolvedSegment[] for the fresh
238
+ * path, or SegmentRevalidationResult for the revalidation path).
239
+ */
240
+ export async function resolveWithErrorBoundary<TEnv, TResult>(
241
+ entry: EntryData,
242
+ params: Record<string, string>,
243
+ resolveFn: () => Promise<TResult>,
244
+ wrapError: (segment: ResolvedSegment) => TResult,
245
+ deps: SegmentResolutionDeps<TEnv>,
246
+ report?: ErrorReportContext,
247
+ pathname?: string,
248
+ ): Promise<TResult> {
249
+ try {
250
+ return await resolveFn();
251
+ } catch (error) {
252
+ if (error instanceof Response) throw error;
253
+ const segment = catchSegmentError(
254
+ error,
255
+ entry,
256
+ params,
257
+ deps,
258
+ report,
259
+ pathname,
260
+ );
261
+ return wrapError(segment);
262
+ }
263
+ }
@@ -5,7 +5,13 @@
5
5
  * this module wraps the loader execution with cache lookup/store using the
6
6
  * getItem()/setItem() methods on SegmentCacheStore.
7
7
  *
8
- * Cache key format: loader:{loaderId}:{pathname}:{sortedParams}
8
+ * Cache key resolution (3-tier, matching CacheScope.resolveKey):
9
+ * 1. options.key(requestCtx) — full override
10
+ * 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
11
+ * 3. loader:{loaderId}:{pathname}:{sortedParams} — default
12
+ *
13
+ * Values are serialized via RSC Flight (serializeResult/deserializeResult),
14
+ * supporting ReactNode, Promises, null, and all RSC-serializable types.
9
15
  *
10
16
  * On hit: returns cached data directly, skips loader execution.
11
17
  * On stale hit (SWR): returns stale data, schedules background revalidation.
@@ -14,11 +20,32 @@
14
20
 
15
21
  import type { LoaderEntry } from "../../server/context.js";
16
22
  import type { HandlerContext } from "../../types.js";
17
- import type { SegmentCacheStore } from "../../cache/types.js";
18
23
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
19
24
  import { getRequestContext } from "../../server/request-context.js";
20
-
21
- const DEFAULT_TTL_SECONDS = 60;
25
+ import { sortedRouteParams } from "../../cache/cache-key-utils.js";
26
+ import {
27
+ resolveTtl,
28
+ resolveSwrWindow,
29
+ resolveCacheKey,
30
+ resolveCacheStore,
31
+ DEFAULT_ROUTE_TTL,
32
+ } from "../../cache/cache-policy.js";
33
+ import { readThroughItem } from "../../cache/read-through-swr.js";
34
+ // Lazy-loaded to avoid pulling @vitejs/plugin-rsc/rsc into modules that
35
+ // import segment-resolution but never use loader caching.
36
+ let _serializeResult: typeof import("../../cache/segment-codec.js").serializeResult;
37
+ let _deserializeResult: typeof import("../../cache/segment-codec.js").deserializeResult;
38
+ async function getCodec() {
39
+ if (!_serializeResult) {
40
+ const mod = await import("../../cache/segment-codec.js");
41
+ _serializeResult = mod.serializeResult;
42
+ _deserializeResult = mod.deserializeResult;
43
+ }
44
+ return {
45
+ serializeResult: _serializeResult,
46
+ deserializeResult: _deserializeResult,
47
+ };
48
+ }
22
49
 
23
50
  function debugLoaderCacheLog(message: string): void {
24
51
  if (INTERNAL_RANGO_DEBUG) {
@@ -26,55 +53,65 @@ function debugLoaderCacheLog(message: string): void {
26
53
  }
27
54
  }
28
55
 
29
- function getLoaderCacheKey(
56
+ function getDefaultLoaderCacheKey(
30
57
  loaderId: string,
31
58
  pathname: string,
32
59
  params: Record<string, string>,
33
60
  ): string {
34
- const paramStr = Object.entries(params)
35
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
36
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
37
- .join("&");
38
-
61
+ const paramStr = sortedRouteParams(params);
39
62
  const base = paramStr ? `${pathname}:${paramStr}` : pathname;
40
63
  return `loader:${loaderId}:${base}`;
41
64
  }
42
65
 
43
- function getLoaderStore(loaderEntry: LoaderEntry): SegmentCacheStore | null {
44
- const cacheConfig = loaderEntry.cache;
45
- if (!cacheConfig || cacheConfig.options === false) return null;
46
- const options = cacheConfig.options;
47
-
48
- // Explicit store from cache() options
49
- if (options.store) return options.store;
50
-
51
- // App-level store from request context
52
- return getRequestContext()?._cacheStore ?? null;
66
+ /**
67
+ * Resolve cache key using the shared 3-tier priority.
68
+ */
69
+ async function resolveLoaderKey(
70
+ loaderEntry: LoaderEntry,
71
+ store: import("../../cache/types.js").SegmentCacheStore,
72
+ loaderId: string,
73
+ pathname: string,
74
+ params: Record<string, string>,
75
+ ): Promise<string> {
76
+ const options = loaderEntry.cache!.options;
77
+ const defaultKey = getDefaultLoaderCacheKey(loaderId, pathname, params);
78
+ if (options === false) return defaultKey;
79
+ return resolveCacheKey(options.key, store, defaultKey, "LoaderCache");
53
80
  }
54
81
 
55
- function getLoaderTtl(
56
- loaderEntry: LoaderEntry,
57
- store: SegmentCacheStore,
58
- ): number {
59
- const cacheConfig = loaderEntry.cache;
60
- if (!cacheConfig || cacheConfig.options === false) return DEFAULT_TTL_SECONDS;
61
- const options = cacheConfig.options;
82
+ /**
83
+ * Resolve tags from cache options (static array or function).
84
+ * Fails open: a thrown tag callback falls back to no tags rather than
85
+ * aborting the request. Tags are additive metadata (not identity), so
86
+ * a missing tag does not cause cache collisions.
87
+ */
88
+ function resolveTags(loaderEntry: LoaderEntry): string[] | undefined {
89
+ const options = loaderEntry.cache?.options;
90
+ if (!options || !options.tags) return undefined;
62
91
 
63
- if (options.ttl !== undefined) return options.ttl;
64
- if (store.defaults?.ttl !== undefined) return store.defaults.ttl;
65
- return DEFAULT_TTL_SECONDS;
92
+ if (typeof options.tags === "function") {
93
+ const requestCtx = getRequestContext();
94
+ if (!requestCtx) return undefined;
95
+ try {
96
+ return options.tags(requestCtx);
97
+ } catch (error) {
98
+ console.error(
99
+ `[LoaderCache] Tags function failed, caching without tags:`,
100
+ error,
101
+ );
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ return options.tags;
66
107
  }
67
108
 
68
- function getLoaderSwr(
109
+ function getLoaderStore(
69
110
  loaderEntry: LoaderEntry,
70
- store: SegmentCacheStore,
71
- ): number | undefined {
111
+ ): import("../../cache/types.js").SegmentCacheStore | null {
72
112
  const cacheConfig = loaderEntry.cache;
73
- if (!cacheConfig || cacheConfig.options === false) return undefined;
74
- const options = cacheConfig.options;
75
-
76
- if (options.swr !== undefined) return options.swr;
77
- return store.defaults?.swr;
113
+ if (!cacheConfig || cacheConfig.options === false) return null;
114
+ return resolveCacheStore(cacheConfig.options.store);
78
115
  }
79
116
 
80
117
  /**
@@ -110,72 +147,39 @@ export function resolveLoaderData<TEnv>(
110
147
  }
111
148
 
112
149
  const loaderId = loaderEntry.loader.$$id;
113
- const key = getLoaderCacheKey(loaderId, pathname, ctx.params);
114
- const ttl = getLoaderTtl(loaderEntry, store);
115
- const swr = getLoaderSwr(loaderEntry, store);
150
+ const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
+ const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
+ const swr = swrWindow || undefined;
153
+ const tags = resolveTags(loaderEntry);
116
154
 
117
155
  // Wrap ctx.use() so cache HIT primes the handler's memoization map.
118
156
  // ctx.use() closes over the match context's loaderPromises (not request context's).
119
157
  // By intercepting ctx.use(), we inject cached data into the correct map.
120
158
  const originalUse = ctx.use;
121
159
  const dataPromise = (async () => {
122
- // Cache lookup
123
- try {
124
- const cached = await store.getItem!(key);
125
-
126
- if (cached) {
127
- const data = JSON.parse(cached.value);
128
-
129
- if (!cached.shouldRevalidate) {
130
- debugLoaderCacheLog(`[LoaderCache] HIT: ${key}`);
131
- return data;
132
- }
133
-
134
- // Stale hit — return stale data, revalidate in background
135
- debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`);
136
- const requestCtx = getRequestContext();
137
- const revalidate = async () => {
138
- try {
139
- const fresh = await originalUse(loaderEntry.loader);
140
- const serialized = JSON.stringify(fresh);
141
- await store.setItem!(key, serialized, { ttl, swr });
142
- } catch {
143
- // Background revalidation failed silently
144
- }
145
- };
146
- if (requestCtx?.waitUntil) {
147
- requestCtx.waitUntil(revalidate);
148
- } else {
149
- revalidate();
150
- }
151
- return data;
152
- }
153
- } catch {
154
- // Cache lookup failed, fall through to fresh execution
155
- }
156
-
157
- // Cache miss — execute loader via ctx.use() (which memoizes it)
158
- debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`);
159
- const data = await originalUse(loaderEntry.loader);
160
-
161
- // Non-blocking cache write
162
- const requestCtx = getRequestContext();
163
- const cacheWrite = async () => {
164
- try {
165
- const serialized = JSON.stringify(data);
166
- await store.setItem!(key, serialized, { ttl, swr });
167
- debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`);
168
- } catch {
169
- // Cache write failed silently
170
- }
171
- };
172
- if (requestCtx?.waitUntil) {
173
- requestCtx.waitUntil(cacheWrite);
174
- } else {
175
- await cacheWrite();
176
- }
177
-
178
- return data;
160
+ const codec = await getCodec();
161
+ const key = await resolveLoaderKey(
162
+ loaderEntry,
163
+ store,
164
+ loaderId,
165
+ pathname,
166
+ ctx.params,
167
+ );
168
+
169
+ return readThroughItem({
170
+ getItem: (k) => store.getItem!(k),
171
+ setItem: (k, v, o) => store.setItem!(k, v, o),
172
+ key,
173
+ execute: () => originalUse(loaderEntry.loader),
174
+ serialize: (d) => codec.serializeResult(d),
175
+ deserialize: (v) => codec.deserializeResult(v),
176
+ storeOptions: { ttl, swr, tags },
177
+ onHit: () => debugLoaderCacheLog(`[LoaderCache] HIT: ${key}`),
178
+ onStale: () => debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`),
179
+ onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
180
+ onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
181
+ host: getRequestContext(),
182
+ });
179
183
  })();
180
184
 
181
185
  // Temporarily replace ctx.use() so the handler's call returns cached data.