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/src/factory.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { Context, Effect, type ManagedRuntime } from "effect";
1
+ import { Context, Effect, type ManagedRuntime, Predicate } from "effect";
2
2
  import { HttpServerRespondable, HttpServerResponse } from "effect/http";
3
3
  import {
4
4
  createContext,
5
5
  data,
6
6
  redirect,
7
7
  type ActionFunctionArgs,
8
+ type ErrorResponse,
8
9
  type UNSAFE_DataWithResponseInit as DataWithResponseInit,
9
10
  type LoaderFunctionArgs,
10
11
  type MiddlewareFunction,
@@ -12,7 +13,15 @@ import {
12
13
  type RouterContextProvider,
13
14
  } from "react-router";
14
15
 
15
- import { attachActionMeta, type WithActionMeta } from "./action-meta.ts";
16
+ import { attachRouteMeta, type WithRouteMeta } from "./route-meta.ts";
17
+ import {
18
+ deferFailureResponse,
19
+ deferRouteError,
20
+ forkDeferred,
21
+ withDeferScope,
22
+ type DeferDiagnose,
23
+ type DeferredPromise,
24
+ } from "./defer.ts";
16
25
  import {
17
26
  isRouteError,
18
27
  Respond as baseRespond,
@@ -124,6 +133,40 @@ export type RecoverOf<DomainError extends Tagged, Returns, E> = {
124
133
  | (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any> ? SuccessValue : never);
125
134
  }[keyof Returns];
126
135
 
136
+ /**
137
+ * The errors the factory's `defer` handles in deferred work: the declared
138
+ * domain errors, and `Respond.early` / `Respond.throw`. A redirect is not one
139
+ * of them: it cannot happen after the response started.
140
+ *
141
+ * @stability unstable
142
+ */
143
+ export type DeferHandled<DomainError> =
144
+ | DomainError
145
+ | ReturnableDataError<unknown>
146
+ | ThrowableDataError<unknown>;
147
+
148
+ /**
149
+ * What a deferred promise resolves with when the work fails with `E`: the
150
+ * value of a `Respond.early` raised directly, and what the registered
151
+ * handlers recover. Unlike loader data, a recovered `Respond.early` gives its
152
+ * value without the `data(...)` wrapper: a promise has no response to set.
153
+ *
154
+ * @stability unstable
155
+ */
156
+ export type DeferRecoverOf<DomainError extends Tagged, Returns, E> =
157
+ | ReturnableBodyOf<E>
158
+ | {
159
+ [Tag in keyof Returns]: [Extract<E, Extract<DomainError, { readonly _tag: Tag }>>] extends [
160
+ never,
161
+ ]
162
+ ? never
163
+ :
164
+ | (Returns[Tag] extends ReturnableDataError<infer B> ? B : never)
165
+ | (Returns[Tag] extends Effect.Effect<infer SuccessValue, any, any>
166
+ ? SuccessValue
167
+ : never);
168
+ }[keyof Returns];
169
+
127
170
  /**
128
171
  * Errors the library deals with on the handler's behalf - so the loader/action/
129
172
  * middleware needn't handle them itself:
@@ -244,6 +287,12 @@ const processRouteError = (
244
287
  * it in middleware and pass it to the factory's `requestContext` - the runner
245
288
  * reads it on every request and provides its services to the loader/action.
246
289
  *
290
+ * With the server adapter, do not set it to a new context. The adapter sets it
291
+ * to the request fiber's context before React Router runs, and a new context
292
+ * replaces those services, such as `HttpServerRequest` and the server span.
293
+ * Provide request services with a router middleware on `ReactRouter.routes`.
294
+ * The middleware below is for an app without the adapter.
295
+ *
247
296
  * It's just a `RouterContext`; create one with React Router's `createContext`:
248
297
  *
249
298
  * ```ts
@@ -257,7 +306,7 @@ const processRouteError = (
257
306
  *
258
307
  * export const requestContext: RequestContextKey<RequestContext> = createContext();
259
308
  *
260
- * // middleware:
309
+ * // middleware, in an app without the server adapter:
261
310
  * export const middleware: Route.MiddlewareFunction[] = [
262
311
  * ({ context, request }, next) => {
263
312
  * context.set(requestContext, Context.make(RequestContext, { userId: readUser(request) }));
@@ -274,8 +323,8 @@ export type RequestContextKey<ReqServices> = RouterContext<Context.Context<ReqSe
274
323
  */
275
324
  const defaultRequestContext: RequestContextKey<never> = createContext(Context.empty());
276
325
 
277
- /** What `actionMeta` reads: the request and the router context of the action. */
278
- export interface ActionMetaArgs {
326
+ /** What `routeMeta` reads: the request and the router context of the loader or action. */
327
+ export interface RouteMetaArgs {
279
328
  readonly request: Request;
280
329
  readonly context: Readonly<RouterContextProvider>;
281
330
  }
@@ -488,13 +537,14 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
488
537
  */
489
538
  respond?: ExtraRespond;
490
539
  /**
491
- * Request-scoped metadata to attach to every action's data under `__meta`,
492
- * read after the action resolves. The server adapter attaches it to every
493
- * action it runs, including plain functions and `makeAction` wrappers. A
494
- * thrown result gets none. Only plain records and `data(...)` bodies include
495
- * it; other values are returned unchanged.
540
+ * Request-scoped metadata to attach to the data of every loader and action
541
+ * under `__meta`, read after the function resolves. The server adapter
542
+ * attaches it to every loader and action it runs, including plain functions
543
+ * and `makeLoader` / `makeAction` wrappers. A thrown result gets none. Only
544
+ * plain records and `data(...)` bodies include it; other values are returned
545
+ * unchanged.
496
546
  */
497
- actionMeta?: (args: ActionMetaArgs) => Meta;
547
+ routeMeta?: (args: RouteMetaArgs) => Meta;
498
548
  },
499
549
  ) {
500
550
  const config = builder(baseRespond);
@@ -511,11 +561,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
511
561
  // Defaults to an empty map when `errorHandlers` is omitted.
512
562
  const userHandlers = (config.errorHandlers ?? {}) as unknown as Record<
513
563
  string,
514
- ErrorHandler<DomainError>
564
+ ErrorHandler<Tagged>
515
565
  >;
516
-
517
- const isUserError = (e: unknown): e is DomainError =>
518
- typeof e === "object" && e !== null && "_tag" in e && (e as Tagged)._tag in userHandlers;
566
+ /** The tags of the registered domain errors. */
567
+ const userTags = Object.keys(userHandlers);
519
568
 
520
569
  /**
521
570
  * Runs a handler's effect to completion: dispatches its whole error channel
@@ -532,7 +581,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
532
581
  // The internal channel is deliberately loose (`unknown` success); the caller
533
582
  // restores the precise resolved type (computed from `E` and the handler map).
534
583
  // Sound at runtime - the values produced are exactly those types.
535
- const program = effect.pipe(
584
+ // Deferred work forks into a scope of this run, closed when the request
585
+ // aborts or the effect fails. It is inside the dispatch, so a failure that
586
+ // a handler recovers also interrupts the work: its promises are not sent.
587
+ const program = withDeferScope(effect, signal).pipe(
536
588
  // Catch the full error channel and dispatch. The refinement is `e is E`
537
589
  // (provably ⊆ E, and a *refinement* not a bare predicate - the predicate
538
590
  // overload crashes tsc over a generic `E`). A declared domain error with no
@@ -542,9 +594,11 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
542
594
  (e): Effect.Effect<unknown, FailureResponse> => {
543
595
  // Registered domain error → remap. A library-error return is processed
544
596
  // by the internal dispatch; an `Effect` return is used as-is.
545
- if (isUserError(e)) {
546
- const out = userHandlers[e._tag](e);
547
- return isRouteError(out) ? processRouteError(out) : out;
597
+ for (const tag of userTags) {
598
+ if (Predicate.isTagged(e, tag)) {
599
+ const out = userHandlers[tag](e);
600
+ return isRouteError(out) ? processRouteError(out) : out;
601
+ }
548
602
  }
549
603
  // A library route error raised directly in the handler → recover/throw.
550
604
  if (isRouteError(e)) return processRouteError(e);
@@ -570,6 +624,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
570
624
  // `provideContext` removes `ReqServices`, and when no `requestContext` is
571
625
  // configured `ReqServices` is `never` (nothing removed).
572
626
  const store = readRequestStore(context);
627
+ // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- the cast is sound, as the comment above explains
573
628
  const provided = (
574
629
  store === undefined ? program : Effect.provideContext(program, store)
575
630
  ) as Effect.Effect<unknown, FailureResponse, RServices>;
@@ -580,7 +635,8 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
580
635
  // signal and discards its result, so the interrupt rejection is never rendered.
581
636
  return runtime
582
637
  ? runtime.runPromise(provided, { signal })
583
- : Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
638
+ : // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- with no runtime `RServices` is `never`
639
+ Effect.runPromise(provided as Effect.Effect<unknown, FailureResponse>, { signal });
584
640
  }
585
641
 
586
642
  function makeLoaderOrAction<Args extends LoaderFunctionArgs | ActionFunctionArgs, A, E, R>(
@@ -593,15 +649,18 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
593
649
  fn: (
594
650
  args: Args,
595
651
  ) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
596
- ): (args: Args) => Promise<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>> {
652
+ ): (
653
+ args: Args,
654
+ ) => Promise<WithRouteMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>> {
597
655
  // The diagnostic is irrelevant inside the body (it's `unknown` for any valid
598
656
  // call); treat `fn` as the plain effect-returning function it is.
599
657
  const run = fn as unknown as (
600
658
  args: Args,
601
659
  ) => Effect.Effect<unknown, E, RServices | ReqServices>;
660
+ // The server adapter's `handlers` attach the metadata to what this resolves with.
602
661
  return (args: Args) =>
603
662
  runProgram(run(args), args.context, args.request.signal) as Promise<
604
- A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>
663
+ WithRouteMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>
605
664
  >;
606
665
  }
607
666
 
@@ -705,6 +764,7 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
705
764
  function runResult(result: unknown, args: RouteArgs): unknown {
706
765
  return Effect.isEffect(result)
707
766
  ? runProgram(
767
+ // 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
708
768
  result as Effect.Effect<unknown, unknown, RServices | ReqServices>,
709
769
  args.context,
710
770
  args.request.signal,
@@ -712,7 +772,80 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
712
772
  : result;
713
773
  }
714
774
 
715
- const actionMeta = config.actionMeta;
775
+ /**
776
+ * A failure of deferred work, through the error handlers: a recovered
777
+ * value resolves the promise, a thrown response rejects it with an
778
+ * `ErrorResponse`. Anything else, such as a domain error with no handler,
779
+ * is a defect: the promise rejects with no server details.
780
+ */
781
+ function dispatchDeferred(e: unknown): Effect.Effect<unknown, ErrorResponse> {
782
+ for (const tag of userTags) {
783
+ if (Predicate.isTagged(e, tag)) {
784
+ const out = userHandlers[tag](e);
785
+ return isRouteError(out)
786
+ ? deferRouteError(out)
787
+ : out.pipe(Effect.catch(deferFailureResponse));
788
+ }
789
+ }
790
+ return isRouteError(e) ? deferRouteError(e) : Effect.die(e);
791
+ }
792
+
793
+ /**
794
+ * Starts an Effect without waiting for it, and gives a promise of its
795
+ * result for loader data. React Router streams the promise to `<Await>`.
796
+ *
797
+ * The work gets the services and the span of the Effect that defers it.
798
+ * It keeps running after the loader returns, and is interrupted when the
799
+ * request aborts or the loader fails. A client-side navigation away does
800
+ * not abort the request, so the work then runs until it ends or the stream
801
+ * times out.
802
+ *
803
+ * The work may fail only with the factory's domain errors,
804
+ * `Respond.early` and `Respond.throw`. The error handlers turn them into
805
+ * the promise's value, or an `ErrorResponse` for `useAsyncError`, as they
806
+ * do for a loader. A handler that redirects, a domain error with no
807
+ * handler, a defect or an interrupt rejects the promise with an error that
808
+ * holds no server details, and the server logs the full cause.
809
+ *
810
+ * ```ts
811
+ * export const { Respond, defer } = routeFactory;
812
+ *
813
+ * export const loader = Effect.fn("loader")(function* () {
814
+ * const user = yield* Users.current;
815
+ * return {
816
+ * user,
817
+ * reviews: yield* defer(Reviews.list(user.id)),
818
+ * stats: yield* Stats.load.pipe(
819
+ * Effect.catchTag("StatsUnavailable", () => Effect.succeed(null)),
820
+ * defer,
821
+ * ),
822
+ * };
823
+ * });
824
+ * ```
825
+ *
826
+ * @stability unstable
827
+ */
828
+ function defer<A, E, R>(
829
+ self: Effect.Effect<A, E, R> & DeferDiagnose<E, DeferHandled<DomainError>>,
830
+ ): Effect.Effect<DeferredPromise<A | DeferRecoverOf<DomainError, Returns, E>>, never, R> {
831
+ const work: Effect.Effect<unknown, ErrorResponse, R> = self.pipe(
832
+ Effect.catch(dispatchDeferred),
833
+ );
834
+ // The dispatch resolves with what `DeferRecoverOf` computes from the handlers.
835
+ return forkDeferred(work) as Effect.Effect<
836
+ DeferredPromise<A | DeferRecoverOf<DomainError, Returns, E>>,
837
+ never,
838
+ R
839
+ >;
840
+ }
841
+
842
+ const routeMeta = config.routeMeta;
843
+
844
+ /** Runs a loader or action's result and attaches the route metadata to its data. */
845
+ async function withRouteMeta(result: unknown, args: RouteArgs): Promise<unknown> {
846
+ const resolved = await runResult(result, args);
847
+ return routeMeta === undefined ? resolved : attachRouteMeta(resolved, routeMeta(args));
848
+ }
716
849
 
717
850
  // React Router calls every route function with args that include at least
718
851
  // `request` and `context`, so each wrapper reads them through `RouteArgs`.
@@ -720,13 +853,9 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
720
853
  // or recovered data, or the awaited plain result. `RouteResult` is its type.
721
854
  const handlers: RouteHandlers = {
722
855
  loader: (fn) => async (args) =>
723
- runResult(fn(args), args) as RouteResult<ReturnType<typeof fn>>,
724
- action: (fn) => async (args) => {
725
- const result = await runResult(fn(args), args);
726
- return (
727
- actionMeta === undefined ? result : attachActionMeta(result, actionMeta(args))
728
- ) as RouteResult<ReturnType<typeof fn>>;
729
- },
856
+ (await withRouteMeta(fn(args), args)) as RouteResult<ReturnType<typeof fn>>,
857
+ action: (fn) => async (args) =>
858
+ (await withRouteMeta(fn(args), args)) as RouteResult<ReturnType<typeof fn>>,
730
859
  middleware: (fn) => async (args, next) => {
731
860
  const routeArgs: RouteArgs = args;
732
861
  // The runner supplies `next` as an effect in place of the one `Args`
@@ -742,20 +871,10 @@ export function makeEffectRouteFactory<DomainError extends Tagged = never>() {
742
871
 
743
872
  return {
744
873
  makeLoader: makeLoaderOrAction,
745
- makeAction: makeLoaderOrAction as <
746
- Args extends LoaderFunctionArgs | ActionFunctionArgs,
747
- A,
748
- E,
749
- R,
750
- >(
751
- fn: (
752
- args: Args,
753
- ) => Effect.Effect<A, E, R> & Diagnose<E, R, DomainError, RServices | ReqServices>,
754
- ) => (
755
- args: Args,
756
- ) => Promise<WithActionMeta<A | DirectRecover<E> | RecoverOf<DomainError, Returns, E>, Meta>>,
874
+ makeAction: makeLoaderOrAction,
757
875
  makeMiddleware,
758
876
  Respond,
877
+ defer,
759
878
  /** The route runner the server adapter applies to every route module. */
760
879
  handlers,
761
880
  /** Type-only: what the adapter's route check and `Register` read. */
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
- export { actionMetaKey } from "./action-meta.ts";
2
- export type { WithActionMeta } from "./action-meta.ts";
1
+ export { routeMetaKey } from "./route-meta.ts";
2
+ export type { WithRouteMeta } from "./route-meta.ts";
3
+ export { deferredFailureMessage } from "./defer.ts";
4
+ export type { DeferDiagnose, DeferredPromise } from "./defer.ts";
3
5
  export {
4
6
  isRouteError,
5
7
  ReturnableDataError,
@@ -9,7 +11,9 @@ export {
9
11
  export type { AnyRouteError } from "./errors.ts";
10
12
  export { makeEffectRouteFactory } from "./factory.ts";
11
13
  export type {
12
- ActionMetaArgs,
14
+ DeferHandled,
15
+ DeferRecoverOf,
16
+ RouteMetaArgs,
13
17
  EffectMiddlewareArgs,
14
18
  EffectNext,
15
19
  ErrorHandler,
package/src/route-data.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Effect } from "effect";
2
2
  import type { UNSAFE_DataWithResponseInit, useLoaderData } from "react-router";
3
3
 
4
- import type { WithActionMeta } from "./action-meta.ts";
4
+ import type { WithRouteMeta } from "./route-meta.ts";
5
5
  import type { ReturnableDataError } from "./errors.ts";
6
6
 
7
7
  // ---------------------------------------------------------------------------
@@ -28,8 +28,8 @@ import type { ReturnableDataError } from "./errors.ts";
28
28
  * interface Register {
29
29
  * // Each error tag a handler recovers, and the data it recovers into.
30
30
  * recover: { FormValidationError: { reply: SubmissionResponse } };
31
- * // What `actionMeta` returns.
32
- * actionMeta: { realtimeEventIds: ReadonlyArray<string> };
31
+ * // What `routeMeta` returns.
32
+ * routeMeta: { realtimeEventIds: ReadonlyArray<string> };
33
33
  * }
34
34
  * }
35
35
  * ```
@@ -39,8 +39,8 @@ export interface Register {}
39
39
  /** The registered recover map: error tag to recovered data. */
40
40
  export type RegisteredRecover = Register extends { readonly recover: infer Recover } ? Recover : {};
41
41
 
42
- /** The registered action metadata, or `never` when there is none. */
43
- export type RegisteredMeta = Register extends { readonly actionMeta: infer Meta } ? Meta : never;
42
+ /** The registered route metadata, or `never` when there is none. */
43
+ export type RegisteredMeta = Register extends { readonly routeMeta: infer Meta } ? Meta : never;
44
44
 
45
45
  /** What a failure of type `E` recovers into: `Respond.early` bodies and registered handlers. */
46
46
  type Recovered<E> =
@@ -59,9 +59,9 @@ export type Serialize<T> = ReturnType<typeof useLoaderData<() => Promise<T>>>;
59
59
  /**
60
60
  * The data an Effect that succeeds with `A` and fails with `E` gives the
61
61
  * component: its success and what its failures recover into, serialized as
62
- * React Router serializes them, with the registered action metadata.
62
+ * React Router serializes them, with the registered route metadata.
63
63
  */
64
- export type EffectData<A, E> = WithActionMeta<Serialize<A | Recovered<E>>, RegisteredMeta>;
64
+ export type EffectData<A, E> = WithRouteMeta<Serialize<A | Recovered<E>>, RegisteredMeta>;
65
65
 
66
66
  /**
67
67
  * The route data of a loader or action, for React Router's hooks:
@@ -108,10 +108,10 @@ type Unwrapped<A> = A extends Response
108
108
  * declarations. `test/consumer` checks the built package for that.
109
109
  */
110
110
  export type BrandData<A, E> = [A] extends [never]
111
- ? WithActionMeta<Recovered<E>, RegisteredMeta>
111
+ ? WithRouteMeta<Recovered<E>, RegisteredMeta>
112
112
  : [A] extends [void]
113
113
  ? unknown
114
- : WithActionMeta<Unwrapped<A> | Recovered<E>, RegisteredMeta>;
114
+ : WithRouteMeta<Unwrapped<A> | Recovered<E>, RegisteredMeta>;
115
115
 
116
116
  declare module "effect/Effect" {
117
117
  // React Router reads loader and action data through this brand, so an
@@ -2,11 +2,11 @@ import { Predicate } from "effect";
2
2
  import { data, type UNSAFE_DataWithResponseInit as DataWithResponseInit } from "react-router";
3
3
 
4
4
  // ---------------------------------------------------------------------------
5
- // Action metadata: request-scoped facts attached to every action's data.
5
+ // Route metadata: request-scoped facts attached to every loader and action's data.
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
- /** The key that holds the metadata in action data. */
9
- export const actionMetaKey = "__meta";
8
+ /** The key that holds the metadata in loader and action data. */
9
+ export const routeMetaKey = "__meta";
10
10
 
11
11
  /**
12
12
  * Adds optional metadata to a record. Only records get metadata, because a
@@ -14,27 +14,29 @@ export const actionMetaKey = "__meta";
14
14
  * values (strings, arrays, `Response`s) have nowhere to put it.
15
15
  */
16
16
  type RecordWithMeta<T, Meta> =
17
- T extends Record<string, unknown>
18
- ? Omit<T, typeof actionMetaKey> & { [actionMetaKey]?: Meta }
19
- : T;
17
+ T extends Record<string, unknown> ? Omit<T, typeof routeMetaKey> & { [routeMetaKey]?: Meta } : T;
20
18
 
21
19
  /**
22
- * The type of action data after metadata is attached. A `data(...)` wrapper
20
+ * The type of loader or action data after metadata is attached. A `data(...)` wrapper
23
21
  * gets metadata on its body. With no configured metadata (`Meta` is `never`)
24
22
  * the data is unchanged.
25
23
  */
26
- export type WithActionMeta<T, Meta> = [Meta] extends [never]
24
+ export type WithRouteMeta<T, Meta> = [Meta] extends [never]
27
25
  ? T
28
26
  : T extends DataWithResponseInit<infer Body>
29
27
  ? DataWithResponseInit<RecordWithMeta<Body, Meta>>
30
28
  : RecordWithMeta<T, Meta>;
31
29
 
32
- // React Router exports the type of the `data(...)` wrapper, but not its class.
33
- const DataWithResponseInitClass = data(undefined).constructor;
34
-
35
- /** True for a `data(...)` wrapper, never for a business field that looks like one. */
30
+ /**
31
+ * True for a `data(...)` wrapper, by the same test React Router applies. A
32
+ * class test fails when the app and the adapter load different copies of
33
+ * React Router, as under the Vite dev server.
34
+ */
36
35
  const isDataWithResponseInit = (value: unknown): value is DataWithResponseInit<unknown> =>
37
- value instanceof DataWithResponseInitClass;
36
+ Predicate.hasProperty(value, "type") &&
37
+ Predicate.hasProperty(value, "data") &&
38
+ Predicate.hasProperty(value, "init") &&
39
+ value.type === "DataWithResponseInit";
38
40
 
39
41
  /** True for a plain object, which can be copied without losing behavior. */
40
42
  const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
@@ -44,15 +46,15 @@ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
44
46
  };
45
47
 
46
48
  /**
47
- * Attaches `meta` to the action result. A plain record gets a copy with the
49
+ * Attaches `meta` to a loader or action result. A plain record gets a copy with the
48
50
  * metadata key; a `data(...)` wrapper gets it on its body and keeps its init.
49
51
  * Any other value is returned unchanged.
50
52
  */
51
- export const attachActionMeta = <T>(value: T, meta: unknown): T => {
53
+ export const attachRouteMeta = <T>(value: T, meta: unknown): T => {
52
54
  if (isDataWithResponseInit(value)) {
53
55
  if (!isPlainRecord(value.data)) return value;
54
56
  // The copy has the same shape as `value` plus the metadata key.
55
- return data({ ...value.data, [actionMetaKey]: meta }, value.init ?? undefined) as T;
57
+ return data({ ...value.data, [routeMetaKey]: meta }, value.init ?? undefined) as T;
56
58
  }
57
- return isPlainRecord(value) ? ({ ...value, [actionMetaKey]: meta } as T) : value;
59
+ return isPlainRecord(value) ? ({ ...value, [routeMetaKey]: meta } as T) : value;
58
60
  };
@@ -1,7 +1,8 @@
1
1
  /** `react-router-effect/server`: serve React Router from the Effect HTTP server. */
2
2
  import "./route-data.ts";
3
3
 
4
- export * as ReactRouterHttp from "./server.ts";
4
+ export { ReactRouter } from "./server.ts";
5
+ export * as ReactRouterAssets from "./assets.ts";
5
6
  export type {
6
7
  RegisterMismatch,
7
8
  RouteEffectCheck,