pure-effect 0.5.0 → 0.7.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/.claude/settings.local.json +12 -0
- package/.dirac-symbol-index/data.db +0 -0
- package/.prettierrc +11 -0
- package/CLAUDE.md +29 -16
- package/README.md +236 -66
- package/index.d.ts +190 -86
- package/index.js +138 -31
- package/opentelemetry-example.js +5 -5
- package/package.json +2 -2
- package/test/all.js +145 -8
- package/test/types.test-d.ts +88 -8
|
Binary file
|
package/.prettierrc
ADDED
package/CLAUDE.md
CHANGED
|
@@ -17,43 +17,56 @@ No build or lint step — the library ships as plain ES modules with no transpil
|
|
|
17
17
|
|
|
18
18
|
### Core abstractions (all in `index.js`)
|
|
19
19
|
|
|
20
|
-
| Export
|
|
21
|
-
|
|
22
|
-
| `Success(value)`
|
|
23
|
-
| `Failure(error, initialInput)`
|
|
24
|
-
| `Command(cmdFn, nextFn, meta)`
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
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
|
+
| `Ask(nextFn)` | `{ type: 'Ask', next }` | Reads the `context` passed to `runEffect`; passes it to `nextFn` |
|
|
26
|
+
| `Retry(effect, options)` | `{ type: 'Retry', effect, options, next }` | Wraps any Effect tree with retry-on-failure semantics; handled natively by the interpreter |
|
|
27
|
+
| `effectPipe(...fns)` | — | Composes functions into a sequential pipeline via `chain` |
|
|
28
|
+
| `runEffect(effect, context, callConfig?)` | async | Interpreter: traverses the effect tree, executes Commands; resolves `Ask` and `Retry`; per-call `callConfig` overrides global defaults |
|
|
29
|
+
| `configureEffect(options)` | — | Sets process-wide telemetry hooks (`onStep`, `onRun`, `onBeforeCommand`) and global `retry` defaults; overridden by per-call `callConfig` |
|
|
28
30
|
|
|
29
31
|
### Data flow
|
|
30
32
|
|
|
31
33
|
```
|
|
32
34
|
effectPipe(f1, f2, f3)(input)
|
|
33
|
-
→ returns tree of Success / Failure / Command values
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
→ returns tree of Success / Failure / Command / Ask / Retry values
|
|
36
|
+
→ f1 runs eagerly here; initialInput is threaded through chain so every
|
|
37
|
+
node in the tree carries it without post-construction mutation
|
|
38
|
+
|
|
39
|
+
runEffect(tree, context, callConfig?)
|
|
40
|
+
→ per-call callConfig merges over global configureEffect defaults
|
|
41
|
+
→ executes Commands async, passes results into next(result), repeats
|
|
42
|
+
→ resolves Ask by calling next(context), continues
|
|
43
|
+
→ resolves Retry via an inner execute() loop (not recursive runEffect),
|
|
44
|
+
so onRun fires exactly once per runEffect call regardless of attempts;
|
|
45
|
+
on exhaustion returns Failure({ retryExhausted: true, lastError, attempts })
|
|
37
46
|
→ resolves to final Success or Failure
|
|
38
47
|
```
|
|
39
48
|
|
|
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.
|
|
49
|
+
The `chain` combinator (internal) drives composition: `Success` passes its value to the next function, `Failure` short-circuits, `Command` defers execution, `Ask` wraps its continuation so the chain propagates through it, `Retry` wraps its continuation via object spread (same pattern as `Ask`). `chain` accepts an optional `initialInput` parameter; `effectPipe` passes the pipeline's starting value through every `chain` call so that `initialInput` is stamped on all nodes — including mid-pipeline `Failure`s — without mutation. `runEffect` loops through the tree with a `while` loop rather than recursion.
|
|
50
|
+
|
|
51
|
+
`configureEffect` options (all overridable per-call via `runEffect`'s third argument):
|
|
41
52
|
|
|
42
|
-
`configureEffect` hooks:
|
|
43
53
|
- `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
|
|
54
|
+
- `onRun` — fires once per `runEffect` call; wraps the whole workflow (use for top-level spans); receives `context.flowName` as the third argument; does **not** fire again for Retry attempts
|
|
45
55
|
- `onBeforeCommand` — intercepts each Command before execution; receives the Command and the `context` passed to `runEffect`
|
|
56
|
+
- `retry` — global Retry defaults `{ attempts, delay, backoff }`; per-use options passed to `Retry(effect, options)` merge on top (defaults: `attempts: 3`, `delay: 100ms`, `backoff: 1` flat)
|
|
46
57
|
|
|
47
58
|
### TypeScript
|
|
48
59
|
|
|
49
60
|
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
61
|
|
|
62
|
+
`Effect<T, E, Ctx>` carries three type parameters: value type, error union, and context type. `Ctx` flows through `AskState`, `CommandState`, `RetryState`, and all `effectPipe` overloads. `Ask<T, E, Ctx>` types the context callback parameter, and `runEffect<T, E, Ctx>` enforces that the supplied context matches. `Ctx` defaults to `unknown` so existing code without context typing is unaffected.
|
|
63
|
+
|
|
51
64
|
### Observability
|
|
52
65
|
|
|
53
66
|
`opentelemetry-example.js` shows how to wire OpenTelemetry spans into `configureEffect`'s hooks — it is reference code, not part of the library.
|
|
54
67
|
|
|
55
68
|
### Tests
|
|
56
69
|
|
|
57
|
-
`test/all.js` contains all runtime tests and uses a user-registration domain as the running example. Tests assert on the
|
|
70
|
+
`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
71
|
|
|
59
72
|
`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
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# Pure Effect
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**Pure Effect**
|
|
3
|
+
[](https://www.npmjs.com/package/pure-effect)
|
|
4
|
+
[](https://bundlephobia.com/package/pure-effect)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
**Pure Effect** is a zero-dependency effect library for JavaScript and TypeScript with built-in support for dependency injection, retry, and OpenTelemetry where business logic is plain data you can test without mocks.
|
|
8
|
+
|
|
9
|
+
- No mocks needed to test async pipelines
|
|
10
|
+
- Inject context without touching function signatures
|
|
11
|
+
- Built-in retry with configurable delay and backoff
|
|
12
|
+
- OpenTelemetry-ready via lifecycle hooks
|
|
13
|
+
- Zero dependencies, under 1 KB minified and gzipped
|
|
14
|
+
- Works in JavaScript and TypeScript (full generics, bundled `.d.ts`)
|
|
15
|
+
- Five primitives: learn the whole API in an afternoon
|
|
16
|
+
|
|
17
|
+
## Table of Contents
|
|
18
|
+
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [Testing Without Mocks](#testing-without-mocks)
|
|
22
|
+
- [Passing Runtime Context](#passing-runtime-context)
|
|
23
|
+
- [Retrying Transient Failures](#retrying-transient-failures)
|
|
24
|
+
- [TypeScript: Typed Errors and Context](#typescript-typed-errors-and-context)
|
|
25
|
+
- [Why Pure Effect](#why-pure-effect)
|
|
26
|
+
- [API Reference](#api-reference)
|
|
8
27
|
|
|
9
28
|
## Installation
|
|
10
29
|
|
|
@@ -12,9 +31,9 @@ It implements the "Functional Core, Imperative Shell" pattern, allowing you to d
|
|
|
12
31
|
npm install pure-effect
|
|
13
32
|
```
|
|
14
33
|
|
|
15
|
-
##
|
|
34
|
+
## Quick Start
|
|
16
35
|
|
|
17
|
-
Here is a complete example of a
|
|
36
|
+
Here is a complete example of a user registration flow:
|
|
18
37
|
|
|
19
38
|
```js
|
|
20
39
|
import { Success, Failure, Command, effectPipe, runEffect } from 'pure-effect';
|
|
@@ -25,40 +44,33 @@ const validateRegistration = (input) => {
|
|
|
25
44
|
return Success(input);
|
|
26
45
|
};
|
|
27
46
|
|
|
28
|
-
//
|
|
29
|
-
// The 'next' function defines what happens with the result of the async call.
|
|
47
|
+
// Theese function return a Command object. They do NOT call the database.
|
|
30
48
|
const findUser = (email) => {
|
|
31
|
-
const cmdFindUser = () => db.findUser(email);
|
|
32
|
-
|
|
33
|
-
return Command(cmdFindUser, next);
|
|
49
|
+
const cmdFindUser = () => db.findUser(email);
|
|
50
|
+
return Command(cmdFindUser, (user) => Success(user));
|
|
34
51
|
};
|
|
35
52
|
|
|
36
53
|
const saveUser = (input) => {
|
|
37
54
|
const cmdSaveUser = () => db.saveUser(input);
|
|
38
|
-
|
|
39
|
-
return Command(cmdSaveUser, next);
|
|
55
|
+
return Command(cmdSaveUser, (saved) => Success(saved));
|
|
40
56
|
};
|
|
41
57
|
|
|
42
|
-
const ensureEmailAvailable = (user) =>
|
|
43
|
-
return user ? Failure('Email already in use.') : Success(true);
|
|
44
|
-
};
|
|
58
|
+
const ensureEmailAvailable = (user) => (user ? Failure('Email already in use.') : Success(true));
|
|
45
59
|
|
|
46
|
-
//
|
|
60
|
+
// effectPipe threads the output of each step into the next.
|
|
61
|
+
// When you need to use a captured variable instead of the piped value,
|
|
62
|
+
// wrap the call in an arrow function.
|
|
47
63
|
const registerUserFlow = (input) =>
|
|
48
64
|
effectPipe(
|
|
49
|
-
validateRegistration,
|
|
50
|
-
() => findUser(input.email),
|
|
51
|
-
ensureEmailAvailable,
|
|
52
|
-
() => saveUser(input)
|
|
65
|
+
validateRegistration, // input -> Success(input)
|
|
66
|
+
() => findUser(input.email), // ignores piped value, uses captured input
|
|
67
|
+
ensureEmailAvailable, // found user (or null) -> Success(true)
|
|
68
|
+
() => saveUser(input) // ignores piped value, uses captured input
|
|
53
69
|
)(input);
|
|
54
70
|
|
|
55
|
-
//
|
|
71
|
+
// Imperative shell: this is the only place side effects run
|
|
56
72
|
async function registerUser(input) {
|
|
57
|
-
|
|
58
|
-
const logic = registerUserFlow(input);
|
|
59
|
-
|
|
60
|
-
// runEffect performs the actual async work
|
|
61
|
-
const result = await runEffect(logic);
|
|
73
|
+
const result = await runEffect(registerUserFlow(input));
|
|
62
74
|
|
|
63
75
|
if (result.type === 'Success') {
|
|
64
76
|
console.log('User created:', result.value);
|
|
@@ -70,80 +82,238 @@ async function registerUser(input) {
|
|
|
70
82
|
|
|
71
83
|
## Testing Without Mocks
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
Because pipelines return plain objects, you can assert on _what the code intends to do_ without executing any of it. Using the registration flow defined in the previous section:
|
|
74
86
|
|
|
75
87
|
```js
|
|
76
|
-
// 1. Test
|
|
88
|
+
// 1. Test validation failure synchronously
|
|
77
89
|
const badInput = { email: 'bad-email', password: '123' };
|
|
78
90
|
const result = registerUserFlow(badInput);
|
|
79
91
|
|
|
80
|
-
assert.deepEqual(result, Failure('Invalid email
|
|
81
|
-
// ✅ Logic tested instantly, no async needed.
|
|
92
|
+
assert.deepEqual(result, Failure('Invalid email.', badInput));
|
|
82
93
|
|
|
83
|
-
// 2.
|
|
94
|
+
// 2. Walk the pipeline to verify intent
|
|
84
95
|
const goodInput = { email: 'test@test.com', password: 'password123' };
|
|
85
96
|
const step1 = registerUserFlow(goodInput);
|
|
86
97
|
|
|
87
|
-
//
|
|
98
|
+
// First thing the code does: look up the user
|
|
88
99
|
assert.equal(step1.type, 'Command');
|
|
89
100
|
assert.equal(step1.cmd.name, 'cmdFindUser');
|
|
90
101
|
|
|
91
|
-
//
|
|
102
|
+
// Next thing: save the user (simulate "user not found" result)
|
|
92
103
|
const step2 = step1.next(null);
|
|
93
104
|
assert.equal(step2.type, 'Command');
|
|
94
105
|
assert.equal(step2.cmd.name, 'cmdSaveUser');
|
|
95
|
-
//
|
|
106
|
+
// The full flow is verified. The database was never touched.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Passing Runtime Context
|
|
110
|
+
|
|
111
|
+
Some values come from the framework layer such as an authenticated tenant, a request trace ID, an environment config rather than from the data being processed. `Ask` lets a pipeline step read the `context` object passed to `runEffect` without threading it through every function signature.
|
|
112
|
+
|
|
113
|
+
In the example below, `ctx.tenant` is resolved from the JWT by the router. The domain layer never receives it as a parameter; it just asks for it when needed:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
import { Success, Failure, Command, Ask, effectPipe, runEffect } from 'pure-effect';
|
|
117
|
+
|
|
118
|
+
const findProduct = (productId) =>
|
|
119
|
+
Ask((ctx) => {
|
|
120
|
+
const cmdFindProduct = () => db[ctx.tenant].findProduct(productId);
|
|
121
|
+
return Command(cmdFindProduct, (product) => (product ? Success(product) : Failure('Product not found.')));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const reserveStock = (product) =>
|
|
125
|
+
Ask((ctx) => {
|
|
126
|
+
const cmdReserveStock = () => db[ctx.tenant].reserveStock(product.id);
|
|
127
|
+
return Command(cmdReserveStock, (reserved) => Success({ product, reserved }));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const checkoutFlow = (productId) => effectPipe(findProduct, ({ product }) => reserveStock(product))(productId);
|
|
131
|
+
|
|
132
|
+
app.post('/checkout', async (req, res) => {
|
|
133
|
+
const result = await runEffect(checkoutFlow(req.body.productId), {
|
|
134
|
+
tenant: req.tenant
|
|
135
|
+
});
|
|
136
|
+
res.json(result);
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Retrying Transient Failures
|
|
141
|
+
|
|
142
|
+
`Retry` wraps any Effect tree with retry-on-failure semantics. Like everything else in Pure Effect, the retry configuration is a plain object you can inspect and assert on without running anything.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { Success, Failure, Command, Retry, effectPipe, runEffect } from 'pure-effect';
|
|
146
|
+
|
|
147
|
+
const fetchWeather = (city) => {
|
|
148
|
+
const cmdFetchWeather = () =>
|
|
149
|
+
fetch(`https://example-weather-api.com/v1/current?city=${city}`).then((r) => r.json());
|
|
150
|
+
return Retry(
|
|
151
|
+
Command(cmdFetchWeather, (data) => (data.error ? Failure(data.error) : Success(data))),
|
|
152
|
+
{ attempts: 3, delay: 200, backoff: 2 } // 200ms, 400ms, 800ms
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Assert on the retry config without making any network calls
|
|
157
|
+
const weatherFn = fetchWeather('Tokyo');
|
|
158
|
+
assert.equal(weatherFn.type, 'Retry');
|
|
159
|
+
assert.equal(weatherFn.options.attempts, 3);
|
|
160
|
+
assert.equal(weatherFn.effect.type, 'Command');
|
|
96
161
|
```
|
|
97
162
|
|
|
163
|
+
When all attempts are exhausted, `runEffect` returns a structured `Failure`:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
{
|
|
167
|
+
retryExhausted: true,
|
|
168
|
+
lastError: <the last error>,
|
|
169
|
+
attempts: 3
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Global defaults can be set via `configureEffect` and overridden per-use:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
configureEffect({
|
|
177
|
+
retry: { attempts: 3, delay: 100, backoff: 1 } // flat delay by default
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Per-use options are merged on top of global defaults
|
|
181
|
+
Retry(effect, { delay: 500 }); // uses global attempts, custom delay
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## TypeScript: Typed Errors and Context
|
|
185
|
+
|
|
186
|
+
### Error union across pipeline steps
|
|
187
|
+
|
|
188
|
+
Each step in `effectPipe` carries its own error type. The compiler collects them into a union automatically, so the return type of `runEffect` tells you exactly which errors are possible with full exhaustive narrowing, no extra boilerplate.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { effectPipe, runEffect, Failure, Success, Command } from 'pure-effect';
|
|
192
|
+
import type { Effect } from 'pure-effect';
|
|
193
|
+
|
|
194
|
+
type ValidationError = 'invalid_email' | 'weak_password';
|
|
195
|
+
type ApiError = 'network_timeout' | 'rate_limited';
|
|
196
|
+
|
|
197
|
+
const validate = (input: { email: string }): Effect<{ email: string }, ValidationError> => {
|
|
198
|
+
if (!input.email.includes('@')) return Failure('invalid_email');
|
|
199
|
+
return Success(input);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const submit = (input: { email: string }): Effect<{ id: number }, ApiError> => {
|
|
203
|
+
const cmdSubmitUser = () =>
|
|
204
|
+
fetch('/api/users', { method: 'POST', body: JSON.stringify(input) }).then((r) => r.json());
|
|
205
|
+
return Command(cmdSubmitUser, (data) => Success(data));
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const flow = effectPipe(validate, submit);
|
|
209
|
+
const result = await runEffect(flow({ email: 'user@example.com' }));
|
|
210
|
+
// result: SuccessState<{ id: number }> | FailureState<ValidationError | ApiError>
|
|
211
|
+
|
|
212
|
+
if (result.type === 'Failure') {
|
|
213
|
+
result.error; // 'invalid_email' | 'weak_password' | 'network_timeout' | 'rate_limited'
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Typed context with `Ask`
|
|
218
|
+
|
|
219
|
+
`Effect<T, E, Ctx>` carries a third type parameter for the context object. TypeScript enforces that `runEffect` receives a matching value when you annotate `Ask` with a context type:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { Ask, Command, Success, Failure, effectPipe, runEffect } from 'pure-effect';
|
|
223
|
+
import type { Effect } from 'pure-effect';
|
|
224
|
+
|
|
225
|
+
type AppContext = { tenant: string; requestId: string };
|
|
226
|
+
|
|
227
|
+
const findProduct = (productId: string): Effect<Product, 'not_found', AppContext> =>
|
|
228
|
+
Ask<Product, 'not_found', AppContext>((ctx) => {
|
|
229
|
+
const cmdFindProduct = () => db[ctx.tenant].findProduct(productId);
|
|
230
|
+
return Command(cmdFindProduct, (product) => (product ? Success(product) : Failure('not_found')));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ctx is typed as AppContext, no cast needed
|
|
234
|
+
const result = await runEffect(findProduct('abc'), { tenant: 'acme', requestId: '123' });
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Why Pure Effect
|
|
238
|
+
|
|
239
|
+
**vs. Effect-TS:** Effect-TS is a full functional programming ecosystem with fibers, streaming, schema validation, dependency injection, and is probably the right choice if you need that breadth. It arguably comes with a steep learning curve though. Pure Effect targets a narrower scope (testable pipelines, context injection, retry) and can be learned in an afternoon.
|
|
240
|
+
|
|
241
|
+
**vs. fp-ts:** fp-ts applies category theory abstractions (functors, monads) to TypeScript. Pure Effect borrows only the concept of effects as data and expresses it without that vocabulary.
|
|
242
|
+
|
|
243
|
+
**vs. plain async/await with mocks:** Mocks can drift from real implementations silently. Pure Effect sidesteps the problem: business logic never executes I/O, so there is nothing to mock.
|
|
244
|
+
|
|
245
|
+
**When to use something else:** If your codebase has little async I/O or test isolation isn't a pain point, plain async/await is the simpler choice.
|
|
246
|
+
|
|
98
247
|
## API Reference
|
|
99
248
|
|
|
100
249
|
### `Success(value)`
|
|
101
250
|
|
|
102
|
-
Returns
|
|
251
|
+
Returns `{ type: 'Success', value }`. Represents a successful computation result.
|
|
103
252
|
|
|
104
|
-
### `Failure(error)`
|
|
253
|
+
### `Failure(error, initialInput?)`
|
|
105
254
|
|
|
106
|
-
Returns
|
|
255
|
+
Returns `{ type: 'Failure', error, initialInput }`. Stops the pipeline immediately and short-circuits any remaining steps.
|
|
107
256
|
|
|
108
|
-
### `Command(cmdFn, nextFn, meta)`
|
|
257
|
+
### `Command(cmdFn, nextFn, meta?)`
|
|
109
258
|
|
|
110
|
-
Returns
|
|
259
|
+
Returns `{ type: 'Command', cmd, next, meta }`.
|
|
111
260
|
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
261
|
+
- `cmdFn`: A function (sync or async) that performs the side effect.
|
|
262
|
+
- `nextFn`: Receives the result of `cmdFn` and returns the next Effect.
|
|
263
|
+
- `meta`: Optional metadata (available to `onBeforeCommand`).
|
|
115
264
|
|
|
116
|
-
### `
|
|
265
|
+
### `Ask(nextFn)`
|
|
266
|
+
|
|
267
|
+
Returns `{ type: 'Ask', next }`. Passes the `context` from `runEffect` into `nextFn`, which returns the next Effect. Works at any point in a pipeline.
|
|
117
268
|
|
|
118
|
-
|
|
269
|
+
```js
|
|
270
|
+
const findProduct = (productId) =>
|
|
271
|
+
Ask((ctx) => {
|
|
272
|
+
const cmdFindProduct = () => db[ctx.tenant].findProduct(productId);
|
|
273
|
+
return Command(cmdFindProduct, (product) => (product ? Success(product) : Failure('Product not found.')));
|
|
274
|
+
});
|
|
275
|
+
```
|
|
119
276
|
|
|
120
|
-
### `
|
|
277
|
+
### `Retry(effect, options?)`
|
|
121
278
|
|
|
122
|
-
|
|
279
|
+
Returns `{ type: 'Retry', effect, options, next }`. Wraps any Effect with retry-on-failure semantics.
|
|
123
280
|
|
|
124
|
-
|
|
281
|
+
- `effect`: Any Effect: a `Command`, an `effectPipe` result, or another `Retry`.
|
|
282
|
+
- `options.attempts`: Max retries, not counting the first try (default: `3`).
|
|
283
|
+
- `options.delay`: Ms before the first retry (default: `100`).
|
|
284
|
+
- `options.backoff`: Multiplier applied to delay on each attempt (default: `1`, flat).
|
|
125
285
|
|
|
126
|
-
|
|
286
|
+
On exhaustion, returns `Failure({ retryExhausted: true, lastError, attempts })`.
|
|
287
|
+
|
|
288
|
+
### `effectPipe(...functions)`
|
|
289
|
+
|
|
290
|
+
Composes functions into a sequential pipeline. Each function receives the unwrapped `Success` value from the previous step. A `Failure` from any step stops the pipeline immediately.
|
|
127
291
|
|
|
128
|
-
|
|
292
|
+
When a step needs to use a captured variable instead of the piped value, wrap it in an arrow function:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
const flow = (input) =>
|
|
296
|
+
effectPipe(
|
|
297
|
+
validate, // receives input
|
|
298
|
+
() => findUser(input.email), // ignores piped value, uses captured input
|
|
299
|
+
ensureAvailable,
|
|
300
|
+
() => saveUser(input) // ignores piped value, uses captured input
|
|
301
|
+
)(input);
|
|
302
|
+
```
|
|
129
303
|
|
|
130
|
-
|
|
304
|
+
### `runEffect(effect, context?, callConfig?)`
|
|
131
305
|
|
|
132
|
-
|
|
133
|
-
Fires once per `runEffect` call. It wraps the entire workflow execution.
|
|
306
|
+
The interpreter. Traverses the effect tree, executes Commands with `async/await`, resolves `Ask` with the supplied `context`, and returns the final `Success` or `Failure`.
|
|
134
307
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
308
|
+
- `context`: Optional object passed to `Ask` continuations and `onBeforeCommand`. `context.flowName` names the workflow in telemetry.
|
|
309
|
+
- `callConfig`: Per-call overrides for `onStep`, `onRun`, `onBeforeCommand`, and `retry`. Takes precedence over `configureEffect` globals.
|
|
310
|
+
- `onRun` fires exactly once per `runEffect` call. Retry attempts run inside that single span. The interpreter does not re-enter `runEffect` per attempt.
|
|
138
311
|
|
|
139
|
-
|
|
140
|
-
Fires every time a `Command` is executed.
|
|
312
|
+
### `configureEffect(options)`
|
|
141
313
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
314
|
+
- `onRun(effect, pipeline, flowName)` wraps the entire workflow; must `await pipeline()`.
|
|
315
|
+
- `onStep(name, type, op)` wraps each Command; must `await op()` and return its result.
|
|
316
|
+
- `onBeforeCommand(command, context)` ires before each Command; throw to abort the pipeline.
|
|
317
|
+
- `retry: { attempts?, delay?, backoff? }` global retry defaults.
|
|
145
318
|
|
|
146
|
-
-
|
|
147
|
-
Fires before a `Command` is executed. Ideal for inspecting metadata and context. If you throw, the pipeline stops immediately.
|
|
148
|
-
- `command`: The `Command` object.
|
|
149
|
-
- `context`: The context object passed to `runEffect`, if any.
|
|
319
|
+
See **opentelemetry-example.js** in the repository for a complete OpenTelemetry wiring example.
|