react-router-effect 2.0.0 → 2.2.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 +724 -171
- package/dist/index.d.mts +2 -342
- package/dist/index.mjs +122 -33
- package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
- package/dist/route-data-CWBxnNAA.d.mts +549 -0
- package/dist/server.d.mts +284 -0
- package/dist/server.mjs +483 -0
- package/dist/vite.d.mts +48 -0
- package/dist/vite.mjs +80 -0
- package/package.json +25 -9
- package/src/action-meta.ts +58 -0
- package/src/assets.ts +212 -0
- package/src/check.ts +213 -0
- package/src/errors.ts +3 -3
- package/src/factory.ts +301 -91
- package/src/index.ts +22 -1
- package/src/route-data.ts +151 -0
- package/src/server-entry.ts +11 -0
- package/src/server.ts +659 -0
- package/src/vite-entry.ts +2 -0
- package/src/vite.ts +169 -0
package/src/factory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, Effect, type ManagedRuntime, Predicate } 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**
|
|
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,9 +241,15 @@ 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
|
*
|
|
247
|
+
* With the server adapter, do not set it to a new context. The adapter sets it
|
|
248
|
+
* to the request fiber's context before React Router runs, and a new context
|
|
249
|
+
* replaces those services, such as `HttpServerRequest` and the server span.
|
|
250
|
+
* Provide request services with a router middleware on `ReactRouter.routes`.
|
|
251
|
+
* The middleware below is for an app without the adapter.
|
|
252
|
+
*
|
|
245
253
|
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
246
254
|
*
|
|
247
255
|
* ```ts
|
|
@@ -255,7 +263,7 @@ const processRouteError = (
|
|
|
255
263
|
*
|
|
256
264
|
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
257
265
|
*
|
|
258
|
-
* // middleware:
|
|
266
|
+
* // middleware, in an app without the server adapter:
|
|
259
267
|
* export const middleware: Route.MiddlewareFunction[] = [
|
|
260
268
|
* ({ context, request }, next) => {
|
|
261
269
|
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
@@ -266,6 +274,99 @@ const processRouteError = (
|
|
|
266
274
|
*/
|
|
267
275
|
export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
|
|
268
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The request store of a factory that configures no `requestContext`. It holds
|
|
279
|
+
* whatever the server adapter and effect middleware put there, and starts empty.
|
|
280
|
+
*/
|
|
281
|
+
const defaultRequestContext: RequestContextKey<never> = createContext(Context.empty());
|
|
282
|
+
|
|
283
|
+
/** What `actionMeta` reads: the request and the router context of the action. */
|
|
284
|
+
export interface ActionMetaArgs {
|
|
285
|
+
readonly request: Request;
|
|
286
|
+
readonly context: Readonly<RouterContextProvider>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The entries of `current` that `started` lacks or holds with another value:
|
|
291
|
+
* what a fiber that began with `started` has provided since.
|
|
292
|
+
*/
|
|
293
|
+
const providedSince = <Services>(
|
|
294
|
+
started: Context.Context<Services>,
|
|
295
|
+
current: Context.Context<never>,
|
|
296
|
+
): Context.Context<never> => {
|
|
297
|
+
const provided = new Map<string, unknown>();
|
|
298
|
+
for (const [key, value] of current.mapUnsafe) {
|
|
299
|
+
if (started.mapUnsafe.get(key) !== value) provided.set(key, value);
|
|
300
|
+
}
|
|
301
|
+
return Context.makeUnsafe(provided);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** The `next` an effect middleware gets: runs the rest of the chain as an effect. */
|
|
305
|
+
export type EffectNext<Result> = () => Effect.Effect<Result, FailureResponse>;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The arguments of an effect middleware: React Router's own middleware args,
|
|
309
|
+
* plus `next` as an effect. The second parameter stays React Router's
|
|
310
|
+
* promise-returning `next`, so a plain middleware works unchanged.
|
|
311
|
+
*/
|
|
312
|
+
export type EffectMiddlewareArgs<Middleware extends AnyMiddlewareFunction = MiddlewareFunction> =
|
|
313
|
+
MiddlewareArgsOf<Middleware> & {
|
|
314
|
+
readonly next: EffectNext<MiddlewareResultOf<Middleware>>;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/** The router context and signal every route function receives. */
|
|
318
|
+
interface RouteArgs {
|
|
319
|
+
readonly request: Request;
|
|
320
|
+
readonly context: Readonly<RouterContextProvider>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* What a route function resolves with: an Effect's success, or the awaited
|
|
325
|
+
* value. A failure that a handler recovers resolves with its data too.
|
|
326
|
+
*/
|
|
327
|
+
export type RouteResult<Out> =
|
|
328
|
+
Out extends Effect.Effect<infer A, infer _E, infer _R> ? A : Awaited<Out>;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Turns route functions into the promise-returning functions React Router
|
|
332
|
+
* calls. An Effect result runs through the factory's error model with the
|
|
333
|
+
* factory's runtime and the request's effect context; any other result passes
|
|
334
|
+
* through. The server adapter applies these to every route module, and tests
|
|
335
|
+
* can call them directly.
|
|
336
|
+
*
|
|
337
|
+
* Each is generic over the route's own args, so a test can pass a route's
|
|
338
|
+
* `Route.LoaderArgs` without a cast. React Router always supplies `request`
|
|
339
|
+
* and `context`.
|
|
340
|
+
*/
|
|
341
|
+
export interface RouteHandlers {
|
|
342
|
+
readonly loader: <Args extends RouteArgs, Out>(
|
|
343
|
+
fn: (args: Args) => Out,
|
|
344
|
+
) => (args: Args) => Promise<RouteResult<Out>>;
|
|
345
|
+
readonly action: <Args extends RouteArgs, Out>(
|
|
346
|
+
fn: (args: Args) => Out,
|
|
347
|
+
) => (args: Args) => Promise<RouteResult<Out>>;
|
|
348
|
+
/**
|
|
349
|
+
* The wrapped middleware takes React Router's args, without `next`: the
|
|
350
|
+
* runner adds `next` as an effect.
|
|
351
|
+
*/
|
|
352
|
+
readonly middleware: <Args extends RouteArgs, Next extends () => Promise<unknown>, Out>(
|
|
353
|
+
fn: (args: Args, next: Next) => Out,
|
|
354
|
+
) => (args: Omit<Args, "next">, next: Next) => Promise<RouteResult<Out> | undefined>;
|
|
355
|
+
/** The router context key that holds the request's effect context. */
|
|
356
|
+
readonly requestContext: RouterContext<Context.Context<never>>;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The types a factory exposes for the server adapter's route check and for
|
|
361
|
+
* `Register`. Never present at runtime.
|
|
362
|
+
*/
|
|
363
|
+
export interface FactoryTypes<DomainError, Returns, Services, Meta> {
|
|
364
|
+
readonly DomainError: DomainError;
|
|
365
|
+
readonly Returns: Returns;
|
|
366
|
+
readonly Services: Services;
|
|
367
|
+
readonly Meta: Meta;
|
|
368
|
+
}
|
|
369
|
+
|
|
269
370
|
// ---------------------------------------------------------------------------
|
|
270
371
|
// Factory.
|
|
271
372
|
// ---------------------------------------------------------------------------
|
|
@@ -277,7 +378,7 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
277
378
|
* Declare the app's **domain errors** as the type argument, then register an
|
|
278
379
|
* *optional* handler per domain error in `errorHandlers`. The handler keys
|
|
279
380
|
* autocomplete to your domain-error tags and each `error` parameter is typed from
|
|
280
|
-
* its key
|
|
381
|
+
* its key - no annotation needed. It's curried so you can pin the domain errors
|
|
281
382
|
* while the handler return types are still inferred:
|
|
282
383
|
*
|
|
283
384
|
* ```ts
|
|
@@ -293,10 +394,10 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
293
394
|
* }));
|
|
294
395
|
* ```
|
|
295
396
|
*
|
|
296
|
-
* The inner config is a **builder**
|
|
397
|
+
* The inner config is a **builder** - it receives the library's `Respond` helpers so
|
|
297
398
|
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
298
399
|
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
299
|
-
* your app imports a *single* `Respond`
|
|
400
|
+
* your app imports a *single* `Respond` - no auto-import ambiguity with a library one.
|
|
300
401
|
*
|
|
301
402
|
* A handler *remaps* the error by returning either a library route error
|
|
302
403
|
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
@@ -305,13 +406,13 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
305
406
|
*
|
|
306
407
|
* **Declared domain errors** may be left unhandled: if one implements
|
|
307
408
|
* `HttpServerRespondable` it's rendered automatically, otherwise it falls through
|
|
308
|
-
* to a 500. **Any other error**
|
|
309
|
-
* isn't a declared domain error
|
|
409
|
+
* to a 500. **Any other error** - a service-specific error the route consumes that
|
|
410
|
+
* isn't a declared domain error - *must* be handled in the loader/action, or
|
|
310
411
|
* `makeLoader`/`makeAction` fails to type-check.
|
|
311
412
|
*
|
|
312
413
|
* Pass a `runtime` (a `ManagedRuntime`, e.g. from `ManagedRuntime.make(AppLayer)`)
|
|
313
414
|
* to provide your app's services once. Loader/action effects may then require those
|
|
314
|
-
* services directly
|
|
415
|
+
* services directly - no per-call `Effect.provide`:
|
|
315
416
|
*
|
|
316
417
|
* ```ts
|
|
317
418
|
* const { makeLoader, makeAction } = makeEffectRouteFactory<DomainErrors>()(() => ({
|
|
@@ -333,9 +434,9 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
333
434
|
* generated `Route.MiddlewareFunction` (a type argument, since middleware has no
|
|
334
435
|
* separate args-only type) to type `args` and `next`'s result from that route; it
|
|
335
436
|
* defaults to React Router's own `MiddlewareFunction`. The `next` handed to the
|
|
336
|
-
* effect is `() => Effect.Effect<Result, FailureResponse>`
|
|
437
|
+
* effect is `() => Effect.Effect<Result, FailureResponse>` - call it with `yield*`
|
|
337
438
|
* to run the rest of the chain, and a downstream throw (e.g. a nested loader's
|
|
338
|
-
* redirect)
|
|
439
|
+
* redirect) becomes a typed failure instead of a rejected promise:
|
|
339
440
|
*
|
|
340
441
|
* ```ts
|
|
341
442
|
* const middleware = makeMiddleware<Route.MiddlewareFunction>((args, next) =>
|
|
@@ -353,6 +454,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
353
454
|
RServices = never,
|
|
354
455
|
ReqServices = never,
|
|
355
456
|
const ExtraRespond extends Record<string, (...args: never[]) => unknown> = {},
|
|
457
|
+
Meta = never,
|
|
356
458
|
>(
|
|
357
459
|
/**
|
|
358
460
|
* Builds the factory config. It receives the library's base `Respond` helpers
|
|
@@ -363,7 +465,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
363
465
|
/**
|
|
364
466
|
* An optional handler per declared domain error, keyed by its tag. The keys
|
|
365
467
|
* autocomplete to your domain-error tags and each handler's `error` parameter
|
|
366
|
-
* is typed automatically
|
|
468
|
+
* is typed automatically - no annotation needed. A handler *remaps* the error
|
|
367
469
|
* by returning a library route error (the builder's `Respond.early` / `throw`
|
|
368
470
|
* / `redirect`) or an `Effect` (`Effect.succeed`/`fail`). Omit to register none.
|
|
369
471
|
*/
|
|
@@ -384,18 +486,29 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
384
486
|
*/
|
|
385
487
|
requestContext?: RequestContextKey<ReqServices>;
|
|
386
488
|
/**
|
|
387
|
-
* App-specific `Respond` helpers
|
|
489
|
+
* App-specific `Respond` helpers - merged onto the base `Respond` that the
|
|
388
490
|
* factory returns, so your app imports one `Respond`. Typically opinionated
|
|
389
491
|
* error constructors; annotate their parameters, e.g.
|
|
390
492
|
* `{ formError: (reply: string) => new FormError({ reply }) }`. The base
|
|
391
493
|
* helpers (`early` / `throw` / `redirect`) always win the merge.
|
|
392
494
|
*/
|
|
393
495
|
respond?: ExtraRespond;
|
|
496
|
+
/**
|
|
497
|
+
* Request-scoped metadata to attach to every action's data under `__meta`,
|
|
498
|
+
* read after the action resolves. The server adapter attaches it to every
|
|
499
|
+
* action it runs, including plain functions and `makeAction` wrappers. A
|
|
500
|
+
* thrown result gets none. Only plain records and `data(...)` bodies include
|
|
501
|
+
* it; other values are returned unchanged.
|
|
502
|
+
*/
|
|
503
|
+
actionMeta?: (args: ActionMetaArgs) => Meta;
|
|
394
504
|
},
|
|
395
505
|
) {
|
|
396
506
|
const config = builder(baseRespond);
|
|
397
507
|
const runtime = config.runtime;
|
|
398
508
|
const requestContextKey = config.requestContext;
|
|
509
|
+
// Where the request's effect context is stored: the app's key, or the library's own.
|
|
510
|
+
const storeKey: RequestContextKey<ReqServices> =
|
|
511
|
+
requestContextKey ?? (defaultRequestContext as RequestContextKey<ReqServices>);
|
|
399
512
|
// The single `Respond` an app uses: base helpers plus any app-specific ones.
|
|
400
513
|
// Base wins the merge so core helpers can't be shadowed.
|
|
401
514
|
const Respond = { ...config.respond, ...baseRespond } as BaseRespond & ExtraRespond;
|
|
@@ -404,18 +517,17 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
404
517
|
// Defaults to an empty map when `errorHandlers` is omitted.
|
|
405
518
|
const userHandlers = (config.errorHandlers ?? {}) as unknown as Record<
|
|
406
519
|
string,
|
|
407
|
-
ErrorHandler<
|
|
520
|
+
ErrorHandler<Tagged>
|
|
408
521
|
>;
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
typeof e === "object" && e !== null && "_tag" in e && (e as Tagged)._tag in userHandlers;
|
|
522
|
+
/** The tags of the registered domain errors. */
|
|
523
|
+
const userTags = Object.keys(userHandlers);
|
|
412
524
|
|
|
413
525
|
/**
|
|
414
526
|
* Runs a handler's effect to completion: dispatches its whole error channel
|
|
415
527
|
* (registered domain errors, library route errors, respondables, or the 500
|
|
416
528
|
* default), provides the per-request context, and executes against the
|
|
417
529
|
* configured runtime. Shared by `makeLoader`/`makeAction` and `makeMiddleware`
|
|
418
|
-
*
|
|
530
|
+
* - they differ only in how they build the effect and what `context` they read.
|
|
419
531
|
*/
|
|
420
532
|
function runProgram<E>(
|
|
421
533
|
effect: Effect.Effect<unknown, E, RServices | ReqServices>,
|
|
@@ -424,10 +536,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
424
536
|
): Promise<unknown> {
|
|
425
537
|
// The internal channel is deliberately loose (`unknown` success); the caller
|
|
426
538
|
// restores the precise resolved type (computed from `E` and the handler map).
|
|
427
|
-
// Sound at runtime
|
|
539
|
+
// Sound at runtime - the values produced are exactly those types.
|
|
428
540
|
const program = effect.pipe(
|
|
429
|
-
// Catch the
|
|
430
|
-
// (provably ⊆ E, and a *refinement* not a bare predicate
|
|
541
|
+
// Catch the full error channel and dispatch. The refinement is `e is E`
|
|
542
|
+
// (provably ⊆ E, and a *refinement* not a bare predicate - the predicate
|
|
431
543
|
// overload crashes tsc over a generic `E`). A declared domain error with no
|
|
432
544
|
// handler (and not respondable) falls through to the 500 default.
|
|
433
545
|
Effect.catchIf(
|
|
@@ -435,9 +547,11 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
435
547
|
(e): Effect.Effect<unknown, FailureResponse> => {
|
|
436
548
|
// Registered domain error → remap. A library-error return is processed
|
|
437
549
|
// by the internal dispatch; an `Effect` return is used as-is.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
550
|
+
for (const tag of userTags) {
|
|
551
|
+
if (Predicate.isTagged(e, tag)) {
|
|
552
|
+
const out = userHandlers[tag](e);
|
|
553
|
+
return isRouteError(out) ? processRouteError(out) : out;
|
|
554
|
+
}
|
|
441
555
|
}
|
|
442
556
|
// A library route error raised directly in the handler → recover/throw.
|
|
443
557
|
if (isRouteError(e)) return processRouteError(e);
|
|
@@ -456,12 +570,16 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
456
570
|
},
|
|
457
571
|
),
|
|
458
572
|
);
|
|
459
|
-
// Provide the
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
573
|
+
// Provide the request's effect context so `ReqServices` are satisfied,
|
|
574
|
+
// leaving only the runtime's `RServices` in the requirements. Under the
|
|
575
|
+
// server adapter the store also holds the HTTP request fiber's context, and
|
|
576
|
+
// whatever an effect middleware provided around `next()`. The cast is sound:
|
|
577
|
+
// `provideContext` removes `ReqServices`, and when no `requestContext` is
|
|
578
|
+
// configured `ReqServices` is `never` (nothing removed).
|
|
579
|
+
const store = readRequestStore(context);
|
|
580
|
+
// oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- the cast is sound, as the comment above explains
|
|
463
581
|
const provided = (
|
|
464
|
-
|
|
582
|
+
store === undefined ? program : Effect.provideContext(program, store)
|
|
465
583
|
) as Effect.Effect<unknown, FailureResponse, RServices>;
|
|
466
584
|
// Run against the configured runtime so its services satisfy the effect's
|
|
467
585
|
// `R`; with no runtime, the effect requires nothing and runs standalone.
|
|
@@ -470,15 +588,16 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
470
588
|
// signal and discards its result, so the interrupt rejection is never rendered.
|
|
471
589
|
return runtime
|
|
472
590
|
? runtime.runPromise(provided, { signal })
|
|
473
|
-
:
|
|
591
|
+
: // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- with no runtime `RServices` is `never`
|
|
592
|
+
Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
|
|
474
593
|
}
|
|
475
594
|
|
|
476
595
|
function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
|
|
477
|
-
// `A`/`E`/`R` infer directly from the effect (naked, so inference stays
|
|
596
|
+
// `A`/`E`/`R` infer directly from the effect (naked, so inference stays intact).
|
|
478
597
|
// The diagnostic is intersected onto the effect's return type: when the effect
|
|
479
598
|
// requires a service the factory doesn't provide (`R` ⊄ runtime+requestContext),
|
|
480
599
|
// or can fail with something the library won't handle, `Diagnose` becomes a
|
|
481
|
-
// message object the effect isn't assignable to
|
|
600
|
+
// message object the effect isn't assignable to - so this argument fails to
|
|
482
601
|
// type-check with a readable explanation, and the offending types, right here.
|
|
483
602
|
fn: (
|
|
484
603
|
args: Args,
|
|
@@ -501,30 +620,30 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
501
620
|
* args-only type) to type `args`, `next` and the result from that route; it defaults
|
|
502
621
|
* to React Router's own `MiddlewareFunction`.
|
|
503
622
|
*
|
|
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
|
|
623
|
+
* The errors the library deals with for a loader/action it deals with here too -
|
|
624
|
+
* declared domain errors, library route errors and respondables - plus the
|
|
506
625
|
* `FailureResponse` `next()` fails with. Anything else must be handled in the effect,
|
|
507
626
|
* as must any service neither the `runtime` nor the `requestContext` provides.
|
|
508
627
|
*
|
|
509
|
-
* The wrapper is typed as the route's own middleware
|
|
510
|
-
* Promise<Result | undefined>`
|
|
628
|
+
* The wrapper is typed as the route's own middleware - `(args, next) =>
|
|
629
|
+
* Promise<Result | undefined>` - so it drops straight into a
|
|
511
630
|
* `Route.MiddlewareFunction[]`. A handler that recovers (`Respond.early`) resolves
|
|
512
631
|
* with `data(...)`, which react-router's server middleware pipeline turns into the
|
|
513
632
|
* short-circuit `Response`.
|
|
514
633
|
*/
|
|
515
634
|
function makeMiddleware<
|
|
516
635
|
Middleware extends AnyMiddlewareFunction = MiddlewareFunction,
|
|
517
|
-
// A pinned call passes an explicit type argument, which makes the
|
|
518
|
-
// explicit
|
|
636
|
+
// A pinned call passes an explicit type argument, which makes every type argument of the call
|
|
637
|
+
// explicit - TypeScript has no partial inference - so these defaults stand in
|
|
519
638
|
// for what would otherwise be inferred: exactly the errors the library handles
|
|
520
639
|
// and the services the factory provides.
|
|
521
640
|
E = LibraryHandled<DomainError, FailureResponse>,
|
|
522
641
|
R = RServices | ReqServices,
|
|
523
642
|
>(
|
|
524
643
|
// Same shape as `makeLoaderOrAction`'s `fn`, but middleware also receives
|
|
525
|
-
// `next`
|
|
644
|
+
// `next` - wrapped as an effect (rather than react-router's raw
|
|
526
645
|
// `() => Promise<Result>`) so downstream failures (e.g. a nested loader's
|
|
527
|
-
// redirect)
|
|
646
|
+
// redirect) become a typed `FailureResponse`, consistent with the rest
|
|
528
647
|
// of the library's error model. The success type is fixed to the route's own
|
|
529
648
|
// `Result` (plus `void`), which is what react-router accepts back.
|
|
530
649
|
fn: (
|
|
@@ -538,33 +657,124 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
|
|
|
538
657
|
) => Promise<MiddlewareResultOf<Middleware> | undefined> {
|
|
539
658
|
// Inside the body the route's own arg/result types are opaque (they're
|
|
540
659
|
// `Parameters<Middleware>[...]` of an unresolved type parameter), so dispatch
|
|
541
|
-
// through the loose shape
|
|
660
|
+
// through the loose shape - as `makeLoaderOrAction` does with its diagnostic.
|
|
542
661
|
// Sound at runtime: the values produced are exactly the declared types.
|
|
543
662
|
const run = fn as unknown as (
|
|
544
663
|
args: MiddlewareArgsOf<Middleware>,
|
|
545
664
|
next: () => Effect.Effect<unknown, FailureResponse>,
|
|
546
665
|
) => Effect.Effect<unknown, E, RServices | ReqServices>;
|
|
547
666
|
return (args, next) => {
|
|
548
|
-
const
|
|
549
|
-
|
|
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
|
-
};
|
|
667
|
+
const { context, request } = args as unknown as RouteArgs;
|
|
668
|
+
const effectNext = makeEffectNext(context, next as unknown as () => Promise<unknown>);
|
|
557
669
|
return runProgram(run(args, effectNext), context, request.signal) as Promise<
|
|
558
670
|
MiddlewareResultOf<Middleware> | undefined
|
|
559
671
|
>;
|
|
560
672
|
};
|
|
561
673
|
}
|
|
562
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Reads the request's effect context from the router context. A test that
|
|
677
|
+
* passes args without a router context gets none, as before.
|
|
678
|
+
*/
|
|
679
|
+
function readRequestStore(
|
|
680
|
+
context: Readonly<RouterContextProvider> | undefined,
|
|
681
|
+
): Context.Context<ReqServices> | undefined {
|
|
682
|
+
// An app key without a default that no middleware has set throws here, as
|
|
683
|
+
// it always has: the app promised its services would be there. The
|
|
684
|
+
// library's own key starts empty.
|
|
685
|
+
return typeof context?.get === "function" ? context.get(storeKey) : undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Wraps React Router's `next` as an effect. Before it runs the rest of the
|
|
690
|
+
* chain, it adds to the request's effect context what the calling fiber
|
|
691
|
+
* provides beyond the context the middleware started with: services it
|
|
692
|
+
* provides around `next()`, log annotations, and its current span. Updates
|
|
693
|
+
* the middleware made with `context.set` stay. A downstream rejection is
|
|
694
|
+
* already a finished response, so it fails with that response as-is.
|
|
695
|
+
*/
|
|
696
|
+
function makeEffectNext(
|
|
697
|
+
context: Readonly<RouterContextProvider>,
|
|
698
|
+
next: () => Promise<unknown>,
|
|
699
|
+
): () => Effect.Effect<unknown, FailureResponse> {
|
|
700
|
+
const started = readRequestStore(context);
|
|
701
|
+
return () =>
|
|
702
|
+
Effect.withFiber((fiber) => {
|
|
703
|
+
if (started !== undefined) {
|
|
704
|
+
context.set(
|
|
705
|
+
storeKey,
|
|
706
|
+
Context.merge(context.get(storeKey), providedSince(started, fiber.context)),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return Effect.tryPromise({ try: next, catch: (e) => e as FailureResponse });
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Runs a loader or action's result: an Effect through the error model, anything else as-is. */
|
|
714
|
+
function runResult(result: unknown, args: RouteArgs): unknown {
|
|
715
|
+
return Effect.isEffect(result)
|
|
716
|
+
? runProgram(
|
|
717
|
+
// oxlint-disable-next-line effecttsgo/any-unknown-in-error-context, effecttsgo/unsafe-effect-type-assertion -- a route function's Effect is only known to be an Effect here; `runProgram` dispatches its whole error channel
|
|
718
|
+
result as Effect.Effect<unknown, unknown, RServices | ReqServices>,
|
|
719
|
+
args.context,
|
|
720
|
+
args.request.signal,
|
|
721
|
+
)
|
|
722
|
+
: result;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const actionMeta = config.actionMeta;
|
|
726
|
+
|
|
727
|
+
// React Router calls every route function with args that include at least
|
|
728
|
+
// `request` and `context`, so each wrapper reads them through `RouteArgs`.
|
|
729
|
+
// The runners resolve with what `runResult` produces: the Effect's success
|
|
730
|
+
// or recovered data, or the awaited plain result. `RouteResult` is its type.
|
|
731
|
+
const handlers: RouteHandlers = {
|
|
732
|
+
loader: (fn) => async (args) =>
|
|
733
|
+
runResult(fn(args), args) as RouteResult<ReturnType<typeof fn>>,
|
|
734
|
+
action: (fn) => async (args) => {
|
|
735
|
+
const result = await runResult(fn(args), args);
|
|
736
|
+
return (
|
|
737
|
+
actionMeta === undefined ? result : attachActionMeta(result, actionMeta(args))
|
|
738
|
+
) as RouteResult<ReturnType<typeof fn>>;
|
|
739
|
+
},
|
|
740
|
+
middleware: (fn) => async (args, next) => {
|
|
741
|
+
const routeArgs: RouteArgs = args;
|
|
742
|
+
// The runner supplies `next` as an effect in place of the one `Args`
|
|
743
|
+
// names, which it cannot prove for an arbitrary `Args`.
|
|
744
|
+
const effectArgs = {
|
|
745
|
+
...args,
|
|
746
|
+
next: makeEffectNext(routeArgs.context, next),
|
|
747
|
+
} as unknown as Parameters<typeof fn>[0];
|
|
748
|
+
return runResult(fn(effectArgs, next), routeArgs) as RouteResult<ReturnType<typeof fn>>;
|
|
749
|
+
},
|
|
750
|
+
requestContext: storeKey as RouterContext<Context.Context<never>>,
|
|
751
|
+
};
|
|
752
|
+
|
|
563
753
|
return {
|
|
564
754
|
makeLoader: makeLoaderOrAction,
|
|
565
|
-
makeAction: makeLoaderOrAction
|
|
755
|
+
makeAction: makeLoaderOrAction as <
|
|
756
|
+
Args extends LoaderFunctionArgs | ActionFunctionArgs,
|
|
757
|
+
A,
|
|
758
|
+
E,
|
|
759
|
+
R,
|
|
760
|
+
>(
|
|
761
|
+
fn: (
|
|
762
|
+
args: Args,
|
|
763
|
+
) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
|
|
764
|
+
) => (
|
|
765
|
+
args: Args,
|
|
766
|
+
) => Promise<WithActionMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>,
|
|
566
767
|
makeMiddleware,
|
|
567
768
|
Respond,
|
|
769
|
+
/** The route runner the server adapter applies to every route module. */
|
|
770
|
+
handlers,
|
|
771
|
+
/** Type-only: what the adapter's route check and `Register` read. */
|
|
772
|
+
"~types": undefined as unknown as FactoryTypes<
|
|
773
|
+
DomainError,
|
|
774
|
+
Returns,
|
|
775
|
+
RServices | ReqServices,
|
|
776
|
+
Meta
|
|
777
|
+
>,
|
|
568
778
|
};
|
|
569
779
|
};
|
|
570
780
|
}
|