pure-effect 0.8.0 → 0.10.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 -5
- package/.prettierrc +2 -1
- package/CLAUDE.md +137 -21
- package/README.md +267 -172
- package/examples/opentelemetry-example.js +164 -0
- package/examples/recording-example.js +104 -0
- package/index.d.ts +168 -8
- package/index.js +879 -24
- package/package.json +12 -6
- package/test/all.js +1786 -26
- package/test/types.test-d.ts +11 -0
- package/.dirac-symbol-index/data.db +0 -0
- package/opentelemetry-example.js +0 -91
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
|
4
|
+
import { configureEffect } from '../index.js';
|
|
5
|
+
|
|
6
|
+
/** @import { Tracer } from "@opentelemetry/api" */
|
|
7
|
+
/** @import { EffectConfiguration, RunWrapper, StepRunner } from "../index.js" */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reference wiring for OpenTelemetry spans
|
|
11
|
+
*
|
|
12
|
+
* `onRun` opens one span per `runEffect` call and `onStep` opens a child span per Command,
|
|
13
|
+
* which is the whole integration: the hooks already sit exactly where spans want to start
|
|
14
|
+
* and end.
|
|
15
|
+
*
|
|
16
|
+
* Importing this file does nothing on its own. Call `startTelemetrySdk()` to stand up an
|
|
17
|
+
* exporter, and `enableTelemetry()` to install the hooks, or `telemetryHooks()` to get them
|
|
18
|
+
* as a configuration you can merge with others.
|
|
19
|
+
*
|
|
20
|
+
* No Command input or output is put on a span, deliberately. Spans carry names, timings, and
|
|
21
|
+
* status; values belong in a trace, where `recorder`'s `redact` strips them before they leave the
|
|
22
|
+
* process and what remains can be replayed. Copying values onto spans would duplicate them into
|
|
23
|
+
* the one place they cannot be redacted after the fact, and an initial input routinely holds a
|
|
24
|
+
* password, a token, or a card number.
|
|
25
|
+
*
|
|
26
|
+
* These hooks use the raw `onStep` and `onRun` rather than `observe`, because a span has to wrap
|
|
27
|
+
* `op`: `startActiveSpan` is what makes each Command span a child of the run's span. An integration
|
|
28
|
+
* that only watches, such as metrics or logging, should use `observe` instead and cannot break a
|
|
29
|
+
* flow by throwing.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} TelemetryOptions
|
|
34
|
+
* @property {string} [tracerName] - Defaults to `'pure-effect'`.
|
|
35
|
+
* @property {Tracer} [tracer] - Supply a tracer directly instead of resolving one by name. Useful in tests.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A span status message, without putting a Command's data on the span.
|
|
40
|
+
*
|
|
41
|
+
* Only a string error or an Error's message is used. A structured error object can hold the very
|
|
42
|
+
* values this file keeps off spans, and `String(someObject)` would say nothing useful anyway.
|
|
43
|
+
*
|
|
44
|
+
* @param {any} error
|
|
45
|
+
* @returns {string | undefined}
|
|
46
|
+
*/
|
|
47
|
+
const statusMessage = (error) =>
|
|
48
|
+
error instanceof Error ? error.message : typeof error === 'string' ? error : undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds the tracing hooks without installing them.
|
|
52
|
+
*
|
|
53
|
+
* Returning a configuration rather than calling `configureEffect` is what lets tracing coexist with
|
|
54
|
+
* recording: there is one slot per hook, so both concerns go into a single `configureEffect` call,
|
|
55
|
+
* which merges them. A helper that installed itself would silently replace whatever was there.
|
|
56
|
+
*
|
|
57
|
+
* @param {TelemetryOptions} [options]
|
|
58
|
+
* @returns {EffectConfiguration}
|
|
59
|
+
*/
|
|
60
|
+
export function telemetryHooks(options = {}) {
|
|
61
|
+
const { tracerName = 'pure-effect', tracer = trace.getTracer(tracerName) } = options;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The root span is named after the flow, so traces are distinguishable in a UI. Every span
|
|
65
|
+
* ends in a `finally`, so a thrown Command cannot leak one.
|
|
66
|
+
* @type {RunWrapper}
|
|
67
|
+
*/
|
|
68
|
+
const onRun = (effect, pipeline, flowName) =>
|
|
69
|
+
tracer.startActiveSpan(flowName || 'Effect Pipeline', async (rootSpan) => {
|
|
70
|
+
rootSpan.setAttribute('effect.flow', flowName || '');
|
|
71
|
+
try {
|
|
72
|
+
const result = await pipeline();
|
|
73
|
+
if (result.type === 'Failure') {
|
|
74
|
+
rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: statusMessage(result.error) });
|
|
75
|
+
} else {
|
|
76
|
+
rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
} catch (/** @type any */ err) {
|
|
80
|
+
rootSpan.recordException(err);
|
|
81
|
+
rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: err?.message });
|
|
82
|
+
throw err;
|
|
83
|
+
} finally {
|
|
84
|
+
rootSpan.end();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `op` has to be awaited and its result returned. Returning a value without calling `op` is how
|
|
90
|
+
* replay suppresses I/O, so a hook that forgot to call it would silently stop every Command.
|
|
91
|
+
* @type {StepRunner}
|
|
92
|
+
*/
|
|
93
|
+
const onStep = (name, type, op) =>
|
|
94
|
+
tracer.startActiveSpan(name, async (span) => {
|
|
95
|
+
span.setAttribute('effect.type', type);
|
|
96
|
+
try {
|
|
97
|
+
const result = await op();
|
|
98
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
99
|
+
return result;
|
|
100
|
+
} catch (/** @type any */ err) {
|
|
101
|
+
span.recordException(err);
|
|
102
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message });
|
|
103
|
+
throw err;
|
|
104
|
+
} finally {
|
|
105
|
+
span.end();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return { onRun, onStep };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Installs tracing on its own. Pass `telemetryHooks()` to `configureEffect` alongside anything else
|
|
114
|
+
* that needs the hooks instead, since a bare call here replaces whatever was configured before.
|
|
115
|
+
*
|
|
116
|
+
* @param {TelemetryOptions} [options]
|
|
117
|
+
*/
|
|
118
|
+
export function enableTelemetry(options) {
|
|
119
|
+
configureEffect(telemetryHooks(options));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Starts a Node SDK exporting over OTLP, and returns it so a caller can shut it down.
|
|
124
|
+
*
|
|
125
|
+
* Kept out of module scope on purpose: importing a reference file should not open exporters, spawn
|
|
126
|
+
* background work, or register process handlers. The SDK is imported dynamically so the hooks above
|
|
127
|
+
* stay usable wherever `@opentelemetry/api` runs, without pulling the Node SDK in behind them.
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} [options]
|
|
130
|
+
* @param {string} [options.serviceName]
|
|
131
|
+
* @param {string} [options.url] - OTLP HTTP endpoint.
|
|
132
|
+
* @param {boolean} [options.handleSigterm] - Flush and exit on SIGTERM. Off by default, since
|
|
133
|
+
* installing a process handler is the application's decision, not a library's.
|
|
134
|
+
*/
|
|
135
|
+
export async function startTelemetrySdk(options = {}) {
|
|
136
|
+
const { serviceName = 'pure-effect', url = 'http://localhost:4318/v1/traces', handleSigterm = false } = options;
|
|
137
|
+
const [{ NodeSDK }, { OTLPTraceExporter }, { SimpleSpanProcessor }] = await Promise.all([
|
|
138
|
+
import('@opentelemetry/sdk-node'),
|
|
139
|
+
import('@opentelemetry/exporter-trace-otlp-proto'),
|
|
140
|
+
import('@opentelemetry/sdk-trace-base')
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const traceExporter = new OTLPTraceExporter({ url });
|
|
144
|
+
// `spanProcessors` replaced the deprecated singular `spanProcessor`, and passing an exporter
|
|
145
|
+
// alongside a processor that already wraps it was redundant.
|
|
146
|
+
const sdk = new NodeSDK({
|
|
147
|
+
serviceName,
|
|
148
|
+
spanProcessors: [new SimpleSpanProcessor(traceExporter)],
|
|
149
|
+
instrumentations: []
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
sdk.start();
|
|
153
|
+
|
|
154
|
+
if (handleSigterm) {
|
|
155
|
+
process.on('SIGTERM', () => {
|
|
156
|
+
sdk.shutdown()
|
|
157
|
+
.then(() => console.log('Tracing terminated'))
|
|
158
|
+
.catch((error) => console.log('Error terminating tracing', error))
|
|
159
|
+
.finally(() => process.exit(0));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return sdk;
|
|
164
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
+
import { configureEffect, recorder } from '../index.js';
|
|
5
|
+
|
|
6
|
+
/** @import { EffectConfiguration, RunWrapper, StepRunner, CommandInterceptor, TraceLog, SuccessState, FailureState } from "../index.js" */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Example wiring for recording every run of an application.
|
|
10
|
+
* `recordEffect` covers tests and scripts, where one call site holds the whole run. This
|
|
11
|
+
* exists for the other case: recording without touching any call site.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} RecordingStore
|
|
16
|
+
* @property {ReturnType<typeof recorder>} rec
|
|
17
|
+
* @property {string} [flowName]
|
|
18
|
+
* @property {any} initialInput
|
|
19
|
+
* @property {any} context
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} RecordingOptions
|
|
24
|
+
* @property {(trace: TraceLog) => Promise<void> | void} [sink] - Receives a finished trace. Writing to
|
|
25
|
+
* S3 or a database fit here. Prefer not to await slow I/O inside
|
|
26
|
+
* a request: hand the trace to a queue or a background task instead.
|
|
27
|
+
* @property {(value: any, name: string, kind: string) => any} [redact] - Runs before any value enters the
|
|
28
|
+
* trace, so nothing sensitive reaches the sink even in memory. It sees results, serialized errors,
|
|
29
|
+
* and the `initialInput` and `context` the trace stores, distinguished by `kind`.
|
|
30
|
+
* @property {number} [maxEntries] - Caps trace length; the overflow count is reported as `dropped`.
|
|
31
|
+
* @property {boolean} [stack] - Records stack traces for thrown errors.
|
|
32
|
+
* @property {(result: SuccessState<any> | FailureState<any>) => boolean} [keep] - Decides which runs
|
|
33
|
+
* reach the sink. Defaults to failures only. Return `true` always to keep everything, or
|
|
34
|
+
* sample successes with a probability check.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the three hooks that record each run, without installing them.
|
|
39
|
+
*
|
|
40
|
+
* Returning a configuration rather than calling `configureEffect` is what lets recording coexist
|
|
41
|
+
* with tracing: there is one slot per hook, so both concerns go into a single `configureEffect` call,
|
|
42
|
+
* which merges them. A helper that installed itself would silently replace whatever was there.
|
|
43
|
+
*
|
|
44
|
+
* @param {RecordingOptions} [options]
|
|
45
|
+
* @returns {EffectConfiguration}
|
|
46
|
+
*/
|
|
47
|
+
export function recordingHooks(options = {}) {
|
|
48
|
+
const {
|
|
49
|
+
sink = async () => {},
|
|
50
|
+
redact,
|
|
51
|
+
maxEntries = 500,
|
|
52
|
+
stack,
|
|
53
|
+
keep = (result) => result.type === 'Failure'
|
|
54
|
+
} = options;
|
|
55
|
+
/** @type {AsyncLocalStorage<RecordingStore>} */
|
|
56
|
+
const scope = new AsyncLocalStorage();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One recorder per run, held in async-local scope so `onStep` can find the right one without a
|
|
60
|
+
* recorder being threaded through any business-logic signature. A single module-level recorder
|
|
61
|
+
* would interleave the steps of concurrent runs into one trace.
|
|
62
|
+
* @type {RunWrapper}
|
|
63
|
+
*/
|
|
64
|
+
const onRun = async (effect, pipeline, flowName) => {
|
|
65
|
+
const rec = recorder({ redact, maxEntries, stack });
|
|
66
|
+
/** @type {RecordingStore} */
|
|
67
|
+
const store = { rec, flowName, initialInput: /** @type {any} */ (effect).initialInput, context: undefined };
|
|
68
|
+
return scope.run(store, async () => {
|
|
69
|
+
const result = await pipeline();
|
|
70
|
+
if (keep(result)) await sink(rec.toTrace(store));
|
|
71
|
+
return result;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Outside a recorded run there is no store, so the Command runs untouched.
|
|
77
|
+
* @type {StepRunner}
|
|
78
|
+
*/
|
|
79
|
+
const onStep = async (name, type, op) => {
|
|
80
|
+
const store = scope.getStore();
|
|
81
|
+
return store ? await store.rec.onStep(name, type, op) : await op();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `onRun` never sees the context, so it is captured from the first Command of the run.
|
|
86
|
+
* @type {CommandInterceptor}
|
|
87
|
+
*/
|
|
88
|
+
const onBeforeCommand = async (command, context) => {
|
|
89
|
+
const store = scope.getStore();
|
|
90
|
+
if (store && store.context === undefined) store.context = context;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return { onRun, onStep, onBeforeCommand };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Installs recording on its own. Pass `recordingHooks()` to `configureEffect` alongside anything else
|
|
98
|
+
* that needs the hooks instead, since a bare call here replaces whatever was configured before.
|
|
99
|
+
*
|
|
100
|
+
* @param {RecordingOptions} [options]
|
|
101
|
+
*/
|
|
102
|
+
export function enableRecording(options) {
|
|
103
|
+
configureEffect(recordingHooks(options));
|
|
104
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -10,11 +10,25 @@ export type FailureState<E = unknown> = {
|
|
|
10
10
|
initialInput?: unknown;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Metadata attached to a Command. A string `name` is read by the interpreter as the Command's
|
|
15
|
+
* identity for traces, replay matching, and telemetry spans; every other key is carried through
|
|
16
|
+
* untouched for `onBeforeCommand`.
|
|
17
|
+
*/
|
|
18
|
+
export type CommandMeta = { name?: string } & Record<string, unknown>;
|
|
19
|
+
|
|
13
20
|
export type CommandState<R, T, E = unknown, Ctx = unknown> = {
|
|
14
21
|
type: 'Command';
|
|
15
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Performs the side effect. Inside a `Parallel` branch it receives an `AbortSignal` that fires when a
|
|
24
|
+
* sibling branch fails; forward it to `fetch`, a driver, or an `AbortController`-aware client to have
|
|
25
|
+
* the work cancelled in flight. Ignoring it is fine and is what every thunk written before this did:
|
|
26
|
+
* the interpreter still refuses to start any *later* Command in a cancelled branch. Outside a
|
|
27
|
+
* `Parallel` no argument is passed at all.
|
|
28
|
+
*/
|
|
29
|
+
cmd: (signal?: AbortSignal) => Promise<R> | R;
|
|
16
30
|
next: (result: R) => Effect<T, E, Ctx>;
|
|
17
|
-
meta?:
|
|
31
|
+
meta?: CommandMeta;
|
|
18
32
|
initialInput?: unknown;
|
|
19
33
|
};
|
|
20
34
|
|
|
@@ -63,10 +77,16 @@ export declare function Success<T>(value: T): SuccessState<T>;
|
|
|
63
77
|
|
|
64
78
|
export declare function Failure<E = unknown>(error: E, initialInput?: unknown): FailureState<E>;
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
/**
|
|
81
|
+
* `next` is optional and defaults to `(result) => Success(result)`, which is what most Commands want.
|
|
82
|
+
*
|
|
83
|
+
* A `meta.name` becomes this Command's identity, which keeps it independent of how `cmd` was declared
|
|
84
|
+
* and immune to minification. Without one the identity falls back to `cmd.name`, then to 'anonymous'.
|
|
85
|
+
*/
|
|
86
|
+
export declare function Command<R, T = R, E = unknown, Ctx = unknown>(
|
|
87
|
+
cmd: (signal?: AbortSignal) => Promise<R> | R,
|
|
88
|
+
next?: (result: R) => Effect<T, E, Ctx>,
|
|
89
|
+
meta?: CommandMeta
|
|
70
90
|
): CommandState<R, T, E, Ctx>;
|
|
71
91
|
|
|
72
92
|
export declare function Ask<T, E = unknown, Ctx = unknown>(
|
|
@@ -218,7 +238,12 @@ export declare function effectPipe<
|
|
|
218
238
|
f8: (i: I) => Effect<J, E8, Ctx>
|
|
219
239
|
): (start: A) => Effect<J, E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8, Ctx>;
|
|
220
240
|
|
|
221
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Wraps one Command execution. `path` is the Command's position in the Effect tree rather than its
|
|
243
|
+
* position in completion order, so it is the same in a replay as in the recorded run even when
|
|
244
|
+
* `Parallel` branches finish in a different order. Hooks that take three parameters are unaffected.
|
|
245
|
+
*/
|
|
246
|
+
export type StepRunner = (name: string, type: string, op: () => Promise<unknown>, path?: string) => Promise<unknown>;
|
|
222
247
|
|
|
223
248
|
export type RunWrapper = (
|
|
224
249
|
effect: Effect<unknown>,
|
|
@@ -235,10 +260,145 @@ export interface EffectConfiguration {
|
|
|
235
260
|
retry?: RetryOptions;
|
|
236
261
|
}
|
|
237
262
|
|
|
238
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Configures the global runner. Several configurations can be passed and are merged, which is how
|
|
265
|
+
* independent concerns share the one slot each hook has: `onStep` and `onRun` nest with the first
|
|
266
|
+
* outermost, `onBeforeCommand` interceptors all run in order, and `retry` merges with later winning.
|
|
267
|
+
* Merging does not accumulate across calls: a later call replaces the previous wiring, and calling
|
|
268
|
+
* this with nothing resets every slot to its default.
|
|
269
|
+
*
|
|
270
|
+
* Returns a function that puts back whatever was installed when this call was made, so a caller can
|
|
271
|
+
* install hooks without owning the global wiring forever. Restoring is a snapshot rather than a stack:
|
|
272
|
+
* if a later call has run since, restoring reverts to the older snapshot and discards the newer one.
|
|
273
|
+
*/
|
|
274
|
+
export declare function configureEffect(...configs: (EffectConfiguration | undefined)[]): () => void;
|
|
239
275
|
|
|
240
276
|
export declare function runEffect<T, E = unknown, Ctx = unknown>(
|
|
241
277
|
effect: Effect<T, E, Ctx>,
|
|
242
278
|
context?: Ctx,
|
|
243
279
|
callConfig?: EffectConfiguration
|
|
244
280
|
): Promise<SuccessState<T> | FailureState<E>>;
|
|
281
|
+
|
|
282
|
+
export type ReplayStep = {
|
|
283
|
+
/**
|
|
284
|
+
* Zero-based position in this run's completion order. Not stable for a flow containing `Parallel`,
|
|
285
|
+
* whose branches finish in whatever order they finish in. Prefer `path`.
|
|
286
|
+
*/
|
|
287
|
+
index: number;
|
|
288
|
+
/** `cmd.name`, or 'anonymous'. */
|
|
289
|
+
name: string;
|
|
290
|
+
/** Always 'Command' today; reserved. */
|
|
291
|
+
type: string;
|
|
292
|
+
/**
|
|
293
|
+
* The Command's position in the Effect tree, stable across runs: steps are numbered within a
|
|
294
|
+
* subtree, and each `Parallel` branch and `Retry` attempt opens its own prefix. This is what a
|
|
295
|
+
* replay matches on, and what a Resolver should key off. Absent on traces recorded before paths.
|
|
296
|
+
*/
|
|
297
|
+
path?: string;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* What production observed for a step. `{ result }` feeds the Command's `next`;
|
|
302
|
+
* `{ error }` is thrown so the interpreter produces a Failure. A Resolver returning
|
|
303
|
+
* `undefined` means "not recorded", which is distinct from `{ result: undefined }`.
|
|
304
|
+
*/
|
|
305
|
+
export type ReplayOutcome = { result: unknown } | { error: unknown };
|
|
306
|
+
|
|
307
|
+
export type Resolver = (step: ReplayStep) => ReplayOutcome | undefined;
|
|
308
|
+
|
|
309
|
+
export type TraceEntry = {
|
|
310
|
+
command: string;
|
|
311
|
+
/**
|
|
312
|
+
* The Command's position in the Effect tree. Order-independent, so a replay lines a recorded step
|
|
313
|
+
* up with the step that asked for it even when `Parallel` branches finish out of order.
|
|
314
|
+
*/
|
|
315
|
+
path?: string;
|
|
316
|
+
result?: unknown;
|
|
317
|
+
error?: unknown;
|
|
318
|
+
/** How long the Command took in production, rounded to microseconds. */
|
|
319
|
+
durationMs?: number;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/** The reference trace format. A convenience, not a contract: write a Resolver for any other shape. */
|
|
323
|
+
export type TraceLog = {
|
|
324
|
+
flowName?: string;
|
|
325
|
+
version?: string;
|
|
326
|
+
initialInput?: unknown;
|
|
327
|
+
context?: unknown;
|
|
328
|
+
dropped?: number;
|
|
329
|
+
trace: TraceEntry[];
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export interface RecorderOptions {
|
|
333
|
+
/**
|
|
334
|
+
* Scrubs every value a trace holds: each Command's result, each serialized error, and the
|
|
335
|
+
* `initialInput` and `context` stored on the trace itself. `kind` is `'result'`, `'error'`,
|
|
336
|
+
* `'initialInput'`, or `'context'`; `name` is the Command's name for the first two and the kind for
|
|
337
|
+
* the last two. This is the single place PII is kept out of a trace, which is why it sees all four.
|
|
338
|
+
*/
|
|
339
|
+
redact?: (value: unknown, name: string, kind: 'result' | 'error' | 'initialInput' | 'context') => unknown;
|
|
340
|
+
/** Cap trace length; further steps are counted in `dropped`, not stored. */
|
|
341
|
+
maxEntries?: number;
|
|
342
|
+
/** Record stack traces for thrown errors. Off by default. */
|
|
343
|
+
stack?: boolean;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface TraceMeta {
|
|
347
|
+
initialInput?: unknown;
|
|
348
|
+
flowName?: string;
|
|
349
|
+
context?: unknown;
|
|
350
|
+
version?: string;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export declare function recorder(options?: RecorderOptions): {
|
|
354
|
+
onStep: StepRunner;
|
|
355
|
+
entries: TraceEntry[];
|
|
356
|
+
toTrace(meta?: TraceMeta): TraceLog;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export declare function recordEffect<T, E = unknown, Ctx = unknown>(
|
|
360
|
+
flowFn: (input: any) => Effect<T, E, Ctx>,
|
|
361
|
+
initialInput: any,
|
|
362
|
+
options?: RecorderOptions & { context?: Ctx; version?: string }
|
|
363
|
+
): Promise<{ result: SuccessState<T> | FailureState<E>; trace: TraceLog }>;
|
|
364
|
+
|
|
365
|
+
export interface ReplayOptions<Ctx = unknown> {
|
|
366
|
+
/** Context for `Ask`. Pass the recorded context to reproduce a run faithfully. */
|
|
367
|
+
context?: Ctx;
|
|
368
|
+
/** Strip `Retry` delays so a replay does not wait out production backoff (default `true`). */
|
|
369
|
+
fastRetry?: boolean;
|
|
370
|
+
/** Allow configured `onRun` / `onBeforeCommand` to fire (default `false`). */
|
|
371
|
+
hooks?: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* What to do when the Resolver has no recording for a step.
|
|
374
|
+
* `'throw'` (default) fails the replay, making side effects impossible.
|
|
375
|
+
* `'execute'` runs the real Command: recorded prefix, live tail.
|
|
376
|
+
*/
|
|
377
|
+
onMissing?: 'throw' | 'execute';
|
|
378
|
+
onResolved?: (step: ReplayStep, outcome: ReplayOutcome | undefined) => void;
|
|
379
|
+
/**
|
|
380
|
+
* Legacy matching mode, used only for a trace whose entries carry no `path`. `true` (default)
|
|
381
|
+
* consumes entries positionally and raises a `TimeParadox` on divergence; `false` matches
|
|
382
|
+
* per-Command FIFO queues, which used to be required for flows containing `Parallel`. Ignored
|
|
383
|
+
* for a trace carrying paths, which is matched by path, and when a Resolver is supplied, since
|
|
384
|
+
* that Resolver has already chosen how it matches.
|
|
385
|
+
*/
|
|
386
|
+
strict?: boolean;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Pass a trace to replay it directly, or a Resolver when traces are stored in some other
|
|
391
|
+
* shape. To observe a replay rather than change how it resolves, use `onResolved`.
|
|
392
|
+
* A malformed trace rejects with a `ReplayError`.
|
|
393
|
+
*/
|
|
394
|
+
export declare function replayEffect<T, E = unknown, Ctx = unknown>(
|
|
395
|
+
effect: Effect<T, E, Ctx>,
|
|
396
|
+
traceOrResolver: Resolver | TraceLog | TraceEntry[],
|
|
397
|
+
options?: ReplayOptions<Ctx>
|
|
398
|
+
): Promise<SuccessState<T> | FailureState<E>>;
|
|
399
|
+
|
|
400
|
+
export declare function timeTravel<T, E = unknown, Ctx = unknown>(
|
|
401
|
+
flowFn: (input: any) => Effect<T, E, Ctx>,
|
|
402
|
+
traceLog: TraceLog,
|
|
403
|
+
options?: { strict?: boolean; log?: (...args: any[]) => void; context?: Ctx; version?: string }
|
|
404
|
+
): Promise<SuccessState<T> | FailureState<E>>;
|