pure-effect 0.7.0 → 0.9.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,13 +1,18 @@
1
1
  import { expectType, expectError } from 'tsd';
2
- import { Success, Failure, Command, Ask, Retry, effectPipe, runEffect } from '../index.js';
2
+ import { Success, Failure, Command, Ask, Retry, Parallel, effectPipe, runEffect, configureEffect } from '../index.js';
3
3
  import type {
4
4
  SuccessState,
5
5
  FailureState,
6
6
  CommandState,
7
7
  AskState,
8
8
  RetryState,
9
+ ParallelState,
9
10
  RetryExhaustedError,
10
- Effect
11
+ Effect,
12
+ EffectConfiguration,
13
+ StepRunner,
14
+ RunWrapper,
15
+ CommandInterceptor
11
16
  } from '../index.js';
12
17
 
13
18
  interface User {
@@ -32,6 +37,17 @@ expectType<FailureState<string>>(f);
32
37
 
33
38
  // --- Command ---
34
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
+
35
51
  const cmd = Command(
36
52
  async () => ({ id: 1, email: 'a@b.com' }) as SavedUser,
37
53
  (saved) => {
@@ -131,3 +147,85 @@ expectType<Effect<SavedUser, ValidationError | DbError>>(typedFlow({ email: 'a@b
131
147
 
132
148
  const typedResult = await runEffect(typedFlow({ email: 'a@b.com', password: 'secret123' }));
133
149
  expectType<SuccessState<SavedUser> | FailureState<ValidationError | DbError>>(typedResult);
150
+
151
+ // --- Parallel ---
152
+
153
+ // Values tuple is correctly typed
154
+ const par = Parallel([Success(42), Success('hello')], ([n, s]) => {
155
+ expectType<number>(n);
156
+ expectType<string>(s);
157
+ return Success({ n, s });
158
+ });
159
+ expectType<ParallelState<[number, string], { n: number; s: string }>>(par);
160
+
161
+ // Parallel in effectPipe preserves type flow
162
+ const parallelFlow = effectPipe((input: User) =>
163
+ Parallel([Success(input.email), Success(input.password)], ([email, password]) => Success({ email, password }))
164
+ );
165
+ expectType<Effect<{ email: string; password: string }>>(parallelFlow({ email: 'a@b.com', password: 'secret123' }));
166
+
167
+ // runEffect return type flows through Parallel
168
+ const parallelResult = await runEffect(Parallel([Success(1), Success('x')], ([n, s]) => Success({ n, s })));
169
+ expectType<SuccessState<{ n: number; s: string }> | FailureState<unknown>>(parallelResult);
170
+
171
+ // --- Ctx (context type) ---
172
+
173
+ interface AppCtx {
174
+ db: string;
175
+ }
176
+
177
+ // Ask infers Ctx from callback parameter type
178
+ const askWithCtx = Ask((ctx: AppCtx) => Success(ctx.db));
179
+ expectType<AskState<string, unknown, AppCtx>>(askWithCtx);
180
+
181
+ // effectPipe propagates Ctx through steps
182
+ const ctxFlow = effectPipe((input: User) => Ask((ctx: AppCtx) => Success({ ...input, conn: ctx.db })));
183
+ expectType<Effect<{ email: string; password: string; conn: string }, unknown, AppCtx>>(
184
+ ctxFlow({ email: 'a@b.com', password: 'secret123' })
185
+ );
186
+
187
+ // runEffect enforces context argument matches Ctx
188
+ const ctxResult = await runEffect(ctxFlow({ email: 'a@b.com', password: 'secret123' }), { db: 'conn' });
189
+ expectType<SuccessState<{ email: string; password: string; conn: string }> | FailureState<unknown>>(ctxResult);
190
+
191
+ // wrong context shape should error
192
+ expectError(runEffect(ctxFlow({ email: 'a@b.com', password: 'secret123' }), { wrong: 'thing' }));
193
+
194
+ // --- configureEffect / EffectConfiguration ---
195
+
196
+ // accepts full configuration
197
+ configureEffect({
198
+ onStep: async (_name, _type, op) => op(),
199
+ onRun: async (_effect, op, _flowName) => op(),
200
+ onBeforeCommand: async (_cmd, _ctx) => {},
201
+ retry: { attempts: 3, delay: 100, backoff: 2 }
202
+ });
203
+
204
+ // accepts partial configuration
205
+ configureEffect({ retry: { attempts: 5 } });
206
+ configureEffect({});
207
+
208
+ // rejects invalid shapes
209
+ expectError(configureEffect({ onStep: 'not-a-function' }));
210
+ expectError(configureEffect({ retry: { attempts: 'three' } }));
211
+
212
+ // hook types are correctly shaped
213
+ const myStep: StepRunner = async (name, type, op) => {
214
+ expectType<string>(name);
215
+ expectType<string>(type);
216
+ return op();
217
+ };
218
+
219
+ const myRun: RunWrapper = async (effect, op, flowName) => {
220
+ expectType<Effect<unknown>>(effect);
221
+ expectType<string | undefined>(flowName);
222
+ return op();
223
+ };
224
+
225
+ const myInterceptor: CommandInterceptor = async (cmd, _ctx) => {
226
+ expectType<CommandState<unknown, unknown>>(cmd);
227
+ };
228
+
229
+ // EffectConfiguration is a usable type
230
+ const config: EffectConfiguration = { onStep: myStep, onRun: myRun, onBeforeCommand: myInterceptor };
231
+ expectType<EffectConfiguration>(config);
Binary file
@@ -1,91 +0,0 @@
1
- // @ts-check
2
-
3
- import { NodeSDK } from '@opentelemetry/sdk-node';
4
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
5
- import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
6
- import { trace, SpanStatusCode } from '@opentelemetry/api';
7
- import { configureEffect } from './index.js';
8
-
9
- /** @import { RunWrapper, StepRunner } from "./index.js" */
10
-
11
- const traceExporter = new OTLPTraceExporter({
12
- url: 'http://localhost:4318/v1/traces'
13
- });
14
-
15
- const sdk = new NodeSDK({
16
- serviceName: 'pure-effect-test',
17
- traceExporter,
18
- spanProcessor: new SimpleSpanProcessor(traceExporter),
19
- instrumentations: []
20
- });
21
-
22
- sdk.start();
23
-
24
- process.on('SIGTERM', () => {
25
- sdk.shutdown()
26
- .then(() => console.log('Tracing terminated'))
27
- .catch((error) => console.log('Error terminating tracing', error))
28
- .finally(() => process.exit(0));
29
- });
30
-
31
- export function enableTelemetry() {
32
- const tracer = trace.getTracer('pure-effect-test');
33
- configureEffect({
34
- /** @type RunWrapper */
35
- onRun: (effect, pipeline, flowName) => {
36
- return tracer.startActiveSpan('Effect Pipeline', async (rootSpan) => {
37
- try {
38
- rootSpan.setAttribute('effect.initialInput', JSON.stringify(effect.initialInput));
39
- rootSpan.setAttribute('effect.flow', flowName || '');
40
-
41
- const result = await pipeline();
42
-
43
- if (result.type === 'Failure') {
44
- rootSpan.setStatus({
45
- code: SpanStatusCode.ERROR,
46
- message: String(result.error)
47
- });
48
- } else {
49
- rootSpan.setStatus({ code: SpanStatusCode.OK });
50
- }
51
-
52
- return result;
53
- } catch (/** @type any */ err) {
54
- rootSpan.recordException(err);
55
- rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
56
- throw err;
57
- } finally {
58
- rootSpan.end();
59
- }
60
- });
61
- },
62
- /** @type StepRunner */
63
- onStep: (name, type, op) => {
64
- return tracer.startActiveSpan(name, async (span) => {
65
- span.setAttribute('effect.type', type);
66
- try {
67
- const result = await op();
68
- try {
69
- if (result !== undefined) {
70
- const outputString = typeof result === 'object' ? JSON.stringify(result) : String(result);
71
- span.setAttribute('effect.output', outputString);
72
- }
73
- } catch (serializationError) {
74
- span.setAttribute('effect.output', '[Circular or Non-Serializable Data]');
75
- }
76
- span.setStatus({ code: SpanStatusCode.OK });
77
- return result;
78
- } catch (/** @type any */ err) {
79
- span.recordException(err);
80
- span.setStatus({
81
- code: SpanStatusCode.ERROR,
82
- message: err.message
83
- });
84
- throw err;
85
- } finally {
86
- span.end();
87
- }
88
- });
89
- }
90
- });
91
- }