react-router-effect 1.0.2 → 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/README.md +548 -173
- package/dist/index.d.mts +2 -342
- package/dist/index.mjs +122 -34
- package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
- package/dist/route-data-MgSnnR33.d.mts +543 -0
- package/dist/server.d.mts +218 -0
- package/dist/server.mjs +337 -0
- package/dist/vite.d.mts +27 -0
- package/dist/vite.mjs +46 -0
- package/package.json +22 -10
- package/src/action-meta.ts +58 -0
- package/src/check.ts +205 -0
- package/src/errors.ts +3 -3
- package/src/factory.ts +291 -84
- package/src/index.ts +22 -1
- package/src/route-data.ts +151 -0
- package/src/server-entry.ts +10 -0
- package/src/server.ts +647 -0
- package/src/vite-entry.ts +2 -0
- package/src/vite.ts +92 -0
package/src/factory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { HttpServerRespondable, HttpServerResponse } from "effect/
|
|
1
|
+
import { Context, Effect, type ManagedRuntime } from "effect";
|
|
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**
|
|
42
|
-
* or `Respond.redirect(url)` (throw)
|
|
43
|
-
* - an **`Effect`**
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
93
|
-
* `errorHandlers` contextually type each handler's `error` param from its key
|
|
94
|
-
* no annotation needed
|
|
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
|
|
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
|
|
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`)
|
|
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`
|
|
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
|
|
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`
|
|
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**
|
|
165
|
-
* domain error, a library route error, or respondable; `unhandledErrors`
|
|
166
|
-
* - **Missing requirement**
|
|
167
|
-
* `runtime` nor the `requestContext` provides; `missingRequirements`
|
|
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
|
|
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
|
-
*
|
|
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`
|
|
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
|
|
185
|
-
* on-site error
|
|
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`
|
|
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`
|
|
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
|
|
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**
|
|
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`
|
|
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**
|
|
309
|
-
* isn't a declared domain error
|
|
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
|
|
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>`
|
|
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)
|
|
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
|
|
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
|
|
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,18 +522,19 @@ 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
|
-
*
|
|
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>,
|
|
422
529
|
context: Readonly<RouterContextProvider>,
|
|
530
|
+
signal: AbortSignal,
|
|
423
531
|
): Promise<unknown> {
|
|
424
532
|
// The internal channel is deliberately loose (`unknown` success); the caller
|
|
425
533
|
// restores the precise resolved type (computed from `E` and the handler map).
|
|
426
|
-
// Sound at runtime
|
|
534
|
+
// Sound at runtime - the values produced are exactly those types.
|
|
427
535
|
const program = effect.pipe(
|
|
428
|
-
// Catch the
|
|
429
|
-
// (provably ⊆ E, and a *refinement* not a bare 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
|
|
430
538
|
// overload crashes tsc over a generic `E`). A declared domain error with no
|
|
431
539
|
// handler (and not respondable) falls through to the 500 default.
|
|
432
540
|
Effect.catchIf(
|
|
@@ -455,26 +563,32 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
455
563
|
},
|
|
456
564
|
),
|
|
457
565
|
);
|
|
458
|
-
// Provide the
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
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);
|
|
462
573
|
const provided = (
|
|
463
|
-
|
|
574
|
+
store === undefined ? program : Effect.provideContext(program, store)
|
|
464
575
|
) as Effect.Effect<unknown, FailureResponse, RServices>;
|
|
465
576
|
// Run against the configured runtime so its services satisfy the effect's
|
|
466
577
|
// `R`; with no runtime, the effect requires nothing and runs standalone.
|
|
578
|
+
// The request's abort signal interrupts the fiber, so finalizers run and
|
|
579
|
+
// in-flight work is cancelled. React Router races the handler against the same
|
|
580
|
+
// signal and discards its result, so the interrupt rejection is never rendered.
|
|
467
581
|
return runtime
|
|
468
|
-
? runtime.runPromise(provided)
|
|
469
|
-
: Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse
|
|
582
|
+
? runtime.runPromise(provided, { signal })
|
|
583
|
+
: Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
|
|
470
584
|
}
|
|
471
585
|
|
|
472
586
|
function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
|
|
473
|
-
// `A`/`E`/`R` infer directly from the effect (naked, so inference stays
|
|
587
|
+
// `A`/`E`/`R` infer directly from the effect (naked, so inference stays intact).
|
|
474
588
|
// The diagnostic is intersected onto the effect's return type: when the effect
|
|
475
589
|
// requires a service the factory doesn't provide (`R` ⊄ runtime+requestContext),
|
|
476
590
|
// or can fail with something the library won't handle, `Diagnose` becomes a
|
|
477
|
-
// message object the effect isn't assignable to
|
|
591
|
+
// message object the effect isn't assignable to - so this argument fails to
|
|
478
592
|
// type-check with a readable explanation, and the offending types, right here.
|
|
479
593
|
fn: (
|
|
480
594
|
args: Args,
|
|
@@ -486,7 +600,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
486
600
|
args: Args,
|
|
487
601
|
) => Effect.Effect<unknown, E, RServices | ReqServices>;
|
|
488
602
|
return (args: Args) =>
|
|
489
|
-
runProgram(run(args), args.context) as Promise<
|
|
603
|
+
runProgram(run(args), args.context, args.request.signal) as Promise<
|
|
490
604
|
A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>
|
|
491
605
|
>;
|
|
492
606
|
}
|
|
@@ -497,30 +611,30 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
497
611
|
* args-only type) to type `args`, `next` and the result from that route; it defaults
|
|
498
612
|
* to React Router's own `MiddlewareFunction`.
|
|
499
613
|
*
|
|
500
|
-
* The errors the library deals with for a loader/action it deals with here too
|
|
501
|
-
* declared domain errors, library route errors and respondables
|
|
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
|
|
502
616
|
* `FailureResponse` `next()` fails with. Anything else must be handled in the effect,
|
|
503
617
|
* as must any service neither the `runtime` nor the `requestContext` provides.
|
|
504
618
|
*
|
|
505
|
-
* The wrapper is typed as the route's own middleware
|
|
506
|
-
* Promise<Result | undefined>`
|
|
619
|
+
* The wrapper is typed as the route's own middleware - `(args, next) =>
|
|
620
|
+
* Promise<Result | undefined>` - so it drops straight into a
|
|
507
621
|
* `Route.MiddlewareFunction[]`. A handler that recovers (`Respond.early`) resolves
|
|
508
622
|
* with `data(...)`, which react-router's server middleware pipeline turns into the
|
|
509
623
|
* short-circuit `Response`.
|
|
510
624
|
*/
|
|
511
625
|
function makeMiddleware<
|
|
512
626
|
Middleware extends AnyMiddlewareFunction = MiddlewareFunction,
|
|
513
|
-
// A pinned call passes an explicit type argument, which makes the
|
|
514
|
-
// explicit
|
|
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
|
|
515
629
|
// for what would otherwise be inferred: exactly the errors the library handles
|
|
516
630
|
// and the services the factory provides.
|
|
517
631
|
E = LibraryHandled<DomainError, FailureResponse>,
|
|
518
632
|
R = RServices | ReqServices,
|
|
519
633
|
>(
|
|
520
634
|
// Same shape as `makeLoaderOrAction`'s `fn`, but middleware also receives
|
|
521
|
-
// `next`
|
|
635
|
+
// `next` - wrapped as an effect (rather than react-router's raw
|
|
522
636
|
// `() => Promise<Result>`) so downstream failures (e.g. a nested loader's
|
|
523
|
-
// redirect)
|
|
637
|
+
// redirect) become a typed `FailureResponse`, consistent with the rest
|
|
524
638
|
// of the library's error model. The success type is fixed to the route's own
|
|
525
639
|
// `Result` (plus `void`), which is what react-router accepts back.
|
|
526
640
|
fn: (
|
|
@@ -534,30 +648,123 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
534
648
|
) => Promise<MiddlewareResultOf<Middleware> | undefined> {
|
|
535
649
|
// Inside the body the route's own arg/result types are opaque (they're
|
|
536
650
|
// `Parameters<Middleware>[...]` of an unresolved type parameter), so dispatch
|
|
537
|
-
// through the loose shape
|
|
651
|
+
// through the loose shape - as `makeLoaderOrAction` does with its diagnostic.
|
|
538
652
|
// Sound at runtime: the values produced are exactly the declared types.
|
|
539
653
|
const run = fn as unknown as (
|
|
540
654
|
args: MiddlewareArgsOf<Middleware>,
|
|
541
655
|
next: () => Effect.Effect<unknown, FailureResponse>,
|
|
542
656
|
) => Effect.Effect<unknown, E, RServices | ReqServices>;
|
|
543
657
|
return (args, next) => {
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
catch: (e) => e as FailureResponse,
|
|
548
|
-
});
|
|
549
|
-
const { context } = args as unknown as { context: Readonly<RouterContextProvider> };
|
|
550
|
-
return runProgram(run(args, effectNext), context) as Promise<
|
|
658
|
+
const { context, request } = args as unknown as RouteArgs;
|
|
659
|
+
const effectNext = makeEffectNext(context, next as unknown as () => Promise<unknown>);
|
|
660
|
+
return runProgram(run(args, effectNext), context, request.signal) as Promise<
|
|
551
661
|
MiddlewareResultOf<Middleware> | undefined
|
|
552
662
|
>;
|
|
553
663
|
};
|
|
554
664
|
}
|
|
555
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
|
+
|
|
556
743
|
return {
|
|
557
744
|
makeLoader: makeLoaderOrAction,
|
|
558
|
-
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>>,
|
|
559
757
|
makeMiddleware,
|
|
560
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
|
+
>,
|
|
561
768
|
};
|
|
562
769
|
};
|
|
563
770
|
}
|