@barnum/barnum 0.2.2 → 0.3.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/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/cli.cjs +33 -0
- package/dist/all.d.ts +12 -0
- package/dist/all.js +8 -0
- package/dist/ast.d.ts +375 -0
- package/dist/ast.js +381 -0
- package/dist/bind.d.ts +62 -0
- package/dist/bind.js +106 -0
- package/dist/builtins.d.ts +257 -0
- package/dist/builtins.js +600 -0
- package/dist/chain.d.ts +2 -0
- package/dist/chain.js +8 -0
- package/dist/effect-id.d.ts +14 -0
- package/dist/effect-id.js +16 -0
- package/dist/handler.d.ts +50 -0
- package/dist/handler.js +146 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/pipe.d.ts +11 -0
- package/dist/pipe.js +11 -0
- package/dist/race.d.ts +53 -0
- package/dist/race.js +141 -0
- package/dist/recursive.d.ts +34 -0
- package/dist/recursive.js +53 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.js +143 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +95 -0
- package/dist/try-catch.d.ts +23 -0
- package/dist/try-catch.js +36 -0
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +46 -0
- package/package.json +40 -16
- package/src/all.ts +89 -0
- package/src/ast.ts +878 -0
- package/src/bind.ts +192 -0
- package/src/builtins.ts +804 -0
- package/src/chain.ts +17 -0
- package/src/effect-id.ts +30 -0
- package/src/handler.ts +279 -0
- package/src/index.ts +30 -0
- package/src/pipe.ts +93 -0
- package/src/race.ts +183 -0
- package/src/recursive.ts +112 -0
- package/src/run.ts +181 -0
- package/src/schema.ts +118 -0
- package/src/try-catch.ts +53 -0
- package/src/worker.ts +56 -0
- package/README.md +0 -19
- package/barnum-config-schema.json +0 -408
- package/cli.js +0 -20
- package/index.js +0 -23
package/src/ast.ts
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Serializable Types — mirror the Rust AST in barnum_ast
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export type Action =
|
|
8
|
+
| InvokeAction
|
|
9
|
+
| ChainAction
|
|
10
|
+
| ForEachAction
|
|
11
|
+
| AllAction
|
|
12
|
+
| BranchAction
|
|
13
|
+
| ResumeHandleAction
|
|
14
|
+
| ResumePerformAction
|
|
15
|
+
| RestartHandleAction
|
|
16
|
+
| RestartPerformAction;
|
|
17
|
+
|
|
18
|
+
export interface InvokeAction {
|
|
19
|
+
kind: "Invoke";
|
|
20
|
+
handler: HandlerKind;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ChainAction {
|
|
24
|
+
kind: "Chain";
|
|
25
|
+
first: Action;
|
|
26
|
+
rest: Action;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ForEachAction {
|
|
30
|
+
kind: "ForEach";
|
|
31
|
+
action: Action;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AllAction {
|
|
35
|
+
kind: "All";
|
|
36
|
+
actions: Action[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BranchAction {
|
|
40
|
+
kind: "Branch";
|
|
41
|
+
cases: Record<string, Action>;
|
|
42
|
+
}
|
|
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;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RestartHandleAction {
|
|
57
|
+
kind: "RestartHandle";
|
|
58
|
+
restart_handler_id: RestartHandlerId;
|
|
59
|
+
body: Action;
|
|
60
|
+
handler: Action;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RestartPerformAction {
|
|
64
|
+
kind: "RestartPerform";
|
|
65
|
+
restart_handler_id: RestartHandlerId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// HandlerKind
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export type HandlerKind = TypeScriptHandler | BuiltinHandler;
|
|
73
|
+
|
|
74
|
+
export interface TypeScriptHandler {
|
|
75
|
+
kind: "TypeScript";
|
|
76
|
+
module: string;
|
|
77
|
+
func: string;
|
|
78
|
+
input_schema?: JSONSchema7;
|
|
79
|
+
output_schema?: JSONSchema7;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BuiltinHandler {
|
|
83
|
+
kind: "Builtin";
|
|
84
|
+
builtin: BuiltinKind;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type BuiltinKind =
|
|
88
|
+
| { kind: "Constant"; value: unknown }
|
|
89
|
+
| { kind: "Identity" }
|
|
90
|
+
| { kind: "Drop" }
|
|
91
|
+
| { kind: "Tag"; value: string }
|
|
92
|
+
| { kind: "Merge" }
|
|
93
|
+
| { kind: "Flatten" }
|
|
94
|
+
| { kind: "ExtractField"; value: string }
|
|
95
|
+
| { kind: "ExtractIndex"; value: number }
|
|
96
|
+
| { kind: "Pick"; value: string[] }
|
|
97
|
+
| { kind: "CollectSome" }
|
|
98
|
+
| { kind: "Sleep"; value: number };
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* When TIn is `never` (handler ignores input), produce `any` so the
|
|
102
|
+
* combinator/pipe can sit in any pipeline position.
|
|
103
|
+
*/
|
|
104
|
+
export type PipeIn<T> = [T] extends [never] ? any : T;
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Config
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export interface Config {
|
|
111
|
+
workflow: Action;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Type utilities
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
type UnionToIntersection<TUnion> = (
|
|
120
|
+
TUnion extends any ? (x: TUnion) => void : never
|
|
121
|
+
) extends (x: infer TIntersection) => void
|
|
122
|
+
? TIntersection
|
|
123
|
+
: never;
|
|
124
|
+
|
|
125
|
+
/** Merge a tuple of objects into a single intersection type. */
|
|
126
|
+
export type MergeTuple<TTuple> = TTuple extends unknown[]
|
|
127
|
+
? UnionToIntersection<TTuple[number]>
|
|
128
|
+
: never;
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Phantom Types — type-safe input/output tracking
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* An action with tracked input/output types. Phantom fields enforce invariance
|
|
136
|
+
* and are never set at runtime — they exist only for the TypeScript compiler.
|
|
137
|
+
*
|
|
138
|
+
* Each type variable gets a contravariant + covariant field pair:
|
|
139
|
+
* In: __in (contravariant) + __in_co (covariant) → invariant
|
|
140
|
+
* Out: __out (covariant) + __out_contra (contravariant) → invariant
|
|
141
|
+
*
|
|
142
|
+
* This ensures exact type matching at every pipeline connection point.
|
|
143
|
+
* Data crosses serialization boundaries to handlers in arbitrary languages
|
|
144
|
+
* (Rust, Python, etc.), so extra/missing fields are runtime errors.
|
|
145
|
+
*/
|
|
146
|
+
export type TypedAction<
|
|
147
|
+
In = unknown,
|
|
148
|
+
Out = unknown,
|
|
149
|
+
Refs extends string = never,
|
|
150
|
+
> = Action & {
|
|
151
|
+
__in?: (input: In) => void;
|
|
152
|
+
__in_co?: In;
|
|
153
|
+
__out?: () => Out;
|
|
154
|
+
__out_contra?: (output: Out) => void;
|
|
155
|
+
__refs?: { _brand: Refs };
|
|
156
|
+
/** Chain this action with another. `a.then(b)` ≡ `chain(a, b)`. */
|
|
157
|
+
then<TNext>(next: Pipeable<Out, TNext>): TypedAction<In, TNext, Refs>;
|
|
158
|
+
/** Apply an action to each element of an array output. `a.forEach(b)` ≡ `a.then(forEach(b))`. */
|
|
159
|
+
forEach<TIn, TElement, TNext, TRefs extends string>(
|
|
160
|
+
this: TypedAction<TIn, TElement[], TRefs>,
|
|
161
|
+
action: Pipeable<TElement, TNext>,
|
|
162
|
+
): TypedAction<TIn, TNext[], TRefs>;
|
|
163
|
+
/** Dispatch on a tagged union output. Auto-unwraps `value` before each case handler. */
|
|
164
|
+
branch<
|
|
165
|
+
TCases extends {
|
|
166
|
+
[K in BranchKeys<Out>]: CaseHandler<BranchPayload<Out, K>, unknown>;
|
|
167
|
+
},
|
|
168
|
+
>(
|
|
169
|
+
cases: [BranchKeys<Out>] extends [never] ? never : TCases,
|
|
170
|
+
): TypedAction<In, ExtractOutput<TCases[keyof TCases & string]>, Refs>;
|
|
171
|
+
/** Flatten a nested array output. `a.flatten()` ≡ `pipe(a, flatten())`. */
|
|
172
|
+
flatten(): TypedAction<
|
|
173
|
+
In,
|
|
174
|
+
Out extends (infer TElement)[][] ? TElement[] : Out,
|
|
175
|
+
Refs
|
|
176
|
+
>;
|
|
177
|
+
/** Discard output. `a.drop()` ≡ `pipe(a, drop)`. */
|
|
178
|
+
drop(): TypedAction<In, never, Refs>;
|
|
179
|
+
/** Wrap output as a tagged union member. Requires full variant map TDef so __def is carried. */
|
|
180
|
+
tag<TDef extends Record<string, unknown>, TKind extends keyof TDef & string>(
|
|
181
|
+
kind: TKind,
|
|
182
|
+
): TypedAction<In, TaggedUnion<TDef>, Refs>;
|
|
183
|
+
/** Extract a field from the output object. `a.get("name")` ≡ `pipe(a, extractField("name"))`. */
|
|
184
|
+
get<TField extends keyof Out & string>(
|
|
185
|
+
field: TField,
|
|
186
|
+
): TypedAction<In, Out[TField], Refs>;
|
|
187
|
+
/**
|
|
188
|
+
* Run this sub-pipeline, then merge its output back into the original input.
|
|
189
|
+
* `pipe(extractField("x"), transform).augment()` takes `In`, runs the
|
|
190
|
+
* sub-pipeline to get `Out`, and returns `In & Out`.
|
|
191
|
+
*
|
|
192
|
+
* Unlike the standalone `augment()` function, the postfix form has access
|
|
193
|
+
* to `In` so the intersection types correctly.
|
|
194
|
+
*/
|
|
195
|
+
augment(): TypedAction<In, In & Out, Refs>;
|
|
196
|
+
/** Merge a tuple of objects into a single object. `a.merge()` ≡ `pipe(a, merge())`. */
|
|
197
|
+
merge(): TypedAction<In, MergeTuple<Out>, Refs>;
|
|
198
|
+
/** Select fields from the output. `a.pick("x", "y")` ≡ `pipe(a, pick("x", "y"))`. */
|
|
199
|
+
pick<TKeys extends (keyof Out & string)[]>(
|
|
200
|
+
...keys: TKeys
|
|
201
|
+
): TypedAction<In, Pick<Out, TKeys[number]>, Refs>;
|
|
202
|
+
/**
|
|
203
|
+
* Transform the Some value inside an Option output. Only callable when
|
|
204
|
+
* Out is Option<T>. Uses `this` parameter constraint to gate availability.
|
|
205
|
+
*/
|
|
206
|
+
mapOption<TIn, T, U, TRefs extends string>(
|
|
207
|
+
this: TypedAction<TIn, Option<T>, TRefs>,
|
|
208
|
+
action: Pipeable<T, U>,
|
|
209
|
+
): TypedAction<TIn, Option<U>, TRefs>;
|
|
210
|
+
/**
|
|
211
|
+
* Transform the Err value of a Result output.
|
|
212
|
+
* `Result<TValue, TError> → Result<TValue, TErrorOut>`
|
|
213
|
+
*
|
|
214
|
+
* Only callable when Out is Result<TValue, TError>.
|
|
215
|
+
*/
|
|
216
|
+
mapErr<TIn, TValue, TError, TErrorOut>(
|
|
217
|
+
this: TypedAction<TIn, Result<TValue, TError>, any>,
|
|
218
|
+
action: Pipeable<TError, TErrorOut>,
|
|
219
|
+
): TypedAction<TIn, Result<TValue, TErrorOut>, Refs>;
|
|
220
|
+
/**
|
|
221
|
+
* Unwrap a Result output. If Ok, pass through the value. If Err, apply
|
|
222
|
+
* the default action. Only callable when Out is Result<TValue, TError>.
|
|
223
|
+
*
|
|
224
|
+
* The `this` constraint provides TValue from context, so throw tokens
|
|
225
|
+
* (Out=never) work without explicit type parameters:
|
|
226
|
+
* `handler.unwrapOr(throwError)`
|
|
227
|
+
*
|
|
228
|
+
* Uses CaseHandler for defaultAction (covariant output only) so that
|
|
229
|
+
* `TypedAction<TError, never>` is assignable to `CaseHandler<TError, TValue>`.
|
|
230
|
+
*
|
|
231
|
+
* Refs position uses `any` in the `this` constraint to avoid TS
|
|
232
|
+
* falling back to the constraint bound `string` when Refs = never.
|
|
233
|
+
* The return type uses the enclosing TypedAction's `Refs` directly.
|
|
234
|
+
*/
|
|
235
|
+
unwrapOr<TIn, TValue, TError>(
|
|
236
|
+
this: TypedAction<TIn, Result<TValue, TError>, any>,
|
|
237
|
+
defaultAction: CaseHandler<TError, TValue>,
|
|
238
|
+
): TypedAction<TIn, TValue, Refs>;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Parameter type for pipe and combinators. Contains the same phantom fields
|
|
243
|
+
* as TypedAction but without methods.
|
|
244
|
+
*
|
|
245
|
+
* Invariance: Both In and Out are invariant, matching TypedAction:
|
|
246
|
+
* In: __in (contravariant) + __in_co (covariant) → invariant
|
|
247
|
+
* Out: __out (covariant) + __out_contra (contravariant) → invariant
|
|
248
|
+
*
|
|
249
|
+
* Why no methods: TypedAction's methods (then, branch, etc.) participate in
|
|
250
|
+
* TS assignability checks in complex, recursive ways that interfere with
|
|
251
|
+
* generic inference in pipe overloads. Pipeable strips methods so that only
|
|
252
|
+
* phantom fields drive inference — predictable covariant/contravariant
|
|
253
|
+
* resolution, with invariance enforced when TS checks candidates from
|
|
254
|
+
* both sides of a connection.
|
|
255
|
+
*
|
|
256
|
+
* TypedAction (with methods) is assignable to Pipeable because Pipeable
|
|
257
|
+
* only requires a subset of properties.
|
|
258
|
+
*/
|
|
259
|
+
export type Pipeable<In = unknown, Out = unknown> = Action & {
|
|
260
|
+
__in?: (input: In) => void;
|
|
261
|
+
__in_co?: In;
|
|
262
|
+
__out?: () => Out;
|
|
263
|
+
__out_contra?: (output: Out) => void;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Contravariant-only input checking for branch case handler positions.
|
|
268
|
+
*
|
|
269
|
+
* Omits __in_co (covariant input) and __out_contra (contravariant output)
|
|
270
|
+
* compared to TypedAction/Pipeable. This gives:
|
|
271
|
+
* In: contravariant only (via __in)
|
|
272
|
+
* Out: covariant only (via __out)
|
|
273
|
+
*
|
|
274
|
+
* Why contravariant input: a handler that accepts `unknown` (like drop)
|
|
275
|
+
* can handle any variant. (input: unknown) => void is assignable to
|
|
276
|
+
* (input: HasErrors) => void because HasErrors extends unknown.
|
|
277
|
+
*
|
|
278
|
+
* Why covariant output: the constraint doesn't restrict output types —
|
|
279
|
+
* they're inferred from the actual case handlers via ExtractOutput.
|
|
280
|
+
* TypedAction's invariant __out_contra with Out=unknown would
|
|
281
|
+
* reject any handler with a specific output type, so we omit it.
|
|
282
|
+
*
|
|
283
|
+
* TypedAction is assignable to CaseHandler because CaseHandler only
|
|
284
|
+
* requires a subset of TypedAction's phantom fields.
|
|
285
|
+
*/
|
|
286
|
+
type CaseHandler<TIn = unknown, TOut = unknown> = Action & {
|
|
287
|
+
__in?: (input: TIn) => void;
|
|
288
|
+
__out?: () => TOut;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Tagged Union — standard { kind, value } convention with phantom __def
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Standard tagged union type. Each variant is `{ kind: K; value: TDef[K] }`
|
|
297
|
+
* with a phantom `__def` field carrying the full variant map. The __def
|
|
298
|
+
* field enables `.branch()` to decompose the union via simple indexing
|
|
299
|
+
* (`keyof ExtractDef<Out>` and `ExtractDef<Out>[K]`) instead of
|
|
300
|
+
* conditional types (`KindOf<Out>` and `Extract<Out, { kind: K }>`).
|
|
301
|
+
*/
|
|
302
|
+
// 0 extends 1 & T detects `any` — preserve as-is to avoid collapsing.
|
|
303
|
+
type VoidToNull<T> = 0 extends 1 & T
|
|
304
|
+
? T
|
|
305
|
+
: [T] extends [never]
|
|
306
|
+
? never
|
|
307
|
+
: [T] extends [void]
|
|
308
|
+
? null
|
|
309
|
+
: T;
|
|
310
|
+
|
|
311
|
+
export type TaggedUnion<TDef extends Record<string, unknown>> = {
|
|
312
|
+
[K in keyof TDef & string]: {
|
|
313
|
+
kind: K;
|
|
314
|
+
value: VoidToNull<TDef[K]>;
|
|
315
|
+
__def?: TDef;
|
|
316
|
+
};
|
|
317
|
+
}[keyof TDef & string];
|
|
318
|
+
|
|
319
|
+
/** Extract the variant map definition from a tagged union's phantom __def. */
|
|
320
|
+
export type ExtractDef<T> = T extends { __def?: infer D } ? D : never;
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Option<T> — standard optional value type
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
export type OptionDef<T> = { Some: T; None: void };
|
|
327
|
+
export type Option<T> = TaggedUnion<OptionDef<T>>;
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Result<TValue, TError> — standard success/error type
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
export type ResultDef<TValue, TError> = { Ok: TValue; Err: TError };
|
|
334
|
+
export type Result<TValue, TError> = TaggedUnion<ResultDef<TValue, TError>>;
|
|
335
|
+
|
|
336
|
+
/** Extract all `kind` string literals from a discriminated union. */
|
|
337
|
+
type KindOf<T> = T extends { kind: infer K extends string } ? K : never;
|
|
338
|
+
|
|
339
|
+
/** Extract the `value` field from a `{ kind, value }` variant. Falls back to T if no `value` field. */
|
|
340
|
+
type UnwrapVariant<T> = T extends { value: infer V } ? V : T;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Branch case keys: prefer ExtractDef (simple keyof indexing) when the
|
|
344
|
+
* output carries __def. Falls back to KindOf (conditional type) for
|
|
345
|
+
* outputs without __def.
|
|
346
|
+
*/
|
|
347
|
+
type BranchKeys<Out> = [ExtractDef<Out>] extends [never]
|
|
348
|
+
? KindOf<Out>
|
|
349
|
+
: keyof ExtractDef<Out> & string;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Branch case payload: prefer ExtractDef[K] (simple indexing) when available.
|
|
353
|
+
* Falls back to UnwrapVariant<Extract<Out, { kind: K }>> for outputs without __def.
|
|
354
|
+
*/
|
|
355
|
+
type BranchPayload<Out, K extends string> = [ExtractDef<Out>] extends [never]
|
|
356
|
+
? UnwrapVariant<Extract<Out, { kind: K }>>
|
|
357
|
+
: K extends keyof ExtractDef<Out>
|
|
358
|
+
? VoidToNull<ExtractDef<Out>[K]>
|
|
359
|
+
: never;
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// typedAction — attach .then() and .forEach() as non-enumerable methods
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
// Shared implementations (one closure, not per-instance)
|
|
366
|
+
function thenMethod<TIn, TOut, TRefs extends string, TNext>(
|
|
367
|
+
this: TypedAction<TIn, TOut, TRefs>,
|
|
368
|
+
next: Pipeable<TOut, TNext>,
|
|
369
|
+
): TypedAction<TIn, TNext, TRefs> {
|
|
370
|
+
return typedAction({ kind: "Chain", first: this, rest: next as Action });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function forEachMethod(this: TypedAction, action: Action): TypedAction {
|
|
374
|
+
return typedAction({
|
|
375
|
+
kind: "Chain",
|
|
376
|
+
first: this,
|
|
377
|
+
rest: { kind: "ForEach", action },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function branchMethod(
|
|
382
|
+
this: TypedAction,
|
|
383
|
+
cases: Record<string, Action>,
|
|
384
|
+
): TypedAction {
|
|
385
|
+
return typedAction({
|
|
386
|
+
kind: "Chain",
|
|
387
|
+
first: this,
|
|
388
|
+
rest: { kind: "Branch", cases: unwrapBranchCases(cases) },
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function flattenMethod(this: TypedAction): TypedAction {
|
|
393
|
+
return typedAction({
|
|
394
|
+
kind: "Chain",
|
|
395
|
+
first: this,
|
|
396
|
+
rest: {
|
|
397
|
+
kind: "Invoke",
|
|
398
|
+
handler: { kind: "Builtin", builtin: { kind: "Flatten" } },
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function dropMethod(this: TypedAction): TypedAction {
|
|
404
|
+
return typedAction({
|
|
405
|
+
kind: "Chain",
|
|
406
|
+
first: this,
|
|
407
|
+
rest: {
|
|
408
|
+
kind: "Invoke",
|
|
409
|
+
handler: { kind: "Builtin", builtin: { kind: "Drop" } },
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function tagMethod(this: TypedAction, kind: string): TypedAction {
|
|
415
|
+
return typedAction({
|
|
416
|
+
kind: "Chain",
|
|
417
|
+
first: this,
|
|
418
|
+
rest: {
|
|
419
|
+
kind: "Invoke",
|
|
420
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: kind } },
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function getMethod(this: TypedAction, field: string): TypedAction {
|
|
426
|
+
return typedAction({
|
|
427
|
+
kind: "Chain",
|
|
428
|
+
first: this,
|
|
429
|
+
rest: {
|
|
430
|
+
kind: "Invoke",
|
|
431
|
+
handler: {
|
|
432
|
+
kind: "Builtin",
|
|
433
|
+
builtin: { kind: "ExtractField", value: field },
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function augmentMethod(this: TypedAction): TypedAction {
|
|
440
|
+
// Construct: All(this, identity) → Merge
|
|
441
|
+
// "this" is the sub-pipeline. augment() wraps it so the original input
|
|
442
|
+
// flows through identity alongside the sub-pipeline, then merges the results.
|
|
443
|
+
return typedAction({
|
|
444
|
+
kind: "Chain",
|
|
445
|
+
first: {
|
|
446
|
+
kind: "All",
|
|
447
|
+
actions: [
|
|
448
|
+
this as Action,
|
|
449
|
+
{
|
|
450
|
+
kind: "Invoke",
|
|
451
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
rest: {
|
|
456
|
+
kind: "Invoke",
|
|
457
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function mergeMethod(this: TypedAction): TypedAction {
|
|
463
|
+
return typedAction({
|
|
464
|
+
kind: "Chain",
|
|
465
|
+
first: this,
|
|
466
|
+
rest: {
|
|
467
|
+
kind: "Invoke",
|
|
468
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function pickMethod(this: TypedAction, ...keys: string[]): TypedAction {
|
|
474
|
+
return typedAction({
|
|
475
|
+
kind: "Chain",
|
|
476
|
+
first: this,
|
|
477
|
+
rest: {
|
|
478
|
+
kind: "Invoke",
|
|
479
|
+
handler: { kind: "Builtin", builtin: { kind: "Pick", value: keys } },
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function mapOptionMethod(this: TypedAction, action: Action): TypedAction {
|
|
485
|
+
// Desugars to: self.then(branch({ Some: pipe(action, tag("Some")), None: tag("None") }))
|
|
486
|
+
// But branch auto-unwraps value, so:
|
|
487
|
+
// Some case: receives T, runs action, wraps as Some
|
|
488
|
+
// None case: receives void, wraps as None
|
|
489
|
+
return typedAction({
|
|
490
|
+
kind: "Chain",
|
|
491
|
+
first: this,
|
|
492
|
+
rest: {
|
|
493
|
+
kind: "Branch",
|
|
494
|
+
cases: unwrapBranchCases({
|
|
495
|
+
Some: {
|
|
496
|
+
kind: "Chain",
|
|
497
|
+
first: action,
|
|
498
|
+
rest: {
|
|
499
|
+
kind: "Invoke",
|
|
500
|
+
handler: {
|
|
501
|
+
kind: "Builtin",
|
|
502
|
+
builtin: { kind: "Tag", value: "Some" },
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
None: {
|
|
507
|
+
kind: "Invoke",
|
|
508
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "None" } },
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function mapErrMethod(this: TypedAction, action: Action): TypedAction {
|
|
516
|
+
// Desugars to: self.then(branch({ Ok: tag("Ok"), Err: pipe(action, tag("Err")) }))
|
|
517
|
+
return typedAction({
|
|
518
|
+
kind: "Chain",
|
|
519
|
+
first: this,
|
|
520
|
+
rest: {
|
|
521
|
+
kind: "Branch",
|
|
522
|
+
cases: unwrapBranchCases({
|
|
523
|
+
Ok: {
|
|
524
|
+
kind: "Invoke",
|
|
525
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
|
|
526
|
+
},
|
|
527
|
+
Err: {
|
|
528
|
+
kind: "Chain",
|
|
529
|
+
first: action,
|
|
530
|
+
rest: {
|
|
531
|
+
kind: "Invoke",
|
|
532
|
+
handler: {
|
|
533
|
+
kind: "Builtin",
|
|
534
|
+
builtin: { kind: "Tag", value: "Err" },
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
}),
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function unwrapOrMethod(this: TypedAction, defaultAction: Action): TypedAction {
|
|
544
|
+
// Desugars to: self.then(branch({ Ok: identity(), Err: defaultAction }))
|
|
545
|
+
return typedAction({
|
|
546
|
+
kind: "Chain",
|
|
547
|
+
first: this,
|
|
548
|
+
rest: {
|
|
549
|
+
kind: "Branch",
|
|
550
|
+
cases: unwrapBranchCases({
|
|
551
|
+
Ok: {
|
|
552
|
+
kind: "Invoke",
|
|
553
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
554
|
+
},
|
|
555
|
+
Err: defaultAction,
|
|
556
|
+
}),
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Attach `.then()` and `.forEach()` methods to a plain Action object.
|
|
563
|
+
* Methods are non-enumerable: invisible to JSON.stringify and toEqual.
|
|
564
|
+
*/
|
|
565
|
+
export function typedAction<
|
|
566
|
+
In = unknown,
|
|
567
|
+
Out = unknown,
|
|
568
|
+
Refs extends string = never,
|
|
569
|
+
>(action: Action): TypedAction<In, Out, Refs> {
|
|
570
|
+
if (!("then" in action)) {
|
|
571
|
+
Object.defineProperties(action, {
|
|
572
|
+
then: { value: thenMethod, configurable: true },
|
|
573
|
+
forEach: { value: forEachMethod, configurable: true },
|
|
574
|
+
branch: { value: branchMethod, configurable: true },
|
|
575
|
+
flatten: { value: flattenMethod, configurable: true },
|
|
576
|
+
drop: { value: dropMethod, configurable: true },
|
|
577
|
+
tag: { value: tagMethod, configurable: true },
|
|
578
|
+
get: { value: getMethod, configurable: true },
|
|
579
|
+
augment: { value: augmentMethod, configurable: true },
|
|
580
|
+
merge: { value: mergeMethod, configurable: true },
|
|
581
|
+
pick: { value: pickMethod, configurable: true },
|
|
582
|
+
mapOption: { value: mapOptionMethod, configurable: true },
|
|
583
|
+
mapErr: { value: mapErrMethod, configurable: true },
|
|
584
|
+
unwrapOr: { value: unwrapOrMethod, configurable: true },
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return action as TypedAction<In, Out, Refs>;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Type extraction utilities
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Extract the input type from a TypedAction.
|
|
596
|
+
*
|
|
597
|
+
* Uses direct phantom field extraction (not full TypedAction matching) to
|
|
598
|
+
* avoid the `TypedAction<any, any, any>` constraint which fails for In=never
|
|
599
|
+
* due to __in contravariance.
|
|
600
|
+
*/
|
|
601
|
+
export type ExtractInput<T> = T extends {
|
|
602
|
+
__in?: (input: infer In) => void;
|
|
603
|
+
}
|
|
604
|
+
? In
|
|
605
|
+
: never;
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Extract the output type from a TypedAction.
|
|
609
|
+
*
|
|
610
|
+
* Uses direct phantom field extraction to avoid constraint issues.
|
|
611
|
+
*/
|
|
612
|
+
export type ExtractOutput<T> = T extends { __out?: () => infer Out }
|
|
613
|
+
? Out
|
|
614
|
+
: never;
|
|
615
|
+
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
// Combinators
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
|
|
620
|
+
export { pipe } from "./pipe.js";
|
|
621
|
+
export { chain } from "./chain.js";
|
|
622
|
+
export { all } from "./all.js";
|
|
623
|
+
export { bind, bindInput, type VarRef, type InferVarRefs } from "./bind.js";
|
|
624
|
+
export { defineRecursiveFunctions } from "./recursive.js";
|
|
625
|
+
export { resetEffectIdCounter } from "./effect-id.js";
|
|
626
|
+
import {
|
|
627
|
+
allocateRestartHandlerId,
|
|
628
|
+
type RestartHandlerId,
|
|
629
|
+
type ResumeHandlerId,
|
|
630
|
+
} from "./effect-id.js";
|
|
631
|
+
export { tryCatch } from "./try-catch.js";
|
|
632
|
+
export { race, sleep, withTimeout } from "./race.js";
|
|
633
|
+
|
|
634
|
+
export function forEach<In, Out>(
|
|
635
|
+
action: Pipeable<In, Out>,
|
|
636
|
+
): TypedAction<In[], Out[]> {
|
|
637
|
+
return typedAction({ kind: "ForEach", action: action as Action });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Insert ExtractField("value") before each case handler in a branch.
|
|
642
|
+
* This implements auto-unwrapping: the engine dispatches on `kind`, then
|
|
643
|
+
* extracts `value` before passing to the handler. Case handlers receive
|
|
644
|
+
* the payload directly, not the full `{ kind, value }` variant.
|
|
645
|
+
*/
|
|
646
|
+
function unwrapBranchCases(
|
|
647
|
+
cases: Record<string, Action>,
|
|
648
|
+
): Record<string, Action> {
|
|
649
|
+
const unwrapped: Record<string, Action> = {};
|
|
650
|
+
for (const key of Object.keys(cases)) {
|
|
651
|
+
unwrapped[key] = {
|
|
652
|
+
kind: "Chain",
|
|
653
|
+
first: {
|
|
654
|
+
kind: "Invoke",
|
|
655
|
+
handler: {
|
|
656
|
+
kind: "Builtin",
|
|
657
|
+
builtin: { kind: "ExtractField", value: "value" },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
rest: cases[key],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return unwrapped;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Compute the branch input type from its cases. For each case key K,
|
|
668
|
+
* wraps the case handler's input type in `{ kind: K; value: T }`.
|
|
669
|
+
* This ensures the branch input is a proper tagged union matching the
|
|
670
|
+
* `{ kind, value }` convention.
|
|
671
|
+
*
|
|
672
|
+
* Example: `BranchInput<{ Yes: TypedAction<number, ...>, No: TypedAction<string, ...> }>`
|
|
673
|
+
* = `{ kind: "Yes"; value: number } | { kind: "No"; value: string }`
|
|
674
|
+
*
|
|
675
|
+
* When a case handler uses `any` as input, the wrapping produces
|
|
676
|
+
* `{ kind: K; value: any }`, which is the correct escape hatch.
|
|
677
|
+
*/
|
|
678
|
+
export type BranchInput<TCases> = {
|
|
679
|
+
[K in keyof TCases & string]: { kind: K; value: ExtractInput<TCases[K]> };
|
|
680
|
+
}[keyof TCases & string];
|
|
681
|
+
|
|
682
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
683
|
+
export function branch<TCases extends Record<string, Action>>(
|
|
684
|
+
cases: TCases,
|
|
685
|
+
): TypedAction<
|
|
686
|
+
BranchInput<TCases>,
|
|
687
|
+
ExtractOutput<TCases[keyof TCases & string]>
|
|
688
|
+
> {
|
|
689
|
+
return typedAction({ kind: "Branch", cases: unwrapBranchCases(cases) });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
type LoopResultDef<TContinue, TBreak> = {
|
|
693
|
+
Continue: TContinue;
|
|
694
|
+
Break: TBreak;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
export type LoopResult<TContinue, TBreak> = TaggedUnion<
|
|
698
|
+
LoopResultDef<TContinue, TBreak>
|
|
699
|
+
>;
|
|
700
|
+
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Shared AST constants for control flow compilation
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
const EXTRACT_PAYLOAD: Action = {
|
|
706
|
+
kind: "Invoke",
|
|
707
|
+
handler: { kind: "Builtin", builtin: { kind: "ExtractIndex", value: 0 } },
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const TAG_CONTINUE: Action = {
|
|
711
|
+
kind: "Invoke",
|
|
712
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Continue" } },
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
export const TAG_BREAK: Action = {
|
|
716
|
+
kind: "Invoke",
|
|
717
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Break" } },
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
export const IDENTITY: Action = {
|
|
721
|
+
kind: "Invoke",
|
|
722
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// ---------------------------------------------------------------------------
|
|
726
|
+
// recur — restart body primitive
|
|
727
|
+
// ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Restartable scope. The body callback receives `restart`, a TypedAction that
|
|
731
|
+
* re-executes the body from the beginning with a new input value.
|
|
732
|
+
*
|
|
733
|
+
* If the body completes normally → output is TOut.
|
|
734
|
+
* If restart fires → body re-executes with the restarted value.
|
|
735
|
+
*
|
|
736
|
+
* Compiled form: `RestartHandle(id, ExtractIndex(0), body)`
|
|
737
|
+
*/
|
|
738
|
+
export function recur<TIn = never, TOut = any>(
|
|
739
|
+
bodyFn: (restart: TypedAction<TIn, never>) => Pipeable<TIn, TOut>,
|
|
740
|
+
): TypedAction<PipeIn<TIn>, TOut> {
|
|
741
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
742
|
+
|
|
743
|
+
const restartAction = typedAction<TIn, never>({
|
|
744
|
+
kind: "RestartPerform",
|
|
745
|
+
restart_handler_id: restartHandlerId,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const body = bodyFn(restartAction) as Action;
|
|
749
|
+
|
|
750
|
+
return typedAction({
|
|
751
|
+
kind: "RestartHandle",
|
|
752
|
+
restart_handler_id: restartHandlerId,
|
|
753
|
+
body,
|
|
754
|
+
handler: EXTRACT_PAYLOAD,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
// earlyReturn — exit scope primitive (built on restart + Branch)
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Early return scope. The body callback receives `earlyReturn`, a TypedAction
|
|
764
|
+
* that exits the scope immediately with the returned value.
|
|
765
|
+
*
|
|
766
|
+
* If the body completes normally → output is TOut.
|
|
767
|
+
* If earlyReturn fires → output is TEarlyReturn.
|
|
768
|
+
* Combined output: TEarlyReturn | TOut.
|
|
769
|
+
*
|
|
770
|
+
* Built on the restart mechanism: input is tagged Continue, body runs inside
|
|
771
|
+
* a Branch. earlyReturn tags with Break and performs — the handler restarts
|
|
772
|
+
* the body, Branch takes the Break path, and the value exits.
|
|
773
|
+
*/
|
|
774
|
+
export function earlyReturn<TEarlyReturn = never, TIn = any, TOut = any>(
|
|
775
|
+
bodyFn: (
|
|
776
|
+
earlyReturn: TypedAction<TEarlyReturn, never>,
|
|
777
|
+
) => Pipeable<TIn, TOut>,
|
|
778
|
+
): TypedAction<TIn, TEarlyReturn | TOut> {
|
|
779
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
780
|
+
|
|
781
|
+
const earlyReturnAction = typedAction<TEarlyReturn, never>({
|
|
782
|
+
kind: "Chain",
|
|
783
|
+
first: TAG_BREAK,
|
|
784
|
+
rest: { kind: "RestartPerform", restart_handler_id: restartHandlerId },
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
const body = bodyFn(earlyReturnAction) as Action;
|
|
788
|
+
|
|
789
|
+
return typedAction(
|
|
790
|
+
buildRestartBranchAction(restartHandlerId, body, IDENTITY),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
// loop — iterative restart with break
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Build the restart+branch compiled form:
|
|
800
|
+
* `Chain(Tag("Continue"), RestartHandle(id, ExtractIndex(0), Branch({ Continue: continueArm, Break: breakArm })))`
|
|
801
|
+
*
|
|
802
|
+
* Input is tagged Continue so the Branch enters the continueArm on first execution.
|
|
803
|
+
* Continue tag → restart → re-enters continueArm. Break tag → restart → runs breakArm, exits `RestartHandle`.
|
|
804
|
+
*
|
|
805
|
+
* Used by earlyReturn, loop, tryCatch, and race.
|
|
806
|
+
*/
|
|
807
|
+
export function buildRestartBranchAction(
|
|
808
|
+
restartHandlerId: RestartHandlerId,
|
|
809
|
+
continueArm: Action,
|
|
810
|
+
breakArm: Action,
|
|
811
|
+
): Action {
|
|
812
|
+
return {
|
|
813
|
+
kind: "Chain",
|
|
814
|
+
first: TAG_CONTINUE,
|
|
815
|
+
rest: {
|
|
816
|
+
kind: "RestartHandle",
|
|
817
|
+
restart_handler_id: restartHandlerId,
|
|
818
|
+
body: {
|
|
819
|
+
kind: "Branch",
|
|
820
|
+
cases: unwrapBranchCases({
|
|
821
|
+
Continue: continueArm,
|
|
822
|
+
Break: breakArm,
|
|
823
|
+
}),
|
|
824
|
+
},
|
|
825
|
+
handler: EXTRACT_PAYLOAD,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Iterative loop. The body callback receives `recur` and `done`:
|
|
832
|
+
* - `recur`: restart the loop with a new input
|
|
833
|
+
* - `done`: exit the loop with the break value
|
|
834
|
+
*
|
|
835
|
+
* Both are TypedAction values (not functions), consistent with throwError in tryCatch.
|
|
836
|
+
*
|
|
837
|
+
* Compiles to `RestartHandle`/`RestartPerform`/Branch — same effect substrate as tryCatch and earlyReturn.
|
|
838
|
+
*/
|
|
839
|
+
export function loop<TBreak = never, TIn = never>(
|
|
840
|
+
bodyFn: (
|
|
841
|
+
recur: TypedAction<TIn, never>,
|
|
842
|
+
done: TypedAction<VoidToNull<TBreak>, never>,
|
|
843
|
+
) => Pipeable<TIn, never>,
|
|
844
|
+
): TypedAction<PipeIn<TIn>, VoidToNull<TBreak>> {
|
|
845
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
846
|
+
|
|
847
|
+
const perform: Action = {
|
|
848
|
+
kind: "RestartPerform",
|
|
849
|
+
restart_handler_id: restartHandlerId,
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const recurAction = typedAction<TIn, never>({
|
|
853
|
+
kind: "Chain",
|
|
854
|
+
first: TAG_CONTINUE,
|
|
855
|
+
rest: perform,
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const doneAction = typedAction<VoidToNull<TBreak>, never>({
|
|
859
|
+
kind: "Chain",
|
|
860
|
+
first: TAG_BREAK,
|
|
861
|
+
rest: perform,
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
const body = bodyFn(recurAction, doneAction) as Action;
|
|
865
|
+
|
|
866
|
+
return typedAction(
|
|
867
|
+
buildRestartBranchAction(restartHandlerId, body, IDENTITY),
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ---------------------------------------------------------------------------
|
|
872
|
+
// Config builders
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
|
|
875
|
+
/** Simple config factory. */
|
|
876
|
+
export function config(workflow: Action): Config {
|
|
877
|
+
return { workflow };
|
|
878
|
+
}
|