pure-effect 0.10.0 → 0.12.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,5 +1,16 @@
1
- import { expectType, expectError } from 'tsd';
2
- import { Success, Failure, Command, Ask, Retry, Parallel, effectPipe, runEffect, configureEffect } from '../index.js';
1
+ import { expectType, expectAssignable } from 'tsd';
2
+ import {
3
+ Success,
4
+ Failure,
5
+ Command,
6
+ Ask,
7
+ Retry,
8
+ Parallel,
9
+ effectPipe,
10
+ runEffect,
11
+ configureEffect,
12
+ replayEffect
13
+ } from '../index.js';
3
14
  import type {
4
15
  SuccessState,
5
16
  FailureState,
@@ -12,7 +23,12 @@ import type {
12
23
  EffectConfiguration,
13
24
  StepRunner,
14
25
  RunWrapper,
15
- CommandInterceptor
26
+ CommandInterceptor,
27
+ TraceEntry,
28
+ TraceLog,
29
+ Resolver,
30
+ ReplayOptions,
31
+ Replay
16
32
  } from '../index.js';
17
33
 
18
34
  interface User {
@@ -28,7 +44,8 @@ interface SavedUser {
28
44
 
29
45
  const s = Success(42);
30
46
  expectType<SuccessState<number>>(s);
31
- expectError(Success()); // missing argument
47
+ // @ts-expect-error missing argument
48
+ Success();
32
49
 
33
50
  // --- Failure ---
34
51
 
@@ -46,7 +63,8 @@ expectType<CommandState<number, string>>(
46
63
  )
47
64
  );
48
65
  Command(() => 42, undefined, { name: 'readRow' });
49
- expectError(Command());
66
+ // @ts-expect-error missing cmd
67
+ Command();
50
68
 
51
69
  const cmd = Command(
52
70
  async () => ({ id: 1, email: 'a@b.com' }) as SavedUser,
@@ -57,6 +75,18 @@ const cmd = Command(
57
75
  );
58
76
  expectType<CommandState<SavedUser, SavedUser, unknown>>(cmd);
59
77
 
78
+ // A cmd that only throws, or returns Promise.reject, infers R as never. That has to stay assignable
79
+ // to Effect under strictFunctionTypes, which is why every state's `next` is a method signature
80
+ // (bivariant parameter) rather than a function-typed property (contravariant, so `never` could not
81
+ // widen to the `any` in `Effect`'s CommandState member).
82
+ const throwing = Command(() => {
83
+ throw new Error('boom');
84
+ });
85
+ expectAssignable<Effect<never>>(throwing);
86
+ expectAssignable<Effect<never>>(Command(() => Promise.reject(new Error('down'))));
87
+ expectAssignable<Effect<never>>(Retry(throwing));
88
+ expectAssignable<Effect<never>>(effectPipe(() => throwing)(null));
89
+
60
90
  // --- effectPipe type propagation ---
61
91
 
62
92
  const step1 = (input: User) => Success(input);
@@ -68,7 +98,8 @@ const step2 = (user: User) =>
68
98
 
69
99
  const flow = effectPipe(step1, step2);
70
100
  expectType<Effect<SavedUser>>(flow({ email: 'a@b.com', password: 'secret123' }));
71
- expectError(flow({ email: 'a@b.com' })); // missing password
101
+ // @ts-expect-error missing password
102
+ flow({ email: 'a@b.com' });
72
103
 
73
104
  // --- runEffect return type ---
74
105
 
@@ -104,13 +135,14 @@ const innerCmd = Command(
104
135
  (n) => Success(n)
105
136
  );
106
137
 
107
- // Retry with options preserves T
138
+ // Retry with options preserves T; the error type is the exhaustion failure, not the inner error,
139
+ // because the interpreter wraps the last inner error rather than letting it escape
108
140
  const retried = Retry(innerCmd, { attempts: 3 });
109
- expectType<RetryState<number, unknown>>(retried);
141
+ expectType<RetryState<number, RetryExhaustedError<unknown>>>(retried);
110
142
 
111
143
  // Retry without options is valid
112
144
  const retriedNoOpts = Retry(innerCmd);
113
- expectType<RetryState<number, unknown>>(retriedNoOpts);
145
+ expectType<RetryState<number, RetryExhaustedError<unknown>>>(retriedNoOpts);
114
146
 
115
147
  // Retry in effectPipe preserves type flow
116
148
  const retryFlow = effectPipe((input: User) =>
@@ -122,7 +154,36 @@ const retryFlow = effectPipe((input: User) =>
122
154
  { attempts: 2 }
123
155
  )
124
156
  );
125
- expectType<Effect<SavedUser>>(retryFlow({ email: 'a@b.com', password: 'secret123' }));
157
+ expectType<Effect<SavedUser, RetryExhaustedError<unknown>>>(retryFlow({ email: 'a@b.com', password: 'secret123' }));
158
+
159
+ // The inner error type survives inside the exhaustion failure, so lastError is typed
160
+ const retriedTyped = Retry(Success(1) as Effect<number, 'net_down'>);
161
+ expectType<RetryState<number, RetryExhaustedError<'net_down'>>>(retriedTyped);
162
+
163
+ const retryResult = await runEffect(Retry(Success(1) as Effect<number, 'flaky'>, { attempts: 1 }));
164
+ if (retryResult.type === 'Failure') {
165
+ expectType<RetryExhaustedError<'flaky'>>(retryResult.error);
166
+ expectType<'flaky'>(retryResult.error.lastError);
167
+ }
168
+
169
+ // onExhausted consumes the exhaustion, so the fallback's error type is what remains
170
+ const recovered = Retry(Success(1) as Effect<number, 'flaky'>, {
171
+ attempts: 1,
172
+ onExhausted: (err) => {
173
+ expectType<RetryExhaustedError<'flaky'>>(err);
174
+ return Success(0) as Effect<number, 'cache_miss'>;
175
+ }
176
+ });
177
+ expectType<RetryState<number, 'cache_miss'>>(recovered);
178
+
179
+ const recoveredResult = await runEffect(recovered);
180
+ if (recoveredResult.type === 'Failure') {
181
+ expectType<'cache_miss'>(recoveredResult.error);
182
+ }
183
+
184
+ // onExhausted is per-use only: global retry defaults cannot carry a fallback
185
+ // @ts-expect-error onExhausted is not a global retry default
186
+ configureEffect({ retry: { attempts: 2, onExhausted: () => Success(1) } });
126
187
 
127
188
  // RetryExhaustedError shape is usable for narrowing exhaustion failures
128
189
  const exhaustedErr: RetryExhaustedError<Error> = {
@@ -168,6 +229,14 @@ expectType<Effect<{ email: string; password: string }>>(parallelFlow({ email: 'a
168
229
  const parallelResult = await runEffect(Parallel([Success(1), Success('x')], ([n, s]) => Success({ n, s })));
169
230
  expectType<SuccessState<{ n: number; s: string }> | FailureState<unknown>>(parallelResult);
170
231
 
232
+ // With next omitted, the Parallel resolves to the values tuple itself
233
+ const parBare = Parallel([Success(42), Success('hello')]);
234
+ expectType<ParallelState<[number, string], [number, string]>>(parBare);
235
+ const parBareResult = await runEffect(parBare);
236
+ if (parBareResult.type === 'Success') {
237
+ expectType<[number, string]>(parBareResult.value);
238
+ }
239
+
171
240
  // --- Ctx (context type) ---
172
241
 
173
242
  interface AppCtx {
@@ -189,7 +258,8 @@ const ctxResult = await runEffect(ctxFlow({ email: 'a@b.com', password: 'secret1
189
258
  expectType<SuccessState<{ email: string; password: string; conn: string }> | FailureState<unknown>>(ctxResult);
190
259
 
191
260
  // wrong context shape should error
192
- expectError(runEffect(ctxFlow({ email: 'a@b.com', password: 'secret123' }), { wrong: 'thing' }));
261
+ // @ts-expect-error context does not match Ctx
262
+ runEffect(ctxFlow({ email: 'a@b.com', password: 'secret123' }), { wrong: 'thing' });
193
263
 
194
264
  // --- configureEffect / EffectConfiguration ---
195
265
 
@@ -206,8 +276,10 @@ configureEffect({ retry: { attempts: 5 } });
206
276
  configureEffect({});
207
277
 
208
278
  // rejects invalid shapes
209
- expectError(configureEffect({ onStep: 'not-a-function' }));
210
- expectError(configureEffect({ retry: { attempts: 'three' } }));
279
+ // @ts-expect-error onStep must be a function
280
+ configureEffect({ onStep: 'not-a-function' });
281
+ // @ts-expect-error attempts must be a number
282
+ configureEffect({ retry: { attempts: 'three' } });
211
283
 
212
284
  // hook types are correctly shaped
213
285
  const myStep: StepRunner = async (name, type, op) => {
@@ -229,3 +301,35 @@ const myInterceptor: CommandInterceptor = async (cmd, _ctx) => {
229
301
  // EffectConfiguration is a usable type
230
302
  const config: EffectConfiguration = { onStep: myStep, onRun: myRun, onBeforeCommand: myInterceptor };
231
303
  expectType<EffectConfiguration>(config);
304
+
305
+ // replayEffect returns the flow's outcome beside the unreached entries for a trace, and no unreached for a Resolver
306
+ const traceLog: TraceLog = { trace: [{ command: 'cmdRead', path: '0', result: 1 }] };
307
+ const readRow = Command(() => 42);
308
+ const replayOptions: ReplayOptions = {
309
+ onResolved: (step, outcome) => {
310
+ expectType<string>(step.name);
311
+ expectType<number>(step.index);
312
+ }
313
+ };
314
+ expectType<Promise<Replay<number, unknown>>>(replayEffect(readRow, traceLog, replayOptions));
315
+ expectType<Promise<Replay<number, unknown>>>(replayEffect(readRow, traceLog.trace));
316
+ (async () => {
317
+ const { result, unreached } = await replayEffect(readRow, traceLog);
318
+ expectType<SuccessState<number> | FailureState<unknown>>(result);
319
+ expectType<TraceEntry[]>(unreached);
320
+ const fromResolver = await replayEffect(readRow, () => ({ result: 42 }));
321
+ expectType<SuccessState<number> | FailureState<unknown>>(fromResolver.result);
322
+ // @ts-expect-error a Resolver cannot know what was left unreached
323
+ fromResolver.unreached;
324
+ })();
325
+ // A source typed as the union (a wrapper forwarding whatever it was handed) still type-checks, with unreached optional
326
+ declare const traceOrResolver: TraceLog | TraceEntry[] | Resolver;
327
+ (async () => {
328
+ const forwarded = await replayEffect(readRow, traceOrResolver);
329
+ expectType<SuccessState<number> | FailureState<unknown>>(forwarded.result);
330
+ expectType<TraceEntry[] | undefined>(forwarded.unreached);
331
+ })();
332
+ // @ts-expect-error strict was removed: paths make it unnecessary
333
+ replayEffect(readRow, traceLog, { strict: false });
334
+ // @ts-expect-error unreached is returned, not observed
335
+ replayEffect(readRow, traceLog, { onUnreached: () => {} });
@@ -1,17 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm test *)",
5
- "Bash(npm view *)",
6
- "Bash(npx mocha *)",
7
- "Bash(echo \"exit: $?\")",
8
- "Bash(npx prettier *)",
9
- "Bash(echo \"npm test exit: $?\")",
10
- "Bash(npx tsc *)",
11
- "Bash(echo \"tsc exit: $?\")",
12
- "Read(//tmp/**)",
13
- "Bash(npx esbuild *)",
14
- "Bash(gzip -9)"
15
- ]
16
- }
17
- }