pure-effect 0.16.0 → 0.18.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.
@@ -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.
@@ -217,7 +217,7 @@ export declare function Retry<T, E = never, Ctx = unknown>(
217
217
  options?: RetryOptions
218
218
  ): RetryState<T, E | RetryExhaustedError, Ctx>;
219
219
 
220
- // `settled?: false` keeps a `settled` known only as boolean from matching; `| []` infers tuples on TypeScript 5.1.
220
+ // `settled?: false | undefined` keeps a `settled` typed boolean from matching; `| []` infers tuples on TypeScript 5.1.
221
221
  /**
222
222
  * Runs the branches at the same time and hands `next` their values in order. The first to fail cancels the rest, unless
223
223
  * `settled`, which hands `next` every outcome.
@@ -249,13 +249,13 @@ export declare function Parallel<B extends readonly unknown[] | [], N extends An
249
249
 
250
250
  export declare function Parallel<B extends readonly unknown[] | []>(
251
251
  effects: ParallelBranches<B>,
252
- options?: ParallelOptions & { settled?: false }
252
+ options?: ParallelOptions & { settled?: false | undefined }
253
253
  ): ParallelState<ParallelValues<B>, ParallelValues<B>, EffectError<B[number]>, ParallelContext<B>>;
254
254
 
255
255
  export declare function Parallel<B extends readonly unknown[] | [], N extends AnyEffect = never>(
256
256
  effects: ParallelBranches<B>,
257
257
  next: (values: ParallelValues<B>) => N,
258
- options?: ParallelOptions & { settled?: false }
258
+ options?: ParallelOptions & { settled?: false | undefined }
259
259
  ): ParallelState<
260
260
  ParallelValues<B>,
261
261
  EffectValue<N>,
@@ -1255,14 +1255,23 @@ export type StepRunner = (
1255
1255
  path: string
1256
1256
  ) => Promise<unknown>;
1257
1257
 
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 };
1258
+ /**
1259
+ * Which branch, if any, cancelled a `Parallel`, and how many branches it had; `branch: null` means an enclosing one
1260
+ * did. A replay refuses a `Parallel` whose count differs, and a decision without one is not checked.
1261
+ */
1262
+ export type ParallelDecision =
1263
+ | { cancelled: false; branches?: number | undefined }
1264
+ | { cancelled: true; branch: number | null; branches?: number | undefined };
1260
1265
 
1261
- /** `flowName` is `context.flowName`, or `''` when the context has none; the interpreter always passes it. */
1266
+ /**
1267
+ * `flowName` is `context.flowName`, or `''` when the context has none. `initialInput` is what the flow was called with
1268
+ * when `effectPipe` built it, and `undefined` otherwise. A hook that calls another passes both on.
1269
+ */
1262
1270
  export type RunWrapper = (
1263
1271
  effect: Effect<unknown>,
1264
1272
  op: () => Promise<SuccessState<unknown> | FailureState<unknown>>,
1265
- flowName: string
1273
+ flowName: string,
1274
+ initialInput: unknown
1266
1275
  ) => Promise<SuccessState<unknown> | FailureState<unknown>>;
1267
1276
 
1268
1277
  // A union, so a JSDoc `@type` on an async function fits it too.
@@ -1272,26 +1281,34 @@ export type CommandInterceptor =
1272
1281
  | ((command: CommandState<unknown, unknown>, context?: any) => void);
1273
1282
 
1274
1283
  export interface EffectConfiguration {
1275
- onStep?: StepRunner;
1276
- onRun?: RunWrapper;
1277
- onBeforeCommand?: CommandInterceptor;
1284
+ onStep?: StepRunner | undefined;
1285
+ onRun?: RunWrapper | undefined;
1286
+ onBeforeCommand?: CommandInterceptor | undefined;
1278
1287
  }
1279
1288
 
1280
1289
  /** Adds a layer of hooks and returns a function that removes it. With no arguments, removes every layer. */
1281
1290
  export declare function configureEffect(...configs: (EffectConfiguration | undefined)[]): () => void;
1282
1291
 
1283
1292
  /** Hooks for one `runEffect` call, inside the installed ones; `inherit: false` leaves those out. */
1284
- export type CallConfiguration = EffectConfiguration & { inherit?: boolean };
1293
+ export type CallConfiguration = EffectConfiguration & { inherit?: boolean | undefined };
1285
1294
 
1286
1295
  /** 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;
1296
+ export type RunContext<Ctx> = unknown extends Ctx
1297
+ ? Ctx
1298
+ : Ctx extends object
1299
+ ? Ctx & { flowName?: string | undefined }
1300
+ : Ctx;
1301
+
1302
+ // TypeScript 5.1's stand-in for NoInfer: it defers this indexed access, so it infers nothing from it.
1303
+ /** A context checked against the one the flow reads, and never inferred from. */
1304
+ type CheckedContext<T> = [T][T extends any ? 0 : never];
1288
1305
 
1289
1306
  /** Runs a flow and returns its `Success` or `Failure`. A flow that reads a context requires one. */
1290
1307
  export declare function runEffect<T, E = unknown, Ctx = unknown>(
1291
1308
  effect: Effect<T, E, Ctx>,
1292
1309
  ...args: unknown extends Ctx
1293
- ? [context?: Ctx, callConfig?: CallConfiguration]
1294
- : [context: RunContext<Ctx>, callConfig?: CallConfiguration]
1310
+ ? [context?: CheckedContext<Ctx>, callConfig?: CallConfiguration]
1311
+ : [context: RunContext<CheckedContext<Ctx>>, callConfig?: CallConfiguration]
1295
1312
  ): Promise<SuccessState<T> | FailureState<E>>;
1296
1313
 
1297
1314
  export type ReplayStep = {
@@ -1311,6 +1328,7 @@ export type ReplayStep = {
1311
1328
  */
1312
1329
  export type ReplayOutcome = { result: unknown } | { error: unknown };
1313
1330
 
1331
+ /** Answers a step from a recording. Any other answer than an outcome or `undefined`, or a throw, rejects the replay. */
1314
1332
  export type Resolver = (step: ReplayStep) => ReplayOutcome | undefined;
1315
1333
 
1316
1334
  /** One recorded step: a Command's result or error, or a `Parallel`'s decision. */
@@ -1322,6 +1340,8 @@ export type TraceEntry = {
1322
1340
  /** Marks a step that threw; `false` is the same as leaving it out. */
1323
1341
  threw?: boolean;
1324
1342
  error?: unknown;
1343
+ /** Why the entry holds no value: `'redact'` threw on it, or copying it threw (`'copy'`). A replay stops there. */
1344
+ unrecorded?: string;
1325
1345
  /** How long the Command took in production, rounded to microseconds. */
1326
1346
  durationMs?: number;
1327
1347
  };
@@ -1332,6 +1352,8 @@ export type TraceLog<I = unknown, C = unknown> = {
1332
1352
  version?: string;
1333
1353
  initialInput?: I;
1334
1354
  context?: C;
1355
+ /** Why the trace holds no `initialInput` or `context`, as an entry's `unrecorded` says it for a step. */
1356
+ unrecorded?: { initialInput?: string; context?: string };
1335
1357
  dropped?: number;
1336
1358
  trace: TraceEntry[];
1337
1359
  };
@@ -1340,17 +1362,17 @@ export interface RecorderOptions {
1340
1362
  /**
1341
1363
  * Scrubs each value before it enters a trace; `kind` says which. It gets a copy, so editing it in place is safe.
1342
1364
  */
1343
- redact?: (value: any, name: string, kind: 'result' | 'error' | 'initialInput' | 'context') => unknown;
1365
+ redact?: ((value: any, name: string, kind: 'result' | 'error' | 'initialInput' | 'context') => unknown) | undefined;
1344
1366
  /** Cap trace length; further steps are counted in `dropped`, not stored. */
1345
- maxEntries?: number;
1367
+ maxEntries?: number | undefined;
1346
1368
  /** Record stack traces for thrown errors. Off by default. */
1347
- stack?: boolean;
1369
+ stack?: boolean | undefined;
1348
1370
  }
1349
1371
 
1350
1372
  /** The trace's own fields, as `toTrace` takes them; the returned trace keeps the input's and context's types. */
1351
1373
  export interface TraceMeta<I = unknown, C = unknown> {
1352
1374
  initialInput?: I;
1353
- flowName?: string;
1375
+ flowName?: string | undefined;
1354
1376
  context?: C;
1355
1377
  /** Accepts `undefined`, so `process.env.BUILD_ID` passes under `exactOptionalPropertyTypes`. */
1356
1378
  version?: string | undefined;
@@ -1365,32 +1387,35 @@ export declare function recorder(options?: RecorderOptions): {
1365
1387
 
1366
1388
  /** `recordEffect`'s options: recorder options, plus the context the run gets and a build id. */
1367
1389
  export type RecordOptions<Ctx = unknown> = RecorderOptions & {
1368
- context?: RunContext<Ctx>;
1390
+ context?: RunContext<Ctx> | undefined;
1369
1391
  version?: string | undefined;
1370
1392
  };
1371
1393
 
1372
- /** Runs a flow for real and returns its outcome with a trace, which keeps the input's type for the replay. */
1394
+ /**
1395
+ * Runs a flow for real and returns its outcome with a trace, which keeps the input's type for the replay. That type is
1396
+ * wrong when `redact` changes the input, or the trace marks it `unrecorded`.
1397
+ */
1373
1398
  export declare function recordEffect<I, T, E = unknown, Ctx = unknown>(
1374
1399
  flowFn: (input: I) => Effect<T, E, Ctx>,
1375
1400
  initialInput: I,
1376
1401
  ...options: unknown extends Ctx
1377
- ? [options?: RecordOptions<Ctx>]
1378
- : [options: RecordOptions<Ctx> & { context: RunContext<Ctx> }]
1402
+ ? [options?: RecordOptions<CheckedContext<Ctx>>]
1403
+ : [options: RecordOptions<CheckedContext<Ctx>> & { context: RunContext<CheckedContext<Ctx>> }]
1379
1404
  ): Promise<{ result: SuccessState<T> | FailureState<E>; trace: TraceLog<I, Ctx> & { initialInput: I } }>;
1380
1405
 
1381
1406
  export interface ReplayOptions<Ctx = unknown> {
1382
1407
  /** Context for `Ask`; defaults to the one the trace recorded. */
1383
- context?: RunContext<Ctx>;
1408
+ context?: RunContext<Ctx> | undefined;
1384
1409
  /** Strip `Retry` delays so a replay does not wait out production backoff (default `true`). */
1385
- fastRetry?: boolean;
1410
+ fastRetry?: boolean | undefined;
1386
1411
  /** Runs the replay inside the installed hooks, so they see it. Off by default. */
1387
- hooks?: boolean;
1412
+ hooks?: boolean | undefined;
1388
1413
  /**
1389
1414
  * `'throw'` (default) fails on a step the trace lacks. `'execute'` runs it for real: use it only against test
1390
1415
  * doubles.
1391
1416
  */
1392
- onMissing?: 'throw' | 'execute';
1393
- onResolved?: (step: ReplayStep, outcome: ReplayOutcome | undefined) => void;
1417
+ onMissing?: 'throw' | 'execute' | undefined;
1418
+ onResolved?: ((step: ReplayStep, outcome: ReplayOutcome | undefined) => void) | undefined;
1394
1419
  }
1395
1420
 
1396
1421
  /** The replay's outcome and, for a trace, the recorded steps the flow never asked for. */
@@ -1399,26 +1424,37 @@ export interface Replay<T, E = unknown> {
1399
1424
  unreached: TraceEntry[];
1400
1425
  }
1401
1426
 
1402
- /** Replays a flow from a trace, or from a Resolver for other storage, with no I/O. */
1427
+ /**
1428
+ * Replays a flow from a trace, or from a Resolver for other storage, with no I/O. A Resolver holds no context, so a
1429
+ * flow that reads one needs `context` passed.
1430
+ */
1403
1431
  export declare function replayEffect<T, E = unknown, Ctx = unknown>(
1404
1432
  effect: Effect<T, E, Ctx>,
1405
1433
  trace: TraceLog | TraceEntry[],
1406
- options?: ReplayOptions<Ctx>
1434
+ options?: ReplayOptions<CheckedContext<Ctx>>
1407
1435
  ): Promise<Replay<T, E>>;
1408
1436
  export declare function replayEffect<T, E = unknown, Ctx = unknown>(
1409
1437
  effect: Effect<T, E, Ctx>,
1410
1438
  resolver: Resolver,
1411
- options?: ReplayOptions<Ctx>
1439
+ ...options: unknown extends Ctx
1440
+ ? [options?: ReplayOptions<CheckedContext<Ctx>>]
1441
+ : [options: ReplayOptions<CheckedContext<Ctx>> & { context: RunContext<CheckedContext<Ctx>> }]
1412
1442
  ): Promise<Omit<Replay<T, E>, 'unreached'>>;
1413
1443
  export declare function replayEffect<T, E = unknown, Ctx = unknown>(
1414
1444
  effect: Effect<T, E, Ctx>,
1415
1445
  traceOrResolver: Resolver | TraceLog | TraceEntry[],
1416
- options?: ReplayOptions<Ctx>
1446
+ ...options: unknown extends Ctx
1447
+ ? [options?: ReplayOptions<CheckedContext<Ctx>>]
1448
+ : [options: ReplayOptions<CheckedContext<Ctx>> & { context: RunContext<CheckedContext<Ctx>> }]
1417
1449
  ): Promise<Omit<Replay<T, E>, 'unreached'> & { unreached?: TraceEntry[] }>;
1418
1450
 
1419
1451
  /** Replays a trace and logs each step with its recorded timing, warning about what will not replay as recorded. */
1420
1452
  export declare function timeTravel<T, E = unknown, Ctx = unknown>(
1421
1453
  flowFn: (input: any) => Effect<T, E, Ctx>,
1422
1454
  traceLog: TraceLog,
1423
- options?: { log?: (...args: any[]) => void; context?: RunContext<Ctx>; version?: string | undefined }
1455
+ options?: {
1456
+ log?: ((...args: any[]) => void) | undefined;
1457
+ context?: RunContext<CheckedContext<Ctx>> | undefined;
1458
+ version?: string | undefined;
1459
+ }
1424
1460
  ): Promise<SuccessState<T> | FailureState<E>>;