react-router-effect 2.1.0 → 2.3.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 +373 -117
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +160 -23
- package/dist/{route-data-MgSnnR33.d.mts → route-data-ByiuG-Yb.d.mts} +85 -26
- package/dist/server.d.mts +141 -75
- package/dist/server.mjs +286 -140
- package/dist/vite.d.mts +35 -14
- package/dist/vite.mjs +47 -13
- package/package.json +12 -7
- package/src/assets.ts +212 -0
- package/src/check.ts +9 -1
- package/src/defer.ts +179 -0
- package/src/factory.ts +161 -42
- package/src/index.ts +7 -3
- package/src/route-data.ts +9 -9
- package/src/{action-meta.ts → route-meta.ts} +19 -17
- package/src/server-entry.ts +2 -1
- package/src/server.ts +307 -295
- package/src/vite.ts +109 -32
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { type
|
|
1
|
+
import { A as ThrowableRedirectError, C as RouteHandlers, D as AnyRouteError, E as makeEffectRouteFactory, M as WithRouteMeta, N as routeMetaKey, O as ReturnableDataError, S as RequestContextKey, T as RouteResult, _ as FactoryTypes, a as RegisteredMeta, c as Serialize, d as deferredFailureMessage, f as DeferHandled, g as ErrorHandler, h as EffectNext, i as Register, j as isRouteError, k as ThrowableDataError, l as DeferDiagnose, m as EffectMiddlewareArgs, n as EffectData, o as RegisteredRecover, p as DeferRecoverOf, r as EffectRouteArgs, s as RouteData, t as BrandData, u as DeferredPromise, w as RouteMetaArgs } from "./route-data-ByiuG-Yb.mjs";
|
|
2
|
+
export { type AnyRouteError, type BrandData, type DeferDiagnose, type DeferHandled, type DeferRecoverOf, type DeferredPromise, type EffectData, type EffectMiddlewareArgs, type EffectNext, type EffectRouteArgs, type ErrorHandler, type FactoryTypes, type Register, type RegisteredMeta, type RegisteredRecover, type RequestContextKey, ReturnableDataError, type RouteData, type RouteHandlers, type RouteMetaArgs, type RouteResult, type Serialize, ThrowableDataError, ThrowableRedirectError, type WithRouteMeta, deferredFailureMessage, isRouteError, makeEffectRouteFactory, routeMetaKey };
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { Context, Data, Effect, Predicate } from "effect";
|
|
2
|
-
import { createContext, data, redirect } from "react-router";
|
|
1
|
+
import { Cause, Context, Data, Effect, Exit, Option, Predicate, Scope } from "effect";
|
|
2
|
+
import { UNSAFE_ErrorResponseImpl, createContext, data, redirect } from "react-router";
|
|
3
3
|
import { HttpServerRespondable, HttpServerResponse } from "effect/http";
|
|
4
|
-
// src/
|
|
5
|
-
/** The key that holds the metadata in action data. */
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
// src/route-meta.ts
|
|
5
|
+
/** The key that holds the metadata in loader and action data. */
|
|
6
|
+
const routeMetaKey = "__meta";
|
|
7
|
+
/**
|
|
8
|
+
* True for a `data(...)` wrapper, by the same test React Router applies. A
|
|
9
|
+
* class test fails when the app and the adapter load different copies of
|
|
10
|
+
* React Router, as under the Vite dev server.
|
|
11
|
+
*/
|
|
12
|
+
const isDataWithResponseInit = (value) => Predicate.hasProperty(value, "type") && Predicate.hasProperty(value, "data") && Predicate.hasProperty(value, "init") && value.type === "DataWithResponseInit";
|
|
10
13
|
/** True for a plain object, which can be copied without losing behavior. */
|
|
11
14
|
const isPlainRecord = (value) => {
|
|
12
15
|
if (!Predicate.isObject(value) || Array.isArray(value)) return false;
|
|
@@ -14,21 +17,21 @@ const isPlainRecord = (value) => {
|
|
|
14
17
|
return prototype === Object.prototype || prototype === null;
|
|
15
18
|
};
|
|
16
19
|
/**
|
|
17
|
-
* Attaches `meta` to
|
|
20
|
+
* Attaches `meta` to a loader or action result. A plain record gets a copy with the
|
|
18
21
|
* metadata key; a `data(...)` wrapper gets it on its body and keeps its init.
|
|
19
22
|
* Any other value is returned unchanged.
|
|
20
23
|
*/
|
|
21
|
-
const
|
|
24
|
+
const attachRouteMeta = (value, meta) => {
|
|
22
25
|
if (isDataWithResponseInit(value)) {
|
|
23
26
|
if (!isPlainRecord(value.data)) return value;
|
|
24
27
|
return data({
|
|
25
28
|
...value.data,
|
|
26
|
-
[
|
|
29
|
+
[routeMetaKey]: meta
|
|
27
30
|
}, value.init ?? void 0);
|
|
28
31
|
}
|
|
29
32
|
return isPlainRecord(value) ? {
|
|
30
33
|
...value,
|
|
31
|
-
[
|
|
34
|
+
[routeMetaKey]: meta
|
|
32
35
|
} : value;
|
|
33
36
|
};
|
|
34
37
|
// src/errors.ts
|
|
@@ -72,6 +75,84 @@ const Respond = {
|
|
|
72
75
|
};
|
|
73
76
|
/** Narrows an unknown value to one of the library's route errors. */
|
|
74
77
|
const isRouteError = (value) => value instanceof ReturnableDataError || value instanceof ThrowableDataError || value instanceof ThrowableRedirectError;
|
|
78
|
+
// src/defer.ts
|
|
79
|
+
/**
|
|
80
|
+
* The scope that deferred work forks into. The route runner sets it for each
|
|
81
|
+
* loader or action run, and closes it when the request aborts or the route
|
|
82
|
+
* Effect fails. Outside a route runner it is unset, and deferred work runs
|
|
83
|
+
* detached.
|
|
84
|
+
*/
|
|
85
|
+
const DeferScope = Context.Reference("react-router-effect/DeferScope", { defaultValue: () => void 0 });
|
|
86
|
+
/**
|
|
87
|
+
* Gives a route Effect a scope for its deferred work. The scope closes, and
|
|
88
|
+
* interrupts the deferred fibers, when `signal` aborts or the Effect fails: a
|
|
89
|
+
* failed loader sends no data, so no `<Await>` waits for the work.
|
|
90
|
+
*/
|
|
91
|
+
const withDeferScope = (effect, signal) => Effect.suspend(() => {
|
|
92
|
+
const scope = Scope.makeUnsafe("parallel");
|
|
93
|
+
const close = (exit) => Scope.close(scope, exit);
|
|
94
|
+
signal.addEventListener("abort", () => Effect.runFork(close(Exit.interrupt())), { once: true });
|
|
95
|
+
return effect.pipe(Effect.provideService(DeferScope, scope), Effect.onError((cause) => close(Exit.failCause(cause))));
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* The message every deferred rejection has. The server logs the cause.
|
|
99
|
+
*
|
|
100
|
+
* @stability unstable
|
|
101
|
+
*/
|
|
102
|
+
const deferredFailureMessage = "Deferred route data failed";
|
|
103
|
+
/**
|
|
104
|
+
* The rejection the browser gets for a defect or an interrupt: a constant
|
|
105
|
+
* message and no stack, in every server mode. React Router removes server
|
|
106
|
+
* details only in production.
|
|
107
|
+
*/
|
|
108
|
+
const deferredFailure = () => {
|
|
109
|
+
const error = /* @__PURE__ */ new Error(deferredFailureMessage);
|
|
110
|
+
error.stack = void 0;
|
|
111
|
+
return error;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Forks `self` into the run's defer scope and gives a promise of its result.
|
|
115
|
+
* A failure rejects the promise with its `ErrorResponse`, which React Router
|
|
116
|
+
* streams to `useAsyncError`. A defect or interrupt rejects it with
|
|
117
|
+
* `deferredFailure`, and the server logs a defect's full cause.
|
|
118
|
+
*/
|
|
119
|
+
const forkDeferred = (self) => Effect.gen(function* () {
|
|
120
|
+
const scope = yield* DeferScope;
|
|
121
|
+
const work = self.pipe(Effect.tapCause((cause) => Cause.hasDies(cause) ? Effect.logError(deferredFailureMessage, cause) : Effect.void));
|
|
122
|
+
const fiber = yield* scope === void 0 ? Effect.forkDetach(work) : Effect.forkIn(work, scope);
|
|
123
|
+
const promise = new Promise((resolve, reject) => {
|
|
124
|
+
fiber.addObserver((exit) => {
|
|
125
|
+
if (Exit.isSuccess(exit)) return resolve(exit.value);
|
|
126
|
+
reject(Cause.hasDies(exit.cause) ? deferredFailure() : Option.getOrElse(Cause.findErrorOption(exit.cause), deferredFailure));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
promise.catch(() => void 0);
|
|
130
|
+
return promise;
|
|
131
|
+
});
|
|
132
|
+
/** A thrown `data(...)` as an `ErrorResponse`, with React Router's defaults. */
|
|
133
|
+
const toErrorResponse = (thrown) => new UNSAFE_ErrorResponseImpl(thrown.init?.status ?? 500, thrown.init?.statusText ?? "Internal Server Error", thrown.data);
|
|
134
|
+
/**
|
|
135
|
+
* A library route error raised in deferred work: `Respond.early` resolves the
|
|
136
|
+
* promise with its value, and `Respond.throw` rejects it with an
|
|
137
|
+
* `ErrorResponse`. A redirect cannot happen after the response started, so it
|
|
138
|
+
* is a defect.
|
|
139
|
+
*/
|
|
140
|
+
const deferRouteError = (e) => {
|
|
141
|
+
if (e instanceof ReturnableDataError) return Effect.succeed(e.data);
|
|
142
|
+
if (e instanceof ThrowableDataError) return Effect.fail(toErrorResponse(data(e.data, e.init)));
|
|
143
|
+
return Effect.die(e);
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* A handler's `Effect.fail(response)` in deferred work, as an `ErrorResponse`:
|
|
147
|
+
* a `data(...)` as React Router converts a thrown one, and a `Response` with
|
|
148
|
+
* its body read as React Router reads it, JSON or text. A redirect is a defect.
|
|
149
|
+
*/
|
|
150
|
+
const deferFailureResponse = (failure) => {
|
|
151
|
+
if (!(failure instanceof Response)) return Effect.fail(toErrorResponse(failure));
|
|
152
|
+
if (failure.status >= 300 && failure.status < 400) return Effect.die(failure);
|
|
153
|
+
const isJson = /\bapplication\/json\b/.test(failure.headers.get("Content-Type") ?? "");
|
|
154
|
+
return Effect.promise(() => isJson && failure.body !== null ? failure.json() : failure.text()).pipe(Effect.flatMap((body) => Effect.fail(new UNSAFE_ErrorResponseImpl(failure.status, failure.statusText, body))));
|
|
155
|
+
};
|
|
75
156
|
// src/factory.ts
|
|
76
157
|
const internalServerError = () => Effect.fail(new Response("Internal Server Error", { status: 500 }));
|
|
77
158
|
/**
|
|
@@ -187,7 +268,8 @@ function makeEffectRouteFactory() {
|
|
|
187
268
|
...Respond
|
|
188
269
|
};
|
|
189
270
|
const userHandlers = config.errorHandlers ?? {};
|
|
190
|
-
|
|
271
|
+
/** The tags of the registered domain errors. */
|
|
272
|
+
const userTags = Object.keys(userHandlers);
|
|
191
273
|
/**
|
|
192
274
|
* Runs a handler's effect to completion: dispatches its whole error channel
|
|
193
275
|
* (registered domain errors, library route errors, respondables, or the 500
|
|
@@ -196,9 +278,9 @@ function makeEffectRouteFactory() {
|
|
|
196
278
|
* - they differ only in how they build the effect and what `context` they read.
|
|
197
279
|
*/
|
|
198
280
|
function runProgram(effect, context, signal) {
|
|
199
|
-
const program = effect.pipe(Effect.catchIf((_e) => true, (e) => {
|
|
200
|
-
if (
|
|
201
|
-
const out = userHandlers[
|
|
281
|
+
const program = withDeferScope(effect, signal).pipe(Effect.catchIf((_e) => true, (e) => {
|
|
282
|
+
for (const tag of userTags) if (Predicate.isTagged(e, tag)) {
|
|
283
|
+
const out = userHandlers[tag](e);
|
|
202
284
|
return isRouteError(out) ? processRouteError(out) : out;
|
|
203
285
|
}
|
|
204
286
|
if (isRouteError(e)) return processRouteError(e);
|
|
@@ -268,19 +350,74 @@ function makeEffectRouteFactory() {
|
|
|
268
350
|
function runResult(result, args) {
|
|
269
351
|
return Effect.isEffect(result) ? runProgram(result, args.context, args.request.signal) : result;
|
|
270
352
|
}
|
|
271
|
-
|
|
353
|
+
/**
|
|
354
|
+
* A failure of deferred work, through the error handlers: a recovered
|
|
355
|
+
* value resolves the promise, a thrown response rejects it with an
|
|
356
|
+
* `ErrorResponse`. Anything else, such as a domain error with no handler,
|
|
357
|
+
* is a defect: the promise rejects with no server details.
|
|
358
|
+
*/
|
|
359
|
+
function dispatchDeferred(e) {
|
|
360
|
+
for (const tag of userTags) if (Predicate.isTagged(e, tag)) {
|
|
361
|
+
const out = userHandlers[tag](e);
|
|
362
|
+
return isRouteError(out) ? deferRouteError(out) : out.pipe(Effect.catch(deferFailureResponse));
|
|
363
|
+
}
|
|
364
|
+
return isRouteError(e) ? deferRouteError(e) : Effect.die(e);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Starts an Effect without waiting for it, and gives a promise of its
|
|
368
|
+
* result for loader data. React Router streams the promise to `<Await>`.
|
|
369
|
+
*
|
|
370
|
+
* The work gets the services and the span of the Effect that defers it.
|
|
371
|
+
* It keeps running after the loader returns, and is interrupted when the
|
|
372
|
+
* request aborts or the loader fails. A client-side navigation away does
|
|
373
|
+
* not abort the request, so the work then runs until it ends or the stream
|
|
374
|
+
* times out.
|
|
375
|
+
*
|
|
376
|
+
* The work may fail only with the factory's domain errors,
|
|
377
|
+
* `Respond.early` and `Respond.throw`. The error handlers turn them into
|
|
378
|
+
* the promise's value, or an `ErrorResponse` for `useAsyncError`, as they
|
|
379
|
+
* do for a loader. A handler that redirects, a domain error with no
|
|
380
|
+
* handler, a defect or an interrupt rejects the promise with an error that
|
|
381
|
+
* holds no server details, and the server logs the full cause.
|
|
382
|
+
*
|
|
383
|
+
* ```ts
|
|
384
|
+
* export const { Respond, defer } = routeFactory;
|
|
385
|
+
*
|
|
386
|
+
* export const loader = Effect.fn("loader")(function* () {
|
|
387
|
+
* const user = yield* Users.current;
|
|
388
|
+
* return {
|
|
389
|
+
* user,
|
|
390
|
+
* reviews: yield* defer(Reviews.list(user.id)),
|
|
391
|
+
* stats: yield* Stats.load.pipe(
|
|
392
|
+
* Effect.catchTag("StatsUnavailable", () => Effect.succeed(null)),
|
|
393
|
+
* defer,
|
|
394
|
+
* ),
|
|
395
|
+
* };
|
|
396
|
+
* });
|
|
397
|
+
* ```
|
|
398
|
+
*
|
|
399
|
+
* @stability unstable
|
|
400
|
+
*/
|
|
401
|
+
function defer(self) {
|
|
402
|
+
const work = self.pipe(Effect.catch(dispatchDeferred));
|
|
403
|
+
return forkDeferred(work);
|
|
404
|
+
}
|
|
405
|
+
const routeMeta = config.routeMeta;
|
|
406
|
+
/** Runs a loader or action's result and attaches the route metadata to its data. */
|
|
407
|
+
async function withRouteMeta(result, args) {
|
|
408
|
+
const resolved = await runResult(result, args);
|
|
409
|
+
return routeMeta === void 0 ? resolved : attachRouteMeta(resolved, routeMeta(args));
|
|
410
|
+
}
|
|
272
411
|
return {
|
|
273
412
|
makeLoader: makeLoaderOrAction,
|
|
274
413
|
makeAction: makeLoaderOrAction,
|
|
275
414
|
makeMiddleware,
|
|
276
415
|
Respond: Respond$1,
|
|
416
|
+
defer,
|
|
277
417
|
/** The route runner the server adapter applies to every route module. */
|
|
278
418
|
handlers: {
|
|
279
|
-
loader: (fn) => async (args) =>
|
|
280
|
-
action: (fn) => async (args) =>
|
|
281
|
-
const result = await runResult(fn(args), args);
|
|
282
|
-
return actionMeta === void 0 ? result : attachActionMeta(result, actionMeta(args));
|
|
283
|
-
},
|
|
419
|
+
loader: (fn) => async (args) => await withRouteMeta(fn(args), args),
|
|
420
|
+
action: (fn) => async (args) => await withRouteMeta(fn(args), args),
|
|
284
421
|
middleware: (fn) => async (args, next) => {
|
|
285
422
|
const routeArgs = args;
|
|
286
423
|
return runResult(fn({
|
|
@@ -295,4 +432,4 @@ function makeEffectRouteFactory() {
|
|
|
295
432
|
};
|
|
296
433
|
};
|
|
297
434
|
}
|
|
298
|
-
export { ReturnableDataError, ThrowableDataError, ThrowableRedirectError,
|
|
435
|
+
export { ReturnableDataError, ThrowableDataError, ThrowableRedirectError, deferredFailureMessage, isRouteError, makeEffectRouteFactory, routeMetaKey };
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { Context, Effect, ManagedRuntime } from "effect";
|
|
1
|
+
import { Context, Effect, ManagedRuntime, Scope } from "effect";
|
|
2
2
|
import { ActionFunctionArgs, LoaderFunctionArgs, MiddlewareFunction, RouterContext, RouterContextProvider, UNSAFE_DataWithResponseInit, useLoaderData } from "react-router";
|
|
3
3
|
import { HttpServerRespondable } from "effect/http";
|
|
4
|
-
/** The key that holds the metadata in action data. */
|
|
5
|
-
declare const
|
|
4
|
+
/** The key that holds the metadata in loader and action data. */
|
|
5
|
+
declare const routeMetaKey = "__meta";
|
|
6
6
|
/**
|
|
7
7
|
* Adds optional metadata to a record. Only records get metadata, because a
|
|
8
8
|
* structural type cannot tell a plain object from a class instance, and other
|
|
9
9
|
* values (strings, arrays, `Response`s) have nowhere to put it.
|
|
10
10
|
*/
|
|
11
|
-
type RecordWithMeta<T, Meta> = T extends Record<string, unknown> ? Omit<T, typeof
|
|
12
|
-
[
|
|
11
|
+
type RecordWithMeta<T, Meta> = T extends Record<string, unknown> ? Omit<T, typeof routeMetaKey> & {
|
|
12
|
+
[routeMetaKey]?: Meta;
|
|
13
13
|
} : T;
|
|
14
14
|
/**
|
|
15
|
-
* The type of action data after metadata is attached. A `data(...)` wrapper
|
|
15
|
+
* The type of loader or action data after metadata is attached. A `data(...)` wrapper
|
|
16
16
|
* gets metadata on its body. With no configured metadata (`Meta` is `never`)
|
|
17
17
|
* the data is unchanged.
|
|
18
18
|
*/
|
|
19
|
-
type
|
|
19
|
+
type WithRouteMeta<T, Meta> = [Meta] extends [never] ? T : T extends UNSAFE_DataWithResponseInit<infer Body> ? UNSAFE_DataWithResponseInit<RecordWithMeta<Body, Meta>> : RecordWithMeta<T, Meta>;
|
|
20
20
|
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 & {
|
|
21
21
|
readonly _tag: "ReturnableRouteError";
|
|
22
22
|
} & Readonly<A>;
|
|
@@ -144,6 +144,25 @@ type ErrorHandlers<DomainError extends Tagged, Returns> = { [Tag in keyof Return
|
|
|
144
144
|
type RecoverOf<DomainError extends Tagged, Returns, E> = { [Tag in keyof Returns]: [Extract<E, Extract<DomainError, {
|
|
145
145
|
readonly _tag: Tag;
|
|
146
146
|
}>>] extends [never] ? never : (Returns[Tag] extends ReturnableDataError<infer B> ? UNSAFE_DataWithResponseInit<B> : never) | (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any> ? SuccessValue : never); }[keyof Returns];
|
|
147
|
+
/**
|
|
148
|
+
* The errors the factory's `defer` handles in deferred work: the declared
|
|
149
|
+
* domain errors, and `Respond.early` / `Respond.throw`. A redirect is not one
|
|
150
|
+
* of them: it cannot happen after the response started.
|
|
151
|
+
*
|
|
152
|
+
* @stability unstable
|
|
153
|
+
*/
|
|
154
|
+
type DeferHandled<DomainError> = DomainError | ReturnableDataError<unknown> | ThrowableDataError<unknown>;
|
|
155
|
+
/**
|
|
156
|
+
* What a deferred promise resolves with when the work fails with `E`: the
|
|
157
|
+
* value of a `Respond.early` raised directly, and what the registered
|
|
158
|
+
* handlers recover. Unlike loader data, a recovered `Respond.early` gives its
|
|
159
|
+
* value without the `data(...)` wrapper: a promise has no response to set.
|
|
160
|
+
*
|
|
161
|
+
* @stability unstable
|
|
162
|
+
*/
|
|
163
|
+
type DeferRecoverOf<DomainError extends Tagged, Returns, E> = ReturnableBodyOf<E> | { [Tag in keyof Returns]: [Extract<E, Extract<DomainError, {
|
|
164
|
+
readonly _tag: Tag;
|
|
165
|
+
}>>] extends [never] ? never : (Returns[Tag] extends ReturnableDataError<infer B> ? B : never) | (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any> ? SuccessValue : never); }[keyof Returns];
|
|
147
166
|
/**
|
|
148
167
|
* Errors the library deals with on the handler's behalf - so the loader/action/
|
|
149
168
|
* middleware needn't handle them itself:
|
|
@@ -209,6 +228,12 @@ type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> = IsAny<E> exten
|
|
|
209
228
|
* it in middleware and pass it to the factory's `requestContext` - the runner
|
|
210
229
|
* reads it on every request and provides its services to the loader/action.
|
|
211
230
|
*
|
|
231
|
+
* With the server adapter, do not set it to a new context. The adapter sets it
|
|
232
|
+
* to the request fiber's context before React Router runs, and a new context
|
|
233
|
+
* replaces those services, such as `HttpServerRequest` and the server span.
|
|
234
|
+
* Provide request services with a router middleware on `ReactRouter.routes`.
|
|
235
|
+
* The middleware below is for an app without the adapter.
|
|
236
|
+
*
|
|
212
237
|
* It's just a `RouterContext`; create one with React Router's `createContext`:
|
|
213
238
|
*
|
|
214
239
|
* ```ts
|
|
@@ -222,7 +247,7 @@ type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> = IsAny<E> exten
|
|
|
222
247
|
*
|
|
223
248
|
* export const requestContext: RequestContextKey<RequestContext> = createContext();
|
|
224
249
|
*
|
|
225
|
-
* // middleware:
|
|
250
|
+
* // middleware, in an app without the server adapter:
|
|
226
251
|
* export const middleware: Route.MiddlewareFunction[] = [
|
|
227
252
|
* ({ context, request }, next) => {
|
|
228
253
|
* context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
|
|
@@ -232,8 +257,8 @@ type Diagnose<E, R, DomainError, Provided, AlsoHandled = never> = IsAny<E> exten
|
|
|
232
257
|
* ```
|
|
233
258
|
*/
|
|
234
259
|
type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqServices>>;
|
|
235
|
-
/** What `
|
|
236
|
-
interface
|
|
260
|
+
/** What `routeMeta` reads: the request and the router context of the loader or action. */
|
|
261
|
+
interface RouteMetaArgs {
|
|
237
262
|
readonly request: Request;
|
|
238
263
|
readonly context: Readonly<RouterContextProvider>;
|
|
239
264
|
}
|
|
@@ -399,27 +424,61 @@ declare function makeEffectRouteFactory<DomainError extends Tagged = never>(): <
|
|
|
399
424
|
*/
|
|
400
425
|
respond?: ExtraRespond;
|
|
401
426
|
/**
|
|
402
|
-
* Request-scoped metadata to attach to every
|
|
403
|
-
* read after the
|
|
404
|
-
* action it runs, including plain functions
|
|
405
|
-
* thrown result gets none. Only
|
|
406
|
-
* it; other values are returned
|
|
427
|
+
* Request-scoped metadata to attach to the data of every loader and action
|
|
428
|
+
* under `__meta`, read after the function resolves. The server adapter
|
|
429
|
+
* attaches it to every loader and action it runs, including plain functions
|
|
430
|
+
* and `makeLoader` / `makeAction` wrappers. A thrown result gets none. Only
|
|
431
|
+
* plain records and `data(...)` bodies include it; other values are returned
|
|
432
|
+
* unchanged.
|
|
407
433
|
*/
|
|
408
|
-
|
|
434
|
+
routeMeta?: (args: RouteMetaArgs) => Meta;
|
|
409
435
|
}) => {
|
|
410
|
-
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 | DirectRecover<E> | RecoverOf<DomainError, Returns, E>>;
|
|
411
|
-
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<
|
|
436
|
+
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<WithRouteMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>;
|
|
437
|
+
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<WithRouteMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>;
|
|
412
438
|
makeMiddleware: <Middleware extends AnyMiddlewareFunction = MiddlewareFunction, E = LibraryHandled<DomainError, FailureResponse>, R = RServices | ReqServices>(fn: (args: MiddlewareArgsOf<Middleware>, next: () => Effect.Effect<MiddlewareResultOf<Middleware>, FailureResponse>) => Effect.Effect<MiddlewareResultOf<Middleware> | void, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices, FailureResponse>) => (args: MiddlewareArgsOf<Middleware>, next: MiddlewareNextOf<Middleware>) => Promise<MiddlewareResultOf<Middleware> | undefined>;
|
|
413
439
|
Respond: {
|
|
414
440
|
early: <D>(value: D, init?: number | ResponseInit) => ReturnableDataError<D>;
|
|
415
441
|
throw: <D>(value: D, init?: number | ResponseInit) => ThrowableDataError<D>;
|
|
416
442
|
redirect: (url: string, init?: number | ResponseInit) => ThrowableRedirectError;
|
|
417
443
|
} & ExtraRespond;
|
|
444
|
+
defer: <A, E, R>(self: Effect.Effect<A, E, R> & DeferDiagnose<E, DeferHandled<DomainError>>) => Effect.Effect<DeferredPromise<A | DeferRecoverOf<DomainError, Returns, E>>, never, R>;
|
|
418
445
|
/** The route runner the server adapter applies to every route module. */
|
|
419
446
|
handlers: RouteHandlers;
|
|
420
447
|
/** Type-only: what the adapter's route check and `Register` read. */
|
|
421
448
|
"~types": FactoryTypes<DomainError, Returns, RServices | ReqServices, Meta>;
|
|
422
449
|
};
|
|
450
|
+
/**
|
|
451
|
+
* The compile-time diagnostic of the factory's `defer`. A deferred Effect
|
|
452
|
+
* runs after the response started, so a failure cannot change the response.
|
|
453
|
+
* The Effect must handle every error it can fail with before it is deferred,
|
|
454
|
+
* except the `Handled` ones, which the error handlers turn into a value or an
|
|
455
|
+
* `ErrorResponse`. Resolves to `unknown` (a no-op intersection) when every
|
|
456
|
+
* error is handled.
|
|
457
|
+
*
|
|
458
|
+
* @stability unstable
|
|
459
|
+
*/
|
|
460
|
+
type DeferDiagnose<E, Handled> = IsAny<E> extends true ? unknown : [Exclude<E, Handled>] extends [never] ? unknown : {
|
|
461
|
+
"react-router-effect": "A deferred effect runs after the response started, so it can fail only with the factory's domain errors, Respond.early and Respond.throw. Handle the other errors before defer: recover them into data (e.g. Effect.catchTag), or make them defects with Effect.orDie.";
|
|
462
|
+
unhandledErrors: NameError<Exclude<E, Handled>>;
|
|
463
|
+
};
|
|
464
|
+
/**
|
|
465
|
+
* The promise that the factory's `defer` gives: a `Promise` of the deferred Effect's
|
|
466
|
+
* success, which React Router streams to `<Await>`. The work started when the
|
|
467
|
+
* Effect was deferred.
|
|
468
|
+
*
|
|
469
|
+
* It is a type of its own so that an Effect that succeeds with it states
|
|
470
|
+
* that the promise is not awaited on purpose. The language service's
|
|
471
|
+
* `promiseInEffectSuccess` rule reports only the global `Promise` type.
|
|
472
|
+
*
|
|
473
|
+
* @stability unstable
|
|
474
|
+
*/
|
|
475
|
+
interface DeferredPromise<A> extends Promise<A> {}
|
|
476
|
+
/**
|
|
477
|
+
* The message every deferred rejection has. The server logs the cause.
|
|
478
|
+
*
|
|
479
|
+
* @stability unstable
|
|
480
|
+
*/
|
|
481
|
+
declare const deferredFailureMessage = "Deferred route data failed";
|
|
423
482
|
/**
|
|
424
483
|
* Registers what the app's route factory turns failures into, so that a
|
|
425
484
|
* loader or action written as a plain `Effect.fn` types its data the way a
|
|
@@ -440,8 +499,8 @@ declare function makeEffectRouteFactory<DomainError extends Tagged = never>(): <
|
|
|
440
499
|
* interface Register {
|
|
441
500
|
* // Each error tag a handler recovers, and the data it recovers into.
|
|
442
501
|
* recover: { FormValidationError: { reply: SubmissionResponse } };
|
|
443
|
-
* // What `
|
|
444
|
-
*
|
|
502
|
+
* // What `routeMeta` returns.
|
|
503
|
+
* routeMeta: { realtimeEventIds: ReadonlyArray<string> };
|
|
445
504
|
* }
|
|
446
505
|
* }
|
|
447
506
|
* ```
|
|
@@ -451,9 +510,9 @@ interface Register$1 {}
|
|
|
451
510
|
type RegisteredRecover = Register$1 extends {
|
|
452
511
|
readonly recover: infer Recover;
|
|
453
512
|
} ? Recover : {};
|
|
454
|
-
/** The registered
|
|
513
|
+
/** The registered route metadata, or `never` when there is none. */
|
|
455
514
|
type RegisteredMeta = Register$1 extends {
|
|
456
|
-
readonly
|
|
515
|
+
readonly routeMeta: infer Meta;
|
|
457
516
|
} ? Meta : never;
|
|
458
517
|
/** What a failure of type `E` recovers into: `Respond.early` bodies and registered handlers. */
|
|
459
518
|
type Recovered<E> = (E extends ReturnableDataError<infer Body> ? Body : never) | (E extends {
|
|
@@ -468,9 +527,9 @@ type Serialize<T> = ReturnType<typeof useLoaderData<() => Promise<T>>>;
|
|
|
468
527
|
/**
|
|
469
528
|
* The data an Effect that succeeds with `A` and fails with `E` gives the
|
|
470
529
|
* component: its success and what its failures recover into, serialized as
|
|
471
|
-
* React Router serializes them, with the registered
|
|
530
|
+
* React Router serializes them, with the registered route metadata.
|
|
472
531
|
*/
|
|
473
|
-
type EffectData<A, E> =
|
|
532
|
+
type EffectData<A, E> = WithRouteMeta<Serialize<A | Recovered<E>>, RegisteredMeta>;
|
|
474
533
|
/**
|
|
475
534
|
* The route data of a loader or action, for React Router's hooks:
|
|
476
535
|
* `useFetcher<RouteData<typeof action>>()`. A function that returns an Effect
|
|
@@ -507,7 +566,7 @@ type Unwrapped<A> = A extends Response ? never : A extends UNSAFE_DataWithRespon
|
|
|
507
566
|
* checks library types (`skipLibCheck: false`) then gets TS2636 in Effect's own
|
|
508
567
|
* declarations. `test/consumer` checks the built package for that.
|
|
509
568
|
*/
|
|
510
|
-
type BrandData<A, E> = [A] extends [never] ?
|
|
569
|
+
type BrandData<A, E> = [A] extends [never] ? WithRouteMeta<Recovered<E>, RegisteredMeta> : [A] extends [void] ? unknown : WithRouteMeta<Unwrapped<A> | Recovered<E>, RegisteredMeta>;
|
|
511
570
|
declare module "effect/Effect" {
|
|
512
571
|
interface Effect<out A, out E, out R> {
|
|
513
572
|
readonly unstable__ReactRouter_SerializesTo: [BrandData<A, E>];
|
|
@@ -540,4 +599,4 @@ type EffectRouteArgs<Args extends {
|
|
|
540
599
|
}> = Omit<Args, "pattern"> & {
|
|
541
600
|
readonly pattern?: string;
|
|
542
601
|
};
|
|
543
|
-
export {
|
|
602
|
+
export { ThrowableRedirectError as A, RouteHandlers as C, AnyRouteError as D, makeEffectRouteFactory as E, WithRouteMeta as M, routeMetaKey as N, ReturnableDataError as O, RequestContextKey as S, RouteResult as T, FactoryTypes as _, RegisteredMeta as a, LibraryHandled as b, Serialize as c, deferredFailureMessage as d, DeferHandled as f, ErrorHandler as g, EffectNext as h, Register$1 as i, isRouteError as j, ThrowableDataError as k, DeferDiagnose as l, EffectMiddlewareArgs as m, EffectData as n, RegisteredRecover as o, DeferRecoverOf as p, EffectRouteArgs as r, RouteData as s, BrandData as t, DeferredPromise as u, FailureResponse as v, RouteMetaArgs as w, NameError as x, IsAny as y };
|