pure-effect 0.11.0 → 0.13.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/CHANGELOG.md +148 -0
- package/README.md +132 -52
- package/index.d.ts +53 -25
- package/index.js +216 -131
- package/package.json +31 -3
- package/.prettierrc +0 -12
- package/CLAUDE.md +0 -202
- package/examples/opentelemetry-example.js +0 -164
- package/examples/recording-example.js +0 -104
- package/test/all.js +0 -2265
- package/test/types.test-d.ts +0 -268
- package/tsconfig.check.json +0 -12
package/test/types.test-d.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { expectType, expectError } from 'tsd';
|
|
2
|
-
import { Success, Failure, Command, Ask, Retry, Parallel, effectPipe, runEffect, configureEffect } from '../index.js';
|
|
3
|
-
import type {
|
|
4
|
-
SuccessState,
|
|
5
|
-
FailureState,
|
|
6
|
-
CommandState,
|
|
7
|
-
AskState,
|
|
8
|
-
RetryState,
|
|
9
|
-
ParallelState,
|
|
10
|
-
RetryExhaustedError,
|
|
11
|
-
Effect,
|
|
12
|
-
EffectConfiguration,
|
|
13
|
-
StepRunner,
|
|
14
|
-
RunWrapper,
|
|
15
|
-
CommandInterceptor
|
|
16
|
-
} from '../index.js';
|
|
17
|
-
|
|
18
|
-
interface User {
|
|
19
|
-
email: string;
|
|
20
|
-
password: string;
|
|
21
|
-
}
|
|
22
|
-
interface SavedUser {
|
|
23
|
-
id: number;
|
|
24
|
-
email: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// --- Success ---
|
|
28
|
-
|
|
29
|
-
const s = Success(42);
|
|
30
|
-
expectType<SuccessState<number>>(s);
|
|
31
|
-
expectError(Success()); // missing argument
|
|
32
|
-
|
|
33
|
-
// --- Failure ---
|
|
34
|
-
|
|
35
|
-
const f = Failure('oops');
|
|
36
|
-
expectType<FailureState<string>>(f);
|
|
37
|
-
|
|
38
|
-
// --- Command ---
|
|
39
|
-
|
|
40
|
-
// next is optional, defaulting to Success
|
|
41
|
-
expectType<CommandState<number, number>>(Command(() => 42));
|
|
42
|
-
expectType<CommandState<number, string>>(
|
|
43
|
-
Command(
|
|
44
|
-
() => 42,
|
|
45
|
-
(n: number) => Success(String(n))
|
|
46
|
-
)
|
|
47
|
-
);
|
|
48
|
-
Command(() => 42, undefined, { name: 'readRow' });
|
|
49
|
-
expectError(Command());
|
|
50
|
-
|
|
51
|
-
const cmd = Command(
|
|
52
|
-
async () => ({ id: 1, email: 'a@b.com' }) as SavedUser,
|
|
53
|
-
(saved) => {
|
|
54
|
-
expectType<SavedUser>(saved);
|
|
55
|
-
return Success(saved);
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
expectType<CommandState<SavedUser, SavedUser, unknown>>(cmd);
|
|
59
|
-
|
|
60
|
-
// --- effectPipe type propagation ---
|
|
61
|
-
|
|
62
|
-
const step1 = (input: User) => Success(input);
|
|
63
|
-
const step2 = (user: User) =>
|
|
64
|
-
Command(
|
|
65
|
-
async () => ({ id: 1, ...user }) as SavedUser,
|
|
66
|
-
(s) => Success(s)
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const flow = effectPipe(step1, step2);
|
|
70
|
-
expectType<Effect<SavedUser>>(flow({ email: 'a@b.com', password: 'secret123' }));
|
|
71
|
-
expectError(flow({ email: 'a@b.com' })); // missing password
|
|
72
|
-
|
|
73
|
-
// --- runEffect return type ---
|
|
74
|
-
|
|
75
|
-
const result = await runEffect(flow({ email: 'a@b.com', password: 'secret123' }));
|
|
76
|
-
expectType<SuccessState<SavedUser> | FailureState<unknown>>(result);
|
|
77
|
-
|
|
78
|
-
// --- discriminated union narrowing ---
|
|
79
|
-
|
|
80
|
-
if (result.type === 'Success') {
|
|
81
|
-
expectType<SavedUser>(result.value);
|
|
82
|
-
} else {
|
|
83
|
-
expectType<unknown>(result.error);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- Failure error type flows through runEffect ---
|
|
87
|
-
|
|
88
|
-
const failFlow = effectPipe((input: User): Effect<User, string> => Failure<string>('bad'));
|
|
89
|
-
const failResult = await runEffect(failFlow({ email: 'a@b.com', password: 'x' }));
|
|
90
|
-
expectType<SuccessState<User> | FailureState<string>>(failResult);
|
|
91
|
-
|
|
92
|
-
// --- Ask ---
|
|
93
|
-
|
|
94
|
-
const ask = Ask((ctx) => Success(ctx as User));
|
|
95
|
-
expectType<AskState<User, unknown>>(ask);
|
|
96
|
-
|
|
97
|
-
const askFlow = effectPipe((input: User) => Ask((_ctx) => Success(input)));
|
|
98
|
-
expectType<Effect<User>>(askFlow({ email: 'a@b.com', password: 'secret123' }));
|
|
99
|
-
|
|
100
|
-
// --- Retry ---
|
|
101
|
-
|
|
102
|
-
const innerCmd = Command(
|
|
103
|
-
async () => 42,
|
|
104
|
-
(n) => Success(n)
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// Retry with options preserves T; the error type is the exhaustion failure, not the inner error,
|
|
108
|
-
// because the interpreter wraps the last inner error rather than letting it escape
|
|
109
|
-
const retried = Retry(innerCmd, { attempts: 3 });
|
|
110
|
-
expectType<RetryState<number, RetryExhaustedError<unknown>>>(retried);
|
|
111
|
-
|
|
112
|
-
// Retry without options is valid
|
|
113
|
-
const retriedNoOpts = Retry(innerCmd);
|
|
114
|
-
expectType<RetryState<number, RetryExhaustedError<unknown>>>(retriedNoOpts);
|
|
115
|
-
|
|
116
|
-
// Retry in effectPipe preserves type flow
|
|
117
|
-
const retryFlow = effectPipe((input: User) =>
|
|
118
|
-
Retry(
|
|
119
|
-
Command(
|
|
120
|
-
async () => ({ id: 1, ...input }) as SavedUser,
|
|
121
|
-
(s) => Success(s)
|
|
122
|
-
),
|
|
123
|
-
{ attempts: 2 }
|
|
124
|
-
)
|
|
125
|
-
);
|
|
126
|
-
expectType<Effect<SavedUser, RetryExhaustedError<unknown>>>(retryFlow({ email: 'a@b.com', password: 'secret123' }));
|
|
127
|
-
|
|
128
|
-
// The inner error type survives inside the exhaustion failure, so lastError is typed
|
|
129
|
-
const retriedTyped = Retry(Success(1) as Effect<number, 'net_down'>);
|
|
130
|
-
expectType<RetryState<number, RetryExhaustedError<'net_down'>>>(retriedTyped);
|
|
131
|
-
|
|
132
|
-
const retryResult = await runEffect(Retry(Success(1) as Effect<number, 'flaky'>, { attempts: 1 }));
|
|
133
|
-
if (retryResult.type === 'Failure') {
|
|
134
|
-
expectType<RetryExhaustedError<'flaky'>>(retryResult.error);
|
|
135
|
-
expectType<'flaky'>(retryResult.error.lastError);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// onExhausted consumes the exhaustion, so the fallback's error type is what remains
|
|
139
|
-
const recovered = Retry(Success(1) as Effect<number, 'flaky'>, {
|
|
140
|
-
attempts: 1,
|
|
141
|
-
onExhausted: (err) => {
|
|
142
|
-
expectType<RetryExhaustedError<'flaky'>>(err);
|
|
143
|
-
return Success(0) as Effect<number, 'cache_miss'>;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
expectType<RetryState<number, 'cache_miss'>>(recovered);
|
|
147
|
-
|
|
148
|
-
const recoveredResult = await runEffect(recovered);
|
|
149
|
-
if (recoveredResult.type === 'Failure') {
|
|
150
|
-
expectType<'cache_miss'>(recoveredResult.error);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// onExhausted is per-use only: global retry defaults cannot carry a fallback
|
|
154
|
-
expectError(configureEffect({ retry: { attempts: 2, onExhausted: () => Success(1) } }));
|
|
155
|
-
|
|
156
|
-
// RetryExhaustedError shape is usable for narrowing exhaustion failures
|
|
157
|
-
const exhaustedErr: RetryExhaustedError<Error> = {
|
|
158
|
-
retryExhausted: true,
|
|
159
|
-
lastError: new Error('boom'),
|
|
160
|
-
attempts: 3
|
|
161
|
-
};
|
|
162
|
-
expectType<true>(exhaustedErr.retryExhausted);
|
|
163
|
-
expectType<Error>(exhaustedErr.lastError);
|
|
164
|
-
expectType<number>(exhaustedErr.attempts);
|
|
165
|
-
|
|
166
|
-
// --- error channel union across effectPipe steps ---
|
|
167
|
-
|
|
168
|
-
type ValidationError = 'invalid_email' | 'weak_password';
|
|
169
|
-
type DbError = 'db_connection' | 'duplicate_key';
|
|
170
|
-
|
|
171
|
-
const validateStep = (_input: User): Effect<User, ValidationError> => Failure<ValidationError>('invalid_email');
|
|
172
|
-
const saveStep = (_user: User): Effect<SavedUser, DbError> => Failure<DbError>('db_connection');
|
|
173
|
-
|
|
174
|
-
const typedFlow = effectPipe(validateStep, saveStep);
|
|
175
|
-
expectType<Effect<SavedUser, ValidationError | DbError>>(typedFlow({ email: 'a@b.com', password: 'secret123' }));
|
|
176
|
-
|
|
177
|
-
const typedResult = await runEffect(typedFlow({ email: 'a@b.com', password: 'secret123' }));
|
|
178
|
-
expectType<SuccessState<SavedUser> | FailureState<ValidationError | DbError>>(typedResult);
|
|
179
|
-
|
|
180
|
-
// --- Parallel ---
|
|
181
|
-
|
|
182
|
-
// Values tuple is correctly typed
|
|
183
|
-
const par = Parallel([Success(42), Success('hello')], ([n, s]) => {
|
|
184
|
-
expectType<number>(n);
|
|
185
|
-
expectType<string>(s);
|
|
186
|
-
return Success({ n, s });
|
|
187
|
-
});
|
|
188
|
-
expectType<ParallelState<[number, string], { n: number; s: string }>>(par);
|
|
189
|
-
|
|
190
|
-
// Parallel in effectPipe preserves type flow
|
|
191
|
-
const parallelFlow = effectPipe((input: User) =>
|
|
192
|
-
Parallel([Success(input.email), Success(input.password)], ([email, password]) => Success({ email, password }))
|
|
193
|
-
);
|
|
194
|
-
expectType<Effect<{ email: string; password: string }>>(parallelFlow({ email: 'a@b.com', password: 'secret123' }));
|
|
195
|
-
|
|
196
|
-
// runEffect return type flows through Parallel
|
|
197
|
-
const parallelResult = await runEffect(Parallel([Success(1), Success('x')], ([n, s]) => Success({ n, s })));
|
|
198
|
-
expectType<SuccessState<{ n: number; s: string }> | FailureState<unknown>>(parallelResult);
|
|
199
|
-
|
|
200
|
-
// With next omitted, the Parallel resolves to the values tuple itself
|
|
201
|
-
const parBare = Parallel([Success(42), Success('hello')]);
|
|
202
|
-
expectType<ParallelState<[number, string], [number, string]>>(parBare);
|
|
203
|
-
const parBareResult = await runEffect(parBare);
|
|
204
|
-
if (parBareResult.type === 'Success') {
|
|
205
|
-
expectType<[number, string]>(parBareResult.value);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// --- Ctx (context type) ---
|
|
209
|
-
|
|
210
|
-
interface AppCtx {
|
|
211
|
-
db: string;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Ask infers Ctx from callback parameter type
|
|
215
|
-
const askWithCtx = Ask((ctx: AppCtx) => Success(ctx.db));
|
|
216
|
-
expectType<AskState<string, unknown, AppCtx>>(askWithCtx);
|
|
217
|
-
|
|
218
|
-
// effectPipe propagates Ctx through steps
|
|
219
|
-
const ctxFlow = effectPipe((input: User) => Ask((ctx: AppCtx) => Success({ ...input, conn: ctx.db })));
|
|
220
|
-
expectType<Effect<{ email: string; password: string; conn: string }, unknown, AppCtx>>(
|
|
221
|
-
ctxFlow({ email: 'a@b.com', password: 'secret123' })
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// runEffect enforces context argument matches Ctx
|
|
225
|
-
const ctxResult = await runEffect(ctxFlow({ email: 'a@b.com', password: 'secret123' }), { db: 'conn' });
|
|
226
|
-
expectType<SuccessState<{ email: string; password: string; conn: string }> | FailureState<unknown>>(ctxResult);
|
|
227
|
-
|
|
228
|
-
// wrong context shape should error
|
|
229
|
-
expectError(runEffect(ctxFlow({ email: 'a@b.com', password: 'secret123' }), { wrong: 'thing' }));
|
|
230
|
-
|
|
231
|
-
// --- configureEffect / EffectConfiguration ---
|
|
232
|
-
|
|
233
|
-
// accepts full configuration
|
|
234
|
-
configureEffect({
|
|
235
|
-
onStep: async (_name, _type, op) => op(),
|
|
236
|
-
onRun: async (_effect, op, _flowName) => op(),
|
|
237
|
-
onBeforeCommand: async (_cmd, _ctx) => {},
|
|
238
|
-
retry: { attempts: 3, delay: 100, backoff: 2 }
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// accepts partial configuration
|
|
242
|
-
configureEffect({ retry: { attempts: 5 } });
|
|
243
|
-
configureEffect({});
|
|
244
|
-
|
|
245
|
-
// rejects invalid shapes
|
|
246
|
-
expectError(configureEffect({ onStep: 'not-a-function' }));
|
|
247
|
-
expectError(configureEffect({ retry: { attempts: 'three' } }));
|
|
248
|
-
|
|
249
|
-
// hook types are correctly shaped
|
|
250
|
-
const myStep: StepRunner = async (name, type, op) => {
|
|
251
|
-
expectType<string>(name);
|
|
252
|
-
expectType<string>(type);
|
|
253
|
-
return op();
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const myRun: RunWrapper = async (effect, op, flowName) => {
|
|
257
|
-
expectType<Effect<unknown>>(effect);
|
|
258
|
-
expectType<string | undefined>(flowName);
|
|
259
|
-
return op();
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const myInterceptor: CommandInterceptor = async (cmd, _ctx) => {
|
|
263
|
-
expectType<CommandState<unknown, unknown>>(cmd);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
// EffectConfiguration is a usable type
|
|
267
|
-
const config: EffectConfiguration = { onStep: myStep, onRun: myRun, onBeforeCommand: myInterceptor };
|
|
268
|
-
expectType<EffectConfiguration>(config);
|
package/tsconfig.check.json
DELETED