@octanejs/remix-router 0.1.1 → 0.1.2

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 (46) hide show
  1. package/README.md +20 -6
  2. package/package.json +12 -7
  3. package/src/index.ts +16 -14
  4. package/src/lib/Await.tsrx +1 -1
  5. package/src/lib/DefaultErrorComponent.tsrx +3 -3
  6. package/src/lib/RenderErrorBoundary.tsrx +1 -1
  7. package/src/lib/actions.ts +1 -1
  8. package/src/lib/components/MemoryRouter.tsrx +1 -1
  9. package/src/lib/components/Navigate.ts +1 -1
  10. package/src/lib/components/Router.tsrx +1 -1
  11. package/src/lib/components/RouterProvider.tsrx +1 -1
  12. package/src/lib/components/Routes.tsrx +1 -1
  13. package/src/lib/components/routes-collector.ts +1 -8
  14. package/src/lib/components/utils.ts +4 -65
  15. package/src/lib/components/with-props.ts +1 -1
  16. package/src/lib/context.ts +2 -4
  17. package/src/lib/dom/Form.tsrx +1 -1
  18. package/src/lib/dom/Link.tsrx +1 -1
  19. package/src/lib/dom/NavLink.tsrx +1 -1
  20. package/src/lib/dom/dom.ts +1 -1
  21. package/src/lib/dom/hooks.ts +1 -1
  22. package/src/lib/dom/lib.ts +8 -7
  23. package/src/lib/dom/routers.tsrx +1 -1
  24. package/src/lib/dom/server.tsrx +6 -28
  25. package/src/lib/errors.ts +1 -1
  26. package/src/lib/hooks.ts +6 -16
  27. package/src/lib/href.ts +9 -4
  28. package/src/lib/router/history.ts +2 -2
  29. package/src/lib/router/instrumentation.ts +391 -187
  30. package/src/lib/router/links.ts +1 -1
  31. package/src/lib/router/router.ts +128 -78
  32. package/src/lib/router/server-runtime-types.ts +8 -11
  33. package/src/lib/router/url.ts +1 -1
  34. package/src/lib/router/utils.ts +107 -31
  35. package/src/lib/server-runtime/cookies.ts +7 -7
  36. package/src/lib/server-runtime/crypto.ts +2 -2
  37. package/src/lib/server-runtime/mode.ts +1 -1
  38. package/src/lib/server-runtime/sessions/cookieStorage.ts +1 -1
  39. package/src/lib/server-runtime/sessions/memoryStorage.ts +1 -1
  40. package/src/lib/server-runtime/sessions.ts +8 -5
  41. package/src/lib/server-runtime/warnings.ts +1 -1
  42. package/src/lib/types/future.ts +1 -7
  43. package/src/lib/types/params.ts +1 -1
  44. package/src/lib/types/register.ts +1 -1
  45. package/src/lib/types/route-module.ts +1 -1
  46. package/src/lib/types/utils.ts +1 -1
@@ -1,9 +1,9 @@
1
- // Vendored from react-router@7.18.1 packages/react-router/lib/router/instrumentation.ts — unmodified except: type-only ../server-runtime/* imports → local ./server-runtime-types stub.
1
+ // Vendored from react-router@8.2.0 packages/react-router/lib/router/instrumentation.ts — unmodified except: type-only ../server-runtime/server import → local ./server-runtime-types stub.
2
2
  // Re-vendor with `node scripts/vendor-remix-router.mjs`; never hand-edit.
3
- import type { AppLoadContext, RequestHandler } from './server-runtime-types';
4
- import type { MiddlewareEnabled } from '../types/future';
3
+ import type { RequestHandler } from './server-runtime-types';
5
4
  import { createPath, invariant } from './history';
6
5
  import type { Router } from './router';
6
+ import { createContext, RouterContextProvider } from './utils';
7
7
  import type {
8
8
  ActionFunctionArgs,
9
9
  DataRouteObject,
@@ -15,7 +15,6 @@ import type {
15
15
  MaybePromise,
16
16
  MiddlewareFunction,
17
17
  RouterContext,
18
- RouterContextProvider,
19
18
  } from './utils';
20
19
 
21
20
  // Public APIs
@@ -35,13 +34,45 @@ export type InstrumentRouterFunction = (router: InstrumentableRouter) => void;
35
34
 
36
35
  export type InstrumentRouteFunction = (route: InstrumentableRoute) => void;
37
36
 
37
+ /**
38
+ * Route metadata available after React Router has matched an instrumented
39
+ * request, navigation, or fetcher call.
40
+ */
41
+ export type InstrumentationResultMeta = {
42
+ url: LoaderFunctionArgs['url'];
43
+ pattern: string;
44
+ params: LoaderFunctionArgs['params'];
45
+ };
46
+
47
+ /**
48
+ * Result returned by route-level instrumented handler calls, such as
49
+ * instrumented loaders, actions, middleware, and lazy route functions.
50
+ */
38
51
  export type InstrumentationHandlerResult =
39
52
  | { status: 'success'; error: undefined }
40
53
  | { status: 'error'; error: Error };
41
54
 
55
+ /**
56
+ * Result returned by client-side router instrumented navigation and fetcher
57
+ * calls.
58
+ */
59
+ export type InstrumentationClientRouterResult = InstrumentationHandlerResult & {
60
+ meta: InstrumentationResultMeta | undefined;
61
+ };
62
+
63
+ /**
64
+ * Result returned by server request handler instrumentation.
65
+ */
66
+ export type InstrumentationServerHandlerResult = InstrumentationHandlerResult & {
67
+ statusCode: number;
68
+ meta: InstrumentationResultMeta | undefined;
69
+ };
70
+
71
+ export type InstrumentationMetaReceiver = (meta: InstrumentationResultMeta | undefined) => void;
72
+
42
73
  // Shared
43
- type InstrumentFunction<T> = (
44
- handler: () => Promise<InstrumentationHandlerResult>,
74
+ type InstrumentFunction<T, TInnerResult = InstrumentationHandlerResult> = (
75
+ handler: () => Promise<TInnerResult>,
45
76
  info: T,
46
77
  ) => Promise<void>;
47
78
 
@@ -58,9 +89,7 @@ type ReadonlyRequest = {
58
89
  headers: Pick<Headers, 'get'>;
59
90
  };
60
91
 
61
- type ReadonlyContext = MiddlewareEnabled extends true
62
- ? Pick<RouterContextProvider, 'get'>
63
- : Readonly<AppLoadContext>;
92
+ type ReadonlyContext = Pick<RouterContextProvider, 'get'>;
64
93
 
65
94
  // Route Instrumentation
66
95
  type InstrumentableRoute = {
@@ -82,12 +111,12 @@ type RouteInstrumentations = {
82
111
 
83
112
  type RouteLazyInstrumentationInfo = undefined;
84
113
 
85
- type RouteHandlerInstrumentationInfo = Readonly<{
86
- request: ReadonlyRequest;
87
- params: LoaderFunctionArgs['params'];
88
- pattern: string;
89
- context: ReadonlyContext;
90
- }>;
114
+ type RouteHandlerInstrumentationInfo = Readonly<
115
+ Omit<LoaderFunctionArgs, 'request' | 'context'> & {
116
+ request: ReadonlyRequest;
117
+ context: ReadonlyContext;
118
+ }
119
+ >;
91
120
 
92
121
  // Router Instrumentation
93
122
  type InstrumentableRouter = {
@@ -95,8 +124,11 @@ type InstrumentableRouter = {
95
124
  };
96
125
 
97
126
  type RouterInstrumentations = {
98
- navigate?: InstrumentFunction<RouterNavigationInstrumentationInfo>;
99
- fetch?: InstrumentFunction<RouterFetchInstrumentationInfo>;
127
+ navigate?: InstrumentFunction<
128
+ RouterNavigationInstrumentationInfo,
129
+ InstrumentationClientRouterResult
130
+ >;
131
+ fetch?: InstrumentFunction<RouterFetchInstrumentationInfo, InstrumentationClientRouterResult>;
100
132
  };
101
133
 
102
134
  type RouterNavigationInstrumentationInfo = Readonly<{
@@ -124,7 +156,10 @@ type InstrumentableRequestHandler = {
124
156
  };
125
157
 
126
158
  type RequestHandlerInstrumentations = {
127
- request?: InstrumentFunction<RequestHandlerInstrumentationInfo>;
159
+ request?: InstrumentFunction<
160
+ RequestHandlerInstrumentationInfo,
161
+ InstrumentationServerHandlerResult
162
+ >;
128
163
  };
129
164
 
130
165
  type RequestHandlerInstrumentationInfo = Readonly<{
@@ -134,18 +169,31 @@ type RequestHandlerInstrumentationInfo = Readonly<{
134
169
 
135
170
  const UninstrumentedSymbol = Symbol('Uninstrumented');
136
171
 
172
+ type InstrumentableFunction = (...args: never[]) => MaybePromise<unknown>;
173
+ type InstrumentedFunction<T extends InstrumentableFunction> = T & {
174
+ [UninstrumentedSymbol]?: T;
175
+ };
176
+
177
+ export const instrumentationResultMetaContext = createContext<InstrumentationResultMeta>();
178
+
179
+ // Client router instrumentations need route metadata captured inside the router
180
+ // after matching, but exposing this on public router state/options would make it
181
+ // part of the API. Keep a private, one-shot receiver keyed by router instance so
182
+ // navigate/fetch instrumentation can receive the metadata without leaking it.
183
+ let instrumentationClientResultMetaReceivers = new WeakMap<Router, InstrumentationMetaReceiver>();
184
+
137
185
  export function getRouteInstrumentationUpdates(
138
186
  fns: InstrumentRouteFunction[],
139
187
  route: Readonly<DataRouteObject>,
140
188
  ) {
141
189
  let aggregated: {
142
- lazy: InstrumentFunction<RouteLazyInstrumentationInfo>[];
143
- 'lazy.loader': InstrumentFunction<RouteLazyInstrumentationInfo>[];
144
- 'lazy.action': InstrumentFunction<RouteLazyInstrumentationInfo>[];
145
- 'lazy.middleware': InstrumentFunction<RouteLazyInstrumentationInfo>[];
146
- middleware: InstrumentFunction<RouteHandlerInstrumentationInfo>[];
147
- loader: InstrumentFunction<RouteHandlerInstrumentationInfo>[];
148
- action: InstrumentFunction<RouteHandlerInstrumentationInfo>[];
190
+ lazy: NonNullable<RouteInstrumentations['lazy']>[];
191
+ 'lazy.loader': NonNullable<RouteInstrumentations['lazy.loader']>[];
192
+ 'lazy.action': NonNullable<RouteInstrumentations['lazy.action']>[];
193
+ 'lazy.middleware': NonNullable<RouteInstrumentations['lazy.middleware']>[];
194
+ middleware: NonNullable<RouteInstrumentations['middleware']>[];
195
+ loader: NonNullable<RouteInstrumentations['loader']>[];
196
+ action: NonNullable<RouteInstrumentations['action']>[];
149
197
  } = {
150
198
  lazy: [],
151
199
  'lazy.loader': [],
@@ -162,11 +210,26 @@ export function getRouteInstrumentationUpdates(
162
210
  index: route.index,
163
211
  path: route.path,
164
212
  instrument(i) {
165
- let keys = Object.keys(aggregated) as Array<keyof typeof aggregated>;
166
- for (let key of keys) {
167
- if (i[key]) {
168
- aggregated[key].push(i[key] as any);
169
- }
213
+ if (i.lazy != null) {
214
+ aggregated.lazy.push(i.lazy);
215
+ }
216
+ if (i['lazy.loader'] != null) {
217
+ aggregated['lazy.loader'].push(i['lazy.loader']);
218
+ }
219
+ if (i['lazy.action'] != null) {
220
+ aggregated['lazy.action'].push(i['lazy.action']);
221
+ }
222
+ if (i['lazy.middleware'] != null) {
223
+ aggregated['lazy.middleware'].push(i['lazy.middleware']);
224
+ }
225
+ if (i.middleware != null) {
226
+ aggregated.middleware.push(i.middleware);
227
+ }
228
+ if (i.loader != null) {
229
+ aggregated.loader.push(i.loader);
230
+ }
231
+ if (i.action != null) {
232
+ aggregated.action.push(i.action);
170
233
  }
171
234
  },
172
235
  }),
@@ -181,63 +244,123 @@ export function getRouteInstrumentationUpdates(
181
244
 
182
245
  // Instrument lazy functions
183
246
  if (typeof route.lazy === 'function' && aggregated.lazy.length > 0) {
184
- let instrumented = wrapImpl(aggregated.lazy, route.lazy, () => undefined);
185
- if (instrumented) {
186
- updates.lazy = instrumented as DataRouteObject['lazy'];
187
- }
247
+ let lazy = route.lazy;
248
+ updates.lazy = async (...args) => {
249
+ let result = await recurseRight(
250
+ aggregated.lazy,
251
+ undefined,
252
+ () => lazy(...args),
253
+ getInstrumentationInnerResult,
254
+ );
255
+ return throwOrReturnResult(result);
256
+ };
188
257
  }
189
258
 
190
259
  // Instrument the lazy object format
191
260
  if (typeof route.lazy === 'object') {
192
261
  let lazyObject: LazyRouteObject<DataRouteObject> = route.lazy;
193
- (['middleware', 'loader', 'action'] as const).forEach((key) => {
194
- let lazyFn = lazyObject[key];
195
- let instrumentations = aggregated[`lazy.${key}`];
196
- if (typeof lazyFn === 'function' && instrumentations.length > 0) {
197
- let instrumented = wrapImpl(instrumentations, lazyFn, () => undefined);
198
- if (instrumented) {
199
- updates.lazy = Object.assign(updates.lazy || {}, {
200
- [key]: instrumented,
201
- });
202
- }
203
- }
204
- });
262
+
263
+ if (typeof lazyObject.middleware === 'function' && aggregated['lazy.middleware'].length > 0) {
264
+ let middleware = lazyObject.middleware;
265
+ updates.lazy = Object.assign(updates.lazy || {}, {
266
+ middleware: async (
267
+ ...args: Parameters<NonNullable<LazyRouteObject<DataRouteObject>['middleware']>>
268
+ ) => {
269
+ let result = await recurseRight(
270
+ aggregated['lazy.middleware'],
271
+ undefined,
272
+ () => middleware(...args),
273
+ getInstrumentationInnerResult,
274
+ );
275
+ return throwOrReturnResult(result);
276
+ },
277
+ });
278
+ }
279
+
280
+ if (typeof lazyObject.loader === 'function' && aggregated['lazy.loader'].length > 0) {
281
+ let loader = lazyObject.loader;
282
+ updates.lazy = Object.assign(updates.lazy || {}, {
283
+ loader: async (
284
+ ...args: Parameters<NonNullable<LazyRouteObject<DataRouteObject>['loader']>>
285
+ ) => {
286
+ let result = await recurseRight(
287
+ aggregated['lazy.loader'],
288
+ undefined,
289
+ () => loader(...args),
290
+ getInstrumentationInnerResult,
291
+ );
292
+ return throwOrReturnResult(result);
293
+ },
294
+ });
295
+ }
296
+
297
+ if (typeof lazyObject.action === 'function' && aggregated['lazy.action'].length > 0) {
298
+ let action = lazyObject.action;
299
+ updates.lazy = Object.assign(updates.lazy || {}, {
300
+ action: async (
301
+ ...args: Parameters<NonNullable<LazyRouteObject<DataRouteObject>['action']>>
302
+ ) => {
303
+ let result = await recurseRight(
304
+ aggregated['lazy.action'],
305
+ undefined,
306
+ () => action(...args),
307
+ getInstrumentationInnerResult,
308
+ );
309
+ return throwOrReturnResult(result);
310
+ },
311
+ });
312
+ }
205
313
  }
206
314
 
207
315
  // Instrument loader/action functions
208
- (['loader', 'action'] as const).forEach((key) => {
209
- let handler = route[key];
210
- if (typeof handler === 'function' && aggregated[key].length > 0) {
211
- // @ts-expect-error
212
- let original = handler[UninstrumentedSymbol] ?? handler;
213
- let instrumented = wrapImpl(aggregated[key], original, (...args) =>
214
- getHandlerInfo(args[0] as LoaderFunctionArgs | ActionFunctionArgs),
316
+ if (typeof route.loader === 'function' && aggregated.loader.length > 0) {
317
+ let original = getUninstrumentedHandler(route.loader);
318
+ let instrumented = async (...args: Parameters<typeof original>) => {
319
+ let result = await recurseRight(
320
+ aggregated.loader,
321
+ getHandlerInfo(args[0]),
322
+ () => original(...args),
323
+ getInstrumentationInnerResult,
215
324
  );
216
- if (instrumented) {
217
- if (key === 'loader' && original.hydrate === true) {
218
- (instrumented as LoaderFunction).hydrate = true;
219
- }
220
- // @ts-expect-error
221
- instrumented[UninstrumentedSymbol] = original;
222
- updates[key] = instrumented;
223
- }
325
+ return throwOrReturnResult(result);
326
+ };
327
+ if (original.hydrate === true) {
328
+ (instrumented as LoaderFunction).hydrate = true;
224
329
  }
225
- });
330
+ setUninstrumentedHandler(instrumented, original);
331
+ updates.loader = instrumented;
332
+ }
333
+
334
+ if (typeof route.action === 'function' && aggregated.action.length > 0) {
335
+ let original = getUninstrumentedHandler(route.action);
336
+ let instrumented = async (...args: Parameters<typeof original>) => {
337
+ let result = await recurseRight(
338
+ aggregated.action,
339
+ getHandlerInfo(args[0]),
340
+ () => original(...args),
341
+ getInstrumentationInnerResult,
342
+ );
343
+ return throwOrReturnResult(result);
344
+ };
345
+ setUninstrumentedHandler(instrumented, original);
346
+ updates.action = instrumented;
347
+ }
226
348
 
227
349
  // Instrument middleware functions
228
350
  if (route.middleware && route.middleware.length > 0 && aggregated.middleware.length > 0) {
229
351
  updates.middleware = route.middleware.map((middleware) => {
230
- // @ts-expect-error
231
- let original = middleware[UninstrumentedSymbol] ?? middleware;
232
- let instrumented = wrapImpl(aggregated.middleware, original, (...args) =>
233
- getHandlerInfo(args[0] as Parameters<MiddlewareFunction>[0]),
234
- );
235
- if (instrumented) {
236
- // @ts-expect-error
237
- instrumented[UninstrumentedSymbol] = original;
238
- return instrumented;
239
- }
240
- return middleware;
352
+ let original = getUninstrumentedHandler(middleware);
353
+ let instrumented = async (...args: Parameters<typeof original>) => {
354
+ let result = await recurseRight(
355
+ aggregated.middleware,
356
+ getHandlerInfo(args[0]),
357
+ () => original(...args),
358
+ getInstrumentationInnerResult,
359
+ );
360
+ return throwOrReturnResult(result);
361
+ };
362
+ setUninstrumentedHandler(instrumented, original);
363
+ return instrumented;
241
364
  });
242
365
  }
243
366
 
@@ -249,8 +372,8 @@ export function instrumentClientSideRouter(
249
372
  fns: InstrumentRouterFunction[],
250
373
  ): Router {
251
374
  let aggregated: {
252
- navigate: InstrumentFunction<RouterNavigationInstrumentationInfo>[];
253
- fetch: InstrumentFunction<RouterFetchInstrumentationInfo>[];
375
+ navigate: NonNullable<RouterInstrumentations['navigate']>[];
376
+ fetch: NonNullable<RouterInstrumentations['fetch']>[];
254
377
  } = {
255
378
  navigate: [],
256
379
  fetch: [],
@@ -259,49 +382,85 @@ export function instrumentClientSideRouter(
259
382
  fns.forEach((fn) =>
260
383
  fn({
261
384
  instrument(i) {
262
- let keys = Object.keys(i) as Array<keyof RouterInstrumentations>;
263
- for (let key of keys) {
264
- if (i[key]) {
265
- aggregated[key].push(i[key] as any);
266
- }
385
+ if (i.navigate != null) {
386
+ aggregated.navigate.push(i.navigate);
387
+ }
388
+ if (i.fetch != null) {
389
+ aggregated.fetch.push(i.fetch);
267
390
  }
268
391
  },
269
392
  }),
270
393
  );
271
394
 
272
395
  if (aggregated.navigate.length > 0) {
273
- // @ts-expect-error
274
- let navigate = router.navigate[UninstrumentedSymbol] ?? router.navigate;
275
- let instrumentedNavigate = wrapImpl(aggregated.navigate, navigate, (...args) => {
276
- let [to, opts] = args as Parameters<Router['navigate']>;
277
- return {
396
+ let navigate = getUninstrumentedHandler(router.navigate);
397
+ let instrumentedNavigate = async (
398
+ ...args: Parameters<typeof navigate>
399
+ ): Promise<Awaited<ReturnType<typeof navigate>>> => {
400
+ let [to, opts] = args;
401
+ let meta: InstrumentationResultMeta | undefined;
402
+ let info: RouterNavigationInstrumentationInfo = {
278
403
  to: typeof to === 'number' || typeof to === 'string' ? to : to ? createPath(to) : '.',
279
404
  ...getRouterInfo(router, opts ?? {}),
280
- } satisfies RouterNavigationInstrumentationInfo;
281
- }) as Router['navigate'];
282
- if (instrumentedNavigate) {
283
- // @ts-expect-error
284
- instrumentedNavigate[UninstrumentedSymbol] = navigate;
285
- router.navigate = instrumentedNavigate;
286
- }
405
+ };
406
+ let result = await recurseRight(
407
+ aggregated.navigate,
408
+ info,
409
+ async () => {
410
+ if (typeof to === 'number') {
411
+ return await navigate(...args);
412
+ }
413
+ let cleanup = setInstrumentationClientResultMetaReceiver(router, (value) => {
414
+ meta = value;
415
+ });
416
+ try {
417
+ return await navigate(...args);
418
+ } finally {
419
+ cleanup();
420
+ }
421
+ },
422
+ (result): InstrumentationClientRouterResult => ({
423
+ ...getInstrumentationInnerResult(result),
424
+ meta,
425
+ }),
426
+ );
427
+ return throwOrReturnResult(result);
428
+ };
429
+ setUninstrumentedHandler(instrumentedNavigate, navigate);
430
+ router.navigate = instrumentedNavigate as Router['navigate'];
287
431
  }
288
432
 
289
433
  if (aggregated.fetch.length > 0) {
290
- // @ts-expect-error
291
- let fetch = router.fetch[UninstrumentedSymbol] ?? router.fetch;
292
- let instrumentedFetch = wrapImpl(aggregated.fetch, fetch, (...args) => {
293
- let [key, , href, opts] = args as Parameters<Router['fetch']>;
294
- return {
295
- href: href ?? '.',
296
- fetcherKey: key,
297
- ...getRouterInfo(router, opts ?? {}),
298
- } satisfies RouterFetchInstrumentationInfo;
299
- }) as Router['fetch'];
300
- if (instrumentedFetch) {
301
- // @ts-expect-error
302
- instrumentedFetch[UninstrumentedSymbol] = fetch;
303
- router.fetch = instrumentedFetch;
304
- }
434
+ let fetch = getUninstrumentedHandler(router.fetch);
435
+ let instrumentedFetch = async (...args: Parameters<typeof fetch>) => {
436
+ let [key, _, href, opts] = args;
437
+ let meta: InstrumentationResultMeta | undefined;
438
+ let result = await recurseRight(
439
+ aggregated.fetch,
440
+ {
441
+ href: href ?? '.',
442
+ fetcherKey: key,
443
+ ...getRouterInfo(router, opts ?? {}),
444
+ } satisfies RouterFetchInstrumentationInfo,
445
+ async () => {
446
+ let cleanup = setInstrumentationClientResultMetaReceiver(router, (value) => {
447
+ meta = value;
448
+ });
449
+ try {
450
+ return await fetch(...args);
451
+ } finally {
452
+ cleanup();
453
+ }
454
+ },
455
+ (result): InstrumentationClientRouterResult => ({
456
+ ...getInstrumentationInnerResult(result),
457
+ meta,
458
+ }),
459
+ );
460
+ return throwOrReturnResult(result);
461
+ };
462
+ setUninstrumentedHandler(instrumentedFetch, fetch);
463
+ router.fetch = instrumentedFetch;
305
464
  }
306
465
 
307
466
  return router;
@@ -312,7 +471,10 @@ export function instrumentHandler(
312
471
  fns: InstrumentRequestHandlerFunction[],
313
472
  ): RequestHandler {
314
473
  let aggregated: {
315
- request: InstrumentFunction<RequestHandlerInstrumentationInfo>[];
474
+ request: InstrumentFunction<
475
+ RequestHandlerInstrumentationInfo,
476
+ InstrumentationServerHandlerResult
477
+ >[];
316
478
  } = {
317
479
  request: [],
318
480
  };
@@ -320,11 +482,8 @@ export function instrumentHandler(
320
482
  fns.forEach((fn) =>
321
483
  fn({
322
484
  instrument(i) {
323
- let keys = Object.keys(i) as Array<keyof typeof i>;
324
- for (let key of keys) {
325
- if (i[key]) {
326
- aggregated[key].push(i[key] as any);
327
- }
485
+ if (i.request != null) {
486
+ aggregated.request.push(i.request);
328
487
  }
329
488
  },
330
489
  }),
@@ -333,73 +492,122 @@ export function instrumentHandler(
333
492
  let instrumentedHandler = handler;
334
493
 
335
494
  if (aggregated.request.length > 0) {
336
- instrumentedHandler = wrapImpl(aggregated.request, handler, (...args) => {
337
- let [request, context] = args as Parameters<RequestHandler>;
338
- return {
339
- request: getReadonlyRequest(request),
340
- context: context != null ? getReadonlyContext(context) : context,
341
- } satisfies RequestHandlerInstrumentationInfo;
342
- }) as RequestHandler;
495
+ instrumentedHandler = async (...args) => {
496
+ let [request, context] = args;
497
+ let instrumentationContext = context ?? new RouterContextProvider();
498
+ let result = await recurseRight(
499
+ aggregated.request,
500
+ {
501
+ request: getReadonlyRequest(request),
502
+ context: getReadonlyContext(instrumentationContext),
503
+ } satisfies RequestHandlerInstrumentationInfo,
504
+ () => handler(request, instrumentationContext),
505
+ (result, info) => {
506
+ let meta: InstrumentationResultMeta | undefined;
507
+ try {
508
+ meta = info.context?.get(instrumentationResultMetaContext);
509
+ } catch {
510
+ // Not all instrumentation contexts have request/route metadata.
511
+ }
512
+ invariant(
513
+ result.value instanceof Response,
514
+ 'Expected a Response from the request handler',
515
+ );
516
+ return {
517
+ ...getInstrumentationInnerResult(result),
518
+ statusCode: result.value.status,
519
+ meta,
520
+ };
521
+ },
522
+ );
523
+ return throwOrReturnResult(result);
524
+ };
343
525
  }
344
526
 
345
527
  return instrumentedHandler;
346
528
  }
347
529
 
348
- function wrapImpl<T extends InstrumentationInfo>(
349
- impls: InstrumentFunction<T>[],
350
- handler: (...args: any[]) => MaybePromise<any>,
351
- getInfo: (...args: unknown[]) => T,
530
+ function getUninstrumentedHandler<T extends InstrumentableFunction>(handler: T): T {
531
+ return (handler as InstrumentedFunction<T>)[UninstrumentedSymbol] ?? handler;
532
+ }
533
+
534
+ function setUninstrumentedHandler<TArgs extends unknown[], TResult>(
535
+ handler: (...args: TArgs) => MaybePromise<TResult>,
536
+ uninstrumentedHandler: (...args: TArgs) => MaybePromise<TResult>,
352
537
  ) {
353
- if (impls.length === 0) {
354
- return null;
355
- }
356
- return async (...args: unknown[]) => {
357
- let result = await recurseRight(
358
- impls,
359
- getInfo(...args),
360
- () => handler(...args),
361
- impls.length - 1,
362
- );
363
- if (result.type === 'error') {
364
- throw result.value;
538
+ (handler as InstrumentedFunction<(...args: TArgs) => MaybePromise<TResult>>)[
539
+ UninstrumentedSymbol
540
+ ] = uninstrumentedHandler;
541
+ }
542
+
543
+ export function setInstrumentationClientResultMetaReceiver(
544
+ router: Router,
545
+ receiver: InstrumentationMetaReceiver,
546
+ ): () => void {
547
+ instrumentationClientResultMetaReceivers.set(router, receiver);
548
+ return () => {
549
+ if (instrumentationClientResultMetaReceivers.get(router) === receiver) {
550
+ instrumentationClientResultMetaReceivers.delete(router);
365
551
  }
366
- return result.value;
367
552
  };
368
553
  }
369
554
 
370
- type RecurseResult = { type: 'success' | 'error'; value: unknown };
555
+ export function consumeInstrumentationClientResultMetaReceiver(
556
+ router: Router,
557
+ ): InstrumentationMetaReceiver | undefined {
558
+ let receiver = instrumentationClientResultMetaReceivers.get(router);
559
+ instrumentationClientResultMetaReceivers.delete(router);
560
+ return receiver;
561
+ }
562
+
563
+ type RecurseResult<TResult> =
564
+ | { type: 'success'; value: TResult }
565
+ | { type: 'error'; value: unknown };
371
566
 
372
- async function recurseRight<T extends InstrumentationInfo>(
373
- impls: InstrumentFunction<T>[],
374
- info: T,
375
- handler: () => MaybePromise<void>,
376
- index: number,
377
- ): Promise<RecurseResult> {
567
+ function throwOrReturnResult<TResult>(result: RecurseResult<TResult>): TResult {
568
+ if (result.type === 'error') {
569
+ throw result.value;
570
+ }
571
+ return result.value;
572
+ }
573
+
574
+ async function recurseRight<
575
+ TResult,
576
+ TInfo extends InstrumentationInfo,
577
+ TInnerResult extends InstrumentationHandlerResult,
578
+ >(
579
+ impls: InstrumentFunction<TInfo, TInnerResult>[],
580
+ info: TInfo,
581
+ handler: () => MaybePromise<TResult>,
582
+ getInnerResult: (result: RecurseResult<TResult>, info: TInfo) => TInnerResult,
583
+ state: RecurseState<TResult, TInnerResult> = {
584
+ result: null,
585
+ innerResult: null,
586
+ },
587
+ index = impls.length - 1,
588
+ ): Promise<RecurseResult<TResult>> {
378
589
  let impl = impls[index];
379
- let result: RecurseResult | undefined;
380
590
  if (!impl) {
381
591
  try {
382
592
  let value = await handler();
383
- result = { type: 'success', value };
593
+ state.result = { type: 'success', value };
384
594
  } catch (e) {
385
- result = { type: 'error', value: e };
595
+ state.result = { type: 'error', value: e };
386
596
  }
597
+ state.innerResult = getInnerResult(state.result, info);
387
598
  } else {
388
599
  // If they forget to call the handler, or if they throw before calling the
389
600
  // handler, we need to ensure the handlers still gets called
390
- let handlerPromise: ReturnType<typeof recurseRight> | undefined = undefined;
391
- let callHandler = async (): Promise<InstrumentationHandlerResult> => {
601
+ let handlerPromise: Promise<RecurseResult<TResult>> | undefined = undefined;
602
+ let callHandler = async (): Promise<TInnerResult> => {
392
603
  if (handlerPromise) {
393
604
  console.error('You cannot call instrumented handlers more than once');
394
605
  } else {
395
- handlerPromise = recurseRight(impls, info, handler, index - 1);
606
+ handlerPromise = recurseRight(impls, info, handler, getInnerResult, state, index - 1);
396
607
  }
397
- result = await handlerPromise;
398
- invariant(result, 'Expected a result');
399
- if (result.type === 'error' && result.value instanceof Error) {
400
- return { status: 'error', error: result.value };
401
- }
402
- return { status: 'success', error: undefined };
608
+ await handlerPromise;
609
+ invariant(state.innerResult, 'Expected an inner result');
610
+ return state.innerResult;
403
611
  };
404
612
 
405
613
  try {
@@ -416,24 +624,40 @@ async function recurseRight<T extends InstrumentationInfo>(
416
624
  await handlerPromise;
417
625
  }
418
626
 
419
- if (result) {
420
- return result;
627
+ if (state.result) {
628
+ return state.result;
421
629
  }
422
630
 
423
- return {
631
+ state.result = {
424
632
  type: 'error',
425
633
  value: new Error('No result assigned in instrumentation chain.'),
426
634
  };
635
+ state.innerResult = getInnerResult(state.result, info);
636
+ return state.result;
637
+ }
638
+
639
+ type RecurseState<TResult, TInnerResult> = {
640
+ result: RecurseResult<TResult> | null;
641
+ innerResult: TInnerResult | null;
642
+ };
643
+
644
+ function getInstrumentationInnerResult<TResult>(
645
+ result: RecurseResult<TResult>,
646
+ ): InstrumentationHandlerResult {
647
+ if (result.type === 'error' && result.value instanceof Error) {
648
+ return { status: 'error', error: result.value };
649
+ }
650
+ return { status: 'success', error: undefined };
427
651
  }
428
652
 
429
653
  function getHandlerInfo(
430
654
  args: LoaderFunctionArgs | ActionFunctionArgs | Parameters<MiddlewareFunction>[0],
431
655
  ): RouteHandlerInstrumentationInfo {
432
- let { request, context, params, pattern } = args;
656
+ let { request, context, params } = args;
433
657
  return {
658
+ ...args,
434
659
  request: getReadonlyRequest(request),
435
660
  params: { ...params },
436
- pattern,
437
661
  context: getReadonlyContext(context),
438
662
  };
439
663
  }
@@ -450,6 +674,7 @@ function getRouterInfo(
450
674
  ...('body' in opts ? { body: opts.body } : {}),
451
675
  };
452
676
  }
677
+
453
678
  // Return a shallow readonly "clone" of the Request with the info they may
454
679
  // want to read from during instrumentation
455
680
  function getReadonlyRequest(request: Request): {
@@ -467,30 +692,9 @@ function getReadonlyRequest(request: Request): {
467
692
  }
468
693
 
469
694
  function getReadonlyContext(
470
- context: MiddlewareEnabled extends true ? RouterContextProvider : AppLoadContext,
471
- ): MiddlewareEnabled extends true ? Pick<RouterContextProvider, 'get'> : Readonly<AppLoadContext> {
472
- if (isPlainObject(context)) {
473
- let frozen = { ...context };
474
- Object.freeze(frozen);
475
- return frozen;
476
- } else {
477
- return {
478
- get: <T>(ctx: RouterContext<T>) => (context as unknown as RouterContextProvider).get(ctx),
479
- };
480
- }
481
- }
482
-
483
- // From turbo-stream-v2/flatten.ts
484
- const objectProtoNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0');
485
-
486
- function isPlainObject(thing: unknown): thing is Record<string | number | symbol, unknown> {
487
- if (thing === null || typeof thing !== 'object') {
488
- return false;
489
- }
490
- const proto = Object.getPrototypeOf(thing);
491
- return (
492
- proto === Object.prototype ||
493
- proto === null ||
494
- Object.getOwnPropertyNames(proto).sort().join('\0') === objectProtoNames
495
- );
695
+ context: Readonly<RouterContextProvider>,
696
+ ): Pick<RouterContextProvider, 'get'> {
697
+ return {
698
+ get: <T>(ctx: RouterContext<T>) => context.get(ctx),
699
+ };
496
700
  }