react-router-effect 0.6.0 → 0.6.1
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 +4 -2
- package/dist/index.d.mts +31 -2
- package/dist/index.mjs +3 -2
- package/package.json +1 -1
- package/src/factory.ts +55 -21
package/README.md
CHANGED
|
@@ -54,8 +54,10 @@ A **declared domain error** may be left unhandled:
|
|
|
54
54
|
|
|
55
55
|
**Any other error** a route consumes — a service-specific error that isn't a declared domain
|
|
56
56
|
error — **must be handled** in the loader/action (caught or mapped), or `makeLoader`/`makeAction`
|
|
57
|
-
fails to type-check. This gives app-wide defaults for
|
|
58
|
-
handling of feature/service-specific ones.
|
|
57
|
+
fails to type-check with a message naming the unhandled error(s). This gives app-wide defaults for
|
|
58
|
+
declared errors while enforcing explicit handling of feature/service-specific ones. (Likewise, an
|
|
59
|
+
effect that requires a service neither the `runtime` nor the `requestContext` provides fails to
|
|
60
|
+
type-check with a message naming the missing service.)
|
|
59
61
|
|
|
60
62
|
## Usage
|
|
61
63
|
|
package/dist/index.d.mts
CHANGED
|
@@ -124,6 +124,35 @@ type LibraryHandled<DomainError> = DomainError | AnyRouteError | HttpServerRespo
|
|
|
124
124
|
* errors. The loader/action must handle these itself (catch or map them).
|
|
125
125
|
*/
|
|
126
126
|
type Unhandled<E, DomainError> = Exclude<E, LibraryHandled<DomainError>>;
|
|
127
|
+
/**
|
|
128
|
+
* A readable compile-time diagnostic. It's intersected onto the *effect return type*
|
|
129
|
+
* of `fn` — so the effect's `A`/`E`/`R` still infer directly (naked), while a mismatch
|
|
130
|
+
* reports this message, and the offending types, *at the `makeLoader`/`makeAction`
|
|
131
|
+
* call* instead of the opaque "Expected 2 arguments". Resolves to `unknown` (a no-op
|
|
132
|
+
* intersection) when the effect is fully handled and all its requirements are provided:
|
|
133
|
+
*
|
|
134
|
+
* - **Unhandled error** — the effect can fail with something that isn't a declared
|
|
135
|
+
* domain error, a library route error, or respondable; `unhandledErrors` names them.
|
|
136
|
+
* - **Missing requirement** — the effect requires a service that neither the
|
|
137
|
+
* `runtime` nor the `requestContext` provides; `missingRequirements` names them.
|
|
138
|
+
*
|
|
139
|
+
* The message lives in an *inline* object literal (no named alias) so tsc/tsgo print
|
|
140
|
+
* its text verbatim rather than collapsing it to an alias name.
|
|
141
|
+
*/
|
|
142
|
+
/**
|
|
143
|
+
* Names the unhandled errors for the diagnostic: a tagged error becomes its `_tag`
|
|
144
|
+
* (the string you'd pass to `Effect.catchTag`), anything untagged stays as its type.
|
|
145
|
+
*/
|
|
146
|
+
type NameError<U> = U extends {
|
|
147
|
+
readonly _tag: infer Tag extends string;
|
|
148
|
+
} ? Tag : U;
|
|
149
|
+
type Diagnose<E, R, DomainError, Provided> = [Unhandled<E, DomainError>] extends [never] ? [Exclude<R, Provided>] extends [never] ? unknown : {
|
|
150
|
+
"react-router-effect": "This loader/action requires a service the factory does not provide. Add it to your runtime or requestContext, or handle it in the effect.";
|
|
151
|
+
missingRequirements: Exclude<R, Provided>;
|
|
152
|
+
} : {
|
|
153
|
+
"react-router-effect": "This loader/action can fail with an error the library does not handle. Catch it in the effect (e.g. Effect.catchTag), make it Respondable, or add it to your DomainError union.";
|
|
154
|
+
unhandledErrors: NameError<Unhandled<E, DomainError>>;
|
|
155
|
+
};
|
|
127
156
|
/**
|
|
128
157
|
* A React Router context key holding a per-request effect `Context.Context`. Set
|
|
129
158
|
* it in middleware and pass it to the factory's `requestContext` — the runner
|
|
@@ -242,8 +271,8 @@ declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
242
271
|
*/
|
|
243
272
|
respond?: ExtraRespond;
|
|
244
273
|
}) => {
|
|
245
|
-
makeLoader: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(fn: (args: Args) => Effect.Effect<A, E,
|
|
246
|
-
makeAction: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(fn: (args: Args) => Effect.Effect<A, E,
|
|
274
|
+
makeLoader: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(fn: (args: Args) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>) => (args: Args) => Promise<A | RecoverOf<DomainError, Returns, E> | DirectRecover<E>>;
|
|
275
|
+
makeAction: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(fn: (args: Args) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>) => (args: Args) => Promise<A | RecoverOf<DomainError, Returns, E> | DirectRecover<E>>;
|
|
247
276
|
Respond: {
|
|
248
277
|
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
249
278
|
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
package/dist/index.mjs
CHANGED
|
@@ -119,9 +119,10 @@ function makeLoaderOrActionFactory() {
|
|
|
119
119
|
};
|
|
120
120
|
const userHandlers = config.errorHandlers ?? {};
|
|
121
121
|
const isUserError = (e) => typeof e === "object" && e !== null && "_tag" in e && e._tag in userHandlers;
|
|
122
|
-
function makeLoaderOrAction(fn
|
|
122
|
+
function makeLoaderOrAction(fn) {
|
|
123
|
+
const run = fn;
|
|
123
124
|
return (args) => {
|
|
124
|
-
const program =
|
|
125
|
+
const program = run(args).pipe(Effect.catchIf((_e) => true, (e) => {
|
|
125
126
|
if (isUserError(e)) {
|
|
126
127
|
const out = userHandlers[e._tag](e);
|
|
127
128
|
return isRouteError(out) ? processRouteError(out) : out;
|
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.6.
|
|
4
|
+
"version": "0.6.1",
|
|
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/factory.ts
CHANGED
|
@@ -112,6 +112,43 @@ type LibraryHandled<DomainError> = DomainError | AnyRouteError | HttpServerRespo
|
|
|
112
112
|
*/
|
|
113
113
|
type Unhandled<E, DomainError> = Exclude<E, LibraryHandled<DomainError>>;
|
|
114
114
|
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// The call-site diagnostic.
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A readable compile-time diagnostic. It's intersected onto the *effect return type*
|
|
121
|
+
* of `fn` — so the effect's `A`/`E`/`R` still infer directly (naked), while a mismatch
|
|
122
|
+
* reports this message, and the offending types, *at the `makeLoader`/`makeAction`
|
|
123
|
+
* call* instead of the opaque "Expected 2 arguments". Resolves to `unknown` (a no-op
|
|
124
|
+
* intersection) when the effect is fully handled and all its requirements are provided:
|
|
125
|
+
*
|
|
126
|
+
* - **Unhandled error** — the effect can fail with something that isn't a declared
|
|
127
|
+
* domain error, a library route error, or respondable; `unhandledErrors` names them.
|
|
128
|
+
* - **Missing requirement** — the effect requires a service that neither the
|
|
129
|
+
* `runtime` nor the `requestContext` provides; `missingRequirements` names them.
|
|
130
|
+
*
|
|
131
|
+
* The message lives in an *inline* object literal (no named alias) so tsc/tsgo print
|
|
132
|
+
* its text verbatim rather than collapsing it to an alias name.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Names the unhandled errors for the diagnostic: a tagged error becomes its `_tag`
|
|
136
|
+
* (the string you'd pass to `Effect.catchTag`), anything untagged stays as its type.
|
|
137
|
+
*/
|
|
138
|
+
type NameError<U> = U extends { readonly _tag: infer Tag extends string } ? Tag : U;
|
|
139
|
+
|
|
140
|
+
type Diagnose<E, R, DomainError, Provided> = [Unhandled<E, DomainError>] extends [never]
|
|
141
|
+
? [Exclude<R, Provided>] extends [never]
|
|
142
|
+
? unknown
|
|
143
|
+
: {
|
|
144
|
+
"react-router-effect": "This loader/action requires a service the factory does not provide. Add it to your runtime or requestContext, or handle it in the effect.";
|
|
145
|
+
missingRequirements: Exclude<R, Provided>;
|
|
146
|
+
}
|
|
147
|
+
: {
|
|
148
|
+
"react-router-effect": "This loader/action can fail with an error the library does not handle. Catch it in the effect (e.g. Effect.catchTag), make it Respondable, or add it to your DomainError union.";
|
|
149
|
+
unhandledErrors: NameError<Unhandled<E, DomainError>>;
|
|
150
|
+
};
|
|
151
|
+
|
|
115
152
|
// ---------------------------------------------------------------------------
|
|
116
153
|
// Internal runtime helpers.
|
|
117
154
|
// ---------------------------------------------------------------------------
|
|
@@ -285,34 +322,31 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
285
322
|
const isUserError = (e: unknown): e is DomainError =>
|
|
286
323
|
typeof e === "object" && e !== null && "_tag" in e && (e as Tagged)._tag in userHandlers;
|
|
287
324
|
|
|
288
|
-
function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
..._handle: [Unhandled<E, DomainError>] extends [never]
|
|
299
|
-
? []
|
|
300
|
-
: [
|
|
301
|
-
theseErrorsAreNotDomainErrorsAndMustBeHandledInTheLoaderOrAction: Unhandled<
|
|
302
|
-
E,
|
|
303
|
-
DomainError
|
|
304
|
-
>,
|
|
305
|
-
]
|
|
325
|
+
function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
|
|
326
|
+
// `A`/`E`/`R` infer directly from the effect (naked, so inference stays robust).
|
|
327
|
+
// The diagnostic is intersected onto the effect's return type: when the effect
|
|
328
|
+
// requires a service the factory doesn't provide (`R` ⊄ runtime+requestContext),
|
|
329
|
+
// or can fail with something the library won't handle, `Diagnose` becomes a
|
|
330
|
+
// message object the effect isn't assignable to — so this argument fails to
|
|
331
|
+
// type-check with a readable explanation, and the offending types, right here.
|
|
332
|
+
fn: (
|
|
333
|
+
args: Args,
|
|
334
|
+
) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
|
|
306
335
|
): (args: Args) => Promise<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>> {
|
|
336
|
+
// The diagnostic is irrelevant inside the body (it's `unknown` for any valid
|
|
337
|
+
// call); treat `fn` as the plain effect-returning function it is.
|
|
338
|
+
const run = fn as unknown as (
|
|
339
|
+
args: Args,
|
|
340
|
+
) => Effect.Effect<unknown, E, RServices | ReqServices>;
|
|
307
341
|
return (args: Args) => {
|
|
308
342
|
// The internal channel is deliberately loose (`unknown` success); the outer
|
|
309
343
|
// cast restores the precise resolved type (computed from `E` and the handler
|
|
310
344
|
// map). Sound at runtime — the values produced are exactly those types.
|
|
311
|
-
const program =
|
|
345
|
+
const program = run(args).pipe(
|
|
312
346
|
// Catch the whole error channel and dispatch. The refinement is `e is E`
|
|
313
347
|
// (provably ⊆ E, and a *refinement* not a bare predicate — the predicate
|
|
314
|
-
// overload crashes tsc over a generic `E`). A declared domain error with
|
|
315
|
-
//
|
|
348
|
+
// overload crashes tsc over a generic `E`). A declared domain error with no
|
|
349
|
+
// handler (and not respondable) falls through to the 500 default.
|
|
316
350
|
Effect.catchIf(
|
|
317
351
|
(_e): _e is E => true,
|
|
318
352
|
(e): Effect.Effect<unknown, FailureResponse> => {
|