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.
- package/.claude/settings.local.json +9 -4
- package/.prettierrc +2 -1
- package/CLAUDE.md +139 -20
- package/README.md +272 -135
- package/examples/opentelemetry-example.js +164 -0
- package/examples/recording-example.js +104 -0
- package/index.d.ts +153 -6
- package/index.js +736 -15
- package/package.json +12 -6
- package/test/all.js +1584 -25
- package/test/types.test-d.ts +100 -2
- package/.dirac-symbol-index/data.db +0 -0
- package/opentelemetry-example.js +0 -91
package/test/types.test-d.ts
CHANGED
|
@@ -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
|
package/opentelemetry-example.js
DELETED
|
@@ -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
|
-
}
|