pure-effect 0.17.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.
- package/CHANGELOG.md +26 -0
- package/README.md +73 -51
- package/examples/opentelemetry-example.js +5 -1
- package/examples/recording-example.js +31 -12
- package/index.d.ts +83 -47
- package/index.js +1381 -1011
- package/package.json +6 -3
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.
|
|
@@ -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`
|
|
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
|
-
/**
|
|
1259
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
1294
|
-
: [context: RunContext<Ctx
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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?: {
|
|
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>>;
|