react-router-effect 0.3.0 → 0.4.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 +56 -4
- package/dist/index.d.mts +42 -6
- package/dist/index.mjs +3 -1
- package/package.json +1 -1
- package/src/factory.ts +61 -7
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -177,12 +177,64 @@ makeLoaderOrActionFactory()({ runtime });
|
|
|
177
177
|
makeLoaderOrActionFactory()({});
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
### Per-request services from middleware
|
|
181
|
+
|
|
182
|
+
The runtime provides app-wide services. For **request-scoped** services — the current user, a
|
|
183
|
+
request id, a per-request transaction — use [middleware](https://reactrouter.com/how-to/middleware).
|
|
184
|
+
Middleware sets a fresh effect `Context` on a React Router context key each request; the factory's
|
|
185
|
+
`requestContext` reads it and provides those services to the loader/action:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// app/request-context.server.ts
|
|
189
|
+
import { Context } from "effect";
|
|
190
|
+
import { createContext } from "react-router";
|
|
191
|
+
import type { RequestContextKey } from "react-router-effect";
|
|
192
|
+
|
|
193
|
+
class RequestContext extends Context.Service<
|
|
194
|
+
RequestContext,
|
|
195
|
+
{
|
|
196
|
+
readonly userId: string;
|
|
197
|
+
}
|
|
198
|
+
>()("app/RequestContext") {}
|
|
199
|
+
|
|
200
|
+
// `requestContext` is a plain React Router context key — `createContext` is RR's own.
|
|
201
|
+
export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
202
|
+
|
|
203
|
+
// app/routes/profile.ts — middleware sets a fresh value per request:
|
|
204
|
+
export const middleware: Route.MiddlewareFunction[] = [
|
|
205
|
+
({ context, request }, next) => {
|
|
206
|
+
context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
207
|
+
return next();
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
// app/route.server.ts — wire the same key into the factory:
|
|
212
|
+
export const { makeLoader } = makeLoaderOrActionFactory<DomainErrors>()({
|
|
213
|
+
runtime: appRuntime,
|
|
214
|
+
requestContext,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// the loader requires RequestContext directly — no provide, fresh each request:
|
|
218
|
+
export const loader = makeLoader(() =>
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const { userId } = yield* RequestContext;
|
|
221
|
+
return { userId };
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Effects may now require both the runtime's services and the request context's; requiring anything
|
|
227
|
+
else is a compile error. `RequestContextKey<ReqServices>` is a type alias for
|
|
228
|
+
`RouterContext<Context.Context<ReqServices>>` — sugar for annotating the key, nothing more.
|
|
229
|
+
|
|
180
230
|
## API
|
|
181
231
|
|
|
182
|
-
- **`makeLoaderOrActionFactory<DomainErrors>()({ errorHandlers?, runtime? })`** →
|
|
183
|
-
`{ makeLoader, makeAction }` (both are the same wrapper).
|
|
184
|
-
non-domain error left in a loader/action's error channel — or a required service the
|
|
185
|
-
|
|
232
|
+
- **`makeLoaderOrActionFactory<DomainErrors>()({ errorHandlers?, runtime?, requestContext? })`** →
|
|
233
|
+
`{ makeLoader, makeAction }` (both are the same wrapper). All config fields are optional. A
|
|
234
|
+
non-domain error left in a loader/action's error channel — or a required service that neither the
|
|
235
|
+
`runtime` nor the `requestContext` provides — is a compile error.
|
|
236
|
+
- **`RequestContextKey<ReqServices>`** — type of the React Router context key for a per-request
|
|
237
|
+
effect context (`RouterContext<Context.Context<ReqServices>>`).
|
|
186
238
|
- **`Respond`** — `early` (recover), `throw`, `redirect`.
|
|
187
239
|
- **`ReturnableDataError`**, **`ThrowableDataError`**, **`ThrowableRedirectError`** — the library
|
|
188
240
|
route errors, and **`isRouteError`** to narrow them.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Effect, ManagedRuntime } from "effect";
|
|
1
|
+
import { Context, Effect, ManagedRuntime } from "effect";
|
|
2
2
|
import { HttpServerRespondable } from "effect/unstable/http";
|
|
3
|
-
import { ActionFunctionArgs, LoaderFunctionArgs, UNSAFE_DataWithResponseInit } from "react-router";
|
|
3
|
+
import { ActionFunctionArgs, LoaderFunctionArgs, RouterContext, UNSAFE_DataWithResponseInit } from "react-router";
|
|
4
4
|
|
|
5
5
|
//#region src/errors.d.ts
|
|
6
6
|
declare const ReturnableDataError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
@@ -114,6 +114,34 @@ type ValidHandlers<DomainError extends Tagged> = { [Tag in DomainError["_tag"]]?
|
|
|
114
114
|
* (`keyof Handlers` escaping the domain tags) or with a bad return makes it `false`.
|
|
115
115
|
*/
|
|
116
116
|
type HandlersAreValid<Handlers, DomainError extends Tagged> = [keyof Handlers] extends [DomainError["_tag"]] ? Handlers extends ValidHandlers<DomainError> ? true : false : false;
|
|
117
|
+
/**
|
|
118
|
+
* A React Router context key holding a per-request effect `Context.Context`. Set
|
|
119
|
+
* it in middleware and pass it to the factory's `requestContext` — the runner
|
|
120
|
+
* reads it on every request and provides its services to the loader/action.
|
|
121
|
+
*
|
|
122
|
+
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
123
|
+
*
|
|
124
|
+
* ```ts
|
|
125
|
+
* import { createContext } from "react-router";
|
|
126
|
+
* import { Context } from "effect";
|
|
127
|
+
* import type { RequestContextKey } from "react-router-effect";
|
|
128
|
+
*
|
|
129
|
+
* class RequestContext extends Context.Service<RequestContext, {
|
|
130
|
+
* readonly userId: string;
|
|
131
|
+
* }>()("app/RequestContext") {}
|
|
132
|
+
*
|
|
133
|
+
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
134
|
+
*
|
|
135
|
+
* // middleware:
|
|
136
|
+
* export const middleware: Route.MiddlewareFunction[] = [
|
|
137
|
+
* ({ context, request }, next) => {
|
|
138
|
+
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
139
|
+
* return next();
|
|
140
|
+
* },
|
|
141
|
+
* ];
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
|
|
117
145
|
/**
|
|
118
146
|
* Build `makeLoader` / `makeAction` for an application, wired to its domain errors.
|
|
119
147
|
*
|
|
@@ -165,7 +193,7 @@ type HandlersAreValid<Handlers, DomainError extends Tagged> = [keyof Handlers] e
|
|
|
165
193
|
* );
|
|
166
194
|
* ```
|
|
167
195
|
*/
|
|
168
|
-
declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>(): <const Handlers = {}, RServices = never>(config: {
|
|
196
|
+
declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>(): <const Handlers = {}, RServices = never, ReqServices = never>(config: {
|
|
169
197
|
/**
|
|
170
198
|
* An optional handler per declared domain error. Omit entirely to register
|
|
171
199
|
* none (e.g. when relying on `HttpServerRespondable` / the 500 default, or
|
|
@@ -179,9 +207,17 @@ declare function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
179
207
|
* omitted, `RServices` is `never` and effects must require nothing.
|
|
180
208
|
*/
|
|
181
209
|
runtime?: ManagedRuntime.ManagedRuntime<RServices, any>;
|
|
210
|
+
/**
|
|
211
|
+
* A React Router context key (see {@link createRequestContext}) holding a
|
|
212
|
+
* per-request effect `Context.Context`. Middleware sets it for each request;
|
|
213
|
+
* the runner reads `args.context.get(requestContext)` and provides those
|
|
214
|
+
* services to the effect. Loader/action effects may then require
|
|
215
|
+
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
216
|
+
*/
|
|
217
|
+
requestContext?: RequestContextKey<ReqServices>;
|
|
182
218
|
}, ..._validate: HandlersAreValid<Handlers, DomainError> extends true ? [] : [eachHandlerMustBeForADeclaredDomainErrorAndReturnARouteErrorOrAnEffect: ValidHandlers<DomainError>]) => {
|
|
183
|
-
makeLoader: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(fn: (args: Args) => Effect.Effect<A, E, RServices>, ..._handle: [Unhandled<E, DomainError>] extends [never] ? [] : [theseErrorsAreNotDomainErrorsAndMustBeHandledInTheLoaderOrAction: Exclude<E, LibraryHandled<DomainError>>]) => (args: Args) => Promise<A | RecoverOf<Handlers, E> | DirectRecover<E>>;
|
|
184
|
-
makeAction: <Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(fn: (args: Args) => Effect.Effect<A, E, RServices>, ..._handle: [Unhandled<E, DomainError>] extends [never] ? [] : [theseErrorsAreNotDomainErrorsAndMustBeHandledInTheLoaderOrAction: Exclude<E, LibraryHandled<DomainError>>]) => (args: Args) => Promise<A | RecoverOf<Handlers, E> | DirectRecover<E>>;
|
|
219
|
+
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<Handlers, E> | DirectRecover<E>>;
|
|
220
|
+
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<Handlers, E> | DirectRecover<E>>;
|
|
185
221
|
};
|
|
186
222
|
//#endregion
|
|
187
|
-
export { type AnyRouteError, type ErrorHandler, Respond, ReturnableDataError, ThrowableDataError, ThrowableRedirectError, isRouteError, makeLoaderOrActionFactory };
|
|
223
|
+
export { type AnyRouteError, type ErrorHandler, type RequestContextKey, Respond, ReturnableDataError, ThrowableDataError, ThrowableRedirectError, isRouteError, makeLoaderOrActionFactory };
|
package/dist/index.mjs
CHANGED
|
@@ -102,6 +102,7 @@ const processRouteError = (e) => {
|
|
|
102
102
|
function makeLoaderOrActionFactory() {
|
|
103
103
|
return function defineErrorHandlers(config, ..._validate) {
|
|
104
104
|
const runtime = config.runtime;
|
|
105
|
+
const requestContextKey = config.requestContext;
|
|
105
106
|
const userHandlers = config.errorHandlers ?? {};
|
|
106
107
|
const isUserError = (e) => typeof e === "object" && e !== null && "_tag" in e && e._tag in userHandlers;
|
|
107
108
|
function makeLoaderOrAction(fn, ..._handle) {
|
|
@@ -115,7 +116,8 @@ function makeLoaderOrActionFactory() {
|
|
|
115
116
|
if (HttpServerRespondable.isRespondable(e)) return HttpServerRespondable.toResponse(e).pipe(Effect.flatMap((res) => Effect.fail(HttpServerResponse.toWeb(res))));
|
|
116
117
|
return internalServerError();
|
|
117
118
|
}));
|
|
118
|
-
|
|
119
|
+
const provided = requestContextKey ? Effect.provideContext(program, args.context.get(requestContextKey)) : program;
|
|
120
|
+
return runtime ? runtime.runPromise(provided) : Effect.runPromise(provided);
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
return {
|
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.4.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/factory.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, type ManagedRuntime } from "effect";
|
|
1
|
+
import { type Context, Effect, type ManagedRuntime } from "effect";
|
|
2
2
|
import { HttpServerRespondable, HttpServerResponse } from "effect/unstable/http";
|
|
3
3
|
import {
|
|
4
4
|
data,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ActionFunctionArgs,
|
|
7
7
|
type UNSAFE_DataWithResponseInit as DataWithResponseInit,
|
|
8
8
|
type LoaderFunctionArgs,
|
|
9
|
+
type RouterContext,
|
|
9
10
|
} from "react-router";
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -127,6 +128,39 @@ const processRouteError = (
|
|
|
127
128
|
return Effect.fail<FailureResponse>(redirect(e.url, e.init));
|
|
128
129
|
};
|
|
129
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Per-request context.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A React Router context key holding a per-request effect `Context.Context`. Set
|
|
137
|
+
* it in middleware and pass it to the factory's `requestContext` — the runner
|
|
138
|
+
* reads it on every request and provides its services to the loader/action.
|
|
139
|
+
*
|
|
140
|
+
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
141
|
+
*
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { createContext } from "react-router";
|
|
144
|
+
* import { Context } from "effect";
|
|
145
|
+
* import type { RequestContextKey } from "react-router-effect";
|
|
146
|
+
*
|
|
147
|
+
* class RequestContext extends Context.Service<RequestContext, {
|
|
148
|
+
* readonly userId: string;
|
|
149
|
+
* }>()("app/RequestContext") {}
|
|
150
|
+
*
|
|
151
|
+
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
152
|
+
*
|
|
153
|
+
* // middleware:
|
|
154
|
+
* export const middleware: Route.MiddlewareFunction[] = [
|
|
155
|
+
* ({ context, request }, next) => {
|
|
156
|
+
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
157
|
+
* return next();
|
|
158
|
+
* },
|
|
159
|
+
* ];
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
|
|
163
|
+
|
|
130
164
|
// ---------------------------------------------------------------------------
|
|
131
165
|
// Factory.
|
|
132
166
|
// ---------------------------------------------------------------------------
|
|
@@ -183,7 +217,7 @@ const processRouteError = (
|
|
|
183
217
|
* ```
|
|
184
218
|
*/
|
|
185
219
|
export function makeLoaderOrActionFactory<DomainError extends Tagged = never>() {
|
|
186
|
-
return function defineErrorHandlers<const Handlers = {}, RServices = never>(
|
|
220
|
+
return function defineErrorHandlers<const Handlers = {}, RServices = never, ReqServices = never>(
|
|
187
221
|
config: {
|
|
188
222
|
/**
|
|
189
223
|
* An optional handler per declared domain error. Omit entirely to register
|
|
@@ -198,6 +232,14 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
198
232
|
* omitted, `RServices` is `never` and effects must require nothing.
|
|
199
233
|
*/
|
|
200
234
|
runtime?: ManagedRuntime.ManagedRuntime<RServices, any>;
|
|
235
|
+
/**
|
|
236
|
+
* A React Router context key (see {@link createRequestContext}) holding a
|
|
237
|
+
* per-request effect `Context.Context`. Middleware sets it for each request;
|
|
238
|
+
* the runner reads `args.context.get(requestContext)` and provides those
|
|
239
|
+
* services to the effect. Loader/action effects may then require
|
|
240
|
+
* `ReqServices` (inferred from here) in addition to the runtime's services.
|
|
241
|
+
*/
|
|
242
|
+
requestContext?: RequestContextKey<ReqServices>;
|
|
201
243
|
},
|
|
202
244
|
// Validation: a handler for a non-domain error, or one that doesn't return a
|
|
203
245
|
// route error / failing `Effect`, makes this rest parameter required and forces
|
|
@@ -209,6 +251,7 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
209
251
|
]
|
|
210
252
|
) {
|
|
211
253
|
const runtime = config.runtime;
|
|
254
|
+
const requestContextKey = config.requestContext;
|
|
212
255
|
|
|
213
256
|
// Uniform call signature for dispatch (the per-tag handler types are narrower).
|
|
214
257
|
// Defaults to an empty map when `errorHandlers` is omitted.
|
|
@@ -221,9 +264,11 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
221
264
|
typeof e === "object" && e !== null && "_tag" in e && (e as Tagged)._tag in userHandlers;
|
|
222
265
|
|
|
223
266
|
function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E>(
|
|
224
|
-
// The effect may require `RServices`
|
|
225
|
-
//
|
|
226
|
-
|
|
267
|
+
// The effect may require `RServices` (provided by the configured `runtime`)
|
|
268
|
+
// and `ReqServices` (provided per-request from `requestContext`). Both are
|
|
269
|
+
// `never` when their source isn't configured, so the effect must require
|
|
270
|
+
// nothing extra.
|
|
271
|
+
fn: (args: Args) => Effect.Effect<A, E, RServices | ReqServices>,
|
|
227
272
|
// If the effect can still fail with something the library won't handle — a
|
|
228
273
|
// service-specific error that isn't a declared domain error, a library route
|
|
229
274
|
// error, or respondable — this rest parameter becomes required and the call
|
|
@@ -269,11 +314,20 @@ export function makeLoaderOrActionFactory<DomainError extends Tagged = never>()
|
|
|
269
314
|
},
|
|
270
315
|
),
|
|
271
316
|
);
|
|
317
|
+
// Provide the per-request context (set by middleware) so `ReqServices` are
|
|
318
|
+
// satisfied, leaving only the runtime's `RServices` in the requirements.
|
|
319
|
+
// The cast is sound: `provideContext` removes `ReqServices`, and when no
|
|
320
|
+
// `requestContext` is configured `ReqServices` is `never` (nothing removed).
|
|
321
|
+
const provided = (
|
|
322
|
+
requestContextKey
|
|
323
|
+
? Effect.provideContext(program, args.context.get(requestContextKey))
|
|
324
|
+
: program
|
|
325
|
+
) as Effect.Effect<unknown, FailureResponse, RServices>;
|
|
272
326
|
// Run against the configured runtime so its services satisfy the effect's
|
|
273
327
|
// `R`; with no runtime, the effect requires nothing and runs standalone.
|
|
274
328
|
const result = runtime
|
|
275
|
-
? runtime.runPromise(
|
|
276
|
-
: Effect.runPromise(
|
|
329
|
+
? runtime.runPromise(provided)
|
|
330
|
+
: Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>);
|
|
277
331
|
return result as Promise<A | DirectRecover<E> | RecoverOf<Handlers, E>>;
|
|
278
332
|
};
|
|
279
333
|
}
|
package/src/index.ts
CHANGED