@effectify/react-remix 0.2.0 → 0.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 CHANGED
@@ -41,7 +41,7 @@ Use the Effect-based loaders and actions in your Remix routes:
41
41
  ```typescript
42
42
  // routes/home.tsx
43
43
  import type * as Route from "./+types.home";
44
- import { Ok, LoaderArgsContext } from "@effectify/react-remix";
44
+ import { httpSuccess, httpFailure, LoaderArgsContext } from "@effectify/react-remix";
45
45
  import { withLoaderEffect } from "~/lib/server-runtime";
46
46
  import * as Effect from "effect/Effect"
47
47
 
@@ -49,7 +49,12 @@ export const loader = withLoaderEffect(
49
49
  Effect.gen(function* () {
50
50
  const { request } = yield* LoaderArgsContext
51
51
  yield* Effect.log("request", request)
52
- return yield* Effect.succeed(new Ok({ data: { hello: 'world' }}))
52
+
53
+ // Improved DX: Simple syntax for success responses
54
+ return yield* httpSuccess({ hello: 'world' })
55
+
56
+ // For error responses, use:
57
+ // return yield* httpFailure("Something went wrong")
53
58
  })
54
59
  )
55
60
 
@@ -61,6 +66,23 @@ export default function Home({ loaderData }: Route.ComponentProps) {
61
66
  </div>
62
67
  )
63
68
  }
69
+
70
+ ### Error Responses
71
+
72
+ For error handling, use the `httpFailure` helper:
73
+ ```typescript
74
+ return yield* httpFailure("Something went wrong")
75
+ // or with more complex error objects
76
+ return yield* httpFailure({ code: 'VALIDATION_ERROR', message: 'Invalid input' })
77
+ ```
78
+
79
+ ### Redirects
80
+
81
+ For redirects, use the `httpRedirect` helper:
82
+ ```typescript
83
+ return yield* httpRedirect('/login')
84
+ // or with custom status/headers
85
+ return yield* httpRedirect('/dashboard', { status: 301 })
64
86
  ```
65
87
 
66
88
  ## API
@@ -88,9 +110,49 @@ Effect context providing access to Remix loader arguments including:
88
110
 
89
111
  ### Response Types
90
112
 
91
- - `Ok(data)`: Successful response with data
92
- - `Redirect(url)`: Redirect response
93
- - `Error(message)`: Error response
113
+ - `HttpResponseSuccess<T>(data)`: Successful HTTP response with data
114
+ - `HttpResponseFailure<T>(cause)`: Failed HTTP response with cause
115
+ - `HttpResponseRedirect(to, init?)`: HTTP redirect response
116
+
117
+ ### Helper Functions
118
+
119
+ - `httpSuccess<T>(data: T)`: Creates a successful Effect with HttpResponseSuccess
120
+ - `httpFailure<T>(cause: T)`: Creates a successful Effect with HttpResponseFailure
121
+ - `httpRedirect(to: string, init?: ResponseInit)`: Creates a successful Effect with HttpResponseRedirect
122
+
123
+ ### Error Handling & Logging
124
+
125
+ The library provides comprehensive error handling with full ErrorBoundary support:
126
+
127
+ - **Automatic Error Logging**: Errors are automatically logged using `Effect.logError`
128
+ - **ErrorBoundary Compatible**: Loader errors are properly thrown as `Response` objects for Remix ErrorBoundary
129
+ - **Configurable Logging**: Users can configure their own logging system through Effect layers
130
+ - **Non-blocking**: Logging doesn't block the main thread
131
+ - **Structured Logging**: Errors are logged with context and structured data
132
+ - **Error Preservation**: Original error context is preserved for better debugging
133
+
134
+ ```typescript
135
+ // The library automatically logs errors like this:
136
+ Effect.tapError((cause) => Effect.logError('Loader effect failed', cause))
137
+
138
+ // Loader errors are automatically converted to Response objects for ErrorBoundary:
139
+ // - Effect errors → Response with { ok: false, errors: [...] } and status 500
140
+ // - HttpResponseFailure → Response with { ok: false, errors: [...] } and status 500
141
+ // - Original Response/Error objects are preserved
142
+
143
+ // Users can configure custom logging through Effect layers
144
+ const customLogger = Logger.make(({ message, cause }) => {
145
+ // Custom logging implementation
146
+ console.log(`[${new Date().toISOString()}] ${message}`, cause)
147
+ })
148
+
149
+ const runtime = make(pipe(
150
+ // Your app layers
151
+ MyAppLayer,
152
+ // Custom logger layer
153
+ Logger.replace(Logger.defaultLogger, customLogger)
154
+ ))
155
+ ```
94
156
 
95
157
  ## Requirements
96
158
 
@@ -1,21 +1,33 @@
1
- declare const Ok_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
2
- readonly _tag: "Ok";
1
+ import * as Effect from 'effect/Effect';
2
+ declare const HttpResponseSuccess_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
3
+ readonly _tag: "HttpResponseSuccess";
3
4
  };
4
- export declare class Ok<T> extends Ok_base<{
5
+ export declare class HttpResponseSuccess<T> extends HttpResponseSuccess_base<{
5
6
  readonly data: T;
6
7
  }> {
7
8
  }
8
- declare const Redirect_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
9
- readonly _tag: "Redirect";
9
+ declare const HttpResponseFailure_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
10
+ readonly _tag: "HttpResponseFailure";
10
11
  };
11
- export declare class Redirect extends Redirect_base<{
12
+ export declare class HttpResponseFailure<T = unknown> extends HttpResponseFailure_base<{
13
+ readonly cause: T;
14
+ }> {
15
+ }
16
+ declare const HttpResponseRedirect_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
17
+ readonly _tag: "HttpResponseRedirect";
18
+ };
19
+ export declare class HttpResponseRedirect extends HttpResponseRedirect_base<{
12
20
  readonly to: string;
13
21
  readonly init?: number | ResponseInit | undefined;
14
22
  }> {
15
23
  }
16
- export type HttpResponse<T> = Redirect | Ok<T>;
24
+ export type HttpResponse<T> = HttpResponseRedirect | HttpResponseSuccess<T> | HttpResponseFailure<unknown>;
17
25
  export declare const matchHttpResponse: <T>() => <P extends {
18
- readonly Ok: (_: Ok<T>) => any;
19
- readonly Redirect: (_: Redirect) => any;
20
- } & { readonly [Tag in Exclude<keyof P, "Ok" | "Redirect">]: never; }>(fields: P) => (input: HttpResponse<T>) => import("effect/Unify").Unify<ReturnType<P[keyof P]>>;
26
+ readonly HttpResponseSuccess: (_: HttpResponseSuccess<T>) => any;
27
+ readonly HttpResponseFailure: (_: HttpResponseFailure<unknown>) => any;
28
+ readonly HttpResponseRedirect: (_: HttpResponseRedirect) => any;
29
+ } & { readonly [Tag in Exclude<keyof P, "HttpResponseSuccess" | "HttpResponseFailure" | "HttpResponseRedirect">]: never; }>(fields: P) => (input: HttpResponse<T>) => import("effect/Unify").Unify<ReturnType<P[keyof P]>>;
30
+ export declare const httpSuccess: <T>(data: T) => Effect.Effect<HttpResponseSuccess<T>, never, never>;
31
+ export declare const httpFailure: <T = unknown>(cause: T) => Effect.Effect<HttpResponseFailure<T>, never, never>;
32
+ export declare const httpRedirect: (to: string, init?: number | ResponseInit) => Effect.Effect<HttpResponseRedirect, never, never>;
21
33
  export {};
@@ -1,7 +1,14 @@
1
1
  import * as Data from 'effect/Data';
2
+ import * as Effect from 'effect/Effect';
2
3
  import * as Match from 'effect/Match';
3
- export class Ok extends Data.TaggedClass('Ok') {
4
+ export class HttpResponseSuccess extends Data.TaggedClass('HttpResponseSuccess') {
4
5
  }
5
- export class Redirect extends Data.TaggedClass('Redirect') {
6
+ export class HttpResponseFailure extends Data.TaggedClass('HttpResponseFailure') {
7
+ }
8
+ export class HttpResponseRedirect extends Data.TaggedClass('HttpResponseRedirect') {
6
9
  }
7
10
  export const matchHttpResponse = () => Match.typeTags();
11
+ // Helper functions for better DX
12
+ export const httpSuccess = (data) => Effect.succeed(new HttpResponseSuccess({ data }));
13
+ export const httpFailure = (cause) => Effect.succeed(new HttpResponseFailure({ cause }));
14
+ export const httpRedirect = (to, init) => Effect.succeed(new HttpResponseRedirect({ to, init }));
@@ -17,5 +17,8 @@ export declare const make: <R, E>(layer: Layer.Layer<R, E, never>) => {
17
17
  }> | {
18
18
  ok: true;
19
19
  response: A;
20
- } | import("@remix-run/node").TypedResponse<never>>;
20
+ } | import("@remix-run/node").TypedResponse<{
21
+ ok: false;
22
+ errors: string[];
23
+ }> | import("@remix-run/node").TypedResponse<never>>;
21
24
  };
@@ -9,21 +9,46 @@ import { matchHttpResponse } from './http-response.js';
9
9
  export const make = (layer) => {
10
10
  const runtime = ManagedRuntime.make(layer);
11
11
  const withLoaderEffect = (self) => (args) => {
12
- const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(LoaderArgsContext, args));
12
+ const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(LoaderArgsContext, args), Effect.tapError((cause) => Effect.logError('Loader effect failed', cause)));
13
13
  return runtime.runPromiseExit(runnable).then(Exit.match({
14
14
  onFailure: (cause) => {
15
- console.error(cause);
16
15
  if (cause._tag === 'Fail') {
17
- throw pipe(cause.error);
16
+ // Preserve the original error for ErrorBoundary
17
+ const error = cause.error;
18
+ if (error instanceof Response) {
19
+ throw error;
20
+ }
21
+ if (error instanceof Error) {
22
+ throw error;
23
+ }
24
+ // Convert other errors to Response for ErrorBoundary with ok: false
25
+ const errorData = { ok: false, errors: [String(error)] };
26
+ throw new Response(JSON.stringify(errorData), {
27
+ status: 500,
28
+ headers: { 'Content-Type': 'application/json' },
29
+ });
18
30
  }
19
- // biome-ignore lint/style/useThrowOnlyError: <library uses non-Error throws>
20
- throw { ok: false, errors: ['Something went wrong'] };
31
+ // Handle other types of failures (interrupts, defects, etc.)
32
+ const errorData = { ok: false, errors: ['Internal server error'] };
33
+ throw new Response(JSON.stringify(errorData), {
34
+ status: 500,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ });
21
37
  },
22
38
  onSuccess: matchHttpResponse()({
23
- Ok: ({ data: response }) => {
39
+ HttpResponseSuccess: ({ data: response }) => {
24
40
  return { ok: true, data: response };
25
41
  },
26
- Redirect: ({ to, init = {} }) => {
42
+ HttpResponseFailure: ({ cause }) => {
43
+ // Convert HttpResponseFailure to Response for ErrorBoundary with ok: false
44
+ const errorMessage = typeof cause === 'string' ? cause : String(cause);
45
+ const errorData = { ok: false, errors: [errorMessage] };
46
+ throw new Response(JSON.stringify(errorData), {
47
+ status: 500,
48
+ headers: { 'Content-Type': 'application/json' },
49
+ });
50
+ },
51
+ HttpResponseRedirect: ({ to, init = {} }) => {
27
52
  redirect(to, init);
28
53
  return { ok: false, errors: ['Redirecting...'] };
29
54
  },
@@ -32,13 +57,16 @@ export const make = (layer) => {
32
57
  };
33
58
  // Don't throw the Error requests, handle them in the normal UI. No ErrorBoundary
34
59
  const withActionEffect = (self) => (args) => {
35
- const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.match({
60
+ const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.tapError((cause) => Effect.logError('Action effect failed', cause)), Effect.match({
36
61
  onFailure: (errors) => json({ ok: false, errors }, { status: 400 }),
37
62
  onSuccess: matchHttpResponse()({
38
- Ok: ({ data: response }) => {
63
+ HttpResponseSuccess: ({ data: response }) => {
39
64
  return { ok: true, response };
40
65
  },
41
- Redirect: ({ to, init = {} }) => {
66
+ HttpResponseFailure: ({ cause }) => {
67
+ return json({ ok: false, errors: [String(cause)] }, { status: 400 });
68
+ },
69
+ HttpResponseRedirect: ({ to, init = {} }) => {
42
70
  return redirect(to, init);
43
71
  },
44
72
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectify/react-remix",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Integration of Remix with Effect for React applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",