pure-effect 0.4.0 → 0.5.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.
Binary file
package/CLAUDE.md ADDED
@@ -0,0 +1,59 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm test # run all tests
9
+ npx mocha test/all.js --grep "pattern" # run a single test by name
10
+ ```
11
+
12
+ No build or lint step — the library ships as plain ES modules with no transpilation.
13
+
14
+ ## Architecture
15
+
16
+ **pure-effect** is a zero-dependency effect system for JavaScript implementing the "Functional Core, Imperative Shell" pattern. Business logic returns plain data structures instead of executing side effects, enabling testing without mocks.
17
+
18
+ ### Core abstractions (all in `index.js`)
19
+
20
+ | Export | Shape | Purpose |
21
+ |--------|-------|---------|
22
+ | `Success(value)` | `{ type: 'Success', value }` | Wraps a successful result |
23
+ | `Failure(error, initialInput)` | `{ type: 'Failure', error, initialInput }` | Short-circuits the pipeline |
24
+ | `Command(cmdFn, nextFn, meta)` | `{ type: 'Command', cmd, next, meta }` | Defers a side effect for the interpreter |
25
+ | `effectPipe(...fns)` | — | Composes functions into a sequential pipeline via `chain` |
26
+ | `runEffect(effect, context)` | async | Interpreter: traverses the effect tree, executes Commands |
27
+ | `configureEffect(options)` | — | Injects telemetry hooks (`onStep`, `onRun`, `onBeforeCommand`) |
28
+
29
+ ### Data flow
30
+
31
+ ```
32
+ effectPipe(f1, f2, f3)(input)
33
+ → returns tree of Success / Failure / Command values
34
+
35
+ runEffect(tree, context)
36
+ → executes Commands async, passes results into next(), repeats
37
+ → resolves to final Success or Failure
38
+ ```
39
+
40
+ The `chain` combinator (internal) drives composition: `Success` passes its value to the next function, `Failure` short-circuits, `Command` defers execution. `runEffect` loops through the tree with a `while` loop rather than recursion.
41
+
42
+ `configureEffect` hooks:
43
+ - `onStep` — fires on every Command execution; wraps the `cmd` call (use for per-command tracing)
44
+ - `onRun` — fires once per `runEffect` call; wraps the whole workflow (use for top-level spans); receives `context.flowName` as the third argument
45
+ - `onBeforeCommand` — intercepts each Command before execution; receives the Command and the `context` passed to `runEffect`
46
+
47
+ ### TypeScript
48
+
49
+ Full generic type declarations are in `index.d.ts` and referenced via the `types` field in `package.json`. Type-level tests live in `test/types.test-d.ts` and run via `tsd` as part of `npm test`.
50
+
51
+ ### Observability
52
+
53
+ `opentelemetry-example.js` shows how to wire OpenTelemetry spans into `configureEffect`'s hooks — it is reference code, not part of the library.
54
+
55
+ ### Tests
56
+
57
+ `test/all.js` contains all runtime tests and uses a user-registration domain as the running example. Tests assert on the *returned data structures* (Commands, Failures) rather than on side effects, which is the core usage pattern to preserve.
58
+
59
+ `test/types.test-d.ts` contains type-level tests using `tsd`, verifying that generic type parameters flow correctly through `effectPipe` and `runEffect`.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  It implements the "Functional Core, Imperative Shell" pattern, allowing you to decouple your business logic from external side effects like database calls or API requests. Instead of executing side effects immediately, your functions return Commands which are executed later by an interpreter.
6
6
 
7
- **Pure Effect** comes with JSDoc type annotations, so it can be used with TypeScript as well.
7
+ **Pure Effect** ships with JSDoc type annotations for JavaScript users and a bundled `index.d.ts` declaration file with full generic types for TypeScript users.
8
8
 
9
9
  ## Installation
10
10
 
package/index.d.ts ADDED
@@ -0,0 +1,127 @@
1
+ export type SuccessState<T> = {
2
+ type: 'Success';
3
+ value: T;
4
+ initialInput?: unknown;
5
+ };
6
+
7
+ export type FailureState<E = unknown> = {
8
+ type: 'Failure';
9
+ error: E;
10
+ initialInput?: unknown;
11
+ };
12
+
13
+ export type CommandState<R, T, E = unknown> = {
14
+ type: 'Command';
15
+ cmd: () => Promise<R> | R;
16
+ next: (result: R) => Effect<T, E>;
17
+ meta?: unknown;
18
+ initialInput?: unknown;
19
+ };
20
+
21
+ export type Effect<T, E = unknown> =
22
+ | SuccessState<T>
23
+ | FailureState<E>
24
+ | CommandState<any, T, E>;
25
+
26
+ export declare function Success<T>(value: T): SuccessState<T>;
27
+
28
+ export declare function Failure<E = unknown>(
29
+ error: E,
30
+ initialInput?: unknown
31
+ ): FailureState<E>;
32
+
33
+ export declare function Command<R, T, E = unknown>(
34
+ cmd: () => Promise<R> | R,
35
+ next: (result: R) => Effect<T, E>,
36
+ meta?: unknown
37
+ ): CommandState<R, T, E>;
38
+
39
+ export declare function effectPipe<A, B, E = unknown>(
40
+ f1: (a: A) => Effect<B, E>
41
+ ): (start: A) => Effect<B, E>;
42
+
43
+ export declare function effectPipe<A, B, C, E = unknown>(
44
+ f1: (a: A) => Effect<B, E>,
45
+ f2: (b: B) => Effect<C, E>
46
+ ): (start: A) => Effect<C, E>;
47
+
48
+ export declare function effectPipe<A, B, C, D, E = unknown>(
49
+ f1: (a: A) => Effect<B, E>,
50
+ f2: (b: B) => Effect<C, E>,
51
+ f3: (c: C) => Effect<D, E>
52
+ ): (start: A) => Effect<D, E>;
53
+
54
+ export declare function effectPipe<A, B, C, D, F, E = unknown>(
55
+ f1: (a: A) => Effect<B, E>,
56
+ f2: (b: B) => Effect<C, E>,
57
+ f3: (c: C) => Effect<D, E>,
58
+ f4: (d: D) => Effect<F, E>
59
+ ): (start: A) => Effect<F, E>;
60
+
61
+ export declare function effectPipe<A, B, C, D, F, G, E = unknown>(
62
+ f1: (a: A) => Effect<B, E>,
63
+ f2: (b: B) => Effect<C, E>,
64
+ f3: (c: C) => Effect<D, E>,
65
+ f4: (d: D) => Effect<F, E>,
66
+ f5: (f: F) => Effect<G, E>
67
+ ): (start: A) => Effect<G, E>;
68
+
69
+ export declare function effectPipe<A, B, C, D, F, G, H, E = unknown>(
70
+ f1: (a: A) => Effect<B, E>,
71
+ f2: (b: B) => Effect<C, E>,
72
+ f3: (c: C) => Effect<D, E>,
73
+ f4: (d: D) => Effect<F, E>,
74
+ f5: (f: F) => Effect<G, E>,
75
+ f6: (g: G) => Effect<H, E>
76
+ ): (start: A) => Effect<H, E>;
77
+
78
+ export declare function effectPipe<A, B, C, D, F, G, H, I, E = unknown>(
79
+ f1: (a: A) => Effect<B, E>,
80
+ f2: (b: B) => Effect<C, E>,
81
+ f3: (c: C) => Effect<D, E>,
82
+ f4: (d: D) => Effect<F, E>,
83
+ f5: (f: F) => Effect<G, E>,
84
+ f6: (g: G) => Effect<H, E>,
85
+ f7: (h: H) => Effect<I, E>
86
+ ): (start: A) => Effect<I, E>;
87
+
88
+ export declare function effectPipe<A, B, C, D, F, G, H, I, J, E = unknown>(
89
+ f1: (a: A) => Effect<B, E>,
90
+ f2: (b: B) => Effect<C, E>,
91
+ f3: (c: C) => Effect<D, E>,
92
+ f4: (d: D) => Effect<F, E>,
93
+ f5: (f: F) => Effect<G, E>,
94
+ f6: (g: G) => Effect<H, E>,
95
+ f7: (h: H) => Effect<I, E>,
96
+ f8: (i: I) => Effect<J, E>
97
+ ): (start: A) => Effect<J, E>;
98
+
99
+ export declare function runEffect<T, E = unknown>(
100
+ effect: Effect<T, E>,
101
+ context?: unknown
102
+ ): Promise<SuccessState<T> | FailureState<E>>;
103
+
104
+ export type StepRunner = (
105
+ name: string,
106
+ type: string,
107
+ op: () => Promise<unknown>
108
+ ) => Promise<unknown>;
109
+
110
+ export type RunWrapper = (
111
+ effect: Effect<unknown>,
112
+ op: () => Promise<SuccessState<unknown> | FailureState<unknown>>,
113
+ flowName?: string
114
+ ) => Promise<SuccessState<unknown> | FailureState<unknown>>;
115
+
116
+ export type CommandInterceptor = (
117
+ command: CommandState<unknown, unknown>,
118
+ context?: any
119
+ ) => Promise<void>;
120
+
121
+ export interface EffectConfiguration {
122
+ onStep?: StepRunner;
123
+ onRun?: RunWrapper;
124
+ onBeforeCommand?: CommandInterceptor;
125
+ }
126
+
127
+ export declare function configureEffect(options: EffectConfiguration): void;
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "pure-effect",
3
- "version": "0.4.0",
4
- "description": "A tiny, zero-dependency effect system for writing pure, testable JavaScript without mocks.",
3
+ "version": "0.5.0",
4
+ "description": "A tiny, zero-dependency effect system for writing pure, testable JavaScript/TypeScript without mocks.",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
7
+ "types": "./index.d.ts",
7
8
  "scripts": {
8
- "test": "mocha"
9
+ "test": "mocha && tsd"
10
+ },
11
+ "tsd": {
12
+ "directory": "test"
9
13
  },
10
14
  "author": "Aycan Gulez",
11
15
  "homepage": "https://github.com/aycangulez/pure-effect",
@@ -14,6 +18,7 @@
14
18
  "@opentelemetry/sdk-node": "^0.211.0",
15
19
  "@types/mocha": "^10.0.10",
16
20
  "@types/node": "^24.10.1",
17
- "mocha": "^11.7.5"
21
+ "mocha": "^11.7.5",
22
+ "tsd": "^0.33.0"
18
23
  }
19
24
  }
@@ -0,0 +1,53 @@
1
+ import { expectType, expectError } from 'tsd';
2
+ import { Success, Failure, Command, effectPipe, runEffect } from '../index.js';
3
+ import type { SuccessState, FailureState, CommandState, Effect } from '../index.js';
4
+
5
+ interface User { email: string; password: string; }
6
+ interface SavedUser { id: number; email: string; }
7
+
8
+ // --- Success ---
9
+
10
+ const s = Success(42);
11
+ expectType<SuccessState<number>>(s);
12
+ expectError(Success()); // missing argument
13
+
14
+ // --- Failure ---
15
+
16
+ const f = Failure('oops');
17
+ expectType<FailureState<string>>(f);
18
+
19
+ // --- Command ---
20
+
21
+ const cmd = Command(
22
+ async () => ({ id: 1, email: 'a@b.com' } as SavedUser),
23
+ (saved) => { expectType<SavedUser>(saved); return Success(saved); }
24
+ );
25
+ expectType<CommandState<SavedUser, SavedUser, unknown>>(cmd);
26
+
27
+ // --- effectPipe type propagation ---
28
+
29
+ const step1 = (input: User) => Success(input);
30
+ const step2 = (user: User) => Command(async () => ({ id: 1, ...user } as SavedUser), (s) => Success(s));
31
+
32
+ const flow = effectPipe(step1, step2);
33
+ expectType<Effect<SavedUser>>(flow({ email: 'a@b.com', password: 'secret123' }));
34
+ expectError(flow({ email: 'a@b.com' })); // missing password
35
+
36
+ // --- runEffect return type ---
37
+
38
+ const result = await runEffect(flow({ email: 'a@b.com', password: 'secret123' }));
39
+ expectType<SuccessState<SavedUser> | FailureState<unknown>>(result);
40
+
41
+ // --- discriminated union narrowing ---
42
+
43
+ if (result.type === 'Success') {
44
+ expectType<SavedUser>(result.value);
45
+ } else {
46
+ expectType<unknown>(result.error);
47
+ }
48
+
49
+ // --- Failure error type flows through runEffect ---
50
+
51
+ const failFlow = effectPipe((input: User): Effect<User, string> => Failure<string>('bad'));
52
+ const failResult = await runEffect(failFlow({ email: 'a@b.com', password: 'x' }));
53
+ expectType<SuccessState<User> | FailureState<string>>(failResult);