@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80

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 (312) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Cookie Store — Next.js-style cookie facade backed by the response-derived model.
3
+ *
4
+ * `cookies()` returns a CookieStore scoped to the current request.
5
+ * Reads merge the original Cookie header with Set-Cookie mutations
6
+ * already queued on the response stub (last-write-wins).
7
+ * Writes append Set-Cookie to the response stub.
8
+ */
9
+
10
+ import type { CookieOptions } from "../router/middleware-types.js";
11
+ import { getRequestContext } from "./request-context.js";
12
+ import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
13
+
14
+ /**
15
+ * A single cookie entry returned by get() and getAll().
16
+ */
17
+ export interface Cookie {
18
+ name: string;
19
+ value: string;
20
+ }
21
+
22
+ /**
23
+ * Request-scoped cookie store.
24
+ *
25
+ * Reads see the effective merged view (original request + same-request mutations).
26
+ * Writes append Set-Cookie headers to the shared response stub.
27
+ */
28
+ export interface CookieStore {
29
+ /** Get a single cookie by name. Returns undefined if not set or deleted. */
30
+ get(name: string): Cookie | undefined;
31
+
32
+ /** Get all effective cookies, or all cookies with a given name. */
33
+ getAll(name?: string): Cookie[];
34
+
35
+ /** Check whether a cookie exists in the effective view. */
36
+ has(name: string): boolean;
37
+
38
+ /** Set a cookie (appends Set-Cookie to the response stub). */
39
+ set(name: string, value: string, options?: CookieOptions): void;
40
+
41
+ /** Delete a cookie (appends Set-Cookie with maxAge=0 to the response stub). */
42
+ delete(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
43
+ }
44
+
45
+ /**
46
+ * Get the request-scoped cookie store.
47
+ *
48
+ * Must be called inside a request context (middleware, handler, loader, action).
49
+ * Throws if called outside request scope.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import { cookies } from "@rangojs/router";
54
+ *
55
+ * // In a handler, loader, or action:
56
+ * const session = cookies().get("session")?.value;
57
+ * cookies().set("session", "new-token", { httpOnly: true });
58
+ * cookies().delete("session");
59
+ * ```
60
+ */
61
+ export function cookies(): CookieStore {
62
+ const ctx = getRequestContext();
63
+ assertNotInsideCacheContext(ctx, "cookies");
64
+ return createCookieStore(ctx);
65
+ }
66
+
67
+ /**
68
+ * Read-only view of HTTP headers.
69
+ * Exposes only the read methods of the Headers API.
70
+ */
71
+ export interface ReadonlyHeaders {
72
+ get(name: string): string | null;
73
+ has(name: string): boolean;
74
+ entries(): HeadersIterator<[string, string]>;
75
+ keys(): HeadersIterator<string>;
76
+ values(): HeadersIterator<string>;
77
+ forEach(
78
+ callback: (value: string, name: string, parent: ReadonlyHeaders) => void,
79
+ ): void;
80
+ [Symbol.iterator](): HeadersIterator<[string, string]>;
81
+ }
82
+
83
+ // Minimal iterator interface (avoids pulling IterableIterator from lib.dom)
84
+ type HeadersIterator<T> = IterableIterator<T>;
85
+
86
+ /**
87
+ * Throw if called inside a "use cache" function.
88
+ * Reading request-scoped data (cookies, headers) inside a cached function
89
+ * produces results that vary per request but the cache key does not include
90
+ * those values, leading to one user's data being served to another.
91
+ */
92
+ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
93
+ if (
94
+ ctx !== null &&
95
+ ctx !== undefined &&
96
+ typeof ctx === "object" &&
97
+ (INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
98
+ ) {
99
+ throw new Error(
100
+ `${fnName}() cannot be called inside a "use cache" function. ` +
101
+ `Request-scoped data (cookies, headers) varies per request but is not ` +
102
+ `reflected in the cache key, so cached results would be served to the ` +
103
+ `wrong users. Extract the value before the cached function and pass it ` +
104
+ `as an argument:\n\n` +
105
+ ` const locale = cookies().get("locale")?.value ?? "en";\n` +
106
+ ` const data = await getCachedData(locale); // locale is now in the cache key`,
107
+ );
108
+ }
109
+ }
110
+
111
+ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
112
+
113
+ /**
114
+ * Get the original request headers (read-only).
115
+ *
116
+ * Must be called inside a request context.
117
+ * Returns a read-only view of the incoming request's headers.
118
+ * Mutation methods (set, append, delete) throw at runtime.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import { headers } from "@rangojs/router";
123
+ *
124
+ * const auth = headers().get("authorization");
125
+ * const contentType = headers().get("content-type");
126
+ * ```
127
+ */
128
+ export function headers(): ReadonlyHeaders {
129
+ const ctx = getRequestContext();
130
+ assertNotInsideCacheContext(ctx, "headers");
131
+ return new Proxy(ctx.request.headers, {
132
+ get(target, prop, receiver) {
133
+ if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
134
+ return () => {
135
+ throw new Error(
136
+ `headers().${prop}() is not allowed. headers() returns a read-only view of request headers. ` +
137
+ `Use ctx.header() to set response headers.`,
138
+ );
139
+ };
140
+ }
141
+ const value = Reflect.get(target, prop, receiver);
142
+ return typeof value === "function" ? value.bind(target) : value;
143
+ },
144
+ }) as unknown as ReadonlyHeaders;
145
+ }
146
+
147
+ /**
148
+ * Create a CookieStore backed by a RequestContext.
149
+ * @internal Shared between cookies() shorthand and context methods.
150
+ */
151
+ function createCookieStore(ctx: {
152
+ cookie(name: string): string | undefined;
153
+ cookies(): Record<string, string>;
154
+ setCookie(name: string, value: string, options?: CookieOptions): void;
155
+ deleteCookie(
156
+ name: string,
157
+ options?: Pick<CookieOptions, "domain" | "path">,
158
+ ): void;
159
+ }): CookieStore {
160
+ return {
161
+ get(name: string): Cookie | undefined {
162
+ const value = ctx.cookie(name);
163
+ return value !== undefined ? { name, value } : undefined;
164
+ },
165
+
166
+ getAll(name?: string): Cookie[] {
167
+ const all = ctx.cookies();
168
+ if (name !== undefined) {
169
+ const value = all[name];
170
+ return value !== undefined ? [{ name, value }] : [];
171
+ }
172
+ return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
173
+ },
174
+
175
+ has(name: string): boolean {
176
+ return ctx.cookie(name) !== undefined;
177
+ },
178
+
179
+ set(name: string, value: string, options?: CookieOptions): void {
180
+ ctx.setCookie(name, value, options);
181
+ },
182
+
183
+ delete(
184
+ name: string,
185
+ options?: Pick<CookieOptions, "domain" | "path">,
186
+ ): void {
187
+ ctx.deleteCookie(name, options);
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Fetchable loader store - internal registry for fetchable loader functions.
3
+ *
4
+ * Extracted into its own module to avoid circular dependencies between
5
+ * loader.rsc.ts and request-context.ts. This module has no imports from
6
+ * either, so both can safely import from here.
7
+ *
8
+ * Populated by createLoader() in loader.rsc.ts.
9
+ * Read by request-context.ts (for ctx.use()) and loader-registry.ts (for GET-based fetching).
10
+ */
11
+
12
+ import type { LoaderFn } from "../types.js";
13
+ import type { MiddlewareFn } from "../router/middleware.js";
14
+
15
+ export interface LoaderRegistryEntry {
16
+ fn: LoaderFn<any, any, any>;
17
+ middleware: MiddlewareFn[];
18
+ /** Whether this loader is fetchable via the _rsc_loader endpoint. */
19
+ fetchable: boolean;
20
+ }
21
+
22
+ const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
23
+
24
+ export function registerFetchableLoader(
25
+ id: string,
26
+ fn: LoaderFn<any, any, any>,
27
+ middleware: MiddlewareFn[],
28
+ fetchable: boolean,
29
+ ): void {
30
+ fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
31
+ }
32
+
33
+ export function getFetchableLoader(
34
+ id: string,
35
+ ): LoaderRegistryEntry | undefined {
36
+ return fetchableLoaderRegistry.get(id);
37
+ }
@@ -13,6 +13,38 @@
13
13
  */
14
14
  export type HandleData = Record<string, Record<string, unknown[]>>;
15
15
 
16
+ /**
17
+ * Build a HandleData snapshot from a HandleStore using segment ordering.
18
+ * Reads data directly from the store for each segment in order.
19
+ */
20
+ export function buildHandleSnapshot(
21
+ handleStore: HandleStore,
22
+ segmentOrder: string[],
23
+ ): HandleData {
24
+ const data: HandleData = {};
25
+ for (const segmentId of segmentOrder) {
26
+ const segData = handleStore.getDataForSegment(segmentId);
27
+ for (const handleName in segData) {
28
+ if (!data[handleName]) data[handleName] = {};
29
+ data[handleName][segmentId] = segData[handleName];
30
+ }
31
+ }
32
+ return data;
33
+ }
34
+
35
+ function createLateHandlePushError(
36
+ handleName: string,
37
+ segmentId: string,
38
+ ): Error {
39
+ const error = new Error(
40
+ `Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
41
+ `This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
42
+ `Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
43
+ );
44
+ error.name = "LateHandlePushError";
45
+ return error;
46
+ }
47
+
16
48
  /**
17
49
  * Deep clone handle data to create a snapshot.
18
50
  * @internal
@@ -44,11 +76,26 @@ export interface HandleStore {
44
76
  track<T>(promise: Promise<T>): Promise<T>;
45
77
 
46
78
  /**
47
- * Promise that resolves when all tracked handlers have settled.
48
- * Does not reject - uses Promise.allSettled internally.
79
+ * Signal that no more track() calls will be made.
80
+ * settled will not resolve until seal() is called AND all tracked
81
+ * promises have settled. Calling stream() or getData() auto-seals.
82
+ */
83
+ seal(): void;
84
+
85
+ /**
86
+ * Promise that resolves when the store is sealed AND all tracked
87
+ * handlers have settled.
49
88
  */
50
89
  readonly settled: Promise<void>;
51
90
 
91
+ /**
92
+ * Optional error callback for late streaming-handle failures.
93
+ * Called when push() throws LateHandlePushError (handle pushed after
94
+ * stream completion). Allows the router to surface these errors
95
+ * to onError and telemetry.
96
+ */
97
+ onError?: (error: Error) => void;
98
+
52
99
  /**
53
100
  * Push handle data for a specific handle and segment.
54
101
  * Multiple pushes to the same handle/segment accumulate in an array.
@@ -58,9 +105,7 @@ export interface HandleStore {
58
105
 
59
106
  /**
60
107
  * Get all collected handle data after all handlers have settled.
61
- * Returns a promise that waits for `settled`, then returns the data.
62
- * The data may contain unresolved promises which RSC will stream.
63
- * @deprecated Use stream() for progressive updates
108
+ * Waits for `settled`, then returns the finalized data.
64
109
  */
65
110
  getData(): Promise<HandleData>;
66
111
 
@@ -81,7 +126,10 @@ export interface HandleStore {
81
126
  * Replay cached handle data back into the store (for cache hits).
82
127
  * Used to restore handle data when serving cached segments.
83
128
  */
84
- replaySegmentData(segmentId: string, segmentHandles: Record<string, unknown[]>): void;
129
+ replaySegmentData(
130
+ segmentId: string,
131
+ segmentHandles: Record<string, unknown[]>,
132
+ ): void;
85
133
  }
86
134
 
87
135
  /**
@@ -105,9 +153,31 @@ export interface HandleStore {
105
153
  * ```
106
154
  */
107
155
  export function createHandleStore(): HandleStore {
108
- const pending: Promise<unknown>[] = [];
109
156
  const data: HandleData = {};
110
157
 
158
+ // Settlement barrier: resolved only when sealed AND inflight === 0.
159
+ // seal() signals "no more track() calls". Each track() increments
160
+ // inflightCount, each promise.finally() decrements. settled resolves
161
+ // once both conditions are met — even if tracks are added while
162
+ // earlier ones are still in flight.
163
+ let sealed = false;
164
+ let inflightCount = 0;
165
+ let drainWaiters: (() => void)[] = [];
166
+
167
+ function notifyDrain() {
168
+ if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
169
+ const waiters = drainWaiters;
170
+ drainWaiters = [];
171
+ for (const resolve of waiters) resolve();
172
+ }
173
+ }
174
+
175
+ function sealInternal() {
176
+ if (sealed) return;
177
+ sealed = true;
178
+ notifyDrain();
179
+ }
180
+
111
181
  // Queue for pending emissions and resolver for waiting consumer
112
182
  let pendingEmissions: HandleData[] = [];
113
183
  let emissionResolver: (() => void) | null = null;
@@ -134,18 +204,38 @@ export function createHandleStore(): HandleStore {
134
204
 
135
205
  return {
136
206
  track<T>(promise: Promise<T>): Promise<T> {
137
- pending.push(promise);
207
+ inflightCount++;
208
+ // Use .then(onSettle, onSettle) instead of .finally() to avoid
209
+ // creating an unhandled rejection branch when the tracked promise
210
+ // rejects (e.g. error route handlers). .finally() re-throws the
211
+ // rejection on a new branch that nobody catches, which can crash
212
+ // the server process.
213
+ const onSettle = () => {
214
+ inflightCount--;
215
+ notifyDrain();
216
+ };
217
+ promise.then(onSettle, onSettle);
138
218
  return promise;
139
219
  },
140
220
 
221
+ seal() {
222
+ sealInternal();
223
+ },
224
+
141
225
  get settled(): Promise<void> {
142
- if (pending.length === 0) {
143
- return Promise.resolve();
144
- }
145
- return Promise.allSettled(pending).then(() => {});
226
+ if (sealed && inflightCount === 0) return Promise.resolve();
227
+ return new Promise<void>((resolve) => {
228
+ drainWaiters.push(resolve);
229
+ });
146
230
  },
147
231
 
148
232
  push(handleName: string, segmentId: string, value: unknown): void {
233
+ if (completed) {
234
+ const error = createLateHandlePushError(handleName, segmentId);
235
+ if (this.onError) this.onError(error);
236
+ throw error;
237
+ }
238
+
149
239
  if (!data[handleName]) {
150
240
  data[handleName] = {};
151
241
  }
@@ -160,10 +250,14 @@ export function createHandleStore(): HandleStore {
160
250
  },
161
251
 
162
252
  getData(): Promise<HandleData> {
163
- return this.settled.then(() => data);
253
+ sealInternal();
254
+ return this.settled.then(() => cloneHandleData(data));
164
255
  },
165
256
 
166
257
  async *stream(): AsyncGenerator<HandleData, void, unknown> {
258
+ // Auto-seal: stream() is called after all track() registrations.
259
+ sealInternal();
260
+
167
261
  // Set up completion handler
168
262
  this.settled.then(() => {
169
263
  completed = true;
@@ -178,7 +272,8 @@ export function createHandleStore(): HandleStore {
178
272
  if (Object.keys(data).length > 0) {
179
273
  // Clear pending emissions since we're yielding current state
180
274
  pendingEmissions = [];
181
- yield cloneHandleData(data);
275
+ const snapshot = cloneHandleData(data);
276
+ yield snapshot;
182
277
  }
183
278
 
184
279
  // Continue streaming on each push
@@ -211,7 +306,10 @@ export function createHandleStore(): HandleStore {
211
306
  return result;
212
307
  },
213
308
 
214
- replaySegmentData(segmentId: string, segmentHandles: Record<string, unknown[]>): void {
309
+ replaySegmentData(
310
+ segmentId: string,
311
+ segmentHandles: Record<string, unknown[]>,
312
+ ): void {
215
313
  for (const handleName in segmentHandles) {
216
314
  if (!data[handleName]) {
217
315
  data[handleName] = {};
@@ -6,13 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { LoaderFn } from "../types.js";
9
- import type { MiddlewareFn } from "../router/middleware.js";
10
- import { getFetchableLoader } from "../loader.rsc.js";
11
-
12
- interface RegisteredLoader {
13
- fn: LoaderFn<any, any, any>;
14
- middleware: MiddlewareFn[];
15
- }
9
+ import {
10
+ getFetchableLoader,
11
+ type LoaderRegistryEntry,
12
+ } from "./fetchable-loader-store.js";
16
13
 
17
14
  // Server-side cache - maps loader $$id to function and middleware
18
15
  // This is a CACHE populated by getLoaderLazy() when loaders are first accessed.
@@ -21,7 +18,7 @@ interface RegisteredLoader {
21
18
  // 1. Avoid repeated lookups/imports for the same loader
22
19
  // 2. Support lazy loading in production (loaders imported on-demand)
23
20
  // 3. Provide a stable reference for the RSC handler
24
- const loaderRegistry = new Map<string, RegisteredLoader>();
21
+ const loaderRegistry = new Map<string, LoaderRegistryEntry>();
25
22
 
26
23
  // Lazy import map - set by the loader manifest
27
24
  // Maps loader $$id to a function that imports the loader module
@@ -32,35 +29,11 @@ let lazyLoaderImports: Map<string, LazyLoaderImport> | null = null;
32
29
  * Set the lazy loader imports map (called by the loader manifest)
33
30
  */
34
31
  export function setLoaderImports(
35
- imports: Record<string, LazyLoaderImport>
32
+ imports: Record<string, LazyLoaderImport>,
36
33
  ): void {
37
34
  lazyLoaderImports = new Map(Object.entries(imports));
38
35
  }
39
36
 
40
- /**
41
- * Register a fetchable loader by $$id
42
- * Called by createLoader when fetchable option is provided
43
- */
44
- export function registerLoader(
45
- id: string,
46
- fn: LoaderFn<any, any, any>,
47
- middleware: MiddlewareFn[] = []
48
- ): void {
49
- if (loaderRegistry.has(id)) {
50
- // Already registered (can happen during HMR)
51
- return;
52
- }
53
- loaderRegistry.set(id, { fn, middleware });
54
- }
55
-
56
- /**
57
- * Get a registered loader by $$id (synchronous)
58
- * Returns undefined if loader is not registered
59
- */
60
- export function getLoader(id: string): RegisteredLoader | undefined {
61
- return loaderRegistry.get(id);
62
- }
63
-
64
37
  /**
65
38
  * Get a loader by $$id, loading it lazily if needed
66
39
  * This is the primary method for the RSC handler to get loaders
@@ -69,22 +42,23 @@ export function getLoader(id: string): RegisteredLoader | undefined {
69
42
  * In dev: IDs are "filePath#exportName", resolved via dynamic import
70
43
  */
71
44
  export async function getLoaderLazy(
72
- id: string
73
- ): Promise<RegisteredLoader | undefined> {
74
- // Check if already cached in main registry
75
- const existing = loaderRegistry.get(id);
76
- if (existing) {
77
- return existing;
78
- }
79
-
80
- // Check the fetchable loader registry (populated by createLoader)
45
+ id: string,
46
+ ): Promise<LoaderRegistryEntry | undefined> {
47
+ // Always check fetchableLoaderRegistry first it's the source of truth.
48
+ // createLoader() updates it during module re-evaluation (HMR), so checking
49
+ // here ensures we pick up the fresh function after a loader file change.
81
50
  const fetchable = getFetchableLoader(id);
82
51
  if (fetchable) {
83
- // Cache in main registry for future requests
84
52
  loaderRegistry.set(id, fetchable);
85
53
  return fetchable;
86
54
  }
87
55
 
56
+ // Fall back to local cache (populated by previous lazy imports in production)
57
+ const existing = loaderRegistry.get(id);
58
+ if (existing) {
59
+ return existing;
60
+ }
61
+
88
62
  // Try to lazy load from the import map (production mode)
89
63
  if (lazyLoaderImports && lazyLoaderImports.size > 0) {
90
64
  const lazyImport = lazyLoaderImports.get(id);
@@ -130,20 +104,6 @@ export async function getLoaderLazy(
130
104
  return undefined;
131
105
  }
132
106
 
133
- /**
134
- * Check if a loader is registered by $$id
135
- */
136
- export function hasLoader(id: string): boolean {
137
- return loaderRegistry.has(id) || getFetchableLoader(id) !== undefined;
138
- }
139
-
140
- /**
141
- * Get all registered loader IDs (for debugging)
142
- */
143
- export function getRegisteredLoaderIds(): string[] {
144
- return Array.from(loaderRegistry.keys());
145
- }
146
-
147
107
  /**
148
108
  * Register a loader by its $$id (injected by Vite plugin)
149
109
  * This is called during module loading to cache loaders
@@ -155,12 +115,8 @@ export function registerLoaderById(loader: {
155
115
  if (!loader.$$id) {
156
116
  return;
157
117
  }
158
- if (loaderRegistry.has(loader.$$id)) {
159
- // Already registered (can happen during HMR)
160
- return;
161
- }
162
-
163
- // For fetchable loaders, fn is stored in the fetchable registry by $$id
118
+ // For fetchable loaders, fn is stored in the fetchable registry by $$id.
119
+ // Always re-check the fetchable registry so HMR picks up the new function.
164
120
  const fetchable = getFetchableLoader(loader.$$id);
165
121
  if (fetchable) {
166
122
  loaderRegistry.set(loader.$$id, fetchable);
@@ -169,6 +125,10 @@ export function registerLoaderById(loader: {
169
125
 
170
126
  // Fall back to using fn from the loader object (non-fetchable loaders)
171
127
  if (loader.fn) {
172
- loaderRegistry.set(loader.$$id, { fn: loader.fn, middleware: [] });
128
+ loaderRegistry.set(loader.$$id, {
129
+ fn: loader.fn,
130
+ middleware: [],
131
+ fetchable: false,
132
+ });
173
133
  }
174
134
  }