@barnum/barnum 0.0.0-main-ef6df91f → 0.0.0-main-e8b82cff
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/artifacts/linux-arm64/barnum +0 -0
- package/artifacts/linux-x64/barnum +0 -0
- package/artifacts/macos-arm64/barnum +0 -0
- package/artifacts/macos-x64/barnum +0 -0
- package/artifacts/win-x64/barnum.exe +0 -0
- package/package.json +2 -1
- package/src/all.ts +116 -26
- package/src/ast.ts +262 -454
- package/src/bind.ts +65 -30
- package/src/builtins.ts +222 -123
- package/src/chain.ts +11 -2
- package/src/effect-id.ts +21 -6
- package/src/handler.ts +75 -32
- package/src/index.ts +11 -3
- package/src/pipe.ts +124 -29
- package/src/race.ts +58 -52
- package/src/run.ts +64 -23
- package/src/schema.ts +118 -0
- package/src/try-catch.ts +29 -30
- package/src/worker.ts +10 -1
package/src/ast.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
|
|
1
3
|
// ---------------------------------------------------------------------------
|
|
2
4
|
// Serializable Types — mirror the Rust AST in barnum_ast
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
@@ -8,9 +10,10 @@ export type Action =
|
|
|
8
10
|
| ForEachAction
|
|
9
11
|
| AllAction
|
|
10
12
|
| BranchAction
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
13
|
+
| ResumeHandleAction
|
|
14
|
+
| ResumePerformAction
|
|
15
|
+
| RestartHandleAction
|
|
16
|
+
| RestartPerformAction;
|
|
14
17
|
|
|
15
18
|
export interface InvokeAction {
|
|
16
19
|
kind: "Invoke";
|
|
@@ -38,25 +41,30 @@ export interface BranchAction {
|
|
|
38
41
|
cases: Record<string, Action>;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
export interface
|
|
42
|
-
kind: "
|
|
43
|
-
|
|
44
|
+
export interface ResumeHandleAction {
|
|
45
|
+
kind: "ResumeHandle";
|
|
46
|
+
resume_handler_id: ResumeHandlerId;
|
|
47
|
+
body: Action;
|
|
48
|
+
handler: Action;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResumePerformAction {
|
|
52
|
+
kind: "ResumePerform";
|
|
53
|
+
resume_handler_id: ResumeHandlerId;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
export interface
|
|
47
|
-
kind: "
|
|
48
|
-
|
|
56
|
+
export interface RestartHandleAction {
|
|
57
|
+
kind: "RestartHandle";
|
|
58
|
+
restart_handler_id: RestartHandlerId;
|
|
49
59
|
body: Action;
|
|
50
60
|
handler: Action;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
export interface
|
|
54
|
-
kind: "
|
|
55
|
-
|
|
63
|
+
export interface RestartPerformAction {
|
|
64
|
+
kind: "RestartPerform";
|
|
65
|
+
restart_handler_id: RestartHandlerId;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
export type StepRef = { kind: "Named"; name: string } | { kind: "Root" };
|
|
59
|
-
|
|
60
68
|
// ---------------------------------------------------------------------------
|
|
61
69
|
// HandlerKind
|
|
62
70
|
// ---------------------------------------------------------------------------
|
|
@@ -67,6 +75,8 @@ export interface TypeScriptHandler {
|
|
|
67
75
|
kind: "TypeScript";
|
|
68
76
|
module: string;
|
|
69
77
|
func: string;
|
|
78
|
+
input_schema?: JSONSchema7;
|
|
79
|
+
output_schema?: JSONSchema7;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export interface BuiltinHandler {
|
|
@@ -86,32 +96,6 @@ export type BuiltinKind =
|
|
|
86
96
|
| { kind: "Pick"; value: string[] }
|
|
87
97
|
| { kind: "CollectSome" };
|
|
88
98
|
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// WorkflowAction — loosened input constraint for workflow entry points
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* A TypedAction suitable as a workflow entry point. Workflows start with
|
|
95
|
-
* no input data, so the action must not require specific input.
|
|
96
|
-
*
|
|
97
|
-
* Uses `__in?: void` to accept both:
|
|
98
|
-
* - `TypedAction<any, Out>` — combinators that ignore input (constant, sleep)
|
|
99
|
-
* - `TypedAction<never, Out>` — handlers that genuinely take no params
|
|
100
|
-
*
|
|
101
|
-
* Rejects `TypedAction<{ artifact: string }, Out>` etc. because
|
|
102
|
-
* `{ artifact: string }` is not assignable to `void`.
|
|
103
|
-
*
|
|
104
|
-
* Only `__in` is checked (no `__phantom_in`) — the contravariant phantom
|
|
105
|
-
* field would accept anything due to `void`'s permissiveness, so omitting
|
|
106
|
-
* it is harmless and avoids deep method signature comparison.
|
|
107
|
-
*/
|
|
108
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
-
export type WorkflowAction<Out = any> = Action & {
|
|
110
|
-
__in?: void;
|
|
111
|
-
__phantom_out?: () => Out;
|
|
112
|
-
__phantom_out_check?: (output: Out) => void;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
99
|
/**
|
|
116
100
|
* When TIn is `never` (handler ignores input), produce `any` so the
|
|
117
101
|
* combinator/pipe can sit in any pipeline position.
|
|
@@ -122,10 +106,8 @@ export type PipeIn<T> = [T] extends [never] ? any : T;
|
|
|
122
106
|
// Config
|
|
123
107
|
// ---------------------------------------------------------------------------
|
|
124
108
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
workflow: WorkflowAction<Out>;
|
|
128
|
-
steps?: Record<string, Action>;
|
|
109
|
+
export interface Config {
|
|
110
|
+
workflow: Action;
|
|
129
111
|
}
|
|
130
112
|
|
|
131
113
|
// ---------------------------------------------------------------------------
|
|
@@ -133,14 +115,16 @@ export interface Config<Out = any> {
|
|
|
133
115
|
// ---------------------------------------------------------------------------
|
|
134
116
|
|
|
135
117
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
-
type UnionToIntersection<TUnion> = (
|
|
137
|
-
x:
|
|
138
|
-
) => void
|
|
118
|
+
type UnionToIntersection<TUnion> = (
|
|
119
|
+
TUnion extends any ? (x: TUnion) => void : never
|
|
120
|
+
) extends (x: infer TIntersection) => void
|
|
139
121
|
? TIntersection
|
|
140
122
|
: never;
|
|
141
123
|
|
|
142
124
|
/** Merge a tuple of objects into a single intersection type. */
|
|
143
|
-
export type MergeTuple<TTuple> = TTuple extends unknown[]
|
|
125
|
+
export type MergeTuple<TTuple> = TTuple extends unknown[]
|
|
126
|
+
? UnionToIntersection<TTuple[number]>
|
|
127
|
+
: never;
|
|
144
128
|
|
|
145
129
|
// ---------------------------------------------------------------------------
|
|
146
130
|
// Phantom Types — type-safe input/output tracking
|
|
@@ -163,8 +147,6 @@ export type MergeTuple<TTuple> = TTuple extends unknown[] ? UnionToIntersection<
|
|
|
163
147
|
* (the contravariant __phantom_in makes never the most permissive input,
|
|
164
148
|
* so the covariant __in is needed for the entry point check).
|
|
165
149
|
*
|
|
166
|
-
* Refs: tracks step reference names through combinators for compile-time
|
|
167
|
-
* validation in registerSteps (see ValidateStepRefs)
|
|
168
150
|
*/
|
|
169
151
|
export type TypedAction<
|
|
170
152
|
In = unknown,
|
|
@@ -181,16 +163,34 @@ export type TypedAction<
|
|
|
181
163
|
next: Pipeable<Out, TNext, TRefs2>,
|
|
182
164
|
): TypedAction<In, TNext, Refs | TRefs2>;
|
|
183
165
|
/** Apply an action to each element of an array output. `a.forEach(b)` ≡ `a.then(forEach(b))`. */
|
|
184
|
-
forEach<
|
|
166
|
+
forEach<
|
|
167
|
+
TIn,
|
|
168
|
+
TElement,
|
|
169
|
+
TNext,
|
|
170
|
+
TRefs extends string,
|
|
171
|
+
TRefs2 extends string = never,
|
|
172
|
+
>(
|
|
185
173
|
this: TypedAction<TIn, TElement[], TRefs>,
|
|
186
174
|
action: Pipeable<TElement, TNext, TRefs2>,
|
|
187
175
|
): TypedAction<TIn, TNext[], TRefs | TRefs2>;
|
|
188
176
|
/** Dispatch on a tagged union output. Auto-unwraps `value` before each case handler. */
|
|
189
|
-
branch<
|
|
177
|
+
branch<
|
|
178
|
+
TCases extends {
|
|
179
|
+
[K in BranchKeys<Out>]: CaseHandler<
|
|
180
|
+
BranchPayload<Out, K>,
|
|
181
|
+
unknown,
|
|
182
|
+
string
|
|
183
|
+
>;
|
|
184
|
+
},
|
|
185
|
+
>(
|
|
190
186
|
cases: [BranchKeys<Out>] extends [never] ? never : TCases,
|
|
191
|
-
): TypedAction<In, ExtractOutput<TCases[keyof TCases & string]>, Refs
|
|
187
|
+
): TypedAction<In, ExtractOutput<TCases[keyof TCases & string]>, Refs>;
|
|
192
188
|
/** Flatten a nested array output. `a.flatten()` ≡ `pipe(a, flatten())`. */
|
|
193
|
-
flatten(): TypedAction<
|
|
189
|
+
flatten(): TypedAction<
|
|
190
|
+
In,
|
|
191
|
+
Out extends (infer TElement)[][] ? TElement[] : Out,
|
|
192
|
+
Refs
|
|
193
|
+
>;
|
|
194
194
|
/** Discard output. `a.drop()` ≡ `pipe(a, drop)`. */
|
|
195
195
|
drop(): TypedAction<In, never, Refs>;
|
|
196
196
|
/** Wrap output as a tagged union member. Requires full variant map TDef so __def is carried. */
|
|
@@ -198,7 +198,9 @@ export type TypedAction<
|
|
|
198
198
|
kind: TKind,
|
|
199
199
|
): TypedAction<In, TaggedUnion<TDef>, Refs>;
|
|
200
200
|
/** Extract a field from the output object. `a.get("name")` ≡ `pipe(a, extractField("name"))`. */
|
|
201
|
-
get<TField extends keyof Out & string>(
|
|
201
|
+
get<TField extends keyof Out & string>(
|
|
202
|
+
field: TField,
|
|
203
|
+
): TypedAction<In, Out[TField], Refs>;
|
|
202
204
|
/**
|
|
203
205
|
* Run this sub-pipeline, then merge its output back into the original input.
|
|
204
206
|
* `pipe(extractField("x"), transform).augment()` takes `In`, runs the
|
|
@@ -303,7 +305,11 @@ export type Pipeable<
|
|
|
303
305
|
* TypedAction is assignable to CaseHandler because CaseHandler only
|
|
304
306
|
* requires a subset of TypedAction's phantom fields.
|
|
305
307
|
*/
|
|
306
|
-
type CaseHandler<
|
|
308
|
+
type CaseHandler<
|
|
309
|
+
TIn = unknown,
|
|
310
|
+
TOut = unknown,
|
|
311
|
+
TRefs extends string = never,
|
|
312
|
+
> = Action & {
|
|
307
313
|
__phantom_in?: (input: TIn) => void;
|
|
308
314
|
__phantom_out?: () => TOut;
|
|
309
315
|
__refs?: { _brand: TRefs };
|
|
@@ -320,8 +326,21 @@ type CaseHandler<TIn = unknown, TOut = unknown, TRefs extends string = never> =
|
|
|
320
326
|
* (`keyof ExtractDef<Out>` and `ExtractDef<Out>[K]`) instead of
|
|
321
327
|
* conditional types (`KindOf<Out>` and `Extract<Out, { kind: K }>`).
|
|
322
328
|
*/
|
|
329
|
+
// 0 extends 1 & T detects `any` — preserve as-is to avoid collapsing.
|
|
330
|
+
type VoidToNull<T> = 0 extends 1 & T
|
|
331
|
+
? T
|
|
332
|
+
: [T] extends [never]
|
|
333
|
+
? never
|
|
334
|
+
: [T] extends [void]
|
|
335
|
+
? null
|
|
336
|
+
: T;
|
|
337
|
+
|
|
323
338
|
export type TaggedUnion<TDef extends Record<string, unknown>> = {
|
|
324
|
-
[K in keyof TDef & string]: {
|
|
339
|
+
[K in keyof TDef & string]: {
|
|
340
|
+
kind: K;
|
|
341
|
+
value: VoidToNull<TDef[K]>;
|
|
342
|
+
__def?: TDef;
|
|
343
|
+
};
|
|
325
344
|
}[keyof TDef & string];
|
|
326
345
|
|
|
327
346
|
/** Extract the variant map definition from a tagged union's phantom __def. */
|
|
@@ -352,50 +371,65 @@ type UnwrapVariant<T> = T extends { value: infer V } ? V : T;
|
|
|
352
371
|
* output carries __def. Falls back to KindOf (conditional type) for
|
|
353
372
|
* outputs without __def.
|
|
354
373
|
*/
|
|
355
|
-
type BranchKeys<Out> =
|
|
356
|
-
|
|
374
|
+
type BranchKeys<Out> = [ExtractDef<Out>] extends [never]
|
|
375
|
+
? KindOf<Out>
|
|
376
|
+
: keyof ExtractDef<Out> & string;
|
|
357
377
|
|
|
358
378
|
/**
|
|
359
379
|
* Branch case payload: prefer ExtractDef[K] (simple indexing) when available.
|
|
360
380
|
* Falls back to UnwrapVariant<Extract<Out, { kind: K }>> for outputs without __def.
|
|
361
381
|
*/
|
|
362
|
-
type BranchPayload<Out, K extends string> =
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
382
|
+
type BranchPayload<Out, K extends string> = [ExtractDef<Out>] extends [never]
|
|
383
|
+
? UnwrapVariant<Extract<Out, { kind: K }>>
|
|
384
|
+
: K extends keyof ExtractDef<Out>
|
|
385
|
+
? VoidToNull<ExtractDef<Out>[K]>
|
|
386
|
+
: never;
|
|
367
387
|
|
|
368
388
|
// ---------------------------------------------------------------------------
|
|
369
389
|
// typedAction — attach .then() and .forEach() as non-enumerable methods
|
|
370
390
|
// ---------------------------------------------------------------------------
|
|
371
391
|
|
|
372
392
|
// Shared implementations (one closure, not per-instance)
|
|
373
|
-
function thenMethod<
|
|
393
|
+
function thenMethod<
|
|
394
|
+
TIn,
|
|
395
|
+
TOut,
|
|
396
|
+
TRefs extends string,
|
|
397
|
+
TNext,
|
|
398
|
+
TRefs2 extends string,
|
|
399
|
+
>(
|
|
374
400
|
this: TypedAction<TIn, TOut, TRefs>,
|
|
375
401
|
next: Pipeable<TOut, TNext, TRefs2>,
|
|
376
402
|
): TypedAction<TIn, TNext, TRefs | TRefs2> {
|
|
377
403
|
return typedAction({ kind: "Chain", first: this, rest: next as Action });
|
|
378
404
|
}
|
|
379
405
|
|
|
380
|
-
function forEachMethod(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
406
|
+
function forEachMethod(this: TypedAction, action: Action): TypedAction {
|
|
407
|
+
return typedAction({
|
|
408
|
+
kind: "Chain",
|
|
409
|
+
first: this,
|
|
410
|
+
rest: { kind: "ForEach", action },
|
|
411
|
+
});
|
|
385
412
|
}
|
|
386
413
|
|
|
387
414
|
function branchMethod(
|
|
388
415
|
this: TypedAction,
|
|
389
416
|
cases: Record<string, Action>,
|
|
390
417
|
): TypedAction {
|
|
391
|
-
return typedAction({
|
|
418
|
+
return typedAction({
|
|
419
|
+
kind: "Chain",
|
|
420
|
+
first: this,
|
|
421
|
+
rest: { kind: "Branch", cases: unwrapBranchCases(cases) },
|
|
422
|
+
});
|
|
392
423
|
}
|
|
393
424
|
|
|
394
425
|
function flattenMethod(this: TypedAction): TypedAction {
|
|
395
426
|
return typedAction({
|
|
396
427
|
kind: "Chain",
|
|
397
428
|
first: this,
|
|
398
|
-
rest: {
|
|
429
|
+
rest: {
|
|
430
|
+
kind: "Invoke",
|
|
431
|
+
handler: { kind: "Builtin", builtin: { kind: "Flatten" } },
|
|
432
|
+
},
|
|
399
433
|
});
|
|
400
434
|
}
|
|
401
435
|
|
|
@@ -403,7 +437,10 @@ function dropMethod(this: TypedAction): TypedAction {
|
|
|
403
437
|
return typedAction({
|
|
404
438
|
kind: "Chain",
|
|
405
439
|
first: this,
|
|
406
|
-
rest: {
|
|
440
|
+
rest: {
|
|
441
|
+
kind: "Invoke",
|
|
442
|
+
handler: { kind: "Builtin", builtin: { kind: "Drop" } },
|
|
443
|
+
},
|
|
407
444
|
});
|
|
408
445
|
}
|
|
409
446
|
|
|
@@ -411,7 +448,10 @@ function tagMethod(this: TypedAction, kind: string): TypedAction {
|
|
|
411
448
|
return typedAction({
|
|
412
449
|
kind: "Chain",
|
|
413
450
|
first: this,
|
|
414
|
-
rest: {
|
|
451
|
+
rest: {
|
|
452
|
+
kind: "Invoke",
|
|
453
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: kind } },
|
|
454
|
+
},
|
|
415
455
|
});
|
|
416
456
|
}
|
|
417
457
|
|
|
@@ -419,7 +459,13 @@ function getMethod(this: TypedAction, field: string): TypedAction {
|
|
|
419
459
|
return typedAction({
|
|
420
460
|
kind: "Chain",
|
|
421
461
|
first: this,
|
|
422
|
-
rest: {
|
|
462
|
+
rest: {
|
|
463
|
+
kind: "Invoke",
|
|
464
|
+
handler: {
|
|
465
|
+
kind: "Builtin",
|
|
466
|
+
builtin: { kind: "ExtractField", value: field },
|
|
467
|
+
},
|
|
468
|
+
},
|
|
423
469
|
});
|
|
424
470
|
}
|
|
425
471
|
|
|
@@ -433,10 +479,16 @@ function augmentMethod(this: TypedAction): TypedAction {
|
|
|
433
479
|
kind: "All",
|
|
434
480
|
actions: [
|
|
435
481
|
this as Action,
|
|
436
|
-
{
|
|
482
|
+
{
|
|
483
|
+
kind: "Invoke",
|
|
484
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
485
|
+
},
|
|
437
486
|
],
|
|
438
487
|
},
|
|
439
|
-
rest: {
|
|
488
|
+
rest: {
|
|
489
|
+
kind: "Invoke",
|
|
490
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
491
|
+
},
|
|
440
492
|
});
|
|
441
493
|
}
|
|
442
494
|
|
|
@@ -444,7 +496,10 @@ function mergeMethod(this: TypedAction): TypedAction {
|
|
|
444
496
|
return typedAction({
|
|
445
497
|
kind: "Chain",
|
|
446
498
|
first: this,
|
|
447
|
-
rest: {
|
|
499
|
+
rest: {
|
|
500
|
+
kind: "Invoke",
|
|
501
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
502
|
+
},
|
|
448
503
|
});
|
|
449
504
|
}
|
|
450
505
|
|
|
@@ -452,7 +507,10 @@ function pickMethod(this: TypedAction, ...keys: string[]): TypedAction {
|
|
|
452
507
|
return typedAction({
|
|
453
508
|
kind: "Chain",
|
|
454
509
|
first: this,
|
|
455
|
-
rest: {
|
|
510
|
+
rest: {
|
|
511
|
+
kind: "Invoke",
|
|
512
|
+
handler: { kind: "Builtin", builtin: { kind: "Pick", value: keys } },
|
|
513
|
+
},
|
|
456
514
|
});
|
|
457
515
|
}
|
|
458
516
|
|
|
@@ -466,9 +524,22 @@ function mapOptionMethod(this: TypedAction, action: Action): TypedAction {
|
|
|
466
524
|
first: this,
|
|
467
525
|
rest: {
|
|
468
526
|
kind: "Branch",
|
|
469
|
-
|
|
470
|
-
Some: {
|
|
471
|
-
|
|
527
|
+
cases: unwrapBranchCases({
|
|
528
|
+
Some: {
|
|
529
|
+
kind: "Chain",
|
|
530
|
+
first: action,
|
|
531
|
+
rest: {
|
|
532
|
+
kind: "Invoke",
|
|
533
|
+
handler: {
|
|
534
|
+
kind: "Builtin",
|
|
535
|
+
builtin: { kind: "Tag", value: "Some" },
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
None: {
|
|
540
|
+
kind: "Invoke",
|
|
541
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "None" } },
|
|
542
|
+
},
|
|
472
543
|
}),
|
|
473
544
|
},
|
|
474
545
|
});
|
|
@@ -481,9 +552,22 @@ function mapErrMethod(this: TypedAction, action: Action): TypedAction {
|
|
|
481
552
|
first: this,
|
|
482
553
|
rest: {
|
|
483
554
|
kind: "Branch",
|
|
484
|
-
|
|
485
|
-
Ok: {
|
|
486
|
-
|
|
555
|
+
cases: unwrapBranchCases({
|
|
556
|
+
Ok: {
|
|
557
|
+
kind: "Invoke",
|
|
558
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
|
|
559
|
+
},
|
|
560
|
+
Err: {
|
|
561
|
+
kind: "Chain",
|
|
562
|
+
first: action,
|
|
563
|
+
rest: {
|
|
564
|
+
kind: "Invoke",
|
|
565
|
+
handler: {
|
|
566
|
+
kind: "Builtin",
|
|
567
|
+
builtin: { kind: "Tag", value: "Err" },
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
487
571
|
}),
|
|
488
572
|
},
|
|
489
573
|
});
|
|
@@ -496,8 +580,11 @@ function unwrapOrMethod(this: TypedAction, defaultAction: Action): TypedAction {
|
|
|
496
580
|
first: this,
|
|
497
581
|
rest: {
|
|
498
582
|
kind: "Branch",
|
|
499
|
-
|
|
500
|
-
Ok: {
|
|
583
|
+
cases: unwrapBranchCases({
|
|
584
|
+
Ok: {
|
|
585
|
+
kind: "Invoke",
|
|
586
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
587
|
+
},
|
|
501
588
|
Err: defaultAction,
|
|
502
589
|
}),
|
|
503
590
|
},
|
|
@@ -508,9 +595,11 @@ function unwrapOrMethod(this: TypedAction, defaultAction: Action): TypedAction {
|
|
|
508
595
|
* Attach `.then()` and `.forEach()` methods to a plain Action object.
|
|
509
596
|
* Methods are non-enumerable: invisible to JSON.stringify and toEqual.
|
|
510
597
|
*/
|
|
511
|
-
export function typedAction<
|
|
512
|
-
|
|
513
|
-
|
|
598
|
+
export function typedAction<
|
|
599
|
+
In = unknown,
|
|
600
|
+
Out = unknown,
|
|
601
|
+
Refs extends string = never,
|
|
602
|
+
>(action: Action): TypedAction<In, Out, Refs> {
|
|
514
603
|
if (!("then" in action)) {
|
|
515
604
|
Object.defineProperties(action, {
|
|
516
605
|
then: { value: thenMethod, configurable: true },
|
|
@@ -542,140 +631,20 @@ export function typedAction<In = unknown, Out = unknown, Refs extends string = n
|
|
|
542
631
|
* avoid the `TypedAction<any, any, any>` constraint which fails for In=never
|
|
543
632
|
* due to __phantom_in contravariance.
|
|
544
633
|
*/
|
|
545
|
-
export type ExtractInput<T> =
|
|
546
|
-
|
|
634
|
+
export type ExtractInput<T> = T extends {
|
|
635
|
+
__phantom_in?: (input: infer In) => void;
|
|
636
|
+
}
|
|
637
|
+
? In
|
|
638
|
+
: never;
|
|
547
639
|
|
|
548
640
|
/**
|
|
549
641
|
* Extract the output type from a TypedAction.
|
|
550
642
|
*
|
|
551
643
|
* Uses direct phantom field extraction to avoid constraint issues.
|
|
552
644
|
*/
|
|
553
|
-
export type ExtractOutput<T> =
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Extract step reference names tracked in a TypedAction's Refs parameter.
|
|
558
|
-
*
|
|
559
|
-
* Uses direct __refs extraction (not full TypedAction matching) to avoid
|
|
560
|
-
* variance issues with contravariant __phantom_in when In = never.
|
|
561
|
-
*/
|
|
562
|
-
export type ExtractRefs<T> =
|
|
563
|
-
T extends { __refs?: { _brand: infer R } }
|
|
564
|
-
? R extends string
|
|
565
|
-
? R
|
|
566
|
-
: never
|
|
567
|
-
: never;
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Validates that all step references in R resolve to known step names
|
|
571
|
-
* within the current batch only (keyof R). Previously registered steps
|
|
572
|
-
* should be accessed via the callback's `steps` parameter, not stepRef.
|
|
573
|
-
*
|
|
574
|
-
* When valid: resolves to {} (transparent intersection).
|
|
575
|
-
* When invalid: resolves to a type with __error that causes a compile error.
|
|
576
|
-
*/
|
|
577
|
-
export type ValidateStepRefs<
|
|
578
|
-
R extends Record<string, Action>,
|
|
579
|
-
> = [ExtractRefs<R[keyof R]>] extends [keyof R]
|
|
580
|
-
? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
581
|
-
{}
|
|
582
|
-
: {
|
|
583
|
-
__error: `Step reference to undefined step: ${Exclude<ExtractRefs<R[keyof R]>, keyof R> & string}`;
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
// ---------------------------------------------------------------------------
|
|
587
|
-
// Step Reference Tracking (Refs type parameter)
|
|
588
|
-
// ---------------------------------------------------------------------------
|
|
589
|
-
//
|
|
590
|
-
// ## Overview
|
|
591
|
-
//
|
|
592
|
-
// Named steps can reference each other via `stepRef("B")`. These references
|
|
593
|
-
// are validated at compile time: if you write `stepRef("Bt")` when only "A"
|
|
594
|
-
// and "B" exist, TypeScript rejects it. This works through a third type
|
|
595
|
-
// parameter on TypedAction called `Refs`.
|
|
596
|
-
//
|
|
597
|
-
// ## How it works
|
|
598
|
-
//
|
|
599
|
-
// 1. `stepRef<N extends string>(name: N)` returns `TypedAction<any, any, N>`.
|
|
600
|
-
// The literal string "B" is captured in the Refs position.
|
|
601
|
-
//
|
|
602
|
-
// 2. Every combinator propagates Refs via union. For example, pipe's 2-arg
|
|
603
|
-
// overload is:
|
|
604
|
-
//
|
|
605
|
-
// pipe<T1, T2, T3, R1 extends string, R2 extends string>(
|
|
606
|
-
// a1: TypedAction<T1, T2, R1>,
|
|
607
|
-
// a2: TypedAction<T2, T3, R2>,
|
|
608
|
-
// ): TypedAction<T1, T3, R1 | R2>
|
|
609
|
-
//
|
|
610
|
-
// So `pipe(check(), stepRef("B"))` has Refs = never | "B" = "B".
|
|
611
|
-
// And `pipe(stepRef("A"), stepRef("B"))` has Refs = "A" | "B".
|
|
612
|
-
//
|
|
613
|
-
// 3. `registerSteps` uses `ValidateStepRefs` to extract all Refs from the
|
|
614
|
-
// registered step values and check they're a subset of the current batch
|
|
615
|
-
// keys only (keyof R). Previously registered steps are accessed via the
|
|
616
|
-
// typed `steps` parameter in the callback form, not via stepRef:
|
|
617
|
-
//
|
|
618
|
-
// registerSteps<R extends Record<string, Action>>(
|
|
619
|
-
// stepsOrBuild:
|
|
620
|
-
// | (R & ValidateStepRefs<R>)
|
|
621
|
-
// | ((ctx: { steps: StripRefs<TSteps>; stepRef: ... })
|
|
622
|
-
// => R & ValidateStepRefs<R>),
|
|
623
|
-
// )
|
|
624
|
-
//
|
|
625
|
-
// When valid, ValidateStepRefs resolves to {} (transparent intersection).
|
|
626
|
-
// When invalid, it resolves to { __error: "Step reference to undefined
|
|
627
|
-
// step: Bt" }, which makes the argument incompatible and produces a
|
|
628
|
-
// readable compile error.
|
|
629
|
-
//
|
|
630
|
-
// stepRef is not exported — it's only available as a parameter in the
|
|
631
|
-
// registerSteps callback. This ensures step references are always
|
|
632
|
-
// validated within a batch context.
|
|
633
|
-
//
|
|
634
|
-
// 4. `StripRefs` removes Refs from step types before they're passed to the
|
|
635
|
-
// workflow callback, since refs have already been validated by
|
|
636
|
-
// registerSteps and shouldn't propagate into the workflow's return type.
|
|
637
|
-
//
|
|
638
|
-
// ## Why Refs is boxed: `__refs?: { _brand: Refs }`
|
|
639
|
-
//
|
|
640
|
-
// The Refs phantom field uses a boxing wrapper `{ _brand: Refs }` instead of
|
|
641
|
-
// a bare `__refs?: Refs`. This is necessary because of how TypeScript infers
|
|
642
|
-
// generic type parameters from optional properties on discriminated unions.
|
|
643
|
-
//
|
|
644
|
-
// When Refs = never (the common case — most actions don't use stepRef), the
|
|
645
|
-
// field `__refs?: never` collapses to `undefined` at the type level. When
|
|
646
|
-
// pipe's overload tries to infer `R1 extends string` from this field, TS
|
|
647
|
-
// sees `undefined`, can't find a valid inference for `R1`, and falls back to
|
|
648
|
-
// the constraint bound `string`.
|
|
649
|
-
//
|
|
650
|
-
// This means `pipe(check(), recur())` — two actions with no step refs —
|
|
651
|
-
// would infer Refs = string | string = string. Then ValidateStepRefs sees
|
|
652
|
-
// Refs = string and rejects it because `string` doesn't extend the step
|
|
653
|
-
// name literals.
|
|
654
|
-
//
|
|
655
|
-
// **Important**: this only happens with the real Action type (a discriminated
|
|
656
|
-
// union of 8 variants). With a simple `{ kind: string }` Action type, TS
|
|
657
|
-
// infers correctly. The union distribution changes how TS resolves optional
|
|
658
|
-
// fields during inference.
|
|
659
|
-
//
|
|
660
|
-
// The fix: `__refs?: { _brand: Refs }`. When Refs = never, the field is
|
|
661
|
-
// `__refs?: { _brand: never }`. The wrapper `{ _brand: never }` is a
|
|
662
|
-
// distinct structural type (not just `undefined`), so TS can match
|
|
663
|
-
// `{ _brand: R1 }` against `{ _brand: never }` and correctly infer
|
|
664
|
-
// R1 = never.
|
|
665
|
-
//
|
|
666
|
-
// ## Why ExtractInput/ExtractOutput/ExtractRefs use structural extraction
|
|
667
|
-
//
|
|
668
|
-
// All three Extract* utilities match on individual phantom fields rather than
|
|
669
|
-
// on the full TypedAction type. The constraint `TypedAction<any, any, any>`
|
|
670
|
-
// fails for actions with In = never because `(input: never) => void` is not
|
|
671
|
-
// assignable to `(input: any) => void` (function params are contravariant).
|
|
672
|
-
// Structural extraction avoids this entirely.
|
|
673
|
-
//
|
|
674
|
-
// ExtractRefs uses a two-step conditional (`infer R` then `R extends string`)
|
|
675
|
-
// rather than `infer R extends string` because the latter falls back to the
|
|
676
|
-
// constraint bound `string` when R = never.
|
|
677
|
-
//
|
|
678
|
-
// ---------------------------------------------------------------------------
|
|
645
|
+
export type ExtractOutput<T> = T extends { __phantom_out?: () => infer Out }
|
|
646
|
+
? Out
|
|
647
|
+
: never;
|
|
679
648
|
|
|
680
649
|
// ---------------------------------------------------------------------------
|
|
681
650
|
// Combinators
|
|
@@ -686,7 +655,11 @@ export { chain } from "./chain.js";
|
|
|
686
655
|
export { all } from "./all.js";
|
|
687
656
|
export { bind, bindInput, type VarRef, type InferVarRefs } from "./bind.js";
|
|
688
657
|
export { resetEffectIdCounter } from "./effect-id.js";
|
|
689
|
-
import {
|
|
658
|
+
import {
|
|
659
|
+
allocateRestartHandlerId,
|
|
660
|
+
type RestartHandlerId,
|
|
661
|
+
type ResumeHandlerId,
|
|
662
|
+
} from "./effect-id.js";
|
|
690
663
|
export { tryCatch } from "./try-catch.js";
|
|
691
664
|
export { race, sleep, withTimeout } from "./race.js";
|
|
692
665
|
|
|
@@ -702,12 +675,20 @@ export function forEach<In, Out, R extends string = never>(
|
|
|
702
675
|
* extracts `value` before passing to the handler. Case handlers receive
|
|
703
676
|
* the payload directly, not the full `{ kind, value }` variant.
|
|
704
677
|
*/
|
|
705
|
-
function unwrapBranchCases(
|
|
678
|
+
function unwrapBranchCases(
|
|
679
|
+
cases: Record<string, Action>,
|
|
680
|
+
): Record<string, Action> {
|
|
706
681
|
const unwrapped: Record<string, Action> = {};
|
|
707
682
|
for (const key of Object.keys(cases)) {
|
|
708
683
|
unwrapped[key] = {
|
|
709
684
|
kind: "Chain",
|
|
710
|
-
first: {
|
|
685
|
+
first: {
|
|
686
|
+
kind: "Invoke",
|
|
687
|
+
handler: {
|
|
688
|
+
kind: "Builtin",
|
|
689
|
+
builtin: { kind: "ExtractField", value: "value" },
|
|
690
|
+
},
|
|
691
|
+
},
|
|
711
692
|
rest: cases[key],
|
|
712
693
|
};
|
|
713
694
|
}
|
|
@@ -723,8 +704,8 @@ function unwrapBranchCases(cases: Record<string, Action>): Record<string, Action
|
|
|
723
704
|
* Example: `BranchInput<{ Yes: TypedAction<number, ...>, No: TypedAction<string, ...> }>`
|
|
724
705
|
* = `{ kind: "Yes"; value: number } | { kind: "No"; value: string }`
|
|
725
706
|
*
|
|
726
|
-
* When a case handler uses `any` as input
|
|
727
|
-
*
|
|
707
|
+
* When a case handler uses `any` as input, the wrapping produces
|
|
708
|
+
* `{ kind: K; value: any }`, which is the correct escape hatch.
|
|
728
709
|
*/
|
|
729
710
|
export type BranchInput<TCases> = {
|
|
730
711
|
[K in keyof TCases & string]: { kind: K; value: ExtractInput<TCases[K]> };
|
|
@@ -735,8 +716,7 @@ export function branch<TCases extends Record<string, Action>>(
|
|
|
735
716
|
cases: TCases,
|
|
736
717
|
): TypedAction<
|
|
737
718
|
BranchInput<TCases>,
|
|
738
|
-
ExtractOutput<TCases[keyof TCases & string]
|
|
739
|
-
ExtractRefs<TCases[keyof TCases & string]>
|
|
719
|
+
ExtractOutput<TCases[keyof TCases & string]>
|
|
740
720
|
> {
|
|
741
721
|
return typedAction({ kind: "Branch", cases: unwrapBranchCases(cases) });
|
|
742
722
|
}
|
|
@@ -746,7 +726,9 @@ type LoopResultDef<TContinue, TBreak> = {
|
|
|
746
726
|
Break: TBreak;
|
|
747
727
|
};
|
|
748
728
|
|
|
749
|
-
export type LoopResult<TContinue, TBreak> = TaggedUnion<
|
|
729
|
+
export type LoopResult<TContinue, TBreak> = TaggedUnion<
|
|
730
|
+
LoopResultDef<TContinue, TBreak>
|
|
731
|
+
>;
|
|
750
732
|
|
|
751
733
|
// ---------------------------------------------------------------------------
|
|
752
734
|
// Shared AST constants for control flow compilation
|
|
@@ -754,12 +736,7 @@ export type LoopResult<TContinue, TBreak> = TaggedUnion<LoopResultDef<TContinue,
|
|
|
754
736
|
|
|
755
737
|
const EXTRACT_PAYLOAD: Action = {
|
|
756
738
|
kind: "Invoke",
|
|
757
|
-
handler: { kind: "Builtin", builtin: { kind: "
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
const TAG_RESTART_BODY: Action = {
|
|
761
|
-
kind: "Invoke",
|
|
762
|
-
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "RestartBody" } },
|
|
739
|
+
handler: { kind: "Builtin", builtin: { kind: "ExtractIndex", value: 0 } },
|
|
763
740
|
};
|
|
764
741
|
|
|
765
742
|
const TAG_CONTINUE: Action = {
|
|
@@ -767,23 +744,16 @@ const TAG_CONTINUE: Action = {
|
|
|
767
744
|
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Continue" } },
|
|
768
745
|
};
|
|
769
746
|
|
|
770
|
-
const TAG_BREAK: Action = {
|
|
747
|
+
export const TAG_BREAK: Action = {
|
|
771
748
|
kind: "Invoke",
|
|
772
749
|
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Break" } },
|
|
773
750
|
};
|
|
774
751
|
|
|
775
|
-
const IDENTITY: Action = {
|
|
752
|
+
export const IDENTITY: Action = {
|
|
776
753
|
kind: "Invoke",
|
|
777
754
|
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
778
755
|
};
|
|
779
756
|
|
|
780
|
-
/** Handler: extract payload, tag as RestartBody. */
|
|
781
|
-
const RESTART_BODY_HANDLER: Action = {
|
|
782
|
-
kind: "Chain",
|
|
783
|
-
first: EXTRACT_PAYLOAD,
|
|
784
|
-
rest: TAG_RESTART_BODY,
|
|
785
|
-
};
|
|
786
|
-
|
|
787
757
|
// ---------------------------------------------------------------------------
|
|
788
758
|
// recur — restart body primitive
|
|
789
759
|
// ---------------------------------------------------------------------------
|
|
@@ -795,25 +765,25 @@ const RESTART_BODY_HANDLER: Action = {
|
|
|
795
765
|
* If the body completes normally → output is TOut.
|
|
796
766
|
* If restart fires → body re-executes with the restarted value.
|
|
797
767
|
*
|
|
798
|
-
* Compiled form:
|
|
768
|
+
* Compiled form: `RestartHandle(id, ExtractIndex(0), body)`
|
|
799
769
|
*/
|
|
800
770
|
export function recur<TIn = never, TOut = any, TRefs extends string = never>(
|
|
801
771
|
bodyFn: (restart: TypedAction<TIn, never>) => Pipeable<TIn, TOut, TRefs>,
|
|
802
772
|
): TypedAction<PipeIn<TIn>, TOut, TRefs> {
|
|
803
|
-
const
|
|
773
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
804
774
|
|
|
805
775
|
const restartAction = typedAction<TIn, never>({
|
|
806
|
-
kind: "
|
|
807
|
-
|
|
776
|
+
kind: "RestartPerform",
|
|
777
|
+
restart_handler_id: restartHandlerId,
|
|
808
778
|
});
|
|
809
779
|
|
|
810
780
|
const body = bodyFn(restartAction) as Action;
|
|
811
781
|
|
|
812
782
|
return typedAction({
|
|
813
|
-
kind: "
|
|
814
|
-
|
|
783
|
+
kind: "RestartHandle",
|
|
784
|
+
restart_handler_id: restartHandlerId,
|
|
815
785
|
body,
|
|
816
|
-
handler:
|
|
786
|
+
handler: EXTRACT_PAYLOAD,
|
|
817
787
|
});
|
|
818
788
|
}
|
|
819
789
|
|
|
@@ -833,20 +803,29 @@ export function recur<TIn = never, TOut = any, TRefs extends string = never>(
|
|
|
833
803
|
* a Branch. earlyReturn tags with Break and performs — the handler restarts
|
|
834
804
|
* the body, Branch takes the Break path, and the value exits.
|
|
835
805
|
*/
|
|
836
|
-
export function earlyReturn<
|
|
837
|
-
|
|
806
|
+
export function earlyReturn<
|
|
807
|
+
TEarlyReturn = never,
|
|
808
|
+
TIn = any,
|
|
809
|
+
TOut = any,
|
|
810
|
+
TRefs extends string = never,
|
|
811
|
+
>(
|
|
812
|
+
bodyFn: (
|
|
813
|
+
earlyReturn: TypedAction<TEarlyReturn, never>,
|
|
814
|
+
) => Pipeable<TIn, TOut, TRefs>,
|
|
838
815
|
): TypedAction<TIn, TEarlyReturn | TOut, TRefs> {
|
|
839
|
-
const
|
|
816
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
840
817
|
|
|
841
818
|
const earlyReturnAction = typedAction<TEarlyReturn, never>({
|
|
842
819
|
kind: "Chain",
|
|
843
820
|
first: TAG_BREAK,
|
|
844
|
-
rest: { kind: "
|
|
821
|
+
rest: { kind: "RestartPerform", restart_handler_id: restartHandlerId },
|
|
845
822
|
});
|
|
846
823
|
|
|
847
824
|
const body = bodyFn(earlyReturnAction) as Action;
|
|
848
825
|
|
|
849
|
-
return typedAction(
|
|
826
|
+
return typedAction(
|
|
827
|
+
buildRestartBranchAction(restartHandlerId, body, IDENTITY),
|
|
828
|
+
);
|
|
850
829
|
}
|
|
851
830
|
|
|
852
831
|
// ---------------------------------------------------------------------------
|
|
@@ -854,27 +833,33 @@ export function earlyReturn<TEarlyReturn = never, TIn = any, TOut = any, TRefs e
|
|
|
854
833
|
// ---------------------------------------------------------------------------
|
|
855
834
|
|
|
856
835
|
/**
|
|
857
|
-
* Build the restart+branch compiled form
|
|
858
|
-
* Chain(Tag("Continue"),
|
|
836
|
+
* Build the restart+branch compiled form:
|
|
837
|
+
* `Chain(Tag("Continue"), RestartHandle(id, ExtractIndex(0), Branch({ Continue: continueArm, Break: breakArm })))`
|
|
859
838
|
*
|
|
860
|
-
* Input is tagged Continue so the Branch enters the
|
|
861
|
-
* Continue tag → restart → re-enters
|
|
839
|
+
* Input is tagged Continue so the Branch enters the continueArm on first execution.
|
|
840
|
+
* Continue tag → restart → re-enters continueArm. Break tag → restart → runs breakArm, exits `RestartHandle`.
|
|
841
|
+
*
|
|
842
|
+
* Used by earlyReturn, loop, tryCatch, and race.
|
|
862
843
|
*/
|
|
863
|
-
function
|
|
844
|
+
export function buildRestartBranchAction(
|
|
845
|
+
restartHandlerId: RestartHandlerId,
|
|
846
|
+
continueArm: Action,
|
|
847
|
+
breakArm: Action,
|
|
848
|
+
): Action {
|
|
864
849
|
return {
|
|
865
850
|
kind: "Chain",
|
|
866
851
|
first: TAG_CONTINUE,
|
|
867
852
|
rest: {
|
|
868
|
-
kind: "
|
|
869
|
-
|
|
853
|
+
kind: "RestartHandle",
|
|
854
|
+
restart_handler_id: restartHandlerId,
|
|
870
855
|
body: {
|
|
871
856
|
kind: "Branch",
|
|
872
857
|
cases: unwrapBranchCases({
|
|
873
|
-
Continue:
|
|
874
|
-
Break:
|
|
858
|
+
Continue: continueArm,
|
|
859
|
+
Break: breakArm,
|
|
875
860
|
}),
|
|
876
861
|
},
|
|
877
|
-
handler:
|
|
862
|
+
handler: EXTRACT_PAYLOAD,
|
|
878
863
|
},
|
|
879
864
|
};
|
|
880
865
|
}
|
|
@@ -886,17 +871,20 @@ function buildLoopAction(effectId: number, body: Action): Action {
|
|
|
886
871
|
*
|
|
887
872
|
* Both are TypedAction values (not functions), consistent with throwError in tryCatch.
|
|
888
873
|
*
|
|
889
|
-
* Compiles to
|
|
874
|
+
* Compiles to `RestartHandle`/`RestartPerform`/Branch — same effect substrate as tryCatch and earlyReturn.
|
|
890
875
|
*/
|
|
891
876
|
export function loop<TBreak = never, TIn = never, TRefs extends string = never>(
|
|
892
877
|
bodyFn: (
|
|
893
878
|
recur: TypedAction<TIn, never>,
|
|
894
|
-
done: TypedAction<TBreak
|
|
879
|
+
done: TypedAction<VoidToNull<TBreak>, never>,
|
|
895
880
|
) => Pipeable<TIn, never, TRefs>,
|
|
896
|
-
): TypedAction<PipeIn<TIn>, TBreak
|
|
897
|
-
const
|
|
881
|
+
): TypedAction<PipeIn<TIn>, VoidToNull<TBreak>, TRefs> {
|
|
882
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
898
883
|
|
|
899
|
-
const perform: Action = {
|
|
884
|
+
const perform: Action = {
|
|
885
|
+
kind: "RestartPerform",
|
|
886
|
+
restart_handler_id: restartHandlerId,
|
|
887
|
+
};
|
|
900
888
|
|
|
901
889
|
const recurAction = typedAction<TIn, never>({
|
|
902
890
|
kind: "Chain",
|
|
@@ -904,7 +892,7 @@ export function loop<TBreak = never, TIn = never, TRefs extends string = never>(
|
|
|
904
892
|
rest: perform,
|
|
905
893
|
});
|
|
906
894
|
|
|
907
|
-
const doneAction = typedAction<TBreak
|
|
895
|
+
const doneAction = typedAction<VoidToNull<TBreak>, never>({
|
|
908
896
|
kind: "Chain",
|
|
909
897
|
first: TAG_BREAK,
|
|
910
898
|
rest: perform,
|
|
@@ -912,196 +900,16 @@ export function loop<TBreak = never, TIn = never, TRefs extends string = never>(
|
|
|
912
900
|
|
|
913
901
|
const body = bodyFn(recurAction, doneAction) as Action;
|
|
914
902
|
|
|
915
|
-
return typedAction(
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Create a typed step reference. The name is tracked at the type level
|
|
920
|
-
* via the Refs parameter so registerSteps can validate all references resolve.
|
|
921
|
-
*
|
|
922
|
-
* Not exported — only available as a parameter in registerSteps callbacks.
|
|
923
|
-
* This ensures step references are always validated within a batch context.
|
|
924
|
-
*
|
|
925
|
-
* **Warning: no input/output type safety.** stepRef returns
|
|
926
|
-
* `TypedAction<any, any, N>` — it validates the reference *name* at compile
|
|
927
|
-
* time, but provides no type checking on input or output. The referenced
|
|
928
|
-
* step's types are unknown at the call site (mutual recursion means the
|
|
929
|
-
* step may not be fully defined yet). Prefer `steps.X` from the callback
|
|
930
|
-
* parameter when referencing previously registered steps, as that preserves
|
|
931
|
-
* full input/output types.
|
|
932
|
-
*/
|
|
933
|
-
function stepRef<N extends string>(name: N): TypedAction<any, any, N> {
|
|
934
|
-
return typedAction({
|
|
935
|
-
kind: "Step",
|
|
936
|
-
step: { kind: "Named", name },
|
|
937
|
-
});
|
|
903
|
+
return typedAction(
|
|
904
|
+
buildRestartBranchAction(restartHandlerId, body, IDENTITY),
|
|
905
|
+
);
|
|
938
906
|
}
|
|
939
907
|
|
|
940
908
|
// ---------------------------------------------------------------------------
|
|
941
909
|
// Config builders
|
|
942
910
|
// ---------------------------------------------------------------------------
|
|
943
911
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* Strip the Refs parameter from registered step types. Refs are a compile-time
|
|
949
|
-
* mechanism for validating step references in registerSteps — once validated,
|
|
950
|
-
* they shouldn't propagate into the workflow callback's return type.
|
|
951
|
-
*/
|
|
952
|
-
type StripRefs<TSteps> = {
|
|
953
|
-
[K in keyof TSteps]: TypedAction<ExtractInput<TSteps[K]>, ExtractOutput<TSteps[K]>>;
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
/** Simple config with no named steps. */
|
|
957
|
-
export function config<Out>(workflow: WorkflowAction<Out>): Config<Out> {
|
|
912
|
+
/** Simple config factory. */
|
|
913
|
+
export function config(workflow: Action): Config {
|
|
958
914
|
return { workflow };
|
|
959
915
|
}
|
|
960
|
-
|
|
961
|
-
/** Builder for configs with type-safe named steps. */
|
|
962
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
963
|
-
export class ConfigBuilder<TSteps extends Record<string, AnyAction> = {}> {
|
|
964
|
-
private readonly _steps: Record<string, Action>;
|
|
965
|
-
|
|
966
|
-
constructor(steps: Record<string, Action> = {}) {
|
|
967
|
-
this._steps = steps;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
/**
|
|
971
|
-
* Register named steps. Two forms:
|
|
972
|
-
*
|
|
973
|
-
* **Object form** — for steps with no cross-references:
|
|
974
|
-
*
|
|
975
|
-
* ```ts
|
|
976
|
-
* .registerSteps({
|
|
977
|
-
* Setup: setup(),
|
|
978
|
-
* Migrate: pipe(listFiles(), forEach(migrate())),
|
|
979
|
-
* })
|
|
980
|
-
* ```
|
|
981
|
-
*
|
|
982
|
-
* **Callback form** — for mutual recursion (via `stepRef`) and/or
|
|
983
|
-
* referencing previously registered steps (via `steps`):
|
|
984
|
-
*
|
|
985
|
-
* ```ts
|
|
986
|
-
* .registerSteps({ Setup: setup() })
|
|
987
|
-
* .registerSteps(({ steps, stepRef }) => ({
|
|
988
|
-
* Pipeline: pipe(steps.Setup, process(), stepRef("FixCycle")),
|
|
989
|
-
* FixCycle: loop(pipe(check(), stepRef("Pipeline"))),
|
|
990
|
-
* }))
|
|
991
|
-
* ```
|
|
992
|
-
*
|
|
993
|
-
* `stepRef` only validates against current-batch keys. Previously
|
|
994
|
-
* registered steps must be accessed via `steps`.
|
|
995
|
-
*/
|
|
996
|
-
registerSteps<R extends Record<string, Action>>(
|
|
997
|
-
steps: R & ValidateStepRefs<R>,
|
|
998
|
-
): ConfigBuilder<TSteps & R>;
|
|
999
|
-
registerSteps<R extends Record<string, Action>>(
|
|
1000
|
-
build: (ctx: {
|
|
1001
|
-
steps: StripRefs<TSteps>;
|
|
1002
|
-
stepRef: <N extends string>(name: N) => TypedAction<any, any, N>;
|
|
1003
|
-
}) => R,
|
|
1004
|
-
): [ExtractRefs<R[keyof R]>] extends [keyof R]
|
|
1005
|
-
? ConfigBuilder<TSteps & R>
|
|
1006
|
-
: ValidateStepRefs<R>;
|
|
1007
|
-
registerSteps<R extends Record<string, Action>>(
|
|
1008
|
-
stepsOrBuild:
|
|
1009
|
-
| (R & ValidateStepRefs<R>)
|
|
1010
|
-
| ((ctx: {
|
|
1011
|
-
steps: StripRefs<TSteps>;
|
|
1012
|
-
stepRef: <N extends string>(name: N) => TypedAction<any, any, N>;
|
|
1013
|
-
}) => R),
|
|
1014
|
-
): ConfigBuilder<TSteps & R> {
|
|
1015
|
-
const resolved =
|
|
1016
|
-
typeof stepsOrBuild === "function"
|
|
1017
|
-
? stepsOrBuild({ steps: this._buildStepRefs() as StripRefs<TSteps>, stepRef })
|
|
1018
|
-
: stepsOrBuild;
|
|
1019
|
-
return new ConfigBuilder({
|
|
1020
|
-
...this._steps,
|
|
1021
|
-
...resolved,
|
|
1022
|
-
}) as ConfigBuilder<TSteps & R>;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/** Build typed step reference objects for previously registered steps. */
|
|
1026
|
-
private _buildStepRefs(): Record<string, Action> {
|
|
1027
|
-
const refs: Record<string, Action> = {};
|
|
1028
|
-
for (const name of Object.keys(this._steps)) {
|
|
1029
|
-
refs[name] = typedAction({ kind: "Step", step: { kind: "Named", name } });
|
|
1030
|
-
}
|
|
1031
|
-
return refs;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Define the workflow entry point.
|
|
1036
|
-
*
|
|
1037
|
-
* @param build - receives `{ steps, self }`.
|
|
1038
|
-
* `self` is `TypedAction<never, never>` — a jump to the workflow
|
|
1039
|
-
* root. Input `never` because it doesn't consume pipeline data.
|
|
1040
|
-
* Output `never` because the execution path restarts (and `never`
|
|
1041
|
-
* is eliminated from unions, so branches with `self` don't pollute
|
|
1042
|
-
* the output type).
|
|
1043
|
-
*
|
|
1044
|
-
* Use `pipe(drop, self)` to place `self` in a branch case.
|
|
1045
|
-
*
|
|
1046
|
-
* Note: ideally `self` would be `TypedAction<never, Out>` so it
|
|
1047
|
-
* carries the workflow's output type, but TypeScript can't infer
|
|
1048
|
-
* a generic from a callback's return and use it in the same
|
|
1049
|
-
* callback's parameter — Out falls back to `unknown`.
|
|
1050
|
-
*/
|
|
1051
|
-
workflow<Out>(
|
|
1052
|
-
build: (ctx: {
|
|
1053
|
-
steps: StripRefs<TSteps>;
|
|
1054
|
-
self: TypedAction<never, never>;
|
|
1055
|
-
}) => WorkflowAction<Out>,
|
|
1056
|
-
): RunnableConfig<Out> {
|
|
1057
|
-
const stepRefs: Record<string, Action> = {};
|
|
1058
|
-
for (const name of Object.keys(this._steps)) {
|
|
1059
|
-
stepRefs[name] = typedAction({ kind: "Step", step: { kind: "Named", name } });
|
|
1060
|
-
}
|
|
1061
|
-
const self = typedAction<never, never>({
|
|
1062
|
-
kind: "Step",
|
|
1063
|
-
step: { kind: "Root" },
|
|
1064
|
-
});
|
|
1065
|
-
const workflowAction = build({ steps: stepRefs as StripRefs<TSteps>, self });
|
|
1066
|
-
const steps = Object.keys(this._steps).length > 0 ? this._steps : undefined;
|
|
1067
|
-
return new RunnableConfig(workflowAction, steps);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* A workflow config with a `.run()` method for execution.
|
|
1073
|
-
*
|
|
1074
|
-
* Serializes to the same JSON shape as `Config` via `toJSON()`, so it
|
|
1075
|
-
* works with `JSON.stringify` and round-trip tests.
|
|
1076
|
-
*/
|
|
1077
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1078
|
-
export class RunnableConfig<Out = any> {
|
|
1079
|
-
readonly workflow: WorkflowAction<Out>;
|
|
1080
|
-
readonly steps?: Record<string, Action>;
|
|
1081
|
-
|
|
1082
|
-
constructor(workflow: WorkflowAction<Out>, steps?: Record<string, Action>) {
|
|
1083
|
-
this.workflow = workflow;
|
|
1084
|
-
this.steps = steps;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
/** Run this workflow to completion. Prints result to stdout. */
|
|
1088
|
-
async run(): Promise<void> {
|
|
1089
|
-
// Dynamic import to avoid pulling in Node.js APIs at module load time
|
|
1090
|
-
// (keeps ast.ts importable in non-Node environments for type checking).
|
|
1091
|
-
const { run } = await import("./run.js");
|
|
1092
|
-
await run(this.toJSON());
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
/** Serialize to the same shape as Config. */
|
|
1096
|
-
toJSON(): Config<Out> {
|
|
1097
|
-
const result: Config<Out> = { workflow: this.workflow };
|
|
1098
|
-
if (this.steps) {
|
|
1099
|
-
result.steps = this.steps;
|
|
1100
|
-
}
|
|
1101
|
-
return result;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
export function workflowBuilder(): ConfigBuilder {
|
|
1106
|
-
return new ConfigBuilder();
|
|
1107
|
-
}
|