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.
@@ -72,7 +72,11 @@ export function telemetryHooks(options = {}) {
72
72
  try {
73
73
  const result = await pipeline();
74
74
  if (result.type === 'Failure') {
75
- rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: statusMessage(result.error) });
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 { Effect, EffectConfiguration, RunWrapper, StepRunner, CommandInterceptor, TraceEntry, TraceLog, SuccessState, FailureState } from "pure-effect" */
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, or some of its
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
- const head = rec.toTrace({ flowName, initialInput: effect.initialInput });
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
- warnAboutReplay(flowName, effect, dropped, trace);
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 {Effect<any, any, any>} effect
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, effect, dropped, trace) => {
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 (effect.initialInput === undefined) {
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
- store.head.context = store.rec.toTrace({ context }).context;
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
- options: RetryOptions & { onExhausted?: (error: RetryExhaustedError) => Effect<R, any, Ctx> };
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?: ParallelOptions;
88
- initialInput?: unknown;
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
- /** Wraps a value for the next step. */
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, initialInput?: unknown): FailureState<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` known only as boolean from matching; `| []` infers tuples on TypeScript 5.1.
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
- /** Which branch, if any, cancelled a `Parallel`; `branch: null` means an enclosing one did. */
1259
- export type ParallelDecision = { cancelled: false } | { cancelled: true; branch: number | null };
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
- /** `flowName` is `context.flowName`, or `''` when the context has none; the interpreter always passes it. */
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 ? Ctx : Ctx extends object ? Ctx & { flowName?: string } : 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, callConfig?: CallConfiguration]
1294
- : [context: RunContext<Ctx>, callConfig?: CallConfiguration]
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
- /** Runs a flow for real and returns its outcome with a trace, which keeps the input's type for the replay. */
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> & { context: RunContext<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
- /** Replays a flow from a trace, or from a Resolver for other storage, with no I/O. */
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?: ReplayOptions<Ctx>
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?: ReplayOptions<Ctx>
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?: { log?: (...args: any[]) => void; context?: RunContext<Ctx>; version?: string | undefined }
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 {};