@ivogt/rsc-router 0.0.0-experimental.1

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 (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Server-side loader registry for GET-based fetching
3
+ *
4
+ * Loaders are loaded lazily via dynamic imports when first requested.
5
+ * The RSC handler looks up loaders by $$id to execute them.
6
+ */
7
+
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
+ }
16
+
17
+ // Server-side cache - maps loader $$id to function and middleware
18
+ // This is a CACHE populated by getLoaderLazy() when loaders are first accessed.
19
+ // The source of truth is fetchableLoaderRegistry in loader.ts, which is populated
20
+ // when createLoader() runs. This cache exists to:
21
+ // 1. Avoid repeated lookups/imports for the same loader
22
+ // 2. Support lazy loading in production (loaders imported on-demand)
23
+ // 3. Provide a stable reference for the RSC handler
24
+ const loaderRegistry = new Map<string, RegisteredLoader>();
25
+
26
+ // Lazy import map - set by the loader manifest
27
+ // Maps loader $$id to a function that imports the loader module
28
+ type LazyLoaderImport = () => Promise<{ $$id: string }>;
29
+ let lazyLoaderImports: Map<string, LazyLoaderImport> | null = null;
30
+
31
+ /**
32
+ * Set the lazy loader imports map (called by the loader manifest)
33
+ */
34
+ export function setLoaderImports(
35
+ imports: Record<string, LazyLoaderImport>
36
+ ): void {
37
+ lazyLoaderImports = new Map(Object.entries(imports));
38
+ }
39
+
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
+ /**
65
+ * Get a loader by $$id, loading it lazily if needed
66
+ * This is the primary method for the RSC handler to get loaders
67
+ *
68
+ * In production: IDs are hashed, looked up via the lazy import map
69
+ * In dev: IDs are "filePath#exportName", resolved via dynamic import
70
+ */
71
+ 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)
81
+ const fetchable = getFetchableLoader(id);
82
+ if (fetchable) {
83
+ // Cache in main registry for future requests
84
+ loaderRegistry.set(id, fetchable);
85
+ return fetchable;
86
+ }
87
+
88
+ // Try to lazy load from the import map (production mode)
89
+ if (lazyLoaderImports && lazyLoaderImports.size > 0) {
90
+ const lazyImport = lazyLoaderImports.get(id);
91
+ if (lazyImport) {
92
+ try {
93
+ // Import the loader module - this triggers createLoader which registers fn
94
+ await lazyImport();
95
+
96
+ // Now try to get from fetchable registry (createLoader registered it)
97
+ const registered = getFetchableLoader(id);
98
+ if (registered) {
99
+ loaderRegistry.set(id, registered);
100
+ return registered;
101
+ }
102
+ } catch (error) {
103
+ console.error(`[LoaderRegistry] Failed to load loader "${id}":`, error);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Dev mode fallback: parse the ID and use Vite's dynamic import
109
+ // ID format in dev: "src/path/to/file.ts#ExportName"
110
+ const hashIndex = id.indexOf("#");
111
+ if (hashIndex !== -1) {
112
+ const filePath = id.slice(0, hashIndex);
113
+
114
+ try {
115
+ // In dev mode, Vite handles dynamic imports
116
+ // Just importing the module triggers createLoader which registers the fn
117
+ await import(/* @vite-ignore */ `/${filePath}`);
118
+
119
+ // Now try to get from fetchable registry
120
+ const registered = getFetchableLoader(id);
121
+ if (registered) {
122
+ loaderRegistry.set(id, registered);
123
+ return registered;
124
+ }
125
+ } catch (error) {
126
+ console.error(`[LoaderRegistry] Failed to load loader "${id}":`, error);
127
+ }
128
+ }
129
+
130
+ return undefined;
131
+ }
132
+
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
+ /**
148
+ * Register a loader by its $$id (injected by Vite plugin)
149
+ * This is called during module loading to cache loaders
150
+ */
151
+ export function registerLoaderById(loader: {
152
+ $$id: string;
153
+ fn?: LoaderFn<any, any, any>;
154
+ }): void {
155
+ if (!loader.$$id) {
156
+ return;
157
+ }
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
164
+ const fetchable = getFetchableLoader(loader.$$id);
165
+ if (fetchable) {
166
+ loaderRegistry.set(loader.$$id, fetchable);
167
+ return;
168
+ }
169
+
170
+ // Fall back to using fn from the loader object (non-fetchable loaders)
171
+ if (loader.fn) {
172
+ loaderRegistry.set(loader.$$id, { fn: loader.fn, middleware: [] });
173
+ }
174
+ }
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Request Context - AsyncLocalStorage for passing request-scoped data throughout rendering
3
+ *
4
+ * This is the unified context used everywhere:
5
+ * - Middleware execution
6
+ * - Route handlers and loaders
7
+ * - Server components during rendering
8
+ * - Error boundaries and streaming
9
+ *
10
+ * Available via getRequestContext() anywhere in the request lifecycle.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import type { CookieOptions } from "../router/middleware.js";
15
+ import type { LoaderDefinition, LoaderContext } from "../types.js";
16
+ import type { Handle } from "../handle.js";
17
+ import { createHandleStore, type HandleStore } from "./handle-store.js";
18
+ import { isHandle } from "../handle.js";
19
+ import { track } from "./context.js";
20
+ import type { SegmentCacheStore } from "../cache/types.js";
21
+
22
+ /**
23
+ * Unified request context available via getRequestContext()
24
+ *
25
+ * This is the same context passed to middleware and handlers.
26
+ * Use this when you need access to request data outside of route handlers.
27
+ */
28
+ export interface RequestContext<
29
+ TEnv = unknown,
30
+ TParams = Record<string, string>,
31
+ > {
32
+ /** Platform bindings (Cloudflare env, etc.) */
33
+ env: TEnv;
34
+ /** Original HTTP request */
35
+ request: Request;
36
+ /** Parsed URL (system params like _rsc* are NOT filtered here) */
37
+ url: URL;
38
+ /** URL pathname */
39
+ pathname: string;
40
+ /** URL search params (system params like _rsc* are NOT filtered here) */
41
+ searchParams: URLSearchParams;
42
+ /** Variables set by middleware (same as ctx.var) */
43
+ var: Record<string, any>;
44
+ /** Get a variable set by middleware */
45
+ get: <K extends string>(key: K) => any;
46
+ /** Set a variable (shared with middleware and handlers) */
47
+ set: <K extends string>(key: K, value: any) => void;
48
+ /**
49
+ * Route params (populated after route matching)
50
+ * Initially empty, then set to matched params
51
+ */
52
+ params: TParams;
53
+ /**
54
+ * Stub response for setting headers/cookies
55
+ * Headers set here are merged into the final response
56
+ */
57
+ res: Response;
58
+
59
+ /** Get a cookie value from the request */
60
+ cookie(name: string): string | undefined;
61
+ /** Get all cookies from the request */
62
+ cookies(): Record<string, string>;
63
+ /** Set a cookie on the response */
64
+ setCookie(name: string, value: string, options?: CookieOptions): void;
65
+ /** Delete a cookie */
66
+ deleteCookie(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
67
+ /** Set a response header */
68
+ header(name: string, value: string): void;
69
+
70
+ /**
71
+ * Access loader data or push handle data.
72
+ *
73
+ * For loaders: Returns a promise that resolves to the loader data.
74
+ * Loaders are executed in parallel and memoized per request.
75
+ *
76
+ * For handles: Returns a push function to add data for this segment.
77
+ * Handle data accumulates across all matched route segments.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Loader usage
82
+ * const cart = await ctx.use(CartLoader);
83
+ *
84
+ * // Handle usage
85
+ * const push = ctx.use(Breadcrumbs);
86
+ * push({ label: "Shop", href: "/shop" });
87
+ * ```
88
+ */
89
+ use: {
90
+ <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>): Promise<T>;
91
+ <TData, TAccumulated = TData[]>(handle: Handle<TData, TAccumulated>): (
92
+ data: TData | Promise<TData> | (() => Promise<TData>)
93
+ ) => void;
94
+ };
95
+
96
+ /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
97
+ method: string;
98
+
99
+ /** @internal Handle store for tracking handle data across segments */
100
+ _handleStore: HandleStore;
101
+
102
+ /** @internal Cache store for segment caching (optional, used by CacheScope) */
103
+ _cacheStore?: SegmentCacheStore;
104
+
105
+ /**
106
+ * Schedule work to run after the response is sent.
107
+ * On Cloudflare Workers, uses ctx.waitUntil().
108
+ * On Node.js, runs as fire-and-forget.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * ctx.waitUntil(async () => {
113
+ * await cacheStore.set(key, data, ttl);
114
+ * });
115
+ * ```
116
+ */
117
+ waitUntil(fn: () => Promise<void>): void;
118
+
119
+ /**
120
+ * Register a callback to run when the response is created.
121
+ * Callbacks are sync and receive the response. They can:
122
+ * - Inspect response status/headers
123
+ * - Return a modified response
124
+ * - Schedule async work via waitUntil
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * ctx.onResponse((res) => {
129
+ * if (res.status === 200) {
130
+ * ctx.waitUntil(async () => await cacheIt());
131
+ * }
132
+ * return res;
133
+ * });
134
+ * ```
135
+ */
136
+ onResponse(callback: (response: Response) => Response): void;
137
+
138
+ /** @internal Registered onResponse callbacks */
139
+ _onResponseCallbacks: Array<(response: Response) => Response>;
140
+ }
141
+
142
+ // AsyncLocalStorage instance for request context
143
+ const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
144
+
145
+ /**
146
+ * Run a function within a request context
147
+ * Used by the RSC handler to provide context to server actions
148
+ */
149
+ export function runWithRequestContext<TEnv, T>(
150
+ context: RequestContext<TEnv>,
151
+ fn: () => T
152
+ ): T {
153
+ return requestContextStorage.run(context, fn);
154
+ }
155
+
156
+ /**
157
+ * Get the current request context
158
+ * Returns undefined if not running within a request context
159
+ */
160
+ export function getRequestContext<TEnv = unknown>():
161
+ | RequestContext<TEnv>
162
+ | undefined {
163
+ return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
164
+ }
165
+
166
+ /**
167
+ * Update params on the current request context
168
+ * Called after route matching to populate route params
169
+ */
170
+ export function setRequestContextParams(params: Record<string, string>): void {
171
+ const ctx = requestContextStorage.getStore();
172
+ if (ctx) {
173
+ ctx.params = params;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Get the current request context, throwing if not available
179
+ * Use this when context is required (e.g., in loader actions)
180
+ */
181
+ export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
182
+ const ctx = getRequestContext<TEnv>();
183
+ if (!ctx) {
184
+ throw new Error(
185
+ "Request context not available. This function must be called from within a server action " +
186
+ "executed through the RSC handler."
187
+ );
188
+ }
189
+ return ctx;
190
+ }
191
+
192
+ /**
193
+ * Cloudflare Workers ExecutionContext (subset we need)
194
+ */
195
+ export interface ExecutionContext {
196
+ waitUntil(promise: Promise<any>): void;
197
+ passThroughOnException(): void;
198
+ }
199
+
200
+ /**
201
+ * Options for creating a request context
202
+ */
203
+ export interface CreateRequestContextOptions<TEnv> {
204
+ env: TEnv;
205
+ request: Request;
206
+ url: URL;
207
+ variables: Record<string, any>;
208
+ /** Optional cache store for segment caching (used by CacheScope) */
209
+ cacheStore?: SegmentCacheStore;
210
+ /** Optional Cloudflare execution context for waitUntil support */
211
+ executionContext?: ExecutionContext;
212
+ }
213
+
214
+ /**
215
+ * Create a full request context with all methods implemented
216
+ *
217
+ * This is used by the RSC handler to create the unified context that's:
218
+ * - Available via getRequestContext() throughout the request
219
+ * - Passed to middleware as ctx
220
+ * - Passed to handlers as ctx
221
+ */
222
+ export function createRequestContext<TEnv>(
223
+ options: CreateRequestContextOptions<TEnv>
224
+ ): RequestContext<TEnv> {
225
+ const { env, request, url, variables, cacheStore, executionContext } = options;
226
+ const cookieHeader = request.headers.get("Cookie");
227
+ let parsedCookies: Record<string, string> | null = null;
228
+
229
+ // Create stub response for collecting headers/cookies
230
+ const stubResponse = new Response(null, { status: 200 });
231
+
232
+ // Create handle store and loader memoization for this request
233
+ const handleStore = createHandleStore();
234
+ const loaderPromises = new Map<string, Promise<any>>();
235
+
236
+ // Lazy parse cookies
237
+ const getParsedCookies = (): Record<string, string> => {
238
+ if (!parsedCookies) {
239
+ parsedCookies = parseCookiesFromHeader(cookieHeader);
240
+ }
241
+ return parsedCookies;
242
+ };
243
+
244
+ // Build the context object first (without use), then add use
245
+ const ctx: RequestContext<TEnv> = {
246
+ env,
247
+ request,
248
+ url,
249
+ pathname: url.pathname,
250
+ searchParams: url.searchParams,
251
+ var: variables,
252
+ get: <K extends string>(key: K) => variables[key],
253
+ set: <K extends string>(key: K, value: any) => {
254
+ variables[key] = value;
255
+ },
256
+ params: {} as Record<string, string>,
257
+ res: stubResponse,
258
+
259
+ cookie(name: string): string | undefined {
260
+ return getParsedCookies()[name];
261
+ },
262
+
263
+ cookies(): Record<string, string> {
264
+ return { ...getParsedCookies() };
265
+ },
266
+
267
+ setCookie(name: string, value: string, options?: CookieOptions): void {
268
+ stubResponse.headers.append(
269
+ "Set-Cookie",
270
+ serializeCookieValue(name, value, options)
271
+ );
272
+ },
273
+
274
+ deleteCookie(
275
+ name: string,
276
+ options?: Pick<CookieOptions, "domain" | "path">
277
+ ): void {
278
+ stubResponse.headers.append(
279
+ "Set-Cookie",
280
+ serializeCookieValue(name, "", { ...options, maxAge: 0 })
281
+ );
282
+ },
283
+
284
+ header(name: string, value: string): void {
285
+ stubResponse.headers.set(name, value);
286
+ },
287
+
288
+ // Placeholder - will be replaced below
289
+ use: null as any,
290
+
291
+ method: request.method,
292
+
293
+ _handleStore: handleStore,
294
+ _cacheStore: cacheStore,
295
+
296
+ waitUntil(fn: () => Promise<void>): void {
297
+ if (executionContext?.waitUntil) {
298
+ // Cloudflare Workers: use native waitUntil
299
+ executionContext.waitUntil(fn());
300
+ } else {
301
+ // Node.js: fire-and-forget
302
+ fn().catch((err) => console.error("[waitUntil]", err));
303
+ }
304
+ },
305
+
306
+ _onResponseCallbacks: [],
307
+
308
+ onResponse(callback: (response: Response) => Response): void {
309
+ this._onResponseCallbacks.push(callback);
310
+ },
311
+ };
312
+
313
+ // Now create use() with access to ctx
314
+ ctx.use = createUseFunction({
315
+ handleStore,
316
+ loaderPromises,
317
+ getContext: () => ctx,
318
+ });
319
+
320
+ return ctx;
321
+ }
322
+
323
+ /**
324
+ * Parse cookies from Cookie header
325
+ */
326
+ function parseCookiesFromHeader(
327
+ cookieHeader: string | null
328
+ ): Record<string, string> {
329
+ if (!cookieHeader) return {};
330
+
331
+ const cookies: Record<string, string> = {};
332
+ const pairs = cookieHeader.split(";");
333
+
334
+ for (const pair of pairs) {
335
+ const [name, ...rest] = pair.trim().split("=");
336
+ if (name) {
337
+ cookies[name] = decodeURIComponent(rest.join("="));
338
+ }
339
+ }
340
+
341
+ return cookies;
342
+ }
343
+
344
+ /**
345
+ * Serialize a cookie for Set-Cookie header
346
+ */
347
+ function serializeCookieValue(
348
+ name: string,
349
+ value: string,
350
+ options: CookieOptions = {}
351
+ ): string {
352
+ let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
353
+
354
+ if (options.domain) cookie += `; Domain=${options.domain}`;
355
+ if (options.path) cookie += `; Path=${options.path}`;
356
+ if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
357
+ if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
358
+ if (options.httpOnly) cookie += "; HttpOnly";
359
+ if (options.secure) cookie += "; Secure";
360
+ if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
361
+
362
+ return cookie;
363
+ }
364
+
365
+ /**
366
+ * Options for creating the use() function
367
+ */
368
+ export interface CreateUseFunctionOptions<TEnv> {
369
+ handleStore: HandleStore;
370
+ loaderPromises: Map<string, Promise<any>>;
371
+ getContext: () => RequestContext<TEnv>;
372
+ }
373
+
374
+ /**
375
+ * Create the use() function for loader and handle composition.
376
+ *
377
+ * This is the unified implementation used by both RequestContext and HandlerContext.
378
+ * - For loaders: executes and memoizes loader functions
379
+ * - For handles: returns a push function to add handle data
380
+ */
381
+ export function createUseFunction<TEnv>(
382
+ options: CreateUseFunctionOptions<TEnv>
383
+ ): RequestContext["use"] {
384
+ const { handleStore, loaderPromises, getContext } = options;
385
+
386
+ return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
387
+ // Handle case: return a push function
388
+ if (isHandle(item)) {
389
+ const handle = item;
390
+ const ctx = getContext();
391
+ const segmentId = (ctx as any)._currentSegmentId;
392
+
393
+ if (!segmentId) {
394
+ throw new Error(
395
+ `Handle "${handle.$$id}" used outside of handler context. ` +
396
+ `Handles must be used within route/layout handlers.`
397
+ );
398
+ }
399
+
400
+ // Return a push function bound to this handle and segment
401
+ return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
402
+ // If it's a function, call it immediately to get the promise
403
+ const valueOrPromise = typeof dataOrFn === "function"
404
+ ? (dataOrFn as () => Promise<unknown>)()
405
+ : dataOrFn;
406
+
407
+ // Push directly - promises will be serialized by RSC and streamed
408
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
409
+ };
410
+ }
411
+
412
+ // Loader case
413
+ const loader = item as LoaderDefinition<any, any>;
414
+
415
+ // Return cached promise if already started
416
+ if (loaderPromises.has(loader.$$id)) {
417
+ return loaderPromises.get(loader.$$id);
418
+ }
419
+
420
+ // Get loader function - either from loader object or fetchable registry
421
+ let loaderFn = loader.fn;
422
+ if (!loaderFn) {
423
+ // Lazy import to avoid circular dependency
424
+ const { getFetchableLoader } = require("../loader.rsc.js");
425
+ const fetchable = getFetchableLoader(loader.$$id);
426
+ if (fetchable) {
427
+ loaderFn = fetchable.fn;
428
+ }
429
+ }
430
+
431
+ if (!loaderFn) {
432
+ throw new Error(
433
+ `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
434
+ );
435
+ }
436
+
437
+ const ctx = getContext();
438
+
439
+ // Create loader context with recursive use() support
440
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
441
+ params: ctx.params,
442
+ request: ctx.request,
443
+ searchParams: ctx.searchParams,
444
+ pathname: ctx.pathname,
445
+ url: ctx.url,
446
+ env: ctx.env as any,
447
+ var: ctx.var as any,
448
+ get: ctx.get as any,
449
+ use: <TDep, TDepParams = any>(
450
+ dep: LoaderDefinition<TDep, TDepParams>
451
+ ): Promise<TDep> => {
452
+ // Recursive call - will start dep loader if not already started
453
+ return ctx.use(dep);
454
+ },
455
+ method: "GET",
456
+ body: undefined,
457
+ };
458
+
459
+ // Start loader execution with tracking
460
+ const doneLoader = track(`loader:${loader.$$id}`);
461
+ const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
462
+ doneLoader();
463
+ });
464
+
465
+ // Memoize for subsequent calls
466
+ loaderPromises.set(loader.$$id, promise);
467
+
468
+ return promise;
469
+ }) as RequestContext["use"];
470
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ import { Outlet } from "rsc-router/client";
3
+
4
+ const MapRootLayout = (
5
+ <>
6
+ <Outlet />
7
+ </>
8
+ ) as ReactNode;
9
+
10
+ export default MapRootLayout;
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "lib": ["ES2020"],
5
+ "types": ["node", "vite/client"],
6
+ "typeRoots": ["../../node_modules/@types"],
7
+ "moduleResolution": "bundler",
8
+ "rootDir": ".",
9
+ "outDir": "../../dist/server",
10
+ "composite": true,
11
+ "verbatimModuleSyntax": true
12
+ },
13
+ "include": ["./**/*"]
14
+ }