react-router-effect 2.0.0 → 2.1.0

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.
package/src/factory.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { type Context, Effect, type ManagedRuntime } from "effect";
1
+ import { Context, Effect, type ManagedRuntime } from "effect";
2
2
  import { HttpServerRespondable, HttpServerResponse } from "effect/http";
3
3
  import {
4
+ createContext,
4
5
  data,
5
6
  redirect,
6
7
  type ActionFunctionArgs,
@@ -11,6 +12,7 @@ import {
11
12
  type RouterContextProvider,
12
13
  } from "react-router";
13
14
 
15
+ import { attachActionMeta, type WithActionMeta } from "./action-meta.ts";
14
16
  import {
15
17
  isRouteError,
16
18
  Respond as baseRespond,
@@ -26,7 +28,7 @@ type BaseRespond = typeof baseRespond;
26
28
  // Type-level plumbing.
27
29
  // ---------------------------------------------------------------------------
28
30
 
29
- type Tagged = { readonly _tag: string };
31
+ export type Tagged = { readonly _tag: string };
30
32
 
31
33
  /**
32
34
  * Everything a failure branch rejects with. React Router routes a thrown
@@ -34,13 +36,13 @@ type Tagged = { readonly _tag: string };
34
36
  * `Response` (including `redirect(...)`) as-is. Pinning every `Effect.fail` branch
35
37
  * to this union keeps inference from fixing the error channel to the first one.
36
38
  */
37
- type FailureResponse = DataWithResponseInit<unknown> | Response;
39
+ export type FailureResponse = DataWithResponseInit<unknown> | Response;
38
40
 
39
41
  /**
40
42
  * What a handler may return, to *remap* a domain error:
41
- * - a **library route error** — `Respond.early(value)` (recover), `Respond.throw(data)`
42
- * or `Respond.redirect(url)` (throw) — which the library then processes; or
43
- * - an **`Effect`** — `Effect.succeed(value)` to recover with `value`, or
43
+ * - a **library route error** - `Respond.early(value)` (recover), `Respond.throw(data)`
44
+ * or `Respond.redirect(url)` (throw) - which the library then processes; or
45
+ * - an **`Effect`** - `Effect.succeed(value)` to recover with `value`, or
44
46
  * `Effect.fail(response)` to throw `response`.
45
47
  */
46
48
  export type ErrorHandler<Err> = (
@@ -55,45 +57,45 @@ type ReturnableBodyOf<E> = E extends ReturnableDataError<infer Body> ? Body : ne
55
57
  * Guarded so a loader that never recovers directly doesn't leak
56
58
  * `DataWithResponseInit<never>` into its return type.
57
59
  */
58
- type DirectRecover<E> = [ReturnableBodyOf<E>] extends [never]
60
+ export type DirectRecover<E> = [ReturnableBodyOf<E>] extends [never]
59
61
  ? never
60
62
  : DataWithResponseInit<ReturnableBodyOf<E>>;
61
63
 
62
64
  /** What a handler may return: a library route error or an `Effect` failing with a response. */
63
65
  type HandlerReturn = AnyRouteError | Effect.Effect<unknown, FailureResponse>;
64
66
 
65
- /** A `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) — `(args, next) => ...`. */
66
- type AnyMiddlewareFunction = (...args: never[]) => unknown;
67
+ /** A `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) - `(args, next) => ...`. */
68
+ export type AnyMiddlewareFunction = (...args: never[]) => unknown;
67
69
 
68
70
  /**
69
71
  * The `args` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) is called
70
- * with — inferred from the type argument so `makeMiddleware` can be pinned to a specific
72
+ * with - inferred from the type argument so `makeMiddleware` can be pinned to a specific
71
73
  * generated route's middleware type.
72
74
  */
73
- type MiddlewareArgsOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[0];
75
+ export type MiddlewareArgsOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[0];
74
76
 
75
77
  /**
76
78
  * The `next` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) is called
77
- * with — react-router's own `() => Promise<Result>`, not exported under its own name.
79
+ * with - react-router's own `() => Promise<Result>`, not exported under its own name.
78
80
  */
79
- type MiddlewareNextOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[1];
81
+ export type MiddlewareNextOf<Middleware extends AnyMiddlewareFunction> = Parameters<Middleware>[1];
80
82
 
81
83
  /**
82
84
  * The `Result` a `Route.MiddlewareFunction` (or the base `MiddlewareFunction`) resolves
83
- * with — i.e. what its `next()` resolves to, and what the middleware itself may return.
85
+ * with - i.e. what its `next()` resolves to, and what the middleware itself may return.
84
86
  * Read through `Awaited` because react-router declares `next` as an *interface* with a
85
87
  * call signature (`MiddlewareNextFunction`), and a route's own may resolve synchronously.
86
88
  */
87
- type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> =
89
+ export type MiddlewareResultOf<Middleware extends AnyMiddlewareFunction> =
88
90
  MiddlewareNextOf<Middleware> extends (...args: never[]) => infer Result ? Awaited<Result> : never;
89
91
 
90
92
  /**
91
93
  * The inferred map of handler RETURN types, keyed by domain-error tag. Making the
92
- * factory generic over *this* (rather than the whole handler map) is what lets
93
- * `errorHandlers` contextually type each handler's `error` param from its key —
94
- * no annotation needed — while still capturing precise return types for recovery.
94
+ * factory generic over *this* (rather than the handler map as a whole) is what lets
95
+ * `errorHandlers` contextually type each handler's `error` param from its key -
96
+ * no annotation needed - while still capturing precise return types for recovery.
95
97
  */
96
- type HandlerReturns<DomainError extends Tagged> = Partial<
98
+ export type HandlerReturns<DomainError extends Tagged> = Partial<
97
99
  Record<DomainError["_tag"], HandlerReturn>
98
100
  >;
99
101
 
@@ -107,14 +109,14 @@ type ErrorHandlers<DomainError extends Tagged, Returns> = {
107
109
  };
108
110
 
109
111
  /**
110
- * What the registered handlers recover into loader/action data — from each
112
+ * What the registered handlers recover into loader/action data - from each
111
113
  * handler's return type, but only for handlers whose error can actually occur in
112
114
  * `E`:
113
115
  * - a remapped `ReturnableDataError<B>` recovers as `DataWithResponseInit<B>`;
114
116
  * - an `Effect.succeed(value)` recovers as `value`;
115
117
  * - throwables, redirects and `Effect.fail(...)` contribute nothing.
116
118
  */
117
- type RecoverOf<DomainError extends Tagged, Returns, E> = {
119
+ export type RecoverOf<DomainError extends Tagged, Returns, E> = {
118
120
  [Tag in keyof Returns]: [Extract<E, Extract<DomainError, { readonly _tag: Tag }>>] extends [never]
119
121
  ? never
120
122
  :
@@ -123,25 +125,25 @@ type RecoverOf<DomainError extends Tagged, Returns, E> = {
123
125
  }[keyof Returns];
124
126
 
125
127
  /**
126
- * Errors the library deals with on the handler's behalf — so the loader/action/
128
+ * Errors the library deals with on the handler's behalf - so the loader/action/
127
129
  * middleware needn't handle them itself:
128
- * - the app's **declared domain errors** (`DomainError`) — handled by a registered
130
+ * - the app's **declared domain errors** (`DomainError`) - handled by a registered
129
131
  * handler, or left to the 500 / auto-respond default;
130
132
  * - **library route errors** raised directly via `Respond`;
131
133
  * - anything that renders itself via **`HttpServerRespondable`**;
132
- * - `AlsoHandled` — extra, call-site-specific already-handled errors. Only
134
+ * - `AlsoHandled` - extra, call-site-specific already-handled errors. Only
133
135
  * `makeMiddleware` uses this, to admit the `FailureResponse` its `next()`
134
- * fails with (a downstream throw, already a finished response — the library
136
+ * fails with (a downstream throw, already a finished response - the library
135
137
  * re-throws it as-is, so the middleware needn't catch it just to re-raise it).
136
138
  */
137
- type LibraryHandled<DomainError, AlsoHandled = never> =
139
+ export type LibraryHandled<DomainError, AlsoHandled = never> =
138
140
  | DomainError
139
141
  | AnyRouteError
140
142
  | HttpServerRespondable.Respondable
141
143
  | AlsoHandled;
142
144
 
143
145
  /**
144
- * What remains in a handler's error channel that the library will NOT handle —
146
+ * What remains in a handler's error channel that the library will NOT handle -
145
147
  * i.e. service-specific errors the route consumes that aren't declared domain
146
148
  * errors. The loader/action/middleware must handle these itself (catch or map them).
147
149
  */
@@ -156,37 +158,37 @@ type Unhandled<E, DomainError, AlsoHandled = never> = Exclude<
156
158
 
157
159
  /**
158
160
  * A readable compile-time diagnostic. It's intersected onto the *effect return type*
159
- * of `fn` — so the effect's `A`/`E`/`R` still infer directly (naked), while a mismatch
161
+ * of `fn` - so the effect's `A`/`E`/`R` still infer directly (naked), while a mismatch
160
162
  * reports this message, and the offending types, *at the `makeLoader`/`makeAction`
161
163
  * call* instead of the opaque "Expected 2 arguments". Resolves to `unknown` (a no-op
162
164
  * intersection) when the effect is fully handled and all its requirements are provided:
163
165
  *
164
- * - **Unhandled error** — the effect can fail with something that isn't a declared
165
- * domain error, a library route error, or respondable; `unhandledErrors` names them.
166
- * - **Missing requirement** — the effect requires a service that neither the
167
- * `runtime` nor the `requestContext` provides; `missingRequirements` names them.
166
+ * - **Unhandled error** - the effect can fail with something that isn't a declared
167
+ * domain error, a library route error, or respondable; `unhandledErrors` lists them.
168
+ * - **Missing requirement** - the effect requires a service that neither the
169
+ * `runtime` nor the `requestContext` provides; `missingRequirements` lists them.
168
170
  *
169
- * The message lives in an *inline* object literal (no named alias) so tsc/tsgo print
171
+ * The message is in an *inline* object literal (no named alias) so tsc/tsgo print
170
172
  * its text verbatim rather than collapsing it to an alias name.
171
173
  */
172
174
  /**
173
- * Names the unhandled errors for the diagnostic: a tagged error becomes its `_tag`
175
+ * Lists the unhandled errors for the diagnostic: a tagged error becomes its `_tag`
174
176
  * (the string you'd pass to `Effect.catchTag`), anything untagged stays as its type.
175
177
  */
176
- type NameError<U> = U extends { readonly _tag: infer Tag extends string } ? Tag : U;
178
+ export type NameError<U> = U extends { readonly _tag: infer Tag extends string } ? Tag : U;
177
179
 
178
180
  /**
179
- * True exactly when `T` is (or, for a union, includes) `any` — not `unknown`, not
181
+ * True exactly when `T` is (or, for a union, includes) `any` - not `unknown`, not
180
182
  * `never`, not any other type. An unrelated mistake elsewhere in the effect (a typo,
181
183
  * a call with the wrong number of arguments, a genuine syntax error) commonly makes
182
184
  * TypeScript infer `any` for `E`/`R` instead of reporting a clean type; running
183
185
  * `Diagnose` on that `any` produces a confusing "unhandled errors: any" message that
184
- * buries the real mistake. Guarding on this and bailing to `unknown` lets tsc's own,
185
- * on-site error surface instead. (`1 & T` is `any` when `T` is `any` — including when
186
- * `any` only appears as a union member, since intersection distributes over unions —
186
+ * buries the actual mistake. Guarding on this and bailing to `unknown` lets tsc's own,
187
+ * on-site error appear instead. (`1 & T` is `any` when `T` is `any` - including when
188
+ * `any` only appears as a union member, since intersection distributes over unions -
187
189
  * and `0` is assignable to `any` but not to any concrete type built from `1 & T`.)
188
190
  */
189
- type IsAny<T> = 0 extends 1 & T ? true : false;
191
+ export type IsAny<T> = 0 extends 1 & T ? true : false;
190
192
 
191
193
  type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> =
192
194
  IsAny<E> extends true
@@ -213,7 +215,7 @@ const internalServerError = () =>
213
215
  Effect.fail<FailureResponse>(new Response("Internal Server Error", { status: 500 }));
214
216
 
215
217
  /**
216
- * A raw `FailureResponse` — a downstream `next()`'s rejection, in `makeMiddleware` —
218
+ * A raw `FailureResponse` - a downstream `next()`'s rejection, in `makeMiddleware` -
217
219
  * is already a finished response; re-throwing it as-is (rather than 500ing it) lets
218
220
  * a nested loader's redirect/thrown response pass back up through the middleware chain.
219
221
  */
@@ -239,7 +241,7 @@ const processRouteError = (
239
241
 
240
242
  /**
241
243
  * A React Router context key holding a per-request effect `Context.Context`. Set
242
- * it in middleware and pass it to the factory's `requestContext` — the runner
244
+ * it in middleware and pass it to the factory's `requestContext` - the runner
243
245
  * reads it on every request and provides its services to the loader/action.
244
246
  *
245
247
  * It's just a `RouterContext`; create one with React Router's `createContext`:
@@ -266,6 +268,99 @@ const processRouteError = (
266
268
  */
267
269
  export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
268
270
 
271
+ /**
272
+ * The request store of a factory that configures no `requestContext`. It holds
273
+ * whatever the server adapter and effect middleware put there, and starts empty.
274
+ */
275
+ const defaultRequestContext: RequestContextKey<never> = createContext(Context.empty());
276
+
277
+ /** What `actionMeta` reads: the request and the router context of the action. */
278
+ export interface ActionMetaArgs {
279
+ readonly request: Request;
280
+ readonly context: Readonly<RouterContextProvider>;
281
+ }
282
+
283
+ /**
284
+ * The entries of `current` that `started` lacks or holds with another value:
285
+ * what a fiber that began with `started` has provided since.
286
+ */
287
+ const providedSince = <Services>(
288
+ started: Context.Context<Services>,
289
+ current: Context.Context<never>,
290
+ ): Context.Context<never> => {
291
+ const provided = new Map<string, unknown>();
292
+ for (const [key, value] of current.mapUnsafe) {
293
+ if (started.mapUnsafe.get(key) !== value) provided.set(key, value);
294
+ }
295
+ return Context.makeUnsafe(provided);
296
+ };
297
+
298
+ /** The `next` an effect middleware gets: runs the rest of the chain as an effect. */
299
+ export type EffectNext<Result> = () => Effect.Effect<Result, FailureResponse>;
300
+
301
+ /**
302
+ * The arguments of an effect middleware: React Router's own middleware args,
303
+ * plus `next` as an effect. The second parameter stays React Router's
304
+ * promise-returning `next`, so a plain middleware works unchanged.
305
+ */
306
+ export type EffectMiddlewareArgs<Middleware extends AnyMiddlewareFunction = MiddlewareFunction> =
307
+ MiddlewareArgsOf<Middleware> & {
308
+ readonly next: EffectNext<MiddlewareResultOf<Middleware>>;
309
+ };
310
+
311
+ /** The router context and signal every route function receives. */
312
+ interface RouteArgs {
313
+ readonly request: Request;
314
+ readonly context: Readonly<RouterContextProvider>;
315
+ }
316
+
317
+ /**
318
+ * What a route function resolves with: an Effect's success, or the awaited
319
+ * value. A failure that a handler recovers resolves with its data too.
320
+ */
321
+ export type RouteResult<Out> =
322
+ Out extends Effect.Effect<infer A, infer _E, infer _R> ? A : Awaited<Out>;
323
+
324
+ /**
325
+ * Turns route functions into the promise-returning functions React Router
326
+ * calls. An Effect result runs through the factory's error model with the
327
+ * factory's runtime and the request's effect context; any other result passes
328
+ * through. The server adapter applies these to every route module, and tests
329
+ * can call them directly.
330
+ *
331
+ * Each is generic over the route's own args, so a test can pass a route's
332
+ * `Route.LoaderArgs` without a cast. React Router always supplies `request`
333
+ * and `context`.
334
+ */
335
+ export interface RouteHandlers {
336
+ readonly loader: <Args extends RouteArgs, Out>(
337
+ fn: (args: Args) => Out,
338
+ ) => (args: Args) => Promise<RouteResult<Out>>;
339
+ readonly action: <Args extends RouteArgs, Out>(
340
+ fn: (args: Args) => Out,
341
+ ) => (args: Args) => Promise<RouteResult<Out>>;
342
+ /**
343
+ * The wrapped middleware takes React Router's args, without `next`: the
344
+ * runner adds `next` as an effect.
345
+ */
346
+ readonly middleware: <Args extends RouteArgs, Next extends () => Promise<unknown>, Out>(
347
+ fn: (args: Args, next: Next) => Out,
348
+ ) => (args: Omit<Args, "next">, next: Next) => Promise<RouteResult<Out> | undefined>;
349
+ /** The router context key that holds the request's effect context. */
350
+ readonly requestContext: RouterContext<Context.Context<never>>;
351
+ }
352
+
353
+ /**
354
+ * The types a factory exposes for the server adapter's route check and for
355
+ * `Register`. Never present at runtime.
356
+ */
357
+ export interface FactoryTypes<DomainError, Returns, Services, Meta> {
358
+ readonly DomainError: DomainError;
359
+ readonly Returns: Returns;
360
+ readonly Services: Services;
361
+ readonly Meta: Meta;
362
+ }
363
+
269
364
  // ---------------------------------------------------------------------------
270
365
  // Factory.
271
366
  // ---------------------------------------------------------------------------
@@ -277,7 +372,7 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
277
372
  * Declare the app's **domain errors** as the type argument, then register an
278
373
  * *optional* handler per domain error in `errorHandlers`. The handler keys
279
374
  * autocomplete to your domain-error tags and each `error` parameter is typed from
280
- * its key — no annotation needed. It's curried so you can pin the domain errors
375
+ * its key - no annotation needed. It's curried so you can pin the domain errors
281
376
  * while the handler return types are still inferred:
282
377
  *
283
378
  * ```ts
@@ -293,10 +388,10 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
293
388
  * }));
294
389
  * ```
295
390
  *
296
- * The inner config is a **builder** — it receives the library's `Respond` helpers so
391
+ * The inner config is a **builder** - it receives the library's `Respond` helpers so
297
392
  * your handlers can recover/throw with them, and returns the config. `Respond` is
298
393
  * also returned from the factory (extended with any `respond` helpers you add), so
299
- * your app imports a *single* `Respond` — no auto-import ambiguity with a library one.
394
+ * your app imports a *single* `Respond` - no auto-import ambiguity with a library one.
300
395
  *
301
396
  * A handler *remaps* the error by returning either a library route error
302
397
  * (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
@@ -305,13 +400,13 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
305
400
  *
306
401
  * **Declared domain errors** may be left unhandled: if one implements
307
402
  * `HttpServerRespondable` it's rendered automatically, otherwise it falls through
308
- * to a 500. **Any other error** — a service-specific error the route consumes that
309
- * isn't a declared domain error — *must* be handled in the loader/action, or
403
+ * to a 500. **Any other error** - a service-specific error the route consumes that
404
+ * isn't a declared domain error - *must* be handled in the loader/action, or
310
405
  * `makeLoader`/`makeAction` fails to type-check.
311
406
  *
312
407
  * Pass a `runtime` (a `ManagedRuntime`, e.g. from `ManagedRuntime.make(AppLayer)`)
313
408
  * to provide your app's services once. Loader/action effects may then require those
314
- * services directly — no per-call `Effect.provide`:
409
+ * services directly - no per-call `Effect.provide`:
315
410
  *
316
411
  * ```ts
317
412
  * const { makeLoader, makeAction } = makeEffectRouteFactory<DomainErrors>()(() => ({
@@ -333,9 +428,9 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
333
428
  * generated `Route.MiddlewareFunction` (a type argument, since middleware has no
334
429
  * separate args-only type) to type `args` and `next`'s result from that route; it
335
430
  * defaults to React Router's own `MiddlewareFunction`. The `next` handed to the
336
- * effect is `() => Effect.Effect<Result, FailureResponse>` — call it with `yield*`
431
+ * effect is `() => Effect.Effect<Result, FailureResponse>` - call it with `yield*`
337
432
  * to run the rest of the chain, and a downstream throw (e.g. a nested loader's
338
- * redirect) surfaces as a typed failure instead of a rejected promise:
433
+ * redirect) becomes a typed failure instead of a rejected promise:
339
434
  *
340
435
  * ```ts
341
436
  * const middleware = makeMiddleware<Route.MiddlewareFunction>((args, next) =>
@@ -353,6 +448,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
353
448
  RServices = never,
354
449
  ReqServices = never,
355
450
  const ExtraRespond extends Record<string, (...args: never[]) => unknown> = {},
451
+ Meta = never,
356
452
  >(
357
453
  /**
358
454
  * Builds the factory config. It receives the library's base `Respond` helpers
@@ -363,7 +459,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
363
459
  /**
364
460
  * An optional handler per declared domain error, keyed by its tag. The keys
365
461
  * autocomplete to your domain-error tags and each handler's `error` parameter
366
- * is typed automatically — no annotation needed. A handler *remaps* the error
462
+ * is typed automatically - no annotation needed. A handler *remaps* the error
367
463
  * by returning a library route error (the builder's `Respond.early` / `throw`
368
464
  * / `redirect`) or an `Effect` (`Effect.succeed`/`fail`). Omit to register none.
369
465
  */
@@ -384,18 +480,29 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
384
480
  */
385
481
  requestContext?: RequestContextKey<ReqServices>;
386
482
  /**
387
- * App-specific `Respond` helpers — merged onto the base `Respond` that the
483
+ * App-specific `Respond` helpers - merged onto the base `Respond` that the
388
484
  * factory returns, so your app imports one `Respond`. Typically opinionated
389
485
  * error constructors; annotate their parameters, e.g.
390
486
  * `{ formError: (reply: string) => new FormError({ reply }) }`. The base
391
487
  * helpers (`early` / `throw` / `redirect`) always win the merge.
392
488
  */
393
489
  respond?: ExtraRespond;
490
+ /**
491
+ * Request-scoped metadata to attach to every action's data under `__meta`,
492
+ * read after the action resolves. The server adapter attaches it to every
493
+ * action it runs, including plain functions and `makeAction` wrappers. A
494
+ * thrown result gets none. Only plain records and `data(...)` bodies include
495
+ * it; other values are returned unchanged.
496
+ */
497
+ actionMeta?: (args: ActionMetaArgs) => Meta;
394
498
  },
395
499
  ) {
396
500
  const config = builder(baseRespond);
397
501
  const runtime = config.runtime;
398
502
  const requestContextKey = config.requestContext;
503
+ // Where the request's effect context is stored: the app's key, or the library's own.
504
+ const storeKey: RequestContextKey<ReqServices> =
505
+ requestContextKey ?? (defaultRequestContext as RequestContextKey<ReqServices>);
399
506
  // The single `Respond` an app uses: base helpers plus any app-specific ones.
400
507
  // Base wins the merge so core helpers can't be shadowed.
401
508
  const Respond = { ...config.respond, ...baseRespond } as BaseRespond & ExtraRespond;
@@ -415,7 +522,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
415
522
  * (registered domain errors, library route errors, respondables, or the 500
416
523
  * default), provides the per-request context, and executes against the
417
524
  * configured runtime. Shared by `makeLoader`/`makeAction` and `makeMiddleware`
418
- * — they differ only in how they build the effect and what `context` they read.
525
+ * - they differ only in how they build the effect and what `context` they read.
419
526
  */
420
527
  function runProgram<E>(
421
528
  effect: Effect.Effect<unknown, E, RServices | ReqServices>,
@@ -424,10 +531,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
424
531
  ): Promise<unknown> {
425
532
  // The internal channel is deliberately loose (`unknown` success); the caller
426
533
  // restores the precise resolved type (computed from `E` and the handler map).
427
- // Sound at runtime — the values produced are exactly those types.
534
+ // Sound at runtime - the values produced are exactly those types.
428
535
  const program = effect.pipe(
429
- // Catch the whole error channel and dispatch. The refinement is `e is E`
430
- // (provably ⊆ E, and a *refinement* not a bare predicate — the predicate
536
+ // Catch the full error channel and dispatch. The refinement is `e is E`
537
+ // (provably ⊆ E, and a *refinement* not a bare predicate - the predicate
431
538
  // overload crashes tsc over a generic `E`). A declared domain error with no
432
539
  // handler (and not respondable) falls through to the 500 default.
433
540
  Effect.catchIf(
@@ -456,12 +563,15 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
456
563
  },
457
564
  ),
458
565
  );
459
- // Provide the per-request context (set by middleware) so `ReqServices` are
460
- // satisfied, leaving only the runtime's `RServices` in the requirements.
461
- // The cast is sound: `provideContext` removes `ReqServices`, and when no
462
- // `requestContext` is configured `ReqServices` is `never` (nothing removed).
566
+ // Provide the request's effect context so `ReqServices` are satisfied,
567
+ // leaving only the runtime's `RServices` in the requirements. Under the
568
+ // server adapter the store also holds the HTTP request fiber's context, and
569
+ // whatever an effect middleware provided around `next()`. The cast is sound:
570
+ // `provideContext` removes `ReqServices`, and when no `requestContext` is
571
+ // configured `ReqServices` is `never` (nothing removed).
572
+ const store = readRequestStore(context);
463
573
  const provided = (
464
- requestContextKey ? Effect.provideContext(program, context.get(requestContextKey)) : program
574
+ store === undefined ? program : Effect.provideContext(program, store)
465
575
  ) as Effect.Effect<unknown, FailureResponse, RServices>;
466
576
  // Run against the configured runtime so its services satisfy the effect's
467
577
  // `R`; with no runtime, the effect requires nothing and runs standalone.
@@ -474,11 +584,11 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
474
584
  }
475
585
 
476
586
  function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
477
- // `A`/`E`/`R` infer directly from the effect (naked, so inference stays robust).
587
+ // `A`/`E`/`R` infer directly from the effect (naked, so inference stays intact).
478
588
  // The diagnostic is intersected onto the effect's return type: when the effect
479
589
  // requires a service the factory doesn't provide (`R` ⊄ runtime+requestContext),
480
590
  // or can fail with something the library won't handle, `Diagnose` becomes a
481
- // message object the effect isn't assignable to — so this argument fails to
591
+ // message object the effect isn't assignable to - so this argument fails to
482
592
  // type-check with a readable explanation, and the offending types, right here.
483
593
  fn: (
484
594
  args: Args,
@@ -501,30 +611,30 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
501
611
  * args-only type) to type `args`, `next` and the result from that route; it defaults
502
612
  * to React Router's own `MiddlewareFunction`.
503
613
  *
504
- * The errors the library deals with for a loader/action it deals with here too —
505
- * declared domain errors, library route errors and respondables — plus the
614
+ * The errors the library deals with for a loader/action it deals with here too -
615
+ * declared domain errors, library route errors and respondables - plus the
506
616
  * `FailureResponse` `next()` fails with. Anything else must be handled in the effect,
507
617
  * as must any service neither the `runtime` nor the `requestContext` provides.
508
618
  *
509
- * The wrapper is typed as the route's own middleware — `(args, next) =>
510
- * Promise<Result | undefined>` — so it drops straight into a
619
+ * The wrapper is typed as the route's own middleware - `(args, next) =>
620
+ * Promise<Result | undefined>` - so it drops straight into a
511
621
  * `Route.MiddlewareFunction[]`. A handler that recovers (`Respond.early`) resolves
512
622
  * with `data(...)`, which react-router's server middleware pipeline turns into the
513
623
  * short-circuit `Response`.
514
624
  */
515
625
  function makeMiddleware<
516
626
  Middleware extends AnyMiddlewareFunction = MiddlewareFunction,
517
- // A pinned call passes an explicit type argument, which makes the whole call
518
- // explicit — TypeScript has no partial inference — so these defaults stand in
627
+ // A pinned call passes an explicit type argument, which makes every type argument of the call
628
+ // explicit - TypeScript has no partial inference - so these defaults stand in
519
629
  // for what would otherwise be inferred: exactly the errors the library handles
520
630
  // and the services the factory provides.
521
631
  E = LibraryHandled<DomainError, FailureResponse>,
522
632
  R = RServices | ReqServices,
523
633
  >(
524
634
  // Same shape as `makeLoaderOrAction`'s `fn`, but middleware also receives
525
- // `next` — wrapped as an effect (rather than react-router's raw
635
+ // `next` - wrapped as an effect (rather than react-router's raw
526
636
  // `() => Promise<Result>`) so downstream failures (e.g. a nested loader's
527
- // redirect) surface as a typed `FailureResponse`, consistent with the rest
637
+ // redirect) become a typed `FailureResponse`, consistent with the rest
528
638
  // of the library's error model. The success type is fixed to the route's own
529
639
  // `Result` (plus `void`), which is what react-router accepts back.
530
640
  fn: (
@@ -538,33 +648,123 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
538
648
  ) => Promise<MiddlewareResultOf<Middleware> | undefined> {
539
649
  // Inside the body the route's own arg/result types are opaque (they're
540
650
  // `Parameters<Middleware>[...]` of an unresolved type parameter), so dispatch
541
- // through the loose shape — as `makeLoaderOrAction` does with its diagnostic.
651
+ // through the loose shape - as `makeLoaderOrAction` does with its diagnostic.
542
652
  // Sound at runtime: the values produced are exactly the declared types.
543
653
  const run = fn as unknown as (
544
654
  args: MiddlewareArgsOf<Middleware>,
545
655
  next: () => Effect.Effect<unknown, FailureResponse>,
546
656
  ) => Effect.Effect<unknown, E, RServices | ReqServices>;
547
657
  return (args, next) => {
548
- const effectNext = () =>
549
- Effect.tryPromise({
550
- try: next as unknown as () => Promise<unknown>,
551
- catch: (e) => e as FailureResponse,
552
- });
553
- const { context, request } = args as unknown as {
554
- context: Readonly<RouterContextProvider>;
555
- request: Request;
556
- };
658
+ const { context, request } = args as unknown as RouteArgs;
659
+ const effectNext = makeEffectNext(context, next as unknown as () => Promise<unknown>);
557
660
  return runProgram(run(args, effectNext), context, request.signal) as Promise<
558
661
  MiddlewareResultOf<Middleware> | undefined
559
662
  >;
560
663
  };
561
664
  }
562
665
 
666
+ /**
667
+ * Reads the request's effect context from the router context. A test that
668
+ * passes args without a router context gets none, as before.
669
+ */
670
+ function readRequestStore(
671
+ context: Readonly<RouterContextProvider> | undefined,
672
+ ): Context.Context<ReqServices> | undefined {
673
+ // An app key without a default that no middleware has set throws here, as
674
+ // it always has: the app promised its services would be there. The
675
+ // library's own key starts empty.
676
+ return typeof context?.get === "function" ? context.get(storeKey) : undefined;
677
+ }
678
+
679
+ /**
680
+ * Wraps React Router's `next` as an effect. Before it runs the rest of the
681
+ * chain, it adds to the request's effect context what the calling fiber
682
+ * provides beyond the context the middleware started with: services it
683
+ * provides around `next()`, log annotations, and its current span. Updates
684
+ * the middleware made with `context.set` stay. A downstream rejection is
685
+ * already a finished response, so it fails with that response as-is.
686
+ */
687
+ function makeEffectNext(
688
+ context: Readonly<RouterContextProvider>,
689
+ next: () => Promise<unknown>,
690
+ ): () => Effect.Effect<unknown, FailureResponse> {
691
+ const started = readRequestStore(context);
692
+ return () =>
693
+ Effect.withFiber((fiber) => {
694
+ if (started !== undefined) {
695
+ context.set(
696
+ storeKey,
697
+ Context.merge(context.get(storeKey), providedSince(started, fiber.context)),
698
+ );
699
+ }
700
+ return Effect.tryPromise({ try: next, catch: (e) => e as FailureResponse });
701
+ });
702
+ }
703
+
704
+ /** Runs a loader or action's result: an Effect through the error model, anything else as-is. */
705
+ function runResult(result: unknown, args: RouteArgs): unknown {
706
+ return Effect.isEffect(result)
707
+ ? runProgram(
708
+ result as Effect.Effect<unknown, unknown, RServices | ReqServices>,
709
+ args.context,
710
+ args.request.signal,
711
+ )
712
+ : result;
713
+ }
714
+
715
+ const actionMeta = config.actionMeta;
716
+
717
+ // React Router calls every route function with args that include at least
718
+ // `request` and `context`, so each wrapper reads them through `RouteArgs`.
719
+ // The runners resolve with what `runResult` produces: the Effect's success
720
+ // or recovered data, or the awaited plain result. `RouteResult` is its type.
721
+ const handlers: RouteHandlers = {
722
+ loader: (fn) => async (args) =>
723
+ runResult(fn(args), args) as RouteResult<ReturnType<typeof fn>>,
724
+ action: (fn) => async (args) => {
725
+ const result = await runResult(fn(args), args);
726
+ return (
727
+ actionMeta === undefined ? result : attachActionMeta(result, actionMeta(args))
728
+ ) as RouteResult<ReturnType<typeof fn>>;
729
+ },
730
+ middleware: (fn) => async (args, next) => {
731
+ const routeArgs: RouteArgs = args;
732
+ // The runner supplies `next` as an effect in place of the one `Args`
733
+ // names, which it cannot prove for an arbitrary `Args`.
734
+ const effectArgs = {
735
+ ...args,
736
+ next: makeEffectNext(routeArgs.context, next),
737
+ } as unknown as Parameters<typeof fn>[0];
738
+ return runResult(fn(effectArgs, next), routeArgs) as RouteResult<ReturnType<typeof fn>>;
739
+ },
740
+ requestContext: storeKey as RouterContext<Context.Context<never>>,
741
+ };
742
+
563
743
  return {
564
744
  makeLoader: makeLoaderOrAction,
565
- makeAction: makeLoaderOrAction,
745
+ makeAction: makeLoaderOrAction as <
746
+ Args extends LoaderFunctionArgs | ActionFunctionArgs,
747
+ A,
748
+ E,
749
+ R,
750
+ >(
751
+ fn: (
752
+ args: Args,
753
+ ) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
754
+ ) => (
755
+ args: Args,
756
+ ) => Promise<WithActionMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>,
566
757
  makeMiddleware,
567
758
  Respond,
759
+ /** The route runner the server adapter applies to every route module. */
760
+ handlers,
761
+ /** Type-only: what the adapter's route check and `Register` read. */
762
+ "~types": undefined as unknown as FactoryTypes<
763
+ DomainError,
764
+ Returns,
765
+ RServices | ReqServices,
766
+ Meta
767
+ >,
568
768
  };
569
769
  };
570
770
  }