@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,287 @@
1
+ /**
2
+ * Router Error Handling Utilities
3
+ *
4
+ * Error boundary and not-found boundary handling for RSC Router.
5
+ * Also includes the shared invokeOnError utility for error callback invocation.
6
+ */
7
+
8
+ import type { ReactNode } from "react";
9
+ import type { EntryData } from "../server/context";
10
+ import type {
11
+ ResolvedSegment,
12
+ ErrorInfo,
13
+ ErrorBoundaryHandler,
14
+ ErrorBoundaryFallbackProps,
15
+ NotFoundInfo,
16
+ NotFoundBoundaryHandler,
17
+ NotFoundBoundaryFallbackProps,
18
+ ErrorPhase,
19
+ OnErrorCallback,
20
+ OnErrorContext,
21
+ } from "../types";
22
+
23
+ /**
24
+ * Context required to invoke the onError callback.
25
+ * This is a subset of OnErrorContext that callers must provide.
26
+ */
27
+ export interface InvokeOnErrorContext<TEnv = any> {
28
+ request: Request;
29
+ url: URL;
30
+ routeKey?: string;
31
+ params?: Record<string, string>;
32
+ segmentId?: string;
33
+ segmentType?: "layout" | "route" | "parallel" | "loader" | "middleware";
34
+ loaderName?: string;
35
+ middlewareId?: string;
36
+ actionId?: string;
37
+ env?: TEnv;
38
+ isPartial?: boolean;
39
+ handledByBoundary?: boolean;
40
+ metadata?: Record<string, unknown>;
41
+ /** Request start time from performance.now() for duration calculation */
42
+ requestStartTime?: number;
43
+ }
44
+
45
+ /**
46
+ * Invoke the onError callback with comprehensive context.
47
+ * Catches any errors in the callback itself to prevent masking the original error.
48
+ *
49
+ * This is a shared utility used by both the router and RSC handler to ensure
50
+ * consistent error callback behavior across the codebase.
51
+ *
52
+ * @param onError - The onError callback to invoke (may be undefined)
53
+ * @param error - The error that occurred
54
+ * @param phase - The phase where the error occurred
55
+ * @param context - Additional context about the error
56
+ * @param logPrefix - Prefix for console.error messages (e.g., "Router" or "RSC")
57
+ */
58
+ export function invokeOnError<TEnv = any>(
59
+ onError: OnErrorCallback<TEnv> | undefined,
60
+ error: unknown,
61
+ phase: ErrorPhase,
62
+ context: InvokeOnErrorContext<TEnv>,
63
+ logPrefix: string = "Router"
64
+ ): void {
65
+ if (!onError) return;
66
+
67
+ const errorObj = error instanceof Error ? error : new Error(String(error));
68
+ const duration = context.requestStartTime
69
+ ? performance.now() - context.requestStartTime
70
+ : undefined;
71
+
72
+ const errorContext: OnErrorContext<TEnv> = {
73
+ error: errorObj,
74
+ phase,
75
+ request: context.request,
76
+ url: context.url,
77
+ pathname: context.url.pathname,
78
+ method: context.request.method,
79
+ routeKey: context.routeKey,
80
+ params: context.params,
81
+ segmentId: context.segmentId,
82
+ segmentType: context.segmentType,
83
+ loaderName: context.loaderName,
84
+ middlewareId: context.middlewareId,
85
+ actionId: context.actionId,
86
+ env: context.env,
87
+ duration,
88
+ isPartial: context.isPartial,
89
+ handledByBoundary: context.handledByBoundary,
90
+ stack: errorObj.stack,
91
+ metadata: context.metadata,
92
+ };
93
+
94
+ try {
95
+ const result = onError(errorContext);
96
+ // If onError returns a promise, catch any rejections
97
+ if (result instanceof Promise) {
98
+ result.catch((callbackError) => {
99
+ console.error(`[${logPrefix}.onError] Callback error:`, callbackError);
100
+ });
101
+ }
102
+ } catch (callbackError) {
103
+ // Log but don't throw - we don't want callback errors to mask the original error
104
+ console.error(`[${logPrefix}.onError] Callback error:`, callbackError);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Find the nearest error boundary by walking up the entry chain
110
+ * Also checks sibling layouts (orphan layouts) for error boundaries
111
+ * Returns the first fallback found, or the default error boundary if configured
112
+ */
113
+ export function findNearestErrorBoundary(
114
+ entry: EntryData | null,
115
+ defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler
116
+ ): ReactNode | ErrorBoundaryHandler | null {
117
+ let current: EntryData | null = entry;
118
+
119
+ while (current) {
120
+ // Check if this entry has error boundaries defined
121
+ if (current.errorBoundary && current.errorBoundary.length > 0) {
122
+ // Return the last error boundary (most recently defined takes precedence)
123
+ return current.errorBoundary[current.errorBoundary.length - 1];
124
+ }
125
+
126
+ // Check orphan layouts for error boundaries
127
+ // Orphan layouts are siblings that render alongside the main route chain
128
+ // They can define error boundaries that catch errors from routes in the same route group
129
+ // Check from first to last (first sibling takes precedence as the "outer" wrapper)
130
+ if (current.layout && current.layout.length > 0) {
131
+ for (const orphan of current.layout) {
132
+ if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
133
+ return orphan.errorBoundary[orphan.errorBoundary.length - 1];
134
+ }
135
+ }
136
+ }
137
+
138
+ current = current.parent;
139
+ }
140
+
141
+ // Return default error boundary if configured
142
+ return defaultErrorBoundary || null;
143
+ }
144
+
145
+ /**
146
+ * Find the nearest notFound boundary by walking up the entry chain
147
+ * Returns the first fallback found, or the default notFound boundary if configured
148
+ */
149
+ export function findNearestNotFoundBoundary(
150
+ entry: EntryData | null,
151
+ defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler
152
+ ): ReactNode | NotFoundBoundaryHandler | null {
153
+ let current: EntryData | null = entry;
154
+
155
+ while (current) {
156
+ // Check if this entry has notFound boundaries defined
157
+ if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
158
+ // Return the last notFound boundary (most recently defined takes precedence)
159
+ return current.notFoundBoundary[current.notFoundBoundary.length - 1];
160
+ }
161
+ current = current.parent;
162
+ }
163
+
164
+ // Return default notFound boundary if configured
165
+ return defaultNotFoundBoundary || null;
166
+ }
167
+
168
+ /**
169
+ * Create ErrorInfo from an error object
170
+ * Sanitizes error details in production
171
+ */
172
+ export function createErrorInfo(
173
+ error: unknown,
174
+ segmentId: string,
175
+ segmentType: ErrorInfo["segmentType"]
176
+ ): ErrorInfo {
177
+ const isDev = process.env.NODE_ENV !== "production";
178
+
179
+ if (error instanceof Error) {
180
+ return {
181
+ message: isDev ? error.message : "An error occurred",
182
+ name: error.name,
183
+ code: (error as any).code,
184
+ stack: isDev ? error.stack : undefined,
185
+ cause: isDev ? error.cause : undefined,
186
+ segmentId,
187
+ segmentType,
188
+ };
189
+ }
190
+
191
+ // Non-Error thrown
192
+ return {
193
+ message: isDev ? String(error) : "An error occurred",
194
+ name: "Error",
195
+ segmentId,
196
+ segmentType,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Create an error segment with the fallback component
202
+ * Renders the fallback with error info and reset function
203
+ */
204
+ export function createErrorSegment(
205
+ errorInfo: ErrorInfo,
206
+ fallback: ReactNode | ErrorBoundaryHandler,
207
+ entry: EntryData,
208
+ params: Record<string, string>
209
+ ): ResolvedSegment {
210
+ // Determine the component to render
211
+ let component: ReactNode;
212
+
213
+ if (typeof fallback === "function") {
214
+ // ErrorBoundaryHandler - call with error info
215
+ const props: ErrorBoundaryFallbackProps = {
216
+ error: errorInfo,
217
+ };
218
+ component = fallback(props);
219
+ } else {
220
+ // Static ReactNode fallback
221
+ component = fallback;
222
+ }
223
+
224
+ // Error segment uses the same ID as the layout that has the error boundary
225
+ // The error boundary content replaces the layout's outlet content
226
+ return {
227
+ id: entry.shortCode,
228
+ namespace: entry.id,
229
+ type: "error",
230
+ index: 0,
231
+ component,
232
+ params,
233
+ error: errorInfo,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Create NotFoundInfo from a DataNotFoundError
239
+ */
240
+ export function createNotFoundInfo(
241
+ error: { message: string },
242
+ segmentId: string,
243
+ segmentType: NotFoundInfo["segmentType"],
244
+ pathname?: string
245
+ ): NotFoundInfo {
246
+ return {
247
+ message: error.message,
248
+ segmentId,
249
+ segmentType,
250
+ pathname,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Create a notFound segment with the fallback component
256
+ * Renders the fallback with not found info
257
+ */
258
+ export function createNotFoundSegment(
259
+ notFoundInfo: NotFoundInfo,
260
+ fallback: ReactNode | NotFoundBoundaryHandler,
261
+ entry: EntryData,
262
+ params: Record<string, string>
263
+ ): ResolvedSegment {
264
+ // Determine the component to render
265
+ let component: ReactNode;
266
+
267
+ if (typeof fallback === "function") {
268
+ // NotFoundBoundaryHandler - call with props
269
+ const props: NotFoundBoundaryFallbackProps = {
270
+ notFound: notFoundInfo,
271
+ };
272
+ component = fallback(props);
273
+ } else {
274
+ // Static ReactNode fallback
275
+ component = fallback;
276
+ }
277
+
278
+ return {
279
+ id: `${entry.shortCode}.notFound`,
280
+ namespace: entry.id,
281
+ type: "notFound",
282
+ index: 0,
283
+ component,
284
+ params,
285
+ notFoundInfo,
286
+ };
287
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Router Handler Context
3
+ *
4
+ * Creates the handler context object passed to route handlers, middleware, and loaders.
5
+ */
6
+
7
+ import type { HandlerContext } from "../types";
8
+ import { getRequestContext } from "../server/request-context.js";
9
+
10
+ /**
11
+ * Create HandlerContext with typed env/var/get/set
12
+ */
13
+ export function createHandlerContext<TEnv>(
14
+ params: Record<string, string>,
15
+ request: Request,
16
+ searchParams: URLSearchParams,
17
+ pathname: string,
18
+ url: URL,
19
+ bindings: any = {}
20
+ ): HandlerContext<any, TEnv> {
21
+ // Get variables from request context - this is the unified context
22
+ // shared between middleware and route handlers
23
+ const requestContext = getRequestContext();
24
+ const variables: any = requestContext?.var ?? {};
25
+
26
+ // Filter system parameters (starting with _rsc) from searchParams
27
+ // This ensures handlers only see user-facing query params
28
+ const cleanSearchParams = new URLSearchParams();
29
+ searchParams.forEach((value, key) => {
30
+ if (!key.startsWith("_rsc")) {
31
+ cleanSearchParams.set(key, value);
32
+ }
33
+ });
34
+
35
+ // Create clean URL without system params
36
+ const cleanUrl = new URL(url);
37
+ cleanUrl.search = cleanSearchParams.toString();
38
+
39
+ return {
40
+ params,
41
+ request,
42
+ searchParams: cleanSearchParams, // Filtered params
43
+ pathname,
44
+ url: cleanUrl, // Clean URL
45
+ env: bindings,
46
+ var: variables,
47
+ get: ((key: string) => variables[key]) as HandlerContext<
48
+ any,
49
+ TEnv
50
+ >["get"],
51
+ set: ((key: string, value: any) => {
52
+ variables[key] = value;
53
+ }) as HandlerContext<any, TEnv>["set"],
54
+ _originalRequest: request, // Raw request for advanced use
55
+ // Placeholder use() - will be replaced with actual implementation during request
56
+ use: () => {
57
+ throw new Error("ctx.use() called before loaders were initialized");
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Router Loader Resolution
3
+ *
4
+ * Loader execution, memoization, and error handling utilities.
5
+ */
6
+
7
+ import type { ReactNode } from "react";
8
+ import { track } from "../server/context";
9
+ import type { EntryData } from "../server/context";
10
+ import type {
11
+ ResolvedSegment,
12
+ HandlerContext,
13
+ LoaderDefinition,
14
+ LoaderContext,
15
+ LoaderDataResult,
16
+ ErrorBoundaryHandler,
17
+ ErrorBoundaryFallbackProps,
18
+ ErrorInfo,
19
+ } from "../types";
20
+ import type { LoaderRevalidationResult, ActionContext } from "./types";
21
+ import { isHandle, type Handle } from "../handle.js";
22
+ import type { HandleStore } from "../server/handle-store.js";
23
+ import { getFetchableLoader } from "../loader.rsc.js";
24
+ import { getRequestContext } from "../server/request-context.js";
25
+
26
+ /**
27
+ * Internal callback signature for loader error notifications.
28
+ * This is a simplified callback for internal use in wrapLoaderWithErrorHandling.
29
+ * The caller (wrapLoaderPromise in router.ts) bridges this to the full OnErrorCallback.
30
+ */
31
+ export type LoaderErrorCallback = (
32
+ error: unknown,
33
+ context: {
34
+ segmentId: string;
35
+ loaderName: string;
36
+ handledByBoundary: boolean;
37
+ }
38
+ ) => void;
39
+
40
+ /**
41
+ * Wrap a loader promise with error handling for deferred client-side resolution.
42
+ * Catches errors and converts them to LoaderDataResult objects that include
43
+ * error info and pre-rendered fallback UI when an error boundary is available.
44
+ *
45
+ * @param onError - Optional callback invoked when loader errors occur.
46
+ * This has a simplified signature for internal use - the caller (typically
47
+ * wrapLoaderPromise in router.ts) is responsible for bridging to the full
48
+ * OnErrorCallback with complete request context (request, url, env, etc.).
49
+ */
50
+ export function wrapLoaderWithErrorHandling<T>(
51
+ promise: Promise<T>,
52
+ entry: EntryData,
53
+ segmentId: string,
54
+ pathname: string,
55
+ findNearestErrorBoundary: (
56
+ entry: EntryData | null
57
+ ) => ReactNode | ErrorBoundaryHandler | null,
58
+ createErrorInfo: (
59
+ error: unknown,
60
+ segmentId: string,
61
+ segmentType: ErrorInfo["segmentType"]
62
+ ) => ErrorInfo,
63
+ onError?: LoaderErrorCallback
64
+ ): Promise<LoaderDataResult<T>> {
65
+ // Extract loader name from segmentId (format: "M1L0D0.loaderName")
66
+ const loaderName = segmentId.split(".").pop() || "unknown";
67
+
68
+ return Promise.resolve(promise)
69
+ .then(
70
+ (data): LoaderDataResult<T> => ({
71
+ __loaderResult: true,
72
+ ok: true,
73
+ data,
74
+ })
75
+ )
76
+ .catch((error): LoaderDataResult<T> => {
77
+ // Find nearest error boundary
78
+ const fallback = findNearestErrorBoundary(entry);
79
+
80
+ // Create error info
81
+ const errorInfo = createErrorInfo(error, segmentId, "loader");
82
+
83
+ // Invoke onError callback if provided
84
+ onError?.(error, {
85
+ segmentId,
86
+ loaderName,
87
+ handledByBoundary: !!fallback,
88
+ });
89
+
90
+ if (!fallback) {
91
+ // No error boundary - return error result without fallback
92
+ // Client will throw this error
93
+ return {
94
+ __loaderResult: true,
95
+ ok: false,
96
+ error: errorInfo,
97
+ fallback: null,
98
+ };
99
+ }
100
+
101
+ // Render fallback on server
102
+ let renderedFallback: ReactNode;
103
+ if (typeof fallback === "function") {
104
+ // ErrorBoundaryHandler - call with error info
105
+ const props: ErrorBoundaryFallbackProps = {
106
+ error: errorInfo,
107
+ };
108
+ renderedFallback = fallback(props);
109
+ } else {
110
+ renderedFallback = fallback;
111
+ }
112
+
113
+ console.log(
114
+ `[Router] Loader error wrapped with boundary fallback in ${segmentId}:`,
115
+ errorInfo.message
116
+ );
117
+
118
+ return {
119
+ __loaderResult: true,
120
+ ok: false,
121
+ error: errorInfo,
122
+ fallback: renderedFallback,
123
+ };
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Set up the use() method on handler context to access loaders and handles.
129
+ *
130
+ * For loaders: Lazily runs loaders, memoizes results per request.
131
+ * For handles: Returns a push function bound to the current segment.
132
+ */
133
+ export function setupLoaderAccess<TEnv>(
134
+ ctx: HandlerContext<any, TEnv>,
135
+ loaderPromises: Map<string, Promise<any>>
136
+ ): void {
137
+ // Get HandleStore from request context
138
+ const getHandleStore = (): HandleStore | undefined => {
139
+ return getRequestContext()?._handleStore;
140
+ };
141
+
142
+ // The use() function handles both loaders and handles
143
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
144
+ // Handle case: return a push function
145
+ if (isHandle(item)) {
146
+ const handle = item;
147
+ const store = getHandleStore();
148
+ const segmentId = ctx._currentSegmentId;
149
+
150
+ if (!segmentId) {
151
+ throw new Error(
152
+ `Handle "${handle.$$id}" used outside of handler context. ` +
153
+ `Handles must be used within route/layout handlers.`
154
+ );
155
+ }
156
+
157
+ // Return a push function bound to this handle and segment
158
+ // Accepts: value, Promise, or async callback (executed immediately)
159
+ // Promises are pushed directly - RSC will serialize and stream them
160
+ return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
161
+ if (!store) return;
162
+
163
+ // If it's a function, call it immediately to get the promise
164
+ const valueOrPromise = typeof dataOrFn === "function"
165
+ ? (dataOrFn as () => Promise<unknown>)()
166
+ : dataOrFn;
167
+
168
+ // Push directly - promises will be serialized by RSC and streamed
169
+ store.push(handle.$$id, segmentId, valueOrPromise);
170
+ };
171
+ }
172
+
173
+ // Loader case: existing behavior
174
+ const loader = item as LoaderDefinition<any, any>;
175
+
176
+ // Return cached promise if already started
177
+ if (loaderPromises.has(loader.$$id)) {
178
+ return loaderPromises.get(loader.$$id);
179
+ }
180
+
181
+ // Get loader function - either from loader object or fetchable registry
182
+ // Fetchable loaders store fn in registry (not on object) to avoid client bundling issues
183
+ let loaderFn = loader.fn;
184
+ if (!loaderFn) {
185
+ const fetchable = getFetchableLoader(loader.$$id);
186
+ if (fetchable) {
187
+ loaderFn = fetchable.fn;
188
+ }
189
+ }
190
+
191
+ // Ensure loader has a function
192
+ if (!loaderFn) {
193
+ throw new Error(
194
+ `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.`
195
+ );
196
+ }
197
+
198
+ // Create loader context with recursive use() support
199
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
200
+ params: ctx.params,
201
+ request: ctx.request,
202
+ searchParams: ctx.searchParams,
203
+ pathname: ctx.pathname,
204
+ url: ctx.url,
205
+ env: ctx.env,
206
+ var: ctx.var,
207
+ get: ctx.get,
208
+ use: <TDep, TDepParams = any>(
209
+ dep: LoaderDefinition<TDep, TDepParams>
210
+ ): Promise<TDep> => {
211
+ // Recursive call - will start dep loader if not already started
212
+ return ctx.use(dep);
213
+ },
214
+ // Default to GET for loaders called through route handlers
215
+ method: "GET",
216
+ body: undefined,
217
+ };
218
+
219
+ // Start loader execution with tracking
220
+ const doneLoader = track(`loader:${loader.$$id}`);
221
+ const promise = Promise.resolve(
222
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>)
223
+ ).finally(() => {
224
+ doneLoader();
225
+ });
226
+
227
+ // Memoize for subsequent calls
228
+ loaderPromises.set(loader.$$id, promise);
229
+
230
+ return promise;
231
+ }) as typeof ctx.use;
232
+ }
233
+
234
+ /**
235
+ * Set up ctx.use() for proactive caching (silent mode).
236
+ * Handles are silently ignored (no push to HandleStore).
237
+ * Loaders work normally but with fresh memoization.
238
+ *
239
+ * This prevents duplicate handle data (breadcrumbs, meta) from being
240
+ * pushed to the response stream during background proactive caching.
241
+ */
242
+ export function setupLoaderAccessSilent<TEnv>(
243
+ ctx: HandlerContext<any, TEnv>,
244
+ loaderPromises: Map<string, Promise<any>>
245
+ ): void {
246
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
247
+ // Handle case: return a no-op push function
248
+ if (isHandle(item)) {
249
+ // Silent mode - return a function that does nothing
250
+ return (_dataOrFn: unknown) => {
251
+ // Intentionally empty - don't push handle data during proactive caching
252
+ };
253
+ }
254
+
255
+ // Loader case: same as setupLoaderAccess
256
+ const loader = item as LoaderDefinition<any, any>;
257
+
258
+ // Return cached promise if already started
259
+ if (loaderPromises.has(loader.$$id)) {
260
+ return loaderPromises.get(loader.$$id);
261
+ }
262
+
263
+ // Get loader function
264
+ let loaderFn = loader.fn;
265
+ if (!loaderFn) {
266
+ const fetchable = getFetchableLoader(loader.$$id);
267
+ if (fetchable) {
268
+ loaderFn = fetchable.fn;
269
+ }
270
+ }
271
+
272
+ if (!loaderFn) {
273
+ throw new Error(
274
+ `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.`
275
+ );
276
+ }
277
+
278
+ // Create loader context with recursive use() support
279
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
280
+ params: ctx.params,
281
+ request: ctx.request,
282
+ searchParams: ctx.searchParams,
283
+ pathname: ctx.pathname,
284
+ url: ctx.url,
285
+ env: ctx.env,
286
+ var: ctx.var,
287
+ get: ctx.get,
288
+ use: <TDep, TDepParams = any>(
289
+ dep: LoaderDefinition<TDep, TDepParams>
290
+ ): Promise<TDep> => {
291
+ return ctx.use(dep);
292
+ },
293
+ method: "GET",
294
+ body: undefined,
295
+ };
296
+
297
+ // Start loader execution with tracking
298
+ const doneLoader = track(`loader:${loader.$$id}`);
299
+ const promise = Promise.resolve(
300
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>)
301
+ ).finally(() => {
302
+ doneLoader();
303
+ });
304
+
305
+ loaderPromises.set(loader.$$id, promise);
306
+ return promise;
307
+ }) as typeof ctx.use;
308
+ }
309
+
310
+ /**
311
+ * Conditional execution based on revalidation
312
+ * Evaluates revalidation logic lazily, then executes appropriate callback
313
+ *
314
+ * @param shouldRevalidate - Async function that determines if revalidation is needed
315
+ * @param onRevalidate - Callback executed if revalidation returns true
316
+ * @param onSkip - Callback executed if revalidation returns false
317
+ * @returns Result from either onRevalidate or onSkip
318
+ */
319
+ export async function revalidate<T>(
320
+ shouldRevalidate: () => Promise<boolean>,
321
+ onRevalidate: () => Promise<T>,
322
+ onSkip: () => T
323
+ ): Promise<T> {
324
+ const needsRevalidation = await shouldRevalidate();
325
+ return needsRevalidation ? await onRevalidate() : onSkip();
326
+ }