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.
@@ -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);
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "allowJs": true,
4
- "checkJs": true,
5
- "noEmit": true,
6
- "target": "es2022",
7
- "module": "nodenext",
8
- "moduleResolution": "nodenext",
9
- "lib": ["es2022", "dom"]
10
- },
11
- "files": ["index.js"]
12
- }