mini-effect 0.0.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 ADDED
@@ -0,0 +1,190 @@
1
+ # 🎯 mini-effect
2
+
3
+ > _Because sometimes you want Effect, but like... mini._
4
+
5
+ A delightfully tiny Effect system for TypeScript that packs a punch! ✨
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:
12
+
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!
19
+
20
+ All in a package so small it practically fits in a tweet. 🐦
21
+
22
+ ## 📦 Installation
23
+
24
+ ```bash
25
+ npm install mini-effect
26
+ # or
27
+ pnpm add mini-effect
28
+ # or
29
+ yarn add mini-effect
30
+ ```
31
+
32
+ ## 🚀 Quick Start
33
+
34
+ ```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);
48
+ ```
49
+
50
+ ## 🎮 The Fun Parts
51
+
52
+ ### Generators (a.k.a. "async/await but cooler")
53
+
54
+ ```typescript
55
+ const program = gen(function* (signal) {
56
+ 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!
61
+ });
62
+
63
+ await run(program); // 6
64
+ ```
65
+
66
+ ### Pipes (a.k.a. "look ma, no nesting!")
67
+
68
+ ```typescript
69
+ const result = await run(
70
+ pipe(
71
+ succeed(5),
72
+ (n: number) => succeed(n * 2), // 10
73
+ (n: number) => succeed(n + 1), // 11
74
+ (n: number) => succeed(`Result: ${n}`), // "Result: 11"
75
+ ),
76
+ );
77
+ ```
78
+
79
+ ### Error Handling (a.k.a. "try/catch grew up")
80
+
81
+ ```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(
91
+ fn(() => {
92
+ throw new NotFoundError(42);
93
+ }),
94
+ catchClass(NotFoundError, (err) => succeed(`Handled: ${err.id}`)),
95
+ );
96
+
97
+ await run(safeProgram); // "Handled: 42"
98
+ ```
99
+
100
+ ### Concurrency (a.k.a. "Promise.all's cooler cousin")
101
+
102
+ ```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
+ );
113
+
114
+ // First one wins! 🏆
115
+ const fastest = await run(
116
+ race([fn(() => fetchFromServer1()), fn(() => fetchFromServer2())]),
117
+ );
118
+
119
+ // First SUCCESS wins (failures don't count)
120
+ const firstSuccess = await run(any([fn(() => tryThis()), fn(() => tryThat())]));
121
+
122
+ // Get all results, even the failures 🤷
123
+ const allResults = await run(
124
+ allSettled([succeed(1), fail(new Error("oops")), succeed(3)]),
125
+ );
126
+ ```
127
+
128
+ ### Cancellation (a.k.a. "STOP THE PRESSES!")
129
+
130
+ ```typescript
131
+ const controller = new AbortController();
132
+
133
+ const longRunning = fn(async (signal) => {
134
+ // Check if we should bail
135
+ if (signal.aborted) {
136
+ throw new DOMException("Aborted!", "AbortError");
137
+ }
138
+
139
+ // Or pass it to fetch
140
+ const res = await fetch("/api/slow", { signal });
141
+ return res.json();
142
+ });
143
+
144
+ // Start it
145
+ const promise = run(longRunning, controller.signal);
146
+
147
+ // Actually, never mind! 🙅
148
+ controller.abort();
149
+ ```
150
+
151
+ ## 🗺️ API at a Glance
152
+
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 |
163
+
164
+ ### Concurrency
165
+
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 |
172
+
173
+ ## 🧠 Why Effects?
174
+
175
+ Effects are like promises, but:
176
+
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
181
+
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()`! 🍳
183
+
184
+ ## 📄 License
185
+
186
+ MIT © [Jacob Ebey](https://github.com/jacob-ebey)
187
+
188
+ ---
189
+
190
+ _Built with ☕ and a healthy disregard for bundle size anxiety._
@@ -0,0 +1,9 @@
1
+ import { Effect, inferError, inferReturn } from "./mini-effect.mjs";
2
+
3
+ //#region src/concurrency.d.ts
4
+ declare const all: <const E extends Effect<any, any>[]>(values: E) => Effect<{ [K in keyof E]: inferReturn<E[K]> }, inferError<E[number]>>;
5
+ declare const allSettled: <const E extends Effect<any, any>[]>(values: E) => Effect<{ [K in keyof E]: PromiseSettledResult<inferReturn<E[K]>> }, inferError<E[number]>>;
6
+ declare const any: <const E extends Effect<any, any>[]>(values: E) => Effect<inferReturn<E[number]>, inferError<E[number]>>;
7
+ declare const race: <const E extends Effect<any, any>[]>(values: E) => Effect<inferReturn<E[number]>, inferError<E[number]>>;
8
+ //#endregion
9
+ export { all, allSettled, any, race };
@@ -0,0 +1,24 @@
1
+ import { _run as run, fn } from "./mini-effect.mjs";
2
+
3
+ //#region src/concurrency.ts
4
+ const localController = (signal) => {
5
+ let controller = new AbortController();
6
+ signal.addEventListener("abort", () => controller.abort(signal.reason));
7
+ return controller;
8
+ };
9
+ const promiseMethod = (method, values) => fn(async (signal) => {
10
+ let controller = localController(signal);
11
+ let localSignal = controller.signal;
12
+ try {
13
+ return await Promise[method](values.map((effect) => run(effect, localSignal)));
14
+ } finally {
15
+ if (!controller.signal.aborted) controller.abort();
16
+ }
17
+ });
18
+ const all = (values) => promiseMethod("all", values);
19
+ const allSettled = (values) => promiseMethod("allSettled", values);
20
+ const any = (values) => promiseMethod("any", values);
21
+ const race = (values) => promiseMethod("race", values);
22
+
23
+ //#endregion
24
+ export { all, allSettled, any, race };
@@ -0,0 +1,37 @@
1
+ //#region src/mini-effect.d.ts
2
+ declare const EXCLUDE: unique symbol;
3
+ declare const run: <T, E$1>(effect: Effect<T, E$1>, signal?: AbortSignal) => Promise<T>;
4
+ declare const fn: <R$1, C = never>(thunk: Thunk<R$1>) => Effect<R$1, C>;
5
+ declare const fail: <C>(cause: C) => Effect<never, C>;
6
+ 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>;
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]>;
13
+ 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;
33
+ type IsNever<T> = [T] extends [never] ? true : false;
34
+ type IsAny<T> = 0 extends 1 & NoInfer<T> ? true : false;
35
+ type ExtendsStrict<Left, Right> = IsAny<Left | Right> extends true ? true : IsNever<Left> extends true ? IsNever<Right> : [Left] extends [Right] ? true : false;
36
+ //#endregion
37
+ export { Effect, IsAny, Pipeable, PipeableThunk, Thunk, WrapEffect, run as _run, run, catchClass, catchSome, fail, fn, gen, inferError, inferInput, inferReturn, pipe, succeed };
@@ -0,0 +1,75 @@
1
+ //#region src/mini-effect.ts
2
+ const SYMBOL = Symbol;
3
+ const RUN = SYMBOL();
4
+ const WRAP = SYMBOL();
5
+ const DEFAULT_SIGNAL = new AbortController().signal;
6
+ const isFunction = (value) => typeof value === "function";
7
+ const quickYieldResult = (iterator, signal, ret) => {
8
+ ret = iterator.next(ret);
9
+ return ret.done ? ret.value : run(ret.value, signal).then((r) => quickYieldResult(iterator, signal, r));
10
+ };
11
+ const run = (effect, signal) => new Promise((resolve) => resolve(effect[RUN](signal ?? DEFAULT_SIGNAL)));
12
+ const fn = (thunk) => new EffectImpl(thunk);
13
+ const fail = (cause) => fn(() => {
14
+ throw cause;
15
+ });
16
+ 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);
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));
21
+ const pipe = (first, ...pipeable) => {
22
+ let run$1 = [];
23
+ let wrap = [];
24
+ let pipe$1;
25
+ let effect;
26
+ let next;
27
+ let handlePipeError = (cause) => {
28
+ for (pipe$1 of wrap) {
29
+ effect = pipe$1(cause);
30
+ if (effect) return effect;
31
+ }
32
+ };
33
+ let handler, wrappedFirst = wrapEffect(first, handlePipeError);
34
+ for (pipe$1 of pipeable) {
35
+ handler = pipe$1[WRAP];
36
+ if (isFunction(handler)) wrap.push(handler);
37
+ if (isFunction(pipe$1) || isFunction(pipe$1[RUN])) run$1.push(pipe$1);
38
+ }
39
+ return gen(function* () {
40
+ next = yield* wrappedFirst;
41
+ for (pipe$1 of run$1) {
42
+ if (isFunction(pipe$1)) pipe$1 = pipe$1(next);
43
+ if (isFunction(pipe$1[RUN])) next = yield* wrapEffect(pipe$1, handlePipeError);
44
+ }
45
+ return next;
46
+ });
47
+ };
48
+ var EffectImpl = class {
49
+ [RUN];
50
+ [WRAP];
51
+ constructor(run$1, catchSome$1) {
52
+ this[RUN] = catchSome$1 && run$1 ? (signal) => {
53
+ let res;
54
+ let handleError = (cause) => {
55
+ const recovery = catchSome$1(cause);
56
+ if (recovery) return recovery[RUN]?.(signal);
57
+ throw cause;
58
+ };
59
+ try {
60
+ res = run$1(signal);
61
+ return isFunction(res?.then) ? res.then(void 0, handleError) : res;
62
+ } catch (cause) {
63
+ return handleError(cause);
64
+ }
65
+ } : run$1;
66
+ this[WRAP] = catchSome$1;
67
+ }
68
+ *[SYMBOL.iterator]() {
69
+ return yield this;
70
+ }
71
+ pipe = (...pipeable) => pipe(this, ...pipeable);
72
+ };
73
+
74
+ //#endregion
75
+ export { run as _run, run, catchClass, catchSome, fail, fn, gen, pipe, succeed };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "mini-effect",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "description": "A mini-effect library for TypeScript.",
6
+ "author": "Jacob Ebey <jacob.ebey@live.com>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/jacob-ebey/mini-effect#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/jacob-ebey/mini-effect.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/jacob-ebey/mini-effect/issues"
15
+ },
16
+ "exports": {
17
+ "./concurrency": "./dist/concurrency.mjs",
18
+ "./mini-effect": "./dist/mini-effect.mjs",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "main": "./dist/index.mjs",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.mts",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsdown",
29
+ "dev": "tsdown --watch",
30
+ "test": "node --test",
31
+ "typecheck": "tsc --noEmit",
32
+ "prepublishOnly": "pnpm run build"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.2.3",
36
+ "tsdown": "^0.18.4",
37
+ "typescript": "^5.9.3"
38
+ }
39
+ }