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.
- package/.dirac-symbol-index/data.db +0 -0
- package/CLAUDE.md +59 -0
- package/README.md +1 -1
- package/index.d.ts +127 -0
- package/package.json +9 -4
- package/test/types.test-d.ts +53 -0
|
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**
|
|
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
|
-
"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);
|