pure-effect 0.17.0 → 0.19.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/CHANGELOG.md +35 -0
- package/README.md +621 -256
- package/examples/opentelemetry-example.js +5 -1
- package/examples/recording-example.js +31 -12
- package/index.d.ts +136 -47
- package/index.js +1883 -1016
- package/package.json +7 -3
|
@@ -72,7 +72,11 @@ export function telemetryHooks(options = {}) {
|
|
|
72
72
|
try {
|
|
73
73
|
const result = await pipeline();
|
|
74
74
|
if (result.type === 'Failure') {
|
|
75
|
-
|
|
75
|
+
// Left out rather than set to undefined, which a project with exactOptionalPropertyTypes refuses.
|
|
76
|
+
const message = statusMessage(result.error);
|
|
77
|
+
rootSpan.setStatus(
|
|
78
|
+
message === undefined ? { code: SpanStatusCode.ERROR } : { code: SpanStatusCode.ERROR, message }
|
|
79
|
+
);
|
|
76
80
|
} else {
|
|
77
81
|
rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
78
82
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
4
|
import { configureEffect, recorder, Failure } from 'pure-effect';
|
|
5
5
|
|
|
6
|
-
/** @import {
|
|
6
|
+
/** @import { EffectConfiguration, RunWrapper, StepRunner, CommandInterceptor, TraceEntry, TraceLog, SuccessState, FailureState } from "pure-effect" */
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Records every run of an application without touching any call site, one trace per run. `recordEffect` covers
|
|
@@ -23,7 +23,8 @@ import { configureEffect, recorder, Failure } from 'pure-effect';
|
|
|
23
23
|
* database. Hand slow writes to a queue rather than awaiting them inside a request.
|
|
24
24
|
* @property {(value: any, name: string, kind: string) => any} [redact] - Scrubs every value before it enters the
|
|
25
25
|
* trace: results, serialized errors, and the stored `initialInput` and `context`, told apart by `kind`.
|
|
26
|
-
* It receives a copy, so changing it in place never reaches the run.
|
|
26
|
+
* It receives a copy, so changing it in place never reaches the run. A value it throws on is left out of
|
|
27
|
+
* the trace, which marks where, as is one the recorder cannot copy.
|
|
27
28
|
* @property {number} [maxEntries] - Caps trace length, 500 by default; the overflow is reported as `dropped`.
|
|
28
29
|
* @property {boolean} [stack] - Records stack traces for thrown errors.
|
|
29
30
|
* @property {(result: SuccessState<any> | FailureState<any>) => boolean} [keep] - Decides which runs reach the sink:
|
|
@@ -32,8 +33,8 @@ import { configureEffect, recorder, Failure } from 'pure-effect';
|
|
|
32
33
|
* @property {(error: unknown, flowName?: string) => void} [onSinkError] - Receives an error thrown by `keep` or
|
|
33
34
|
* `sink`. Defaults to `console.error`.
|
|
34
35
|
* @property {(message: string, flowName?: string) => void} [onWarning] - Receives a warning, once per flow, about a
|
|
35
|
-
* kept trace that will not replay as recorded: it has no input, `maxEntries` cut it short,
|
|
36
|
-
* steps are named 'anonymous'. Defaults to `console.warn`.
|
|
36
|
+
* kept trace that will not replay as recorded: it has no input, `maxEntries` cut it short, some of its values
|
|
37
|
+
* could not be recorded, or some of its steps are named 'anonymous'. Defaults to `console.warn`.
|
|
37
38
|
*/
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -60,10 +61,12 @@ export function recordingHooks(options = {}) {
|
|
|
60
61
|
const warned = new Set();
|
|
61
62
|
|
|
62
63
|
/** @type {RunWrapper} */
|
|
63
|
-
const onRun = async (effect, pipeline, flowName) => {
|
|
64
|
+
const onRun = async (effect, pipeline, flowName, initialInput) => {
|
|
64
65
|
const rec = recorder({ redact, maxEntries, stack });
|
|
65
66
|
// Packaged before the run, so a Command that changes its input cannot rewrite what the trace says it received.
|
|
66
|
-
|
|
67
|
+
// `toTrace` never throws, which matters here, where a throw would stop the run before it started: an input it
|
|
68
|
+
// cannot record is left out and named in the trace's `unrecorded`.
|
|
69
|
+
const head = rec.toTrace({ flowName, initialInput });
|
|
67
70
|
return scope.run({ rec, head, contextCaptured: false }, async () => {
|
|
68
71
|
/** @type {{ result: SuccessState<any> | FailureState<any> } | { error: unknown }} */
|
|
69
72
|
let outcome;
|
|
@@ -78,7 +81,10 @@ export function recordingHooks(options = {}) {
|
|
|
78
81
|
try {
|
|
79
82
|
if (keep(result)) {
|
|
80
83
|
const { dropped = 0, trace } = rec.toTrace();
|
|
81
|
-
|
|
84
|
+
const unrecorded =
|
|
85
|
+
Object.keys(head.unrecorded ?? {}).length +
|
|
86
|
+
trace.filter((e) => e.unrecorded !== undefined).length;
|
|
87
|
+
warnAboutReplay(flowName, initialInput, dropped, trace, unrecorded);
|
|
82
88
|
await sink({ ...head, dropped, trace });
|
|
83
89
|
}
|
|
84
90
|
} catch (error) {
|
|
@@ -92,17 +98,18 @@ export function recordingHooks(options = {}) {
|
|
|
92
98
|
/**
|
|
93
99
|
* Warns, once per flow, about a kept trace that will not replay as recorded.
|
|
94
100
|
* @param {string} flowName
|
|
95
|
-
* @param {
|
|
101
|
+
* @param {unknown} initialInput
|
|
96
102
|
* @param {number} dropped
|
|
97
103
|
* @param {TraceEntry[]} trace
|
|
104
|
+
* @param {number} unrecorded - How many values the trace leaves out because they could not be recorded
|
|
98
105
|
*/
|
|
99
|
-
const warnAboutReplay = (flowName,
|
|
106
|
+
const warnAboutReplay = (flowName, initialInput, dropped, trace, unrecorded) => {
|
|
100
107
|
const warnOnce = (/** @type {string} */ kind, /** @type {string} */ message) => {
|
|
101
108
|
if (warned.has(`${kind}:${flowName}`)) return;
|
|
102
109
|
warned.add(`${kind}:${flowName}`);
|
|
103
110
|
onWarning(`Recording '${flowName || 'flow'}': ${message}`, flowName);
|
|
104
111
|
};
|
|
105
|
-
if (
|
|
112
|
+
if (initialInput === undefined) {
|
|
106
113
|
warnOnce(
|
|
107
114
|
'input',
|
|
108
115
|
'the flow carries no input, so its traces hold none and timeTravel rebuilds it from undefined. ' +
|
|
@@ -116,6 +123,14 @@ export function recordingHooks(options = {}) {
|
|
|
116
123
|
'the first step it lacks. Raise maxEntries for this flow to replay whole runs.'
|
|
117
124
|
);
|
|
118
125
|
}
|
|
126
|
+
if (unrecorded > 0) {
|
|
127
|
+
warnOnce(
|
|
128
|
+
'unrecorded',
|
|
129
|
+
`${unrecorded} of the values a kept trace holds could not be recorded, because redact threw on them or ` +
|
|
130
|
+
'they could not be copied, so it replays only up to the first of them. Make redact handle every ' +
|
|
131
|
+
"value it is given, null included, and keep Commands' results to plain data."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
119
134
|
const anonymous = trace.filter((entry) => entry.command === 'anonymous').length;
|
|
120
135
|
if (anonymous > 0) {
|
|
121
136
|
warnOnce(
|
|
@@ -146,14 +161,18 @@ export function recordingHooks(options = {}) {
|
|
|
146
161
|
|
|
147
162
|
/**
|
|
148
163
|
* `onRun` never sees the context, so it is copied from the run's first Command, before that Command can change
|
|
149
|
-
* it. A run that stops before any Command records none.
|
|
164
|
+
* it. A run that stops before any Command records none. `toTrace` never throws, which matters here, where a throw
|
|
165
|
+
* would veto the Command.
|
|
150
166
|
* @type {CommandInterceptor}
|
|
151
167
|
*/
|
|
152
168
|
const onBeforeCommand = async (command, context) => {
|
|
153
169
|
const store = scope.getStore();
|
|
154
170
|
if (!store || store.contextCaptured) return;
|
|
155
171
|
store.contextCaptured = true;
|
|
156
|
-
|
|
172
|
+
const withContext = store.rec.toTrace({ context });
|
|
173
|
+
store.head.context = withContext.context;
|
|
174
|
+
// A context it could not record is left out, and the trace the run started with has to say so too.
|
|
175
|
+
if (withContext.unrecorded) store.head.unrecorded = { ...store.head.unrecorded, ...withContext.unrecorded };
|
|
157
176
|
};
|
|
158
177
|
|
|
159
178
|
return { onRun, onStep, onBeforeCommand };
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
export type SuccessState<T> = {
|
|
2
2
|
type: 'Success';
|
|
3
3
|
value: T;
|
|
4
|
-
initialInput?: unknown;
|
|
5
4
|
};
|
|
6
5
|
|
|
7
6
|
export type FailureState<E = unknown> = {
|
|
8
7
|
type: 'Failure';
|
|
9
8
|
error: E;
|
|
10
|
-
initialInput?: unknown;
|
|
11
9
|
};
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -27,22 +25,21 @@ export type CommandState<R, T, E = unknown, Ctx = unknown> = {
|
|
|
27
25
|
/** Receives `cmd`'s result and returns the next step. */
|
|
28
26
|
next(result: R): Effect<T, E, Ctx>;
|
|
29
27
|
meta?: CommandMeta;
|
|
30
|
-
initialInput?: unknown;
|
|
31
28
|
};
|
|
32
29
|
|
|
33
30
|
export type AskState<T, E = unknown, Ctx = unknown> = {
|
|
34
31
|
type: 'Ask';
|
|
35
32
|
next(context: Ctx): Effect<T, E, Ctx>;
|
|
36
|
-
initialInput?: unknown;
|
|
37
33
|
};
|
|
38
34
|
|
|
35
|
+
// Every optional option and hook takes `undefined`, which keeps its default, under `exactOptionalPropertyTypes` too.
|
|
39
36
|
export type RetryOptions = {
|
|
40
37
|
/** Tries after the first, so `2` makes at most 3 calls. A positive integer, 3 by default. */
|
|
41
|
-
attempts?: number;
|
|
38
|
+
attempts?: number | undefined;
|
|
42
39
|
/** Milliseconds before the first retry, 100 by default. */
|
|
43
|
-
delay?: number;
|
|
40
|
+
delay?: number | undefined;
|
|
44
41
|
/** Multiplies the wait before each later retry, so `2` doubles it. 1 by default, the same wait each time. */
|
|
45
|
-
backoff?: number;
|
|
42
|
+
backoff?: number | undefined;
|
|
46
43
|
};
|
|
47
44
|
|
|
48
45
|
/**
|
|
@@ -52,9 +49,9 @@ export type RetryOptions = {
|
|
|
52
49
|
export type RetryState<T, E = unknown, Ctx = unknown, R = T> = {
|
|
53
50
|
type: 'Retry';
|
|
54
51
|
effect: Effect<R, any, Ctx>;
|
|
55
|
-
|
|
52
|
+
/** A frozen copy of the options the `Retry` was built with. */
|
|
53
|
+
options: Readonly<RetryOptions & { onExhausted?: (error: RetryExhaustedError) => Effect<R, any, Ctx> }>;
|
|
56
54
|
next(value: R): Effect<T, E, Ctx>;
|
|
57
|
-
initialInput?: unknown;
|
|
58
55
|
};
|
|
59
56
|
|
|
60
57
|
/** The error a `Retry` fails with once every attempt has thrown. `lastError` is what the last attempt threw. */
|
|
@@ -67,9 +64,9 @@ export type RetryExhaustedError<Thrown = unknown> = {
|
|
|
67
64
|
|
|
68
65
|
export type ParallelOptions = {
|
|
69
66
|
/** Most branches in flight at once. Results and paths stay in array order regardless. */
|
|
70
|
-
limit?: number;
|
|
67
|
+
limit?: number | undefined;
|
|
71
68
|
/** Hand every branch's outcome to `next` instead of failing on the first one. */
|
|
72
|
-
settled?: boolean;
|
|
69
|
+
settled?: boolean | undefined;
|
|
73
70
|
};
|
|
74
71
|
|
|
75
72
|
/** `V` is what `next` receives: the branch values, or their outcomes under `settled`. `BranchError` types `effects`. */
|
|
@@ -84,8 +81,8 @@ export type ParallelState<
|
|
|
84
81
|
type: 'Parallel';
|
|
85
82
|
effects: { [K in keyof T]: Effect<T[K], BranchError, Ctx> };
|
|
86
83
|
next(values: [...V]): Effect<R, E, Ctx>;
|
|
87
|
-
options
|
|
88
|
-
|
|
84
|
+
/** A frozen copy of the options the `Parallel` was built with. */
|
|
85
|
+
options?: Readonly<ParallelOptions>;
|
|
89
86
|
};
|
|
90
87
|
|
|
91
88
|
export type Effect<T, E = unknown, Ctx = unknown> =
|
|
@@ -161,11 +158,14 @@ export type ParallelContext<B extends readonly unknown[]> = {
|
|
|
161
158
|
? Ctx
|
|
162
159
|
: unknown;
|
|
163
160
|
|
|
164
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Wraps a value for the next step. A string in an object widens to `string`, as in any object literal; write
|
|
163
|
+
* `Success<T>(...)` where a later step tells values apart by it.
|
|
164
|
+
*/
|
|
165
165
|
export declare function Success<T>(value: T): SuccessState<T>;
|
|
166
166
|
|
|
167
167
|
/** Stops the pipeline with `error`. A literal keeps its exact type, and an object or array error is readonly. */
|
|
168
|
-
export declare function Failure<const E = unknown>(error: E
|
|
168
|
+
export declare function Failure<const E = unknown>(error: E): FailureState<E>;
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
171
|
* Defers a side effect: `cmd` makes the call and `next` decides what follows; without `next` the result passes through.
|
|
@@ -194,6 +194,9 @@ export declare function Command<R, T = R, E = unknown, Ctx = unknown>(
|
|
|
194
194
|
/** The name a Command is known by in traces, replays and spans: `meta.name`, else `cmd.name`, else `'anonymous'`. */
|
|
195
195
|
export declare function commandName(command: CommandState<any, any, any, any>): string;
|
|
196
196
|
|
|
197
|
+
/** Reads the context passed to `runEffect`. With no argument it succeeds with the context; name its type as `Ask<AppCtx>()`. */
|
|
198
|
+
export declare function Ask<Ctx = unknown>(): AskState<Ctx, never, Ctx>;
|
|
199
|
+
|
|
197
200
|
/** Reads the context passed to `runEffect`. Give the value, error and context types, or none. */
|
|
198
201
|
export declare function Ask<T = never, E = never, Ctx = unknown, N extends AnyEffect = Effect<T, E, Ctx>>(
|
|
199
202
|
next: (context: Ctx) => N
|
|
@@ -217,7 +220,7 @@ export declare function Retry<T, E = never, Ctx = unknown>(
|
|
|
217
220
|
options?: RetryOptions
|
|
218
221
|
): RetryState<T, E | RetryExhaustedError, Ctx>;
|
|
219
222
|
|
|
220
|
-
// `settled?: false` keeps a `settled`
|
|
223
|
+
// `settled?: false | undefined` keeps a `settled` typed boolean from matching; `| []` infers tuples on TypeScript 5.1.
|
|
221
224
|
/**
|
|
222
225
|
* Runs the branches at the same time and hands `next` their values in order. The first to fail cancels the rest, unless
|
|
223
226
|
* `settled`, which hands `next` every outcome.
|
|
@@ -249,13 +252,13 @@ export declare function Parallel<B extends readonly unknown[] | [], N extends An
|
|
|
249
252
|
|
|
250
253
|
export declare function Parallel<B extends readonly unknown[] | []>(
|
|
251
254
|
effects: ParallelBranches<B>,
|
|
252
|
-
options?: ParallelOptions & { settled?: false }
|
|
255
|
+
options?: ParallelOptions & { settled?: false | undefined }
|
|
253
256
|
): ParallelState<ParallelValues<B>, ParallelValues<B>, EffectError<B[number]>, ParallelContext<B>>;
|
|
254
257
|
|
|
255
258
|
export declare function Parallel<B extends readonly unknown[] | [], N extends AnyEffect = never>(
|
|
256
259
|
effects: ParallelBranches<B>,
|
|
257
260
|
next: (values: ParallelValues<B>) => N,
|
|
258
|
-
options?: ParallelOptions & { settled?: false }
|
|
261
|
+
options?: ParallelOptions & { settled?: false | undefined }
|
|
259
262
|
): ParallelState<
|
|
260
263
|
ParallelValues<B>,
|
|
261
264
|
EffectValue<N>,
|
|
@@ -1245,6 +1248,53 @@ export declare function effectPipe<
|
|
|
1245
1248
|
>;
|
|
1246
1249
|
// END effectPipe overloads.
|
|
1247
1250
|
|
|
1251
|
+
// The symbol and the iterator's yield type exist only in the declarations, never at runtime: together they are how
|
|
1252
|
+
// `effectGen` reads each step's Effect type out of the body's yield type.
|
|
1253
|
+
declare const stepEffect: unique symbol;
|
|
1254
|
+
declare const stepContext: unique symbol;
|
|
1255
|
+
|
|
1256
|
+
/** A step for `yield*` inside `effectGen`, which gives back the Effect's value. Using it any other way throws. */
|
|
1257
|
+
// The context is also a function parameter, which TypeScript compares the opposite way to the Effect, so of two steps
|
|
1258
|
+
// it merges in a generator's yield type it can only drop the one that reads less context.
|
|
1259
|
+
export type Step<X extends AnyEffect, C = EffectContext<X>> = {
|
|
1260
|
+
readonly [stepEffect]?: X;
|
|
1261
|
+
readonly [stepContext]?: (context: C) => void;
|
|
1262
|
+
[Symbol.iterator](): Generator<Step<X, C>, EffectValue<X>, unknown>;
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
/** Wraps an Effect for `yield*` inside `effectGen`: `const user = yield* step(findUser(email))`. */
|
|
1266
|
+
export declare function step<X extends AnyEffect>(effect: X): Step<X>;
|
|
1267
|
+
|
|
1268
|
+
type StepEffectOf<Y> = Y extends Step<infer X, any> ? X : never;
|
|
1269
|
+
// An `any` return would match both branches and turn the error union into `any`, so it counts as a plain value.
|
|
1270
|
+
type GenIsAny<R> = 0 extends 1 & R ? true : false;
|
|
1271
|
+
type ReturnedEffect<R> = GenIsAny<R> extends true ? never : R extends AnyEffect ? R : never;
|
|
1272
|
+
type GenValue<R> = GenIsAny<R> extends true ? any : R extends AnyEffect ? EffectValue<R> : R;
|
|
1273
|
+
type GenError<Y, R> = EffectError<StepEffectOf<Y>> | EffectError<ReturnedEffect<R>>;
|
|
1274
|
+
// A step that reads no context gives `unknown`, which would erase the others in an intersection, so it is dropped. Each
|
|
1275
|
+
// context is boxed, so one that is itself a union is intersected whole rather than member by member.
|
|
1276
|
+
type GenNeeds<X> = X extends AnyEffect
|
|
1277
|
+
? EffectContext<X> extends infer C
|
|
1278
|
+
? unknown extends C
|
|
1279
|
+
? never
|
|
1280
|
+
: { context: C }
|
|
1281
|
+
: never
|
|
1282
|
+
: never;
|
|
1283
|
+
type GenIntersection<U> = (U extends any ? (u: U) => void : never) extends (i: infer I) => void ? I : never;
|
|
1284
|
+
type GenContext<Y, R> = [GenNeeds<StepEffectOf<Y> | ReturnedEffect<R>>] extends [never]
|
|
1285
|
+
? unknown
|
|
1286
|
+
: GenIntersection<GenNeeds<StepEffectOf<Y> | ReturnedEffect<R>>> extends { context: infer C }
|
|
1287
|
+
? C
|
|
1288
|
+
: unknown;
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Builds a flow function from a generator function, written with `yield* step(effect)` where `async` code writes
|
|
1292
|
+
* `await`. A plain return value is a `Success`; return a `Failure` to stop.
|
|
1293
|
+
*/
|
|
1294
|
+
export declare function effectGen<I, Y extends Step<any, any>, R>(
|
|
1295
|
+
body: (input: I) => Generator<Y, R, unknown>
|
|
1296
|
+
): (input: I) => Effect<GenValue<R>, GenError<Y, R>, GenContext<Y, R>>;
|
|
1297
|
+
|
|
1248
1298
|
/**
|
|
1249
1299
|
* Wraps each Command, and each `Parallel`. Call `op` and return its result; a hook that calls another passes `path` on.
|
|
1250
1300
|
*/
|
|
@@ -1255,14 +1305,23 @@ export type StepRunner = (
|
|
|
1255
1305
|
path: string
|
|
1256
1306
|
) => Promise<unknown>;
|
|
1257
1307
|
|
|
1258
|
-
/**
|
|
1259
|
-
|
|
1308
|
+
/**
|
|
1309
|
+
* Which branch, if any, cancelled a `Parallel`, whether it threw, and how many branches it had; `branch: null` means an
|
|
1310
|
+
* enclosing one did. A replay refuses a `Parallel` whose count differs, and a decision without one is not checked.
|
|
1311
|
+
*/
|
|
1312
|
+
export type ParallelDecision =
|
|
1313
|
+
| { cancelled: false; branches?: number | undefined }
|
|
1314
|
+
| { cancelled: true; branch: number | null; branches?: number | undefined; threw?: true | undefined };
|
|
1260
1315
|
|
|
1261
|
-
/**
|
|
1316
|
+
/**
|
|
1317
|
+
* `flowName` is `context.flowName`, or `''` when the context has none. `initialInput` is what the flow was called with
|
|
1318
|
+
* when `effectPipe` built it, and `undefined` otherwise. A hook that calls another passes both on.
|
|
1319
|
+
*/
|
|
1262
1320
|
export type RunWrapper = (
|
|
1263
1321
|
effect: Effect<unknown>,
|
|
1264
1322
|
op: () => Promise<SuccessState<unknown> | FailureState<unknown>>,
|
|
1265
|
-
flowName: string
|
|
1323
|
+
flowName: string,
|
|
1324
|
+
initialInput: unknown
|
|
1266
1325
|
) => Promise<SuccessState<unknown> | FailureState<unknown>>;
|
|
1267
1326
|
|
|
1268
1327
|
// A union, so a JSDoc `@type` on an async function fits it too.
|
|
@@ -1272,26 +1331,34 @@ export type CommandInterceptor =
|
|
|
1272
1331
|
| ((command: CommandState<unknown, unknown>, context?: any) => void);
|
|
1273
1332
|
|
|
1274
1333
|
export interface EffectConfiguration {
|
|
1275
|
-
onStep?: StepRunner;
|
|
1276
|
-
onRun?: RunWrapper;
|
|
1277
|
-
onBeforeCommand?: CommandInterceptor;
|
|
1334
|
+
onStep?: StepRunner | undefined;
|
|
1335
|
+
onRun?: RunWrapper | undefined;
|
|
1336
|
+
onBeforeCommand?: CommandInterceptor | undefined;
|
|
1278
1337
|
}
|
|
1279
1338
|
|
|
1280
1339
|
/** Adds a layer of hooks and returns a function that removes it. With no arguments, removes every layer. */
|
|
1281
1340
|
export declare function configureEffect(...configs: (EffectConfiguration | undefined)[]): () => void;
|
|
1282
1341
|
|
|
1283
1342
|
/** Hooks for one `runEffect` call, inside the installed ones; `inherit: false` leaves those out. */
|
|
1284
|
-
export type CallConfiguration = EffectConfiguration & { inherit?: boolean };
|
|
1343
|
+
export type CallConfiguration = EffectConfiguration & { inherit?: boolean | undefined };
|
|
1285
1344
|
|
|
1286
1345
|
/** The context a run is given: what the flow reads, and a `flowName` naming the run in traces and spans. */
|
|
1287
|
-
export type RunContext<Ctx> = unknown extends Ctx
|
|
1346
|
+
export type RunContext<Ctx> = unknown extends Ctx
|
|
1347
|
+
? Ctx
|
|
1348
|
+
: Ctx extends object
|
|
1349
|
+
? Ctx & { flowName?: string | undefined }
|
|
1350
|
+
: Ctx;
|
|
1351
|
+
|
|
1352
|
+
// TypeScript 5.1's stand-in for NoInfer: it defers this indexed access, so it infers nothing from it.
|
|
1353
|
+
/** A context checked against the one the flow reads, and never inferred from. */
|
|
1354
|
+
type CheckedContext<T> = [T][T extends any ? 0 : never];
|
|
1288
1355
|
|
|
1289
1356
|
/** Runs a flow and returns its `Success` or `Failure`. A flow that reads a context requires one. */
|
|
1290
1357
|
export declare function runEffect<T, E = unknown, Ctx = unknown>(
|
|
1291
1358
|
effect: Effect<T, E, Ctx>,
|
|
1292
1359
|
...args: unknown extends Ctx
|
|
1293
|
-
? [context?: Ctx
|
|
1294
|
-
: [context: RunContext<Ctx
|
|
1360
|
+
? [context?: CheckedContext<Ctx>, callConfig?: CallConfiguration]
|
|
1361
|
+
: [context: RunContext<CheckedContext<Ctx>>, callConfig?: CallConfiguration]
|
|
1295
1362
|
): Promise<SuccessState<T> | FailureState<E>>;
|
|
1296
1363
|
|
|
1297
1364
|
export type ReplayStep = {
|
|
@@ -1311,6 +1378,7 @@ export type ReplayStep = {
|
|
|
1311
1378
|
*/
|
|
1312
1379
|
export type ReplayOutcome = { result: unknown } | { error: unknown };
|
|
1313
1380
|
|
|
1381
|
+
/** Answers a step from a recording. Any other answer than an outcome or `undefined`, or a throw, rejects the replay. */
|
|
1314
1382
|
export type Resolver = (step: ReplayStep) => ReplayOutcome | undefined;
|
|
1315
1383
|
|
|
1316
1384
|
/** One recorded step: a Command's result or error, or a `Parallel`'s decision. */
|
|
@@ -1322,6 +1390,8 @@ export type TraceEntry = {
|
|
|
1322
1390
|
/** Marks a step that threw; `false` is the same as leaving it out. */
|
|
1323
1391
|
threw?: boolean;
|
|
1324
1392
|
error?: unknown;
|
|
1393
|
+
/** Why the entry holds no value: `'redact'` threw on it, or copying it threw (`'copy'`). A replay stops there. */
|
|
1394
|
+
unrecorded?: string;
|
|
1325
1395
|
/** How long the Command took in production, rounded to microseconds. */
|
|
1326
1396
|
durationMs?: number;
|
|
1327
1397
|
};
|
|
@@ -1332,6 +1402,8 @@ export type TraceLog<I = unknown, C = unknown> = {
|
|
|
1332
1402
|
version?: string;
|
|
1333
1403
|
initialInput?: I;
|
|
1334
1404
|
context?: C;
|
|
1405
|
+
/** Why the trace holds no `initialInput` or `context`, as an entry's `unrecorded` says it for a step. */
|
|
1406
|
+
unrecorded?: { initialInput?: string; context?: string };
|
|
1335
1407
|
dropped?: number;
|
|
1336
1408
|
trace: TraceEntry[];
|
|
1337
1409
|
};
|
|
@@ -1340,17 +1412,17 @@ export interface RecorderOptions {
|
|
|
1340
1412
|
/**
|
|
1341
1413
|
* Scrubs each value before it enters a trace; `kind` says which. It gets a copy, so editing it in place is safe.
|
|
1342
1414
|
*/
|
|
1343
|
-
redact?: (value: any, name: string, kind: 'result' | 'error' | 'initialInput' | 'context') => unknown;
|
|
1415
|
+
redact?: ((value: any, name: string, kind: 'result' | 'error' | 'initialInput' | 'context') => unknown) | undefined;
|
|
1344
1416
|
/** Cap trace length; further steps are counted in `dropped`, not stored. */
|
|
1345
|
-
maxEntries?: number;
|
|
1417
|
+
maxEntries?: number | undefined;
|
|
1346
1418
|
/** Record stack traces for thrown errors. Off by default. */
|
|
1347
|
-
stack?: boolean;
|
|
1419
|
+
stack?: boolean | undefined;
|
|
1348
1420
|
}
|
|
1349
1421
|
|
|
1350
1422
|
/** The trace's own fields, as `toTrace` takes them; the returned trace keeps the input's and context's types. */
|
|
1351
1423
|
export interface TraceMeta<I = unknown, C = unknown> {
|
|
1352
1424
|
initialInput?: I;
|
|
1353
|
-
flowName?: string;
|
|
1425
|
+
flowName?: string | undefined;
|
|
1354
1426
|
context?: C;
|
|
1355
1427
|
/** Accepts `undefined`, so `process.env.BUILD_ID` passes under `exactOptionalPropertyTypes`. */
|
|
1356
1428
|
version?: string | undefined;
|
|
@@ -1365,32 +1437,35 @@ export declare function recorder(options?: RecorderOptions): {
|
|
|
1365
1437
|
|
|
1366
1438
|
/** `recordEffect`'s options: recorder options, plus the context the run gets and a build id. */
|
|
1367
1439
|
export type RecordOptions<Ctx = unknown> = RecorderOptions & {
|
|
1368
|
-
context?: RunContext<Ctx
|
|
1440
|
+
context?: RunContext<Ctx> | undefined;
|
|
1369
1441
|
version?: string | undefined;
|
|
1370
1442
|
};
|
|
1371
1443
|
|
|
1372
|
-
/**
|
|
1444
|
+
/**
|
|
1445
|
+
* Runs a flow for real and returns its outcome with a trace, which keeps the input's type for the replay. That type is
|
|
1446
|
+
* wrong when `redact` changes the input, or the trace marks it `unrecorded`.
|
|
1447
|
+
*/
|
|
1373
1448
|
export declare function recordEffect<I, T, E = unknown, Ctx = unknown>(
|
|
1374
1449
|
flowFn: (input: I) => Effect<T, E, Ctx>,
|
|
1375
1450
|
initialInput: I,
|
|
1376
1451
|
...options: unknown extends Ctx
|
|
1377
|
-
? [options?: RecordOptions<Ctx
|
|
1378
|
-
: [options: RecordOptions<Ctx
|
|
1452
|
+
? [options?: RecordOptions<CheckedContext<Ctx>>]
|
|
1453
|
+
: [options: RecordOptions<CheckedContext<Ctx>> & { context: RunContext<CheckedContext<Ctx>> }]
|
|
1379
1454
|
): Promise<{ result: SuccessState<T> | FailureState<E>; trace: TraceLog<I, Ctx> & { initialInput: I } }>;
|
|
1380
1455
|
|
|
1381
1456
|
export interface ReplayOptions<Ctx = unknown> {
|
|
1382
1457
|
/** Context for `Ask`; defaults to the one the trace recorded. */
|
|
1383
|
-
context?: RunContext<Ctx
|
|
1458
|
+
context?: RunContext<Ctx> | undefined;
|
|
1384
1459
|
/** Strip `Retry` delays so a replay does not wait out production backoff (default `true`). */
|
|
1385
|
-
fastRetry?: boolean;
|
|
1460
|
+
fastRetry?: boolean | undefined;
|
|
1386
1461
|
/** Runs the replay inside the installed hooks, so they see it. Off by default. */
|
|
1387
|
-
hooks?: boolean;
|
|
1462
|
+
hooks?: boolean | undefined;
|
|
1388
1463
|
/**
|
|
1389
1464
|
* `'throw'` (default) fails on a step the trace lacks. `'execute'` runs it for real: use it only against test
|
|
1390
1465
|
* doubles.
|
|
1391
1466
|
*/
|
|
1392
|
-
onMissing?: 'throw' | 'execute';
|
|
1393
|
-
onResolved?: (step: ReplayStep, outcome: ReplayOutcome | undefined) => void;
|
|
1467
|
+
onMissing?: 'throw' | 'execute' | undefined;
|
|
1468
|
+
onResolved?: ((step: ReplayStep, outcome: ReplayOutcome | undefined) => void) | undefined;
|
|
1394
1469
|
}
|
|
1395
1470
|
|
|
1396
1471
|
/** The replay's outcome and, for a trace, the recorded steps the flow never asked for. */
|
|
@@ -1399,26 +1474,40 @@ export interface Replay<T, E = unknown> {
|
|
|
1399
1474
|
unreached: TraceEntry[];
|
|
1400
1475
|
}
|
|
1401
1476
|
|
|
1402
|
-
/**
|
|
1477
|
+
/**
|
|
1478
|
+
* Replays a flow from a trace, or from a Resolver for other storage, with no I/O. A Resolver holds no context, so a
|
|
1479
|
+
* flow that reads one needs `context` passed.
|
|
1480
|
+
*/
|
|
1403
1481
|
export declare function replayEffect<T, E = unknown, Ctx = unknown>(
|
|
1404
1482
|
effect: Effect<T, E, Ctx>,
|
|
1405
1483
|
trace: TraceLog | TraceEntry[],
|
|
1406
|
-
options?: ReplayOptions<Ctx
|
|
1484
|
+
options?: ReplayOptions<CheckedContext<Ctx>>
|
|
1407
1485
|
): Promise<Replay<T, E>>;
|
|
1408
1486
|
export declare function replayEffect<T, E = unknown, Ctx = unknown>(
|
|
1409
1487
|
effect: Effect<T, E, Ctx>,
|
|
1410
1488
|
resolver: Resolver,
|
|
1411
|
-
options
|
|
1489
|
+
...options: unknown extends Ctx
|
|
1490
|
+
? [options?: ReplayOptions<CheckedContext<Ctx>>]
|
|
1491
|
+
: [options: ReplayOptions<CheckedContext<Ctx>> & { context: RunContext<CheckedContext<Ctx>> }]
|
|
1412
1492
|
): Promise<Omit<Replay<T, E>, 'unreached'>>;
|
|
1413
1493
|
export declare function replayEffect<T, E = unknown, Ctx = unknown>(
|
|
1414
1494
|
effect: Effect<T, E, Ctx>,
|
|
1415
1495
|
traceOrResolver: Resolver | TraceLog | TraceEntry[],
|
|
1416
|
-
options
|
|
1496
|
+
...options: unknown extends Ctx
|
|
1497
|
+
? [options?: ReplayOptions<CheckedContext<Ctx>>]
|
|
1498
|
+
: [options: ReplayOptions<CheckedContext<Ctx>> & { context: RunContext<CheckedContext<Ctx>> }]
|
|
1417
1499
|
): Promise<Omit<Replay<T, E>, 'unreached'> & { unreached?: TraceEntry[] }>;
|
|
1418
1500
|
|
|
1419
1501
|
/** Replays a trace and logs each step with its recorded timing, warning about what will not replay as recorded. */
|
|
1420
1502
|
export declare function timeTravel<T, E = unknown, Ctx = unknown>(
|
|
1421
1503
|
flowFn: (input: any) => Effect<T, E, Ctx>,
|
|
1422
1504
|
traceLog: TraceLog,
|
|
1423
|
-
options?: {
|
|
1505
|
+
options?: {
|
|
1506
|
+
log?: ((...args: any[]) => void) | undefined;
|
|
1507
|
+
context?: RunContext<CheckedContext<Ctx>> | undefined;
|
|
1508
|
+
version?: string | undefined;
|
|
1509
|
+
}
|
|
1424
1510
|
): Promise<SuccessState<T> | FailureState<E>>;
|
|
1511
|
+
|
|
1512
|
+
// Only the declarations marked `export` are public; without this, a declaration file exports every top-level one.
|
|
1513
|
+
export {};
|