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
package/index.d.ts
CHANGED
|
@@ -10,102 +10,202 @@ export type FailureState<E = unknown> = {
|
|
|
10
10
|
initialInput?: unknown;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export type CommandState<R, T, E = unknown> = {
|
|
13
|
+
export type CommandState<R, T, E = unknown, Ctx = unknown> = {
|
|
14
14
|
type: 'Command';
|
|
15
15
|
cmd: () => Promise<R> | R;
|
|
16
|
-
next: (result: R) => Effect<T, E>;
|
|
16
|
+
next: (result: R) => Effect<T, E, Ctx>;
|
|
17
17
|
meta?: unknown;
|
|
18
18
|
initialInput?: unknown;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export type
|
|
21
|
+
export type AskState<T, E = unknown, Ctx = unknown> = {
|
|
22
|
+
type: 'Ask';
|
|
23
|
+
next: (context: Ctx) => Effect<T, E, Ctx>;
|
|
24
|
+
initialInput?: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RetryOptions = {
|
|
28
|
+
attempts?: number;
|
|
29
|
+
delay?: number;
|
|
30
|
+
backoff?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type RetryState<T, E = unknown, Ctx = unknown> = {
|
|
34
|
+
type: 'Retry';
|
|
35
|
+
effect: Effect<T, E, Ctx>;
|
|
36
|
+
options: RetryOptions;
|
|
37
|
+
next: (value: T) => Effect<T, E, Ctx>;
|
|
38
|
+
initialInput?: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type RetryExhaustedError<E = unknown> = {
|
|
42
|
+
retryExhausted: true;
|
|
43
|
+
lastError: E;
|
|
44
|
+
attempts: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type Effect<T, E = unknown, Ctx = unknown> =
|
|
22
48
|
| SuccessState<T>
|
|
23
49
|
| FailureState<E>
|
|
24
|
-
| CommandState<any, T, E
|
|
50
|
+
| CommandState<any, T, E, Ctx>
|
|
51
|
+
| AskState<T, E, Ctx>
|
|
52
|
+
| RetryState<T, E, Ctx>;
|
|
25
53
|
|
|
26
54
|
export declare function Success<T>(value: T): SuccessState<T>;
|
|
27
55
|
|
|
28
|
-
export declare function Failure<E = unknown>(
|
|
29
|
-
error: E,
|
|
30
|
-
initialInput?: unknown
|
|
31
|
-
): FailureState<E>;
|
|
56
|
+
export declare function Failure<E = unknown>(error: E, initialInput?: unknown): FailureState<E>;
|
|
32
57
|
|
|
33
|
-
export declare function Command<R, T, E = unknown>(
|
|
58
|
+
export declare function Command<R, T, E = unknown, Ctx = unknown>(
|
|
34
59
|
cmd: () => Promise<R> | R,
|
|
35
|
-
next: (result: R) => Effect<T, E>,
|
|
60
|
+
next: (result: R) => Effect<T, E, Ctx>,
|
|
36
61
|
meta?: unknown
|
|
37
|
-
): CommandState<R, T, E>;
|
|
38
|
-
|
|
39
|
-
export declare function
|
|
40
|
-
|
|
41
|
-
):
|
|
42
|
-
|
|
43
|
-
export declare function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
):
|
|
47
|
-
|
|
48
|
-
export declare function effectPipe<A, B,
|
|
49
|
-
f1: (a: A) => Effect<B,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
62
|
+
): CommandState<R, T, E, Ctx>;
|
|
63
|
+
|
|
64
|
+
export declare function Ask<T, E = unknown, Ctx = unknown>(
|
|
65
|
+
next: (context: Ctx) => Effect<T, E, Ctx>
|
|
66
|
+
): AskState<T, E, Ctx>;
|
|
67
|
+
|
|
68
|
+
export declare function Retry<T, E = unknown, Ctx = unknown>(
|
|
69
|
+
effect: Effect<T, E, Ctx>,
|
|
70
|
+
options?: RetryOptions
|
|
71
|
+
): RetryState<T, E, Ctx>;
|
|
72
|
+
|
|
73
|
+
export declare function effectPipe<A, B, E1 = unknown, Ctx = unknown>(
|
|
74
|
+
f1: (a: A) => Effect<B, E1, Ctx>
|
|
75
|
+
): (start: A) => Effect<B, E1, Ctx>;
|
|
76
|
+
|
|
77
|
+
export declare function effectPipe<A, B, C, E1 = unknown, E2 = unknown, Ctx = unknown>(
|
|
78
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
79
|
+
f2: (b: B) => Effect<C, E2, Ctx>
|
|
80
|
+
): (start: A) => Effect<C, E1 | E2, Ctx>;
|
|
81
|
+
|
|
82
|
+
export declare function effectPipe<A, B, C, D, E1 = unknown, E2 = unknown, E3 = unknown, Ctx = unknown>(
|
|
83
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
84
|
+
f2: (b: B) => Effect<C, E2, Ctx>,
|
|
85
|
+
f3: (c: C) => Effect<D, E3, Ctx>
|
|
86
|
+
): (start: A) => Effect<D, E1 | E2 | E3, Ctx>;
|
|
87
|
+
|
|
88
|
+
export declare function effectPipe<
|
|
89
|
+
A,
|
|
90
|
+
B,
|
|
91
|
+
C,
|
|
92
|
+
D,
|
|
93
|
+
F,
|
|
94
|
+
E1 = unknown,
|
|
95
|
+
E2 = unknown,
|
|
96
|
+
E3 = unknown,
|
|
97
|
+
E4 = unknown,
|
|
98
|
+
Ctx = unknown
|
|
99
|
+
>(
|
|
100
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
101
|
+
f2: (b: B) => Effect<C, E2, Ctx>,
|
|
102
|
+
f3: (c: C) => Effect<D, E3, Ctx>,
|
|
103
|
+
f4: (d: D) => Effect<F, E4, Ctx>
|
|
104
|
+
): (start: A) => Effect<F, E1 | E2 | E3 | E4, Ctx>;
|
|
105
|
+
|
|
106
|
+
export declare function effectPipe<
|
|
107
|
+
A,
|
|
108
|
+
B,
|
|
109
|
+
C,
|
|
110
|
+
D,
|
|
111
|
+
F,
|
|
112
|
+
G,
|
|
113
|
+
E1 = unknown,
|
|
114
|
+
E2 = unknown,
|
|
115
|
+
E3 = unknown,
|
|
116
|
+
E4 = unknown,
|
|
117
|
+
E5 = unknown,
|
|
118
|
+
Ctx = unknown
|
|
119
|
+
>(
|
|
120
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
121
|
+
f2: (b: B) => Effect<C, E2, Ctx>,
|
|
122
|
+
f3: (c: C) => Effect<D, E3, Ctx>,
|
|
123
|
+
f4: (d: D) => Effect<F, E4, Ctx>,
|
|
124
|
+
f5: (f: F) => Effect<G, E5, Ctx>
|
|
125
|
+
): (start: A) => Effect<G, E1 | E2 | E3 | E4 | E5, Ctx>;
|
|
126
|
+
|
|
127
|
+
export declare function effectPipe<
|
|
128
|
+
A,
|
|
129
|
+
B,
|
|
130
|
+
C,
|
|
131
|
+
D,
|
|
132
|
+
F,
|
|
133
|
+
G,
|
|
134
|
+
H,
|
|
135
|
+
E1 = unknown,
|
|
136
|
+
E2 = unknown,
|
|
137
|
+
E3 = unknown,
|
|
138
|
+
E4 = unknown,
|
|
139
|
+
E5 = unknown,
|
|
140
|
+
E6 = unknown,
|
|
141
|
+
Ctx = unknown
|
|
142
|
+
>(
|
|
143
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
144
|
+
f2: (b: B) => Effect<C, E2, Ctx>,
|
|
145
|
+
f3: (c: C) => Effect<D, E3, Ctx>,
|
|
146
|
+
f4: (d: D) => Effect<F, E4, Ctx>,
|
|
147
|
+
f5: (f: F) => Effect<G, E5, Ctx>,
|
|
148
|
+
f6: (g: G) => Effect<H, E6, Ctx>
|
|
149
|
+
): (start: A) => Effect<H, E1 | E2 | E3 | E4 | E5 | E6, Ctx>;
|
|
150
|
+
|
|
151
|
+
export declare function effectPipe<
|
|
152
|
+
A,
|
|
153
|
+
B,
|
|
154
|
+
C,
|
|
155
|
+
D,
|
|
156
|
+
F,
|
|
157
|
+
G,
|
|
158
|
+
H,
|
|
159
|
+
I,
|
|
160
|
+
E1 = unknown,
|
|
161
|
+
E2 = unknown,
|
|
162
|
+
E3 = unknown,
|
|
163
|
+
E4 = unknown,
|
|
164
|
+
E5 = unknown,
|
|
165
|
+
E6 = unknown,
|
|
166
|
+
E7 = unknown,
|
|
167
|
+
Ctx = unknown
|
|
168
|
+
>(
|
|
169
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
170
|
+
f2: (b: B) => Effect<C, E2, Ctx>,
|
|
171
|
+
f3: (c: C) => Effect<D, E3, Ctx>,
|
|
172
|
+
f4: (d: D) => Effect<F, E4, Ctx>,
|
|
173
|
+
f5: (f: F) => Effect<G, E5, Ctx>,
|
|
174
|
+
f6: (g: G) => Effect<H, E6, Ctx>,
|
|
175
|
+
f7: (h: H) => Effect<I, E7, Ctx>
|
|
176
|
+
): (start: A) => Effect<I, E1 | E2 | E3 | E4 | E5 | E6 | E7, Ctx>;
|
|
177
|
+
|
|
178
|
+
export declare function effectPipe<
|
|
179
|
+
A,
|
|
180
|
+
B,
|
|
181
|
+
C,
|
|
182
|
+
D,
|
|
183
|
+
F,
|
|
184
|
+
G,
|
|
185
|
+
H,
|
|
186
|
+
I,
|
|
187
|
+
J,
|
|
188
|
+
E1 = unknown,
|
|
189
|
+
E2 = unknown,
|
|
190
|
+
E3 = unknown,
|
|
191
|
+
E4 = unknown,
|
|
192
|
+
E5 = unknown,
|
|
193
|
+
E6 = unknown,
|
|
194
|
+
E7 = unknown,
|
|
195
|
+
E8 = unknown,
|
|
196
|
+
Ctx = unknown
|
|
197
|
+
>(
|
|
198
|
+
f1: (a: A) => Effect<B, E1, Ctx>,
|
|
199
|
+
f2: (b: B) => Effect<C, E2, Ctx>,
|
|
200
|
+
f3: (c: C) => Effect<D, E3, Ctx>,
|
|
201
|
+
f4: (d: D) => Effect<F, E4, Ctx>,
|
|
202
|
+
f5: (f: F) => Effect<G, E5, Ctx>,
|
|
203
|
+
f6: (g: G) => Effect<H, E6, Ctx>,
|
|
204
|
+
f7: (h: H) => Effect<I, E7, Ctx>,
|
|
205
|
+
f8: (i: I) => Effect<J, E8, Ctx>
|
|
206
|
+
): (start: A) => Effect<J, E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8, Ctx>;
|
|
207
|
+
|
|
208
|
+
export type StepRunner = (name: string, type: string, op: () => Promise<unknown>) => Promise<unknown>;
|
|
109
209
|
|
|
110
210
|
export type RunWrapper = (
|
|
111
211
|
effect: Effect<unknown>,
|
|
@@ -113,15 +213,19 @@ export type RunWrapper = (
|
|
|
113
213
|
flowName?: string
|
|
114
214
|
) => Promise<SuccessState<unknown> | FailureState<unknown>>;
|
|
115
215
|
|
|
116
|
-
export type CommandInterceptor = (
|
|
117
|
-
command: CommandState<unknown, unknown>,
|
|
118
|
-
context?: any
|
|
119
|
-
) => Promise<void>;
|
|
216
|
+
export type CommandInterceptor = (command: CommandState<unknown, unknown>, context?: any) => Promise<void>;
|
|
120
217
|
|
|
121
218
|
export interface EffectConfiguration {
|
|
122
219
|
onStep?: StepRunner;
|
|
123
220
|
onRun?: RunWrapper;
|
|
124
221
|
onBeforeCommand?: CommandInterceptor;
|
|
222
|
+
retry?: RetryOptions;
|
|
125
223
|
}
|
|
126
224
|
|
|
127
225
|
export declare function configureEffect(options: EffectConfiguration): void;
|
|
226
|
+
|
|
227
|
+
export declare function runEffect<T, E = unknown, Ctx = unknown>(
|
|
228
|
+
effect: Effect<T, E, Ctx>,
|
|
229
|
+
context?: Ctx,
|
|
230
|
+
callConfig?: EffectConfiguration
|
|
231
|
+
): Promise<SuccessState<T> | FailureState<E>>;
|
package/index.js
CHANGED
|
@@ -11,10 +11,27 @@
|
|
|
11
11
|
* initialInput?: any
|
|
12
12
|
* }} CommandState
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* type: 'Ask',
|
|
17
|
+
* next: (context: any) => Effect,
|
|
18
|
+
* initialInput?: any
|
|
19
|
+
* }} AskState
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {{
|
|
24
|
+
* type: 'Retry',
|
|
25
|
+
* effect: Effect,
|
|
26
|
+
* options: { attempts?: number, delay?: number, backoff?: number },
|
|
27
|
+
* next: (value: any) => Effect,
|
|
28
|
+
* initialInput?: any
|
|
29
|
+
* }} RetryState
|
|
30
|
+
*/
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* The Union type for all possible states
|
|
17
|
-
* @typedef {SuccessState | FailureState | CommandState} Effect
|
|
34
|
+
* @typedef {SuccessState | FailureState | CommandState | AskState | RetryState} Effect
|
|
18
35
|
*/
|
|
19
36
|
|
|
20
37
|
/**
|
|
@@ -30,7 +47,11 @@ const Success = (value) => ({ type: 'Success', value });
|
|
|
30
47
|
* @param {any} [initialInput] - initial input passed to the flow (optional)
|
|
31
48
|
* @returns {FailureState}
|
|
32
49
|
*/
|
|
33
|
-
const Failure = (error, initialInput) => ({
|
|
50
|
+
const Failure = (error, initialInput) => ({
|
|
51
|
+
type: 'Failure',
|
|
52
|
+
error,
|
|
53
|
+
initialInput
|
|
54
|
+
});
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
57
|
* Represents a side effect to be executed later
|
|
@@ -41,23 +62,64 @@ const Failure = (error, initialInput) => ({ type: 'Failure', error, initialInput
|
|
|
41
62
|
*/
|
|
42
63
|
const Command = (cmd, next, meta) => ({ type: 'Command', cmd, next, meta });
|
|
43
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Reads the context object from the current `runEffect` call.
|
|
67
|
+
* @param {(context: any) => Effect} next - Receives the context and returns the next Effect
|
|
68
|
+
* @returns {AskState}
|
|
69
|
+
*/
|
|
70
|
+
const Ask = (next) => ({ type: 'Ask', next });
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wraps an Effect tree with retry-on-failure semantics.
|
|
74
|
+
* @param {Effect} effect - The inner Effect tree to retry
|
|
75
|
+
* @param {Object} [options] - Per-use retry options; merged over global defaults at runtime
|
|
76
|
+
* @param {number} [options.attempts] - Max retries (not counting first try)
|
|
77
|
+
* @param {number} [options.delay] - Ms before first retry
|
|
78
|
+
* @param {number} [options.backoff] - Multiplier applied to delay on each subsequent retry
|
|
79
|
+
* @returns {RetryState}
|
|
80
|
+
*/
|
|
81
|
+
const Retry = (effect, options = {}) => ({
|
|
82
|
+
type: 'Retry',
|
|
83
|
+
effect,
|
|
84
|
+
options,
|
|
85
|
+
next: (value) => Success(value)
|
|
86
|
+
});
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* Connects an Effect to the next function in the pipeline.
|
|
46
|
-
* Handles the branching logic for Success, Failure, and
|
|
90
|
+
* Handles the branching logic for Success, Failure, Command, Ask, and Retry.
|
|
47
91
|
*
|
|
48
92
|
* @param {Effect} effect - The current Effect object
|
|
49
93
|
* @param {(value: any) => Effect} fn - The next function to run if the current effect is a Success
|
|
50
94
|
* @returns {Effect} The composed Effect
|
|
51
95
|
*/
|
|
52
|
-
|
|
96
|
+
/**
|
|
97
|
+
* @param {Effect} effect
|
|
98
|
+
* @param {(value: any) => Effect} fn
|
|
99
|
+
* @param {any} [initialInput]
|
|
100
|
+
* @returns {Effect}
|
|
101
|
+
*/
|
|
102
|
+
const chain = (effect, fn, initialInput) => {
|
|
103
|
+
const withII = (/** @type {Effect} */ e) =>
|
|
104
|
+
initialInput !== undefined && e.initialInput === undefined ? { ...e, initialInput } : e;
|
|
105
|
+
|
|
53
106
|
switch (effect.type) {
|
|
54
107
|
case 'Success':
|
|
55
|
-
return fn(effect.value);
|
|
108
|
+
return withII(fn(effect.value));
|
|
56
109
|
case 'Failure':
|
|
57
|
-
return effect;
|
|
58
|
-
case 'Command':
|
|
59
|
-
const next = (/** @type {
|
|
60
|
-
return Command(effect.cmd, next, effect.meta);
|
|
110
|
+
return withII(effect);
|
|
111
|
+
case 'Command': {
|
|
112
|
+
const next = (/** @type {any} */ result) => chain(effect.next(result), fn, initialInput);
|
|
113
|
+
return withII(Command(effect.cmd, next, effect.meta));
|
|
114
|
+
}
|
|
115
|
+
case 'Ask': {
|
|
116
|
+
const next = (/** @type {any} */ ctx) => chain(effect.next(ctx), fn, initialInput);
|
|
117
|
+
return withII(Ask(next));
|
|
118
|
+
}
|
|
119
|
+
case 'Retry': {
|
|
120
|
+
const next = (/** @type {any} */ result) => chain(effect.next(result), fn, initialInput);
|
|
121
|
+
return withII({ ...effect, next });
|
|
122
|
+
}
|
|
61
123
|
}
|
|
62
124
|
};
|
|
63
125
|
|
|
@@ -65,14 +127,13 @@ const chain = (effect, fn) => {
|
|
|
65
127
|
* Composes a list of functions into a single Effect pipeline.
|
|
66
128
|
* Each function receives the output of the previous one.
|
|
67
129
|
*
|
|
68
|
-
* @param {...(input: any) => Effect} fns - Functions that return Success, Failure, or
|
|
130
|
+
* @param {...(input: any) => Effect} fns - Functions that return Success, Failure, Command, or Ask.
|
|
69
131
|
* @returns {(start: any) => Effect} A function that accepts an initial input and returns the final Effect tree.
|
|
70
132
|
*/
|
|
71
133
|
const effectPipe = (...fns) => {
|
|
72
134
|
return (start) => {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return effect;
|
|
135
|
+
const chainWithII = (/** @type {Effect} */ eff, /** @type {(v: any) => Effect} */ fn) => chain(eff, fn, start);
|
|
136
|
+
return fns.reduce(chainWithII, /** @type {Effect} */ (Success(start)));
|
|
76
137
|
};
|
|
77
138
|
};
|
|
78
139
|
|
|
@@ -92,11 +153,15 @@ let stepRunner = defaultStepRunner;
|
|
|
92
153
|
let runWrapper = defaultRunWrapper;
|
|
93
154
|
let commandInterceptor = defaultCommandInterceptor;
|
|
94
155
|
|
|
156
|
+
const defaultRetryOptions = { attempts: 3, delay: 100, backoff: 1 };
|
|
157
|
+
let retryDefaults = { ...defaultRetryOptions };
|
|
158
|
+
|
|
95
159
|
/**
|
|
96
160
|
* @typedef {Object} EffectConfiguration
|
|
97
161
|
* @property {StepRunner} [onStep] - Fires once per runEffect call. It wraps the entire workflow execution.
|
|
98
162
|
* @property {RunWrapper} [onRun] - Fires every time a Command is executed.
|
|
99
163
|
* @property {CommandInterceptor} [onBeforeCommand] - Intercepts a Command and any context passed to runEffect before execution.
|
|
164
|
+
* @property {{ attempts?: number, delay?: number, backoff?: number }} [retry] - Global Retry defaults; merged under per-use options.
|
|
100
165
|
*/
|
|
101
166
|
|
|
102
167
|
/**
|
|
@@ -108,36 +173,78 @@ const configureEffect = (options) => {
|
|
|
108
173
|
stepRunner = options.onStep ? options.onStep : defaultStepRunner;
|
|
109
174
|
runWrapper = options.onRun ? options.onRun : defaultRunWrapper;
|
|
110
175
|
commandInterceptor = options.onBeforeCommand ? options.onBeforeCommand : defaultCommandInterceptor;
|
|
176
|
+
retryDefaults = options.retry ? { ...defaultRetryOptions, ...options.retry } : defaultRetryOptions;
|
|
111
177
|
};
|
|
112
178
|
|
|
113
179
|
const runEffect =
|
|
114
180
|
/**
|
|
115
181
|
* The Interpreter
|
|
116
182
|
* Iterates through the Effect tree, executing Commands and handling async flow.
|
|
183
|
+
* Ask effects are resolved synchronously with the context object.
|
|
184
|
+
*
|
|
185
|
+
* Per-call config takes precedence over global configureEffect defaults.
|
|
186
|
+
* onRun fires exactly once per runEffect call — Retry attempts run inside that
|
|
187
|
+
* single span rather than spawning their own, keeping telemetry non-duplicated.
|
|
117
188
|
*
|
|
118
189
|
* @param {Effect} effect - The Effect tree returned by a pipeline
|
|
119
|
-
* @param {any} [context] - Optional context object
|
|
190
|
+
* @param {any} [context] - Optional context object. Passed to Ask continuations and the Command Interceptor.
|
|
191
|
+
* @param {EffectConfiguration} [callConfig] - Per-call overrides; merged over global configureEffect defaults.
|
|
120
192
|
* @returns {Promise<SuccessState | FailureState>}
|
|
121
193
|
*/
|
|
122
|
-
async function runEffect(effect, context = {}) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
194
|
+
async function runEffect(effect, context = {}, callConfig = {}) {
|
|
195
|
+
const localStepRunner = callConfig.onStep ? callConfig.onStep : stepRunner;
|
|
196
|
+
const localRunWrapper = callConfig.onRun ? callConfig.onRun : runWrapper;
|
|
197
|
+
const localCommandInterceptor = callConfig.onBeforeCommand ? callConfig.onBeforeCommand : commandInterceptor;
|
|
198
|
+
const localRetryDefaults = callConfig.retry ? { ...retryDefaults, ...callConfig.retry } : retryDefaults;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {Effect} eff
|
|
202
|
+
* @returns {Promise<SuccessState | FailureState>}
|
|
203
|
+
*/
|
|
204
|
+
async function execute(eff) {
|
|
205
|
+
while (eff.type === 'Command' || eff.type === 'Ask' || eff.type === 'Retry') {
|
|
206
|
+
if (eff.type === 'Ask') {
|
|
207
|
+
eff = eff.next(context);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (eff.type === 'Retry') {
|
|
211
|
+
const opts = { ...localRetryDefaults, ...eff.options };
|
|
212
|
+
const { attempts } = opts;
|
|
213
|
+
let lastError;
|
|
214
|
+
let succeeded = false;
|
|
215
|
+
|
|
216
|
+
for (let attempt = 0; attempt <= attempts; attempt++) {
|
|
217
|
+
if (attempt > 0) {
|
|
218
|
+
await new Promise((r) => setTimeout(r, opts.delay * Math.pow(opts.backoff, attempt - 1)));
|
|
219
|
+
}
|
|
220
|
+
const result = await execute(eff.effect);
|
|
221
|
+
if (result.type === 'Success') {
|
|
222
|
+
eff = eff.next(result.value);
|
|
223
|
+
succeeded = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
lastError = result.error;
|
|
134
227
|
}
|
|
228
|
+
|
|
229
|
+
if (!succeeded) {
|
|
230
|
+
return Failure({ retryExhausted: true, lastError, attempts }, eff.initialInput);
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const cmdName = eff.cmd.name || 'anonymous';
|
|
235
|
+
const initialInput = eff.initialInput;
|
|
236
|
+
try {
|
|
237
|
+
await localCommandInterceptor(eff, context);
|
|
238
|
+
const result = await localStepRunner(cmdName, 'Command', eff.cmd);
|
|
239
|
+
eff = eff.next(result);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return Failure(e, initialInput);
|
|
135
242
|
}
|
|
243
|
+
}
|
|
244
|
+
return eff;
|
|
245
|
+
}
|
|
136
246
|
|
|
137
|
-
|
|
138
|
-
},
|
|
139
|
-
context?.flowName || ''
|
|
140
|
-
);
|
|
247
|
+
return localRunWrapper(effect, () => execute(effect), context?.flowName || '');
|
|
141
248
|
};
|
|
142
249
|
|
|
143
|
-
export { Success, Failure, Command, effectPipe, runEffect, configureEffect };
|
|
250
|
+
export { Success, Failure, Command, Ask, Retry, effectPipe, runEffect, configureEffect };
|
package/opentelemetry-example.js
CHANGED
|
@@ -9,14 +9,14 @@ import { configureEffect } from './index.js';
|
|
|
9
9
|
/** @import { RunWrapper, StepRunner } from "./index.js" */
|
|
10
10
|
|
|
11
11
|
const traceExporter = new OTLPTraceExporter({
|
|
12
|
-
url: 'http://localhost:4318/v1/traces'
|
|
12
|
+
url: 'http://localhost:4318/v1/traces'
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
const sdk = new NodeSDK({
|
|
16
16
|
serviceName: 'pure-effect-test',
|
|
17
17
|
traceExporter,
|
|
18
18
|
spanProcessor: new SimpleSpanProcessor(traceExporter),
|
|
19
|
-
instrumentations: []
|
|
19
|
+
instrumentations: []
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
sdk.start();
|
|
@@ -43,7 +43,7 @@ export function enableTelemetry() {
|
|
|
43
43
|
if (result.type === 'Failure') {
|
|
44
44
|
rootSpan.setStatus({
|
|
45
45
|
code: SpanStatusCode.ERROR,
|
|
46
|
-
message: String(result.error)
|
|
46
|
+
message: String(result.error)
|
|
47
47
|
});
|
|
48
48
|
} else {
|
|
49
49
|
rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
@@ -79,13 +79,13 @@ export function enableTelemetry() {
|
|
|
79
79
|
span.recordException(err);
|
|
80
80
|
span.setStatus({
|
|
81
81
|
code: SpanStatusCode.ERROR,
|
|
82
|
-
message: err.message
|
|
82
|
+
message: err.message
|
|
83
83
|
});
|
|
84
84
|
throw err;
|
|
85
85
|
} finally {
|
|
86
86
|
span.end();
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
|
-
}
|
|
89
|
+
}
|
|
90
90
|
});
|
|
91
91
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pure-effect",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "A TypeScript effect library where effects are plain objects you can inspect, test, and reason about. Learn the whole API in an afternoon. Tiny footprint, zero dependencies, works with plain JavaScript too.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./index.js",
|
|
7
7
|
"types": "./index.d.ts",
|