mini-effect 0.0.0 → 0.0.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 CHANGED
@@ -1,190 +1,205 @@
1
- # 🎯 mini-effect
1
+ # mini-effect
2
2
 
3
- > _Because sometimes you want Effect, but like... mini._
3
+ Minimal Effect system for TypeScript. Lazy, composable, cancellable.
4
4
 
5
- A delightfully tiny Effect system for TypeScript that packs a punch! ✨
5
+ ## Installation
6
6
 
7
- ## 🤔 What's This?
8
-
9
- Ever looked at [Effect-TS](https://effect.website/) and thought "wow, that's amazing but also... a lot"? Same.
10
-
11
- **mini-effect** gives you the good stuff:
7
+ ```bash
8
+ npm install mini-effect
9
+ ```
12
10
 
13
- - 🦥 **Lazy evaluation** - Nothing runs until you say so
14
- - 🔗 **Pipes** - Compose like a functional programming wizard
15
- - 🎭 **Generators** - Write async code that doesn't look like callback spaghetti
16
- - 🎣 **Error handling** - Catch 'em all (or just some of 'em)
17
- - ⚡ **Concurrency** - `all`, `race`, `any`, `allSettled` - the gang's all here
18
- - 🛑 **AbortSignal support** - Cancel everything! EVERYTHING!
11
+ ## Modules
19
12
 
20
- All in a package so small it practically fits in a tweet. 🐦
13
+ - `mini-effect` core primitives
14
+ - `mini-effect/concurrency` — parallel execution
15
+ - `mini-effect/tag` — tagged error handling
16
+ - `mini-effect/fetch` — HTTP requests
17
+ - `mini-effect/schema` — validation (Valibot)
21
18
 
22
- ## 📦 Installation
19
+ ## Core
23
20
 
24
- ```bash
25
- npm install mini-effect
26
- # or
27
- pnpm add mini-effect
28
- # or
29
- yarn add mini-effect
21
+ ```typescript
22
+ import { fn, succeed, fail, gen, pipe, run, catchSome } from "mini-effect";
30
23
  ```
31
24
 
32
- ## 🚀 Quick Start
25
+ ### Create Effects
33
26
 
34
27
  ```typescript
35
- import { fn, run, succeed, fail, gen, pipe } from "mini-effect";
36
- import { all } from "mini-effect/concurrency";
37
-
38
- // Create an effect (it's lazy - nothing happens yet!)
39
- const fetchTodo = fn(async (signal) => {
40
- const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
41
- signal,
42
- });
43
- return res.json();
44
- });
45
-
46
- // Run it! 🏃‍♂️
47
- const todo = await run(fetchTodo);
28
+ const delay = fn(
29
+ (signal) =>
30
+ new Promise<void>((resolve, reject) => {
31
+ const id = setTimeout(resolve, 1000);
32
+ signal.addEventListener("abort", () => {
33
+ clearTimeout(id);
34
+ reject(signal.reason);
35
+ });
36
+ }),
37
+ );
38
+ const success = succeed(42);
39
+ const failure = fail(new Error("failed"));
48
40
  ```
49
41
 
50
- ## 🎮 The Fun Parts
51
-
52
- ### Generators (a.k.a. "async/await but cooler")
42
+ ### Generators
53
43
 
54
44
  ```typescript
55
45
  const program = gen(function* (signal) {
56
46
  const a = yield* fn(() => Promise.resolve(1));
57
- const b = yield* fn(() => Promise.resolve(2));
58
- const c = yield* succeed(3);
59
-
60
- return a + b + c; // 6!
47
+ const b = yield* succeed(2);
48
+ return a + b;
61
49
  });
62
-
63
- await run(program); // 6
64
50
  ```
65
51
 
66
- ### Pipes (a.k.a. "look ma, no nesting!")
52
+ ### Pipe
67
53
 
68
54
  ```typescript
69
55
  const result = await run(
70
56
  pipe(
71
57
  succeed(5),
72
- (n: number) => succeed(n * 2), // 10
73
- (n: number) => succeed(n + 1), // 11
74
- (n: number) => succeed(`Result: ${n}`), // "Result: 11"
58
+ (n: number) => succeed(n * 2),
59
+ (n: number) => succeed(n + 1),
75
60
  ),
76
61
  );
77
62
  ```
78
63
 
79
- ### Error Handling (a.k.a. "try/catch grew up")
64
+ ### Error Recovery
80
65
 
81
66
  ```typescript
82
- import { catchClass, catchSome } from "mini-effect";
83
-
84
- class NotFoundError extends Error {
85
- constructor(public id: number) {
86
- super(`Not found: ${id}`);
87
- }
88
- }
89
-
90
- const safeProgram = pipe(
67
+ pipe(
91
68
  fn(() => {
92
- throw new NotFoundError(42);
69
+ throw new Error("x");
93
70
  }),
94
- catchClass(NotFoundError, (err) => succeed(`Handled: ${err.id}`)),
71
+ catchSome((cause) =>
72
+ cause instanceof Error ? succeed("recovered") : undefined,
73
+ ),
95
74
  );
75
+ ```
76
+
77
+ ### Execution
96
78
 
97
- await run(safeProgram); // "Handled: 42"
79
+ ```typescript
80
+ const result = await run(effect);
81
+ const result = await run(effect, abortController.signal);
98
82
  ```
99
83
 
100
- ### Concurrency (a.k.a. "Promise.all's cooler cousin")
84
+ ## Concurrency
101
85
 
102
86
  ```typescript
103
- import { all, race, any, allSettled } from "mini-effect/concurrency";
104
-
105
- // Run 'em all at once!
106
- const results = await run(
107
- all([
108
- fn(() => fetch("/api/users")),
109
- fn(() => fetch("/api/posts")),
110
- fn(() => fetch("/api/comments")),
111
- ]),
112
- );
87
+ import { all, allSettled, any, race, fork } from "mini-effect/concurrency";
88
+ ```
113
89
 
114
- // First one wins! 🏆
115
- const fastest = await run(
116
- race([fn(() => fetchFromServer1()), fn(() => fetchFromServer2())]),
117
- );
90
+ | Function | Behavior |
91
+ | ------------ | -------------------------------------- |
92
+ | `all` | Parallel execution, fail-fast |
93
+ | `allSettled` | Parallel execution, collect all |
94
+ | `any` | First success wins |
95
+ | `race` | First completion wins |
96
+ | `fork` | Background execution, returns abort fn |
97
+
98
+ ```typescript
99
+ await run(all([effectA, effectB, effectC]));
100
+ const abort = await run(fork(backgroundEffect));
101
+ ```
118
102
 
119
- // First SUCCESS wins (failures don't count)
120
- const firstSuccess = await run(any([fn(() => tryThis()), fn(() => tryThat())]));
103
+ ## Tagged Errors
121
104
 
122
- // Get all results, even the failures 🤷
123
- const allResults = await run(
124
- allSettled([succeed(1), fail(new Error("oops")), succeed(3)]),
105
+ ```typescript
106
+ import { failure, catchTags } from "mini-effect/tag";
107
+
108
+ const NotFound = failure("NotFound");
109
+ const Unauthorized = failure("Unauthorized");
110
+
111
+ pipe(
112
+ fn(() => {
113
+ throw new NotFound();
114
+ }),
115
+ catchTags({
116
+ NotFound: () => succeed("fallback"),
117
+ Unauthorized: () => succeed("login"),
118
+ }),
125
119
  );
126
120
  ```
127
121
 
128
- ### Cancellation (a.k.a. "STOP THE PRESSES!")
122
+ ## Fetch
129
123
 
130
124
  ```typescript
131
- const controller = new AbortController();
125
+ import { request, json, text, blob, bytes, formData } from "mini-effect/fetch";
126
+ ```
132
127
 
133
- const longRunning = fn(async (signal) => {
134
- // Check if we should bail
135
- if (signal.aborted) {
136
- throw new DOMException("Aborted!", "AbortError");
137
- }
128
+ | Function | Returns | Error Tag |
129
+ | ---------- | ------------ | --------------- |
130
+ | `request` | `Response` | `FailedToFetch` |
131
+ | `json` | `unknown` | `FailedToRead` |
132
+ | `text` | `string` | `FailedToRead` |
133
+ | `blob` | `Blob` | `FailedToRead` |
134
+ | `bytes` | `Uint8Array` | `FailedToRead` |
135
+ | `formData` | `FormData` | `FailedToRead` |
138
136
 
139
- // Or pass it to fetch
140
- const res = await fetch("/api/slow", { signal });
141
- return res.json();
142
- });
137
+ ```typescript
138
+ pipe(request("https://api.example.com/data"), (res) => json(res));
139
+ ```
143
140
 
144
- // Start it
145
- const promise = run(longRunning, controller.signal);
141
+ ## Schema
146
142
 
147
- // Actually, never mind! 🙅
148
- controller.abort();
143
+ ```typescript
144
+ import { validate, object, string, number } from "mini-effect/schema";
145
+
146
+ const User = object({ name: string(), age: number() });
147
+ const validated = validate(User)({ name: "Alice", age: 30 });
149
148
  ```
150
149
 
151
- ## 🗺️ API at a Glance
150
+ Error tag: `FailedToValidate`
151
+
152
+ Re-exports all of [Valibot](https://valibot.dev/).
153
+
154
+ ## API Reference
155
+
156
+ ### `mini-effect`
152
157
 
153
- | Function | What it does |
154
- | ---------------------------- | ----------------------------------- |
155
- | `fn(thunk)` | Create an effect from a function |
156
- | `succeed(value)` | Create a successful effect |
157
- | `fail(error)` | Create a failed effect |
158
- | `gen(generator)` | Create an effect from a generator |
159
- | `pipe(effect, ...steps)` | Compose effects left-to-right |
160
- | `run(effect, signal?)` | Execute an effect (returns Promise) |
161
- | `catchSome(handler)` | Catch errors selectively |
162
- | `catchClass(Class, handler)` | Catch errors by type |
158
+ | Export | Signature |
159
+ | ----------- | ---------------------------------------------------------------- |
160
+ | `fn` | `(thunk: (signal: AbortSignal) => T \| Promise<T>) => Effect<T>` |
161
+ | `succeed` | `(value: T) => Effect<T, never>` |
162
+ | `fail` | `(error: E) => Effect<never, E>` |
163
+ | `gen` | `(generator: (signal) => Generator<Effect>) => Effect` |
164
+ | `pipe` | `(effect, ...fns) => Effect` |
165
+ | `run` | `(effect, signal?) => Promise<T>` |
166
+ | `catchSome` | `(handler: (cause) => Effect \| undefined) => WrapEffect` |
163
167
 
164
- ### Concurrency
168
+ ### `mini-effect/concurrency`
165
169
 
166
- | Function | What it does |
167
- | --------------------- | ---------------------------- |
168
- | `all(effects)` | Run all, fail if any fails |
169
- | `allSettled(effects)` | Run all, collect all results |
170
- | `any(effects)` | First success wins |
171
- | `race(effects)` | First to finish wins |
170
+ | Export | Signature |
171
+ | ------------ | ------------------------------------------------ |
172
+ | `all` | `(effects: Effect[]) => Effect<T[]>` |
173
+ | `allSettled` | `(effects: Effect[]) => Effect<SettledResult[]>` |
174
+ | `any` | `(effects: Effect[]) => Effect<T>` |
175
+ | `race` | `(effects: Effect[]) => Effect<T>` |
176
+ | `fork` | `(effect: Effect) => Effect<() => void>` |
172
177
 
173
- ## 🧠 Why Effects?
178
+ ### `mini-effect/tag`
174
179
 
175
- Effects are like promises, but:
180
+ | Export | Signature |
181
+ | ----------- | ------------------------------------------------ |
182
+ | `failure` | `(tag: string) => FailureConstructor` |
183
+ | `catchTags` | `(handlers: Record<Tag, Handler>) => WrapEffect` |
176
184
 
177
- 1. **Lazy** - They don't run until you explicitly `run()` them
178
- 2. **Composable** - Pipe and combine them without execution
179
- 3. **Typed errors** - Your errors are part of the type signature
180
- 4. **Cancellable** - AbortSignal support baked right in
185
+ ### `mini-effect/fetch`
181
186
 
182
- Think of an Effect as a _description_ of a computation, not the computation itself. You're building a recipe, not cooking the meal. When you're ready to eat, call `run()`! 🍳
187
+ | Export | Type |
188
+ | ---------- | -------- |
189
+ | `request` | Function |
190
+ | `json` | Function |
191
+ | `text` | Function |
192
+ | `blob` | Function |
193
+ | `bytes` | Function |
194
+ | `formData` | Function |
183
195
 
184
- ## 📄 License
196
+ ### `mini-effect/schema`
185
197
 
186
- MIT © [Jacob Ebey](https://github.com/jacob-ebey)
198
+ | Export | Description |
199
+ | ---------- | ------------------------------- |
200
+ | `validate` | `(schema) => (value) => Effect` |
201
+ | `*` | All Valibot exports |
187
202
 
188
- ---
203
+ ## License
189
204
 
190
- _Built with ☕ and a healthy disregard for bundle size anxiety._
205
+ MIT
@@ -1,9 +1,10 @@
1
1
  import { Effect, inferError, inferReturn } from "./mini-effect.mjs";
2
2
 
3
3
  //#region src/concurrency.d.ts
4
+ declare const fork: <const E extends Effect<any, any>>(effect: E) => Effect<() => void, never>;
4
5
  declare const all: <const E extends Effect<any, any>[]>(values: E) => Effect<{ [K in keyof E]: inferReturn<E[K]> }, inferError<E[number]>>;
5
6
  declare const allSettled: <const E extends Effect<any, any>[]>(values: E) => Effect<{ [K in keyof E]: PromiseSettledResult<inferReturn<E[K]>> }, inferError<E[number]>>;
6
7
  declare const any: <const E extends Effect<any, any>[]>(values: E) => Effect<inferReturn<E[number]>, inferError<E[number]>>;
7
8
  declare const race: <const E extends Effect<any, any>[]>(values: E) => Effect<inferReturn<E[number]>, inferError<E[number]>>;
8
9
  //#endregion
9
- export { all, allSettled, any, race };
10
+ export { all, allSettled, any, fork, race };
@@ -6,6 +6,11 @@ const localController = (signal) => {
6
6
  signal.addEventListener("abort", () => controller.abort(signal.reason));
7
7
  return controller;
8
8
  };
9
+ const fork = (effect) => fn((signal) => {
10
+ const controller = localController(signal);
11
+ run(effect, controller.signal).catch();
12
+ return () => controller.abort();
13
+ });
9
14
  const promiseMethod = (method, values) => fn(async (signal) => {
10
15
  let controller = localController(signal);
11
16
  let localSignal = controller.signal;
@@ -21,4 +26,4 @@ const any = (values) => promiseMethod("any", values);
21
26
  const race = (values) => promiseMethod("race", values);
22
27
 
23
28
  //#endregion
24
- export { all, allSettled, any, race };
29
+ export { all, allSettled, any, fork, race };
@@ -0,0 +1,13 @@
1
+ import { Effect } from "./mini-effect.mjs";
2
+ import { FailureConstructor } from "./tag.mjs";
3
+
4
+ //#region src/fetch.d.ts
5
+ declare const FailedToReadError: FailureConstructor<"FailedToRead">;
6
+ declare const request: (input: string | URL, init?: RequestInit) => Effect<Response, never>;
7
+ declare const blob: (response: Response) => Effect<any, never>;
8
+ declare const bytes: (response: Response) => Effect<any, never>;
9
+ declare const formData: (response: Response) => Effect<any, never>;
10
+ declare const json: (response: Response) => Effect<unknown, typeof FailedToReadError>;
11
+ declare const text: (response: Response) => Effect<any, never>;
12
+ //#endregion
13
+ export { blob, bytes, formData, json, request, text };
package/dist/fetch.mjs ADDED
@@ -0,0 +1,22 @@
1
+ import { catchSome, fail, fn } from "./mini-effect.mjs";
2
+ import { failure } from "./tag.mjs";
3
+
4
+ //#region src/fetch.ts
5
+ const FailedToFetchError = failure("FailedToFetch");
6
+ const FailedToReadError = failure("FailedToRead");
7
+ const request = (input, init) => {
8
+ return fn((signal) => fetch(input, {
9
+ signal,
10
+ ...init
11
+ })).pipe(catchSome((cause) => fail(new FailedToFetchError({ cause }))));
12
+ };
13
+ const read = (method, response) => fn(() => response[method]()).pipe(catchSome((cause) => fail(new FailedToReadError({ cause }))));
14
+ new Response().blob;
15
+ const blob = (response) => read("blob", response);
16
+ const bytes = (response) => read("bytes", response);
17
+ const formData = (response) => read("formData", response);
18
+ const json = (response) => read("json", response);
19
+ const text = (response) => read("text", response);
20
+
21
+ //#endregion
22
+ export { blob, bytes, formData, json, request, text };
@@ -1,37 +1,41 @@
1
1
  //#region src/mini-effect.d.ts
2
- declare const EXCLUDE: unique symbol;
2
+ declare const SYMBOL: SymbolConstructor;
3
+ declare const REMOVE: unique symbol;
4
+ declare const RUN: unique symbol;
5
+ declare const ERROR: unique symbol;
3
6
  declare const run: <T, E$1>(effect: Effect<T, E$1>, signal?: AbortSignal) => Promise<T>;
4
7
  declare const fn: <R$1, C = never>(thunk: Thunk<R$1>) => Effect<R$1, C>;
5
8
  declare const fail: <C>(cause: C) => Effect<never, C>;
6
9
  declare const succeed: <R$1>(result: R$1) => Effect<R$1, never>;
7
- declare const catchSome: <R$1 = any, E$1 = any, EE = never>(thunk: (cause: unknown) => Effect<R$1, E$1> | undefined) => WrapEffect<R$1, E$1, EE>;
8
- declare const catchClass: <T extends {
9
- new (...args: any[]): any;
10
- }, E$1 extends Effect<any, any>>(type: T, thunk: (cause: T) => E$1) => Pipeable<any, inferReturn<E$1>, inferError<E$1>, T>;
10
+ declare const catchSome: <T = any, C = any, CE = any>(thunk: (cause: unknown) => Effect<T, C> | undefined) => WrapEffect<T, C, CE>;
11
11
  declare const gen: <T extends Effect<any, any>, TReturn, TNext>(thunk: (signal: AbortSignal) => Generator<T, TReturn, TNext>) => Effect<TReturn, inferError<T>>;
12
- declare const pipe: <F extends Effect<any, any>, P$1 extends [Pipeable<any, any, any>, ...Pipeable<any, any, any>[]]>(first: F, ...pipeable: PipeReturn<[F, ...P$1]> extends never ? never : P$1) => PipeReturn<[F, ...P$1]>;
12
+ declare const pipe: <F extends Effect, P$1 extends [Pipeable, ...Pipeable[]]>(first: F, ...pipeable: PipeReturn<[F, ...P$1]> extends never ? never : P$1) => PipeReturn<[F, ...P$1]>;
13
+ declare class Effect<T = any, C = any> {
14
+ [RUN]?: (signal: AbortSignal) => T | PromiseLike<T>;
15
+ [ERROR]?: (cause: unknown) => Effect | undefined;
16
+ constructor(run?: (signal: AbortSignal) => PromiseLike<T> | T, catchSome?: (cause: unknown) => Effect | undefined);
17
+ [SYMBOL.iterator](): Generator<Effect<T, C>, T, any>;
18
+ readonly pipe: <P$1 extends [Pipeable, ...Pipeable[]]>(...pipeable: P$1) => PipeReturn<[Effect<T, C>, ...P$1]>;
19
+ }
20
+ declare class WrapEffect<T = any, C = any, CE = any> {
21
+ readonly [RUN]?: (signal: AbortSignal) => PromiseLike<T> | T;
22
+ readonly [ERROR]?: (cause: unknown) => Effect<any, any> | undefined;
23
+ readonly [REMOVE]: CE;
24
+ constructor(run?: (signal: AbortSignal) => PromiseLike<T> | T, catchSome?: (cause: unknown) => Effect | undefined);
25
+ [SYMBOL.iterator](): Generator<Effect<T, C>, T, any>;
26
+ }
13
27
  type Thunk<out R$1> = (signal: AbortSignal) => R$1 | PromiseLike<R$1>;
14
- type PipeableThunk<in I = any, out T = any, out E$1 = any> = (next: I) => Effect<T, E$1> | WrapEffect<T, E$1, any>;
15
- type Pipeable<I = any, T = any, E$1 = any, EE = any> = PipeableThunk<I, T, E$1> | Effect<T, E$1> | WrapEffect<T, E$1, EE>;
16
- type Effect<out T, out C> = {
17
- [RUN]?(signal: AbortSignal): PromiseLike<T> | T;
18
- [WRAP]?(cause: unknown): Effect<T, C> | undefined;
19
- [Symbol.iterator](): Generator<Effect<T, C>, T, any>;
20
- pipe<P$1 extends [Pipeable<any, any, any>, ...Pipeable<any, any, any>[]]>(...pipeable: P$1): PipeReturn<[Effect<T, C>, ...P$1]>;
21
- };
22
- type WrapEffect<T, C, EE> = {
23
- [WRAP]?(cause: unknown): Effect<T, C> | undefined;
24
- [EXCLUDE]?: EE;
25
- };
26
- type inferInput<T> = T extends PipeableThunk<infer I, any, any> ? I : never;
27
- type inferReturn<T> = T extends Pipeable<any, infer R, any> ? R : T extends Effect<infer R, any> ? R : never;
28
- type inferError<T> = T extends Pipeable<any, any, infer E> ? E : T extends Effect<any, infer E> ? E : never;
29
- type mergePipeable<A extends Pipeable, B extends Pipeable> = A extends Pipeable<any, infer A2, infer A3> ? B extends Pipeable<any, infer B2, infer B3, infer B4> ? Pipeable<any, A2 | B2, Exclude<A3 | B3, B4>, never> : A : B;
30
- type asEffect<P$1> = P$1 extends Pipeable<any, infer R, infer E> ? Effect<R, E> : never;
31
- type PipeReturn<F extends Pipeable[]> = asEffect<mergePipeable<CheckPipe<F, F[0]>, Effect<never, inferError<F[number]>>>>;
32
- type CheckPipe<F extends Pipeable[], L extends Pipeable> = F extends [Pipeable, WrapEffect<any, any, any>, ...infer R] ? R extends Pipeable[] ? CheckPipe<[mergePipeable<L, mergePipeable<F[0], F[1]>>, ...R], mergePipeable<L, mergePipeable<F[0], F[1]>>> : never : F extends [Pipeable, Pipeable, ...infer P] ? ExtendsStrict<inferReturn<F[0]>, inferInput<F[1]>> extends true ? P extends Pipeable[] ? CheckPipe<[F[1], ...P], F[1]> : never : never : F extends [WrapEffect<any, any, any>] ? mergePipeable<F[0], L> : F extends [Pipeable<any, infer R, infer E>] ? Effect<R, E> : never;
28
+ type PipeableThunk<T = any, C = any, I = any> = (next: I) => Effect<T, C>;
29
+ type Pipeable = PipeableThunk | Effect | WrapEffect;
30
+ type inferInput<T> = T extends PipeableThunk<any, any, infer I> ? I : never;
31
+ type inferReturn<T> = T extends WrapEffect<infer R> ? R : T extends Effect<infer R> ? R : T extends PipeableThunk<infer R> ? R : never;
32
+ type inferError<T> = T extends PipeableThunk<any, infer E> ? E : T extends WrapEffect<any, infer E> ? E : T extends Effect<any, infer E> ? E : never;
33
+ type inferExclude<T> = T extends WrapEffect<any, any, infer CE> ? CE : never;
34
+ type Simplify<T> = T extends Effect<infer R, infer E> ? Effect<R, E> : T;
35
+ type PipeReturn<F extends Pipeable[]> = IsNever<CheckPipe<F>> extends true ? never : Simplify<Effect<inferReturn<CheckPipe<F>> | inferReturn<Extract<F[number], WrapEffect<any, any, any>>>, Exclude<inferError<CheckPipe<F>>, inferExclude<F[number]>>>>;
36
+ type CheckPipe<F extends Pipeable[]> = F extends [Effect, WrapEffect, ...infer R] ? R extends [Pipeable, ...Pipeable[]] ? CheckPipe<R> : F[0] : F extends [Pipeable, Pipeable, ...infer P] ? ExtendsStrict<inferReturn<F[0]>, inferInput<F[1]>> extends true ? P extends Pipeable[] ? CheckPipe<[F[1], ...P]> : never : never : F extends [WrapEffect] ? Effect<inferReturn<F[0]>> : F extends [Pipeable] ? F[0] : never;
33
37
  type IsNever<T> = [T] extends [never] ? true : false;
34
38
  type IsAny<T> = 0 extends 1 & NoInfer<T> ? true : false;
35
39
  type ExtendsStrict<Left, Right> = IsAny<Left | Right> extends true ? true : IsNever<Left> extends true ? IsNever<Right> : [Left] extends [Right] ? true : false;
36
40
  //#endregion
37
- export { Effect, IsAny, Pipeable, PipeableThunk, Thunk, WrapEffect, run as _run, run, catchClass, catchSome, fail, fn, gen, inferError, inferInput, inferReturn, pipe, succeed };
41
+ export { Effect, Pipeable, PipeableThunk, Thunk, WrapEffect, run as _run, run, catchSome, fail, fn, gen, inferError, inferExclude, inferInput, inferReturn, pipe, succeed };
@@ -1,7 +1,8 @@
1
1
  //#region src/mini-effect.ts
2
2
  const SYMBOL = Symbol;
3
+ const REMOVE = /* @__PURE__ */ SYMBOL();
3
4
  const RUN = SYMBOL();
4
- const WRAP = SYMBOL();
5
+ const ERROR = SYMBOL();
5
6
  const DEFAULT_SIGNAL = new AbortController().signal;
6
7
  const isFunction = (value) => typeof value === "function";
7
8
  const quickYieldResult = (iterator, signal, ret) => {
@@ -9,15 +10,14 @@ const quickYieldResult = (iterator, signal, ret) => {
9
10
  return ret.done ? ret.value : run(ret.value, signal).then((r) => quickYieldResult(iterator, signal, r));
10
11
  };
11
12
  const run = (effect, signal) => new Promise((resolve) => resolve(effect[RUN](signal ?? DEFAULT_SIGNAL)));
12
- const fn = (thunk) => new EffectImpl(thunk);
13
+ const fn = (thunk) => new Effect(thunk);
13
14
  const fail = (cause) => fn(() => {
14
15
  throw cause;
15
16
  });
16
17
  const succeed = (result) => fn(() => result);
17
- const catchSome = (thunk) => new EffectImpl(void 0, thunk);
18
- const catchClass = (type, thunk) => catchSome((cause) => cause instanceof type ? thunk(cause) : void 0);
18
+ const catchSome = (thunk) => new WrapEffect(void 0, thunk);
19
19
  const gen = (thunk) => fn((signal) => quickYieldResult(thunk(signal), signal));
20
- const wrapEffect = (pipe$1, onErr) => new EffectImpl(pipe$1[RUN], (cause) => pipe$1[WRAP]?.(cause) ?? onErr(cause));
20
+ const wrapEffect = (pipe$1, onErr) => new WrapEffect(pipe$1[RUN], (cause) => pipe$1[ERROR]?.(cause) ?? onErr(cause));
21
21
  const pipe = (first, ...pipeable) => {
22
22
  let run$1 = [];
23
23
  let wrap = [];
@@ -32,7 +32,7 @@ const pipe = (first, ...pipeable) => {
32
32
  };
33
33
  let handler, wrappedFirst = wrapEffect(first, handlePipeError);
34
34
  for (pipe$1 of pipeable) {
35
- handler = pipe$1[WRAP];
35
+ handler = pipe$1[ERROR];
36
36
  if (isFunction(handler)) wrap.push(handler);
37
37
  if (isFunction(pipe$1) || isFunction(pipe$1[RUN])) run$1.push(pipe$1);
38
38
  }
@@ -45,9 +45,9 @@ const pipe = (first, ...pipeable) => {
45
45
  return next;
46
46
  });
47
47
  };
48
- var EffectImpl = class {
48
+ var Effect = class {
49
49
  [RUN];
50
- [WRAP];
50
+ [ERROR];
51
51
  constructor(run$1, catchSome$1) {
52
52
  this[RUN] = catchSome$1 && run$1 ? (signal) => {
53
53
  let res;
@@ -63,13 +63,38 @@ var EffectImpl = class {
63
63
  return handleError(cause);
64
64
  }
65
65
  } : run$1;
66
- this[WRAP] = catchSome$1;
66
+ this[ERROR] = catchSome$1;
67
67
  }
68
68
  *[SYMBOL.iterator]() {
69
69
  return yield this;
70
70
  }
71
71
  pipe = (...pipeable) => pipe(this, ...pipeable);
72
72
  };
73
+ var WrapEffect = class {
74
+ [RUN];
75
+ [ERROR];
76
+ [REMOVE];
77
+ constructor(run$1, catchSome$1) {
78
+ this[RUN] = catchSome$1 && run$1 ? (signal) => {
79
+ let res;
80
+ let handleError = (cause) => {
81
+ const recovery = catchSome$1(cause);
82
+ if (recovery) return recovery[RUN]?.(signal);
83
+ throw cause;
84
+ };
85
+ try {
86
+ res = run$1(signal);
87
+ return isFunction(res?.then) ? res.then(void 0, handleError) : res;
88
+ } catch (cause) {
89
+ return handleError(cause);
90
+ }
91
+ } : run$1;
92
+ this[ERROR] = catchSome$1;
93
+ }
94
+ *[SYMBOL.iterator]() {
95
+ return yield this;
96
+ }
97
+ };
73
98
 
74
99
  //#endregion
75
- export { run as _run, run, catchClass, catchSome, fail, fn, gen, pipe, succeed };
100
+ export { Effect, WrapEffect, run as _run, run, catchSome, fail, fn, gen, pipe, succeed };
@@ -0,0 +1,9 @@
1
+ import { Effect } from "./mini-effect.mjs";
2
+ import * as v from "valibot";
3
+ export * from "valibot";
4
+
5
+ //#region src/schema.d.ts
6
+ type Schema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>> | v.BaseSchemaAsync<unknown, unknown, v.BaseIssue<unknown>>;
7
+ declare const validate: <const S extends Schema>(schema: S) => (value: unknown) => Effect<v.InferOutput<S>, never>;
8
+ //#endregion
9
+ export { Schema, validate };
@@ -0,0 +1,12 @@
1
+ import { catchSome, fail, fn } from "./mini-effect.mjs";
2
+ import { failure } from "./tag.mjs";
3
+ import * as v from "valibot";
4
+
5
+ export * from "valibot"
6
+
7
+ //#region src/schema.ts
8
+ const FaildToValidateError = failure("FailedToValidate");
9
+ const validate = (schema) => (value) => fn(() => v.parseAsync(schema, value)).pipe(catchSome((cause) => fail(new FaildToValidateError({ cause }))));
10
+
11
+ //#endregion
12
+ export { validate };
package/dist/tag.d.mts ADDED
@@ -0,0 +1,14 @@
1
+ import { Effect, WrapEffect, inferError, inferReturn } from "./mini-effect.mjs";
2
+
3
+ //#region src/tag.d.ts
4
+ type Failure<T extends string> = {
5
+ __tag: T;
6
+ cause?: unknown;
7
+ };
8
+ type FailureConstructor<T extends string> = {
9
+ new (options?: ErrorOptions): Failure<T>;
10
+ };
11
+ declare const failure: <T extends string>(tag: T) => FailureConstructor<T>;
12
+ declare const catchTags: <CE extends string, E extends Effect>(handlers: Record<CE, (cause: unknown) => E | undefined>) => WrapEffect<inferReturn<E>, inferError<E>, Failure<CE>>;
13
+ //#endregion
14
+ export { Failure, FailureConstructor, catchTags, failure };
package/dist/tag.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import { catchSome } from "./mini-effect.mjs";
2
+
3
+ //#region src/tag.ts
4
+ const failure = (tag) => class Failure extends Error {
5
+ __tag = tag;
6
+ constructor(options) {
7
+ super(tag, options);
8
+ }
9
+ };
10
+ const catchTags = (handlers) => catchSome((cause) => {
11
+ let tag = cause?.__tag;
12
+ return typeof tag === "string" ? Object.entries(handlers).find(([t, h]) => t === tag)?.[1]?.(cause) : void 0;
13
+ });
14
+
15
+ //#endregion
16
+ export { catchTags, failure };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mini-effect",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.1",
5
5
  "description": "A mini-effect library for TypeScript.",
6
6
  "author": "Jacob Ebey <jacob.ebey@live.com>",
7
7
  "license": "MIT",
@@ -15,7 +15,10 @@
15
15
  },
16
16
  "exports": {
17
17
  "./concurrency": "./dist/concurrency.mjs",
18
+ "./fetch": "./dist/fetch.mjs",
18
19
  "./mini-effect": "./dist/mini-effect.mjs",
20
+ "./schema": "./dist/schema.mjs",
21
+ "./tag": "./dist/tag.mjs",
19
22
  "./package.json": "./package.json"
20
23
  },
21
24
  "main": "./dist/index.mjs",
@@ -33,7 +36,10 @@
33
36
  },
34
37
  "devDependencies": {
35
38
  "@types/node": "^25.2.3",
36
- "tsdown": "^0.18.4",
39
+ "tsdown": "~0.18.4",
37
40
  "typescript": "^5.9.3"
41
+ },
42
+ "dependencies": {
43
+ "valibot": "^1.2.0"
38
44
  }
39
45
  }