react-router-effect 0.5.0 → 0.6.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 +38 -31
- package/dist/index.d.mts +44 -20
- package/dist/index.mjs +34 -19
- package/package.json +1 -1
- package/src/errors.ts +10 -7
- package/src/factory.ts +63 -32
- package/src/index.ts +0 -1
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ handling of feature/service-specific ones.
|
|
|
64
64
|
```ts
|
|
65
65
|
// app/route.server.ts
|
|
66
66
|
import { Data, Effect } from "effect";
|
|
67
|
-
import { makeLoaderOrActionFactory
|
|
67
|
+
import { makeLoaderOrActionFactory } from "react-router-effect";
|
|
68
68
|
|
|
69
69
|
class FormError extends Data.TaggedError("FormError")<{ reply: SubmissionResponse }> {}
|
|
70
70
|
class BadInputError extends Data.TaggedError("BadInputError")<{ message: string }> {}
|
|
@@ -74,27 +74,31 @@ class DbError extends Data.TaggedError("DbError")<{ query: string }> {}
|
|
|
74
74
|
// so it falls through to the 500 default.
|
|
75
75
|
type DomainErrors = FormError | BadInputError | DbError;
|
|
76
76
|
|
|
77
|
-
// Curried: pin the domain errors, then the handler types are inferred.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
77
|
+
// Curried: pin the domain errors, then the handler types are inferred. The config
|
|
78
|
+
// is a *builder* — it receives `Respond` (the library's `early`/`throw`/`redirect`)
|
|
79
|
+
// so your handlers can recover/throw, and the factory hands back a single `Respond`
|
|
80
|
+
// for your routes to import (no auto-import ambiguity with a library export).
|
|
81
|
+
export const { makeLoader, makeAction, Respond } = makeLoaderOrActionFactory<DomainErrors>()(
|
|
82
|
+
(Respond) => ({
|
|
83
|
+
// App-specific helpers — merged onto the returned `Respond` (base helpers win):
|
|
84
|
+
respond: {
|
|
85
|
+
formError: (reply: SubmissionResponse) => new FormError({ reply }),
|
|
86
|
+
},
|
|
87
|
+
errorHandlers: {
|
|
88
|
+
// keys autocomplete to your domain-error tags; `error` is typed from its key.
|
|
89
|
+
// recover: short-circuit and hand the reply to the component
|
|
90
|
+
FormError: (error) => Respond.early({ reply: error.reply }),
|
|
91
|
+
// throw: send to the error boundary
|
|
92
|
+
BadInputError: (error) => Effect.fail(new Response(error.message, { status: 400 })),
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
93
96
|
```
|
|
94
97
|
|
|
95
|
-
> **
|
|
96
|
-
> `
|
|
97
|
-
>
|
|
98
|
+
> **One `Respond`, no annotations.** Your routes import the `Respond` returned here — there's
|
|
99
|
+
> no library-level `Respond` to clash with it on auto-import. Handler keys autocomplete to your
|
|
100
|
+
> declared domain-error tags, each `error` parameter is typed from its key, and the precise
|
|
101
|
+
> recover types are derived from each handler's return.
|
|
98
102
|
|
|
99
103
|
### 2. Write loaders/actions as effects
|
|
100
104
|
|
|
@@ -153,10 +157,10 @@ import { ManagedRuntime } from "effect";
|
|
|
153
157
|
export const appRuntime = ManagedRuntime.make(AppLayer); // provides Database, MyService, ...
|
|
154
158
|
|
|
155
159
|
// app/route.server.ts
|
|
156
|
-
export const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()({
|
|
160
|
+
export const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()(() => ({
|
|
157
161
|
runtime: appRuntime,
|
|
158
162
|
errorHandlers: { ... },
|
|
159
|
-
});
|
|
163
|
+
}));
|
|
160
164
|
|
|
161
165
|
// app/routes/profile.ts — `MyService` is satisfied by the runtime, not provided here:
|
|
162
166
|
const loader = makeLoader((args: Route.LoaderArgs) =>
|
|
@@ -174,8 +178,8 @@ runtime provides type-checks, while requiring one it _doesn't_ is a compile erro
|
|
|
174
178
|
`errorHandlers` is optional too — configure a factory with just a runtime, or with nothing:
|
|
175
179
|
|
|
176
180
|
```ts
|
|
177
|
-
makeLoaderOrActionFactory()({ runtime });
|
|
178
|
-
makeLoaderOrActionFactory()({});
|
|
181
|
+
makeLoaderOrActionFactory()(() => ({ runtime }));
|
|
182
|
+
makeLoaderOrActionFactory()(() => ({}));
|
|
179
183
|
```
|
|
180
184
|
|
|
181
185
|
### Per-request services from middleware
|
|
@@ -210,10 +214,10 @@ export const middleware: Route.MiddlewareFunction[] = [
|
|
|
210
214
|
];
|
|
211
215
|
|
|
212
216
|
// app/route.server.ts — wire the same key into the factory:
|
|
213
|
-
export const { makeLoader } = makeLoaderOrActionFactory<DomainErrors>()({
|
|
217
|
+
export const { makeLoader } = makeLoaderOrActionFactory<DomainErrors>()(() => ({
|
|
214
218
|
runtime: appRuntime,
|
|
215
219
|
requestContext,
|
|
216
|
-
});
|
|
220
|
+
}));
|
|
217
221
|
|
|
218
222
|
// the loader requires RequestContext directly — no provide, fresh each request:
|
|
219
223
|
export const loader = makeLoader(() =>
|
|
@@ -230,13 +234,16 @@ else is a compile error. `RequestContextKey<ReqServices>` is a type alias for
|
|
|
230
234
|
|
|
231
235
|
## API
|
|
232
236
|
|
|
233
|
-
- **`makeLoaderOrActionFactory<DomainErrors>()({ errorHandlers?, runtime?, requestContext? })`**
|
|
234
|
-
`{ makeLoader, makeAction }` (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
- **`makeLoaderOrActionFactory<DomainErrors>()((Respond) => ({ errorHandlers?, runtime?, requestContext?, respond? }))`**
|
|
238
|
+
→ `{ makeLoader, makeAction, Respond }` (the two makers are the same wrapper). The config is a
|
|
239
|
+
_builder_ that receives the base `Respond`; all its fields are optional. The returned `Respond` is
|
|
240
|
+
the base helpers merged with your `respond` extensions (base helpers win). A non-domain error left
|
|
241
|
+
in a loader/action's error channel — or a required service that neither the `runtime` nor the
|
|
242
|
+
`requestContext` provides — is a compile error.
|
|
237
243
|
- **`RequestContextKey<ReqServices>`** — type of the React Router context key for a per-request
|
|
238
244
|
effect context (`RouterContext<Context.Context<ReqServices>>`).
|
|
239
|
-
- **`Respond`** — `early` (recover), `throw`, `redirect
|
|
245
|
+
- **`Respond`** (returned from the factory) — `early` (recover), `throw`, `redirect`, plus any
|
|
246
|
+
`respond` helpers you add.
|
|
240
247
|
- **`ReturnableDataError`**, **`ThrowableDataError`**, **`ThrowableRedirectError`** — the library
|
|
241
248
|
route errors, and **`isRouteError`** to narrow them.
|
|
242
249
|
- **`ErrorHandler<Err>`** — the handler signature type.
|
package/dist/index.d.mts
CHANGED
|
@@ -30,15 +30,18 @@ declare class ThrowableRedirectError extends ThrowableRedirectError_base<{
|
|
|
30
30
|
/** Every library route error, as a single union. */
|
|
31
31
|
type AnyRouteError = ReturnableDataError<unknown> | ThrowableDataError<unknown> | ThrowableRedirectError;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* The library's base helpers for raising route errors (`early` / `throw` /
|
|
34
|
+
* `redirect`). This is internal: it isn't exported from the package. The factory
|
|
35
|
+
* hands it to your config builder and returns it (merged with any `respond`
|
|
36
|
+
* extensions) so your app imports a single `Respond`:
|
|
36
37
|
*
|
|
37
38
|
* ```ts
|
|
38
|
-
* export const Respond =
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* }
|
|
39
|
+
* export const { makeLoader, Respond } = makeLoaderOrActionFactory<DomainErrors>()(
|
|
40
|
+
* (Respond) => ({
|
|
41
|
+
* respond: { formError: (reply) => new FormError({ reply }) },
|
|
42
|
+
* errorHandlers: { FormError: (e) => Respond.early({ reply: e.reply }) },
|
|
43
|
+
* }),
|
|
44
|
+
* );
|
|
42
45
|
* ```
|
|
43
46
|
*/
|
|
44
47
|
declare const Respond: {
|
|
@@ -50,6 +53,8 @@ declare const Respond: {
|
|
|
50
53
|
declare const isRouteError: (value: unknown) => value is AnyRouteError;
|
|
51
54
|
//#endregion
|
|
52
55
|
//#region src/factory.d.ts
|
|
56
|
+
/** The library's base `Respond` helpers (`early` / `throw` / `redirect`). */
|
|
57
|
+
type BaseRespond = typeof Respond;
|
|
53
58
|
type Tagged = {
|
|
54
59
|
readonly _tag: string;
|
|
55
60
|
};
|
|
@@ -159,15 +164,21 @@ type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>
|
|
|
159
164
|
* ```ts
|
|
160
165
|
* type DomainErrors = MyDomainError | DbError | NotAuthorizedError;
|
|
161
166
|
*
|
|
162
|
-
* const { makeLoader, makeAction } =
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* export const { makeLoader, makeAction, Respond } =
|
|
168
|
+
* makeLoaderOrActionFactory<DomainErrors>()((Respond) => ({
|
|
169
|
+
* errorHandlers: {
|
|
170
|
+
* // `error` is typed as MyDomainError automatically. throw → error boundary
|
|
171
|
+
* MyDomainError: (error) => Respond.throw({ message: error.message }, 400),
|
|
172
|
+
* // DbError has no handler → falls through to the 500 default.
|
|
173
|
+
* },
|
|
174
|
+
* }));
|
|
169
175
|
* ```
|
|
170
176
|
*
|
|
177
|
+
* The inner config is a **builder** — it receives the library's `Respond` helpers so
|
|
178
|
+
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
179
|
+
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
180
|
+
* your app imports a *single* `Respond` — no auto-import ambiguity with a library one.
|
|
181
|
+
*
|
|
171
182
|
* A handler *remaps* the error by returning either a library route error
|
|
172
183
|
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
173
184
|
* `Effect` (`Effect.succeed(value)` to recover `value`; `Effect.fail(response)` to
|
|
@@ -184,10 +195,10 @@ type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>
|
|
|
184
195
|
* services directly — no per-call `Effect.provide`:
|
|
185
196
|
*
|
|
186
197
|
* ```ts
|
|
187
|
-
* const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()({
|
|
198
|
+
* const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()(() => ({
|
|
188
199
|
* runtime: getAppRuntime(), // provides Database, MyService, ...
|
|
189
200
|
* errorHandlers: { ... },
|
|
190
|
-
* });
|
|
201
|
+
* }));
|
|
191
202
|
*
|
|
192
203
|
* // `MyService` is satisfied by the runtime, not provided here:
|
|
193
204
|
* const loader = makeLoader((args: Route.LoaderArgs) =>
|
|
@@ -198,13 +209,13 @@ type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>
|
|
|
198
209
|
* );
|
|
199
210
|
* ```
|
|
200
211
|
*/
|
|
201
|
-
declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>(): <const Returns extends HandlerReturns<DomainError> = {}, RServices = never, ReqServices = never>(
|
|
212
|
+
declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>(): <const Returns extends HandlerReturns<DomainError> = {}, RServices = never, ReqServices = never, const ExtraRespond extends Record<string, (...args: never[]) => unknown> = {}>(builder: (respond: BaseRespond) => {
|
|
202
213
|
/**
|
|
203
214
|
* An optional handler per declared domain error, keyed by its tag. The keys
|
|
204
215
|
* autocomplete to your domain-error tags and each handler's `error` parameter
|
|
205
216
|
* is typed automatically — no annotation needed. A handler *remaps* the error
|
|
206
|
-
* by returning a library route error (`Respond.early` / `throw`
|
|
207
|
-
* or an `Effect` (`Effect.succeed`/`fail`). Omit
|
|
217
|
+
* by returning a library route error (the builder's `Respond.early` / `throw`
|
|
218
|
+
* / `redirect`) or an `Effect` (`Effect.succeed`/`fail`). Omit to register none.
|
|
208
219
|
*/
|
|
209
220
|
errorHandlers?: ErrorHandlers<DomainError, Returns>;
|
|
210
221
|
/**
|
|
@@ -222,9 +233,22 @@ declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
222
233
|
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
223
234
|
*/
|
|
224
235
|
requestContext?: RequestContextKey<ReqServices>;
|
|
236
|
+
/**
|
|
237
|
+
* App-specific `Respond` helpers — merged onto the base `Respond` that the
|
|
238
|
+
* factory returns, so your app imports one `Respond`. Typically opinionated
|
|
239
|
+
* error constructors; annotate their parameters, e.g.
|
|
240
|
+
* `{ formError: (reply: string) => new FormError({ reply }) }`. The base
|
|
241
|
+
* helpers (`early` / `throw` / `redirect`) always win the merge.
|
|
242
|
+
*/
|
|
243
|
+
respond?: ExtraRespond;
|
|
225
244
|
}) => {
|
|
226
245
|
makeLoader: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(fn: (args: Args) => Effect.Effect<A, E, RServices | ReqServices>, ..._handle: [Unhandled<E, DomainError>] extends [never] ? [] : [theseErrorsAreNotDomainErrorsAndMustBeHandledInTheLoaderOrAction: Exclude<E, LibraryHandled<DomainError>>]) => (args: Args) => Promise<A | RecoverOf<DomainError, Returns, E> | DirectRecover<E>>;
|
|
227
246
|
makeAction: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(fn: (args: Args) => Effect.Effect<A, E, RServices | ReqServices>, ..._handle: [Unhandled<E, DomainError>] extends [never] ? [] : [theseErrorsAreNotDomainErrorsAndMustBeHandledInTheLoaderOrAction: Exclude<E, LibraryHandled<DomainError>>]) => (args: Args) => Promise<A | RecoverOf<DomainError, Returns, E> | DirectRecover<E>>;
|
|
247
|
+
Respond: {
|
|
248
|
+
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
249
|
+
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
|
250
|
+
redirect: (url: string, init?: number | ResponseInit) => ThrowableRedirectError;
|
|
251
|
+
} & ExtraRespond;
|
|
228
252
|
};
|
|
229
253
|
//#endregion
|
|
230
|
-
export { type AnyRouteError, type ErrorHandler, type RequestContextKey,
|
|
254
|
+
export { type AnyRouteError, type ErrorHandler, type RequestContextKey, ReturnableDataError, ThrowableDataError, ThrowableRedirectError, isRouteError, makeLoaderOrActionFactory };
|
package/dist/index.mjs
CHANGED
|
@@ -9,15 +9,18 @@ var ThrowableDataError = class extends Data.TaggedError("ThrowableRouteError") {
|
|
|
9
9
|
/** Throwable: rejects with a redirect `Response`. */
|
|
10
10
|
var ThrowableRedirectError = class extends Data.TaggedError("ThrowableRedirectError") {};
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The library's base helpers for raising route errors (`early` / `throw` /
|
|
13
|
+
* `redirect`). This is internal: it isn't exported from the package. The factory
|
|
14
|
+
* hands it to your config builder and returns it (merged with any `respond`
|
|
15
|
+
* extensions) so your app imports a single `Respond`:
|
|
15
16
|
*
|
|
16
17
|
* ```ts
|
|
17
|
-
* export const Respond =
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* }
|
|
18
|
+
* export const { makeLoader, Respond } = makeLoaderOrActionFactory<DomainErrors>()(
|
|
19
|
+
* (Respond) => ({
|
|
20
|
+
* respond: { formError: (reply) => new FormError({ reply }) },
|
|
21
|
+
* errorHandlers: { FormError: (e) => Respond.early({ reply: e.reply }) },
|
|
22
|
+
* }),
|
|
23
|
+
* );
|
|
21
24
|
* ```
|
|
22
25
|
*/
|
|
23
26
|
const Respond = {
|
|
@@ -60,15 +63,21 @@ const processRouteError = (e) => {
|
|
|
60
63
|
* ```ts
|
|
61
64
|
* type DomainErrors = MyDomainError | DbError | NotAuthorizedError;
|
|
62
65
|
*
|
|
63
|
-
* const { makeLoader, makeAction } =
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
66
|
+
* export const { makeLoader, makeAction, Respond } =
|
|
67
|
+
* makeLoaderOrActionFactory<DomainErrors>()((Respond) => ({
|
|
68
|
+
* errorHandlers: {
|
|
69
|
+
* // `error` is typed as MyDomainError automatically. throw → error boundary
|
|
70
|
+
* MyDomainError: (error) => Respond.throw({ message: error.message }, 400),
|
|
71
|
+
* // DbError has no handler → falls through to the 500 default.
|
|
72
|
+
* },
|
|
73
|
+
* }));
|
|
70
74
|
* ```
|
|
71
75
|
*
|
|
76
|
+
* The inner config is a **builder** — it receives the library's `Respond` helpers so
|
|
77
|
+
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
78
|
+
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
79
|
+
* your app imports a *single* `Respond` — no auto-import ambiguity with a library one.
|
|
80
|
+
*
|
|
72
81
|
* A handler *remaps* the error by returning either a library route error
|
|
73
82
|
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
74
83
|
* `Effect` (`Effect.succeed(value)` to recover `value`; `Effect.fail(response)` to
|
|
@@ -85,10 +94,10 @@ const processRouteError = (e) => {
|
|
|
85
94
|
* services directly — no per-call `Effect.provide`:
|
|
86
95
|
*
|
|
87
96
|
* ```ts
|
|
88
|
-
* const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()({
|
|
97
|
+
* const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()(() => ({
|
|
89
98
|
* runtime: getAppRuntime(), // provides Database, MyService, ...
|
|
90
99
|
* errorHandlers: { ... },
|
|
91
|
-
* });
|
|
100
|
+
* }));
|
|
92
101
|
*
|
|
93
102
|
* // `MyService` is satisfied by the runtime, not provided here:
|
|
94
103
|
* const loader = makeLoader((args: Route.LoaderArgs) =>
|
|
@@ -100,9 +109,14 @@ const processRouteError = (e) => {
|
|
|
100
109
|
* ```
|
|
101
110
|
*/
|
|
102
111
|
function makeLoaderOrActionFactory() {
|
|
103
|
-
return function defineErrorHandlers(
|
|
112
|
+
return function defineErrorHandlers(builder) {
|
|
113
|
+
const config = builder(Respond);
|
|
104
114
|
const runtime = config.runtime;
|
|
105
115
|
const requestContextKey = config.requestContext;
|
|
116
|
+
const Respond$1 = {
|
|
117
|
+
...config.respond,
|
|
118
|
+
...Respond
|
|
119
|
+
};
|
|
106
120
|
const userHandlers = config.errorHandlers ?? {};
|
|
107
121
|
const isUserError = (e) => typeof e === "object" && e !== null && "_tag" in e && e._tag in userHandlers;
|
|
108
122
|
function makeLoaderOrAction(fn, ..._handle) {
|
|
@@ -122,9 +136,10 @@ function makeLoaderOrActionFactory() {
|
|
|
122
136
|
}
|
|
123
137
|
return {
|
|
124
138
|
makeLoader: makeLoaderOrAction,
|
|
125
|
-
makeAction: makeLoaderOrAction
|
|
139
|
+
makeAction: makeLoaderOrAction,
|
|
140
|
+
Respond: Respond$1
|
|
126
141
|
};
|
|
127
142
|
};
|
|
128
143
|
}
|
|
129
144
|
//#endregion
|
|
130
|
-
export {
|
|
145
|
+
export { ReturnableDataError, ThrowableDataError, ThrowableRedirectError, isRouteError, makeLoaderOrActionFactory };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://schemastore.org/package.json",
|
|
3
3
|
"name": "react-router-effect",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Wrap React Router loaders/actions with Effect, with typed recover/throw/redirect error handling.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"action",
|
package/src/errors.ts
CHANGED
|
@@ -29,15 +29,18 @@ export type AnyRouteError =
|
|
|
29
29
|
| ThrowableRedirectError;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* The library's base helpers for raising route errors (`early` / `throw` /
|
|
33
|
+
* `redirect`). This is internal: it isn't exported from the package. The factory
|
|
34
|
+
* hands it to your config builder and returns it (merged with any `respond`
|
|
35
|
+
* extensions) so your app imports a single `Respond`:
|
|
35
36
|
*
|
|
36
37
|
* ```ts
|
|
37
|
-
* export const Respond =
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* }
|
|
38
|
+
* export const { makeLoader, Respond } = makeLoaderOrActionFactory<DomainErrors>()(
|
|
39
|
+
* (Respond) => ({
|
|
40
|
+
* respond: { formError: (reply) => new FormError({ reply }) },
|
|
41
|
+
* errorHandlers: { FormError: (e) => Respond.early({ reply: e.reply }) },
|
|
42
|
+
* }),
|
|
43
|
+
* );
|
|
41
44
|
* ```
|
|
42
45
|
*/
|
|
43
46
|
export const Respond = {
|
package/src/factory.ts
CHANGED
|
@@ -11,11 +11,15 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
isRouteError,
|
|
14
|
+
Respond as baseRespond,
|
|
14
15
|
ReturnableDataError,
|
|
15
16
|
ThrowableDataError,
|
|
16
17
|
type AnyRouteError,
|
|
17
18
|
} from "./errors.ts";
|
|
18
19
|
|
|
20
|
+
/** The library's base `Respond` helpers (`early` / `throw` / `redirect`). */
|
|
21
|
+
type BaseRespond = typeof baseRespond;
|
|
22
|
+
|
|
19
23
|
// ---------------------------------------------------------------------------
|
|
20
24
|
// Type-level plumbing.
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
@@ -173,15 +177,21 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
173
177
|
* ```ts
|
|
174
178
|
* type DomainErrors = MyDomainError | DbError | NotAuthorizedError;
|
|
175
179
|
*
|
|
176
|
-
* const { makeLoader, makeAction } =
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
180
|
+
* export const { makeLoader, makeAction, Respond } =
|
|
181
|
+
* makeLoaderOrActionFactory<DomainErrors>()((Respond) => ({
|
|
182
|
+
* errorHandlers: {
|
|
183
|
+
* // `error` is typed as MyDomainError automatically. throw → error boundary
|
|
184
|
+
* MyDomainError: (error) => Respond.throw({ message: error.message }, 400),
|
|
185
|
+
* // DbError has no handler → falls through to the 500 default.
|
|
186
|
+
* },
|
|
187
|
+
* }));
|
|
183
188
|
* ```
|
|
184
189
|
*
|
|
190
|
+
* The inner config is a **builder** — it receives the library's `Respond` helpers so
|
|
191
|
+
* your handlers can recover/throw with them, and returns the config. `Respond` is
|
|
192
|
+
* also returned from the factory (extended with any `respond` helpers you add), so
|
|
193
|
+
* your app imports a *single* `Respond` — no auto-import ambiguity with a library one.
|
|
194
|
+
*
|
|
185
195
|
* A handler *remaps* the error by returning either a library route error
|
|
186
196
|
* (`Respond.early` to recover; `Respond.throw` / `Respond.redirect` to throw) or an
|
|
187
197
|
* `Effect` (`Effect.succeed(value)` to recover `value`; `Effect.fail(response)` to
|
|
@@ -198,10 +208,10 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
|
|
|
198
208
|
* services directly — no per-call `Effect.provide`:
|
|
199
209
|
*
|
|
200
210
|
* ```ts
|
|
201
|
-
* const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()({
|
|
211
|
+
* const { makeLoader, makeAction } = makeLoaderOrActionFactory<DomainErrors>()(() => ({
|
|
202
212
|
* runtime: getAppRuntime(), // provides Database, MyService, ...
|
|
203
213
|
* errorHandlers: { ... },
|
|
204
|
-
* });
|
|
214
|
+
* }));
|
|
205
215
|
*
|
|
206
216
|
* // `MyService` is satisfied by the runtime, not provided here:
|
|
207
217
|
* const loader = makeLoader((args: Route.LoaderArgs) =>
|
|
@@ -217,33 +227,53 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
217
227
|
const Returns extends HandlerReturns<DomainError> = {},
|
|
218
228
|
RServices = never,
|
|
219
229
|
ReqServices = never,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
* An optional handler per declared domain error, keyed by its tag. The keys
|
|
223
|
-
* autocomplete to your domain-error tags and each handler's `error` parameter
|
|
224
|
-
* is typed automatically — no annotation needed. A handler *remaps* the error
|
|
225
|
-
* by returning a library route error (`Respond.early` / `throw` / `redirect`)
|
|
226
|
-
* or an `Effect` (`Effect.succeed`/`fail`). Omit entirely to register none.
|
|
227
|
-
*/
|
|
228
|
-
errorHandlers?: ErrorHandlers<DomainError, Returns>;
|
|
229
|
-
/**
|
|
230
|
-
* The app runtime that provides services to loader/action effects. When
|
|
231
|
-
* set, effects may require its services (`RServices`, inferred from here)
|
|
232
|
-
* without providing layers, and runs go through `runtime.runPromise`. When
|
|
233
|
-
* omitted, `RServices` is `never` and effects must require nothing.
|
|
234
|
-
*/
|
|
235
|
-
runtime?: ManagedRuntime.ManagedRuntime<RServices, any>;
|
|
230
|
+
const ExtraRespond extends Record<string, (...args: never[]) => unknown> = {},
|
|
231
|
+
>(
|
|
236
232
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* services to the effect. Loader/action effects may then require
|
|
241
|
-
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
233
|
+
* Builds the factory config. It receives the library's base `Respond` helpers
|
|
234
|
+
* (`early` / `throw` / `redirect`) so your `errorHandlers` can recover/throw
|
|
235
|
+
* with them, and returns the config object.
|
|
242
236
|
*/
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
builder: (respond: BaseRespond) => {
|
|
238
|
+
/**
|
|
239
|
+
* An optional handler per declared domain error, keyed by its tag. The keys
|
|
240
|
+
* autocomplete to your domain-error tags and each handler's `error` parameter
|
|
241
|
+
* is typed automatically — no annotation needed. A handler *remaps* the error
|
|
242
|
+
* by returning a library route error (the builder's `Respond.early` / `throw`
|
|
243
|
+
* / `redirect`) or an `Effect` (`Effect.succeed`/`fail`). Omit to register none.
|
|
244
|
+
*/
|
|
245
|
+
errorHandlers?: ErrorHandlers<DomainError, Returns>;
|
|
246
|
+
/**
|
|
247
|
+
* The app runtime that provides services to loader/action effects. When
|
|
248
|
+
* set, effects may require its services (`RServices`, inferred from here)
|
|
249
|
+
* without providing layers, and runs go through `runtime.runPromise`. When
|
|
250
|
+
* omitted, `RServices` is `never` and effects must require nothing.
|
|
251
|
+
*/
|
|
252
|
+
runtime?: ManagedRuntime.ManagedRuntime<RServices, any>;
|
|
253
|
+
/**
|
|
254
|
+
* A React Router context key (a {@link RequestContextKey}) holding a
|
|
255
|
+
* per-request effect `Context.Context`. Middleware sets it for each request;
|
|
256
|
+
* the runner reads `args.context.get(requestContext)` and provides those
|
|
257
|
+
* services to the effect. Loader/action effects may then require
|
|
258
|
+
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
259
|
+
*/
|
|
260
|
+
requestContext?: RequestContextKey<ReqServices>;
|
|
261
|
+
/**
|
|
262
|
+
* App-specific `Respond` helpers — merged onto the base `Respond` that the
|
|
263
|
+
* factory returns, so your app imports one `Respond`. Typically opinionated
|
|
264
|
+
* error constructors; annotate their parameters, e.g.
|
|
265
|
+
* `{ formError: (reply: string) => new FormError({ reply }) }`. The base
|
|
266
|
+
* helpers (`early` / `throw` / `redirect`) always win the merge.
|
|
267
|
+
*/
|
|
268
|
+
respond?: ExtraRespond;
|
|
269
|
+
},
|
|
270
|
+
) {
|
|
271
|
+
const config = builder(baseRespond);
|
|
245
272
|
const runtime = config.runtime;
|
|
246
273
|
const requestContextKey = config.requestContext;
|
|
274
|
+
// The single `Respond` an app uses: base helpers plus any app-specific ones.
|
|
275
|
+
// Base wins the merge so core helpers can't be shadowed.
|
|
276
|
+
const Respond = { ...config.respond, ...baseRespond } as BaseRespond & ExtraRespond;
|
|
247
277
|
|
|
248
278
|
// Uniform call signature for dispatch (the per-tag handler types are narrower).
|
|
249
279
|
// Defaults to an empty map when `errorHandlers` is omitted.
|
|
@@ -327,6 +357,7 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
327
357
|
return {
|
|
328
358
|
makeLoader: makeLoaderOrAction,
|
|
329
359
|
makeAction: makeLoaderOrAction,
|
|
360
|
+
Respond,
|
|
330
361
|
};
|
|
331
362
|
};
|
|
332
363
|
}
|