@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.
Files changed (56) hide show
  1. package/artifacts/linux-arm64/barnum +0 -0
  2. package/artifacts/linux-x64/barnum +0 -0
  3. package/artifacts/macos-arm64/barnum +0 -0
  4. package/artifacts/macos-x64/barnum +0 -0
  5. package/artifacts/win-x64/barnum.exe +0 -0
  6. package/cli.cjs +33 -0
  7. package/dist/all.d.ts +12 -0
  8. package/dist/all.js +8 -0
  9. package/dist/ast.d.ts +375 -0
  10. package/dist/ast.js +381 -0
  11. package/dist/bind.d.ts +62 -0
  12. package/dist/bind.js +106 -0
  13. package/dist/builtins.d.ts +257 -0
  14. package/dist/builtins.js +600 -0
  15. package/dist/chain.d.ts +2 -0
  16. package/dist/chain.js +8 -0
  17. package/dist/effect-id.d.ts +14 -0
  18. package/dist/effect-id.js +16 -0
  19. package/dist/handler.d.ts +50 -0
  20. package/dist/handler.js +146 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +5 -0
  23. package/dist/pipe.d.ts +11 -0
  24. package/dist/pipe.js +11 -0
  25. package/dist/race.d.ts +53 -0
  26. package/dist/race.js +141 -0
  27. package/dist/recursive.d.ts +34 -0
  28. package/dist/recursive.js +53 -0
  29. package/dist/run.d.ts +7 -0
  30. package/dist/run.js +143 -0
  31. package/dist/schema.d.ts +8 -0
  32. package/dist/schema.js +95 -0
  33. package/dist/try-catch.d.ts +23 -0
  34. package/dist/try-catch.js +36 -0
  35. package/dist/worker.d.ts +11 -0
  36. package/dist/worker.js +46 -0
  37. package/package.json +40 -16
  38. package/src/all.ts +89 -0
  39. package/src/ast.ts +878 -0
  40. package/src/bind.ts +192 -0
  41. package/src/builtins.ts +804 -0
  42. package/src/chain.ts +17 -0
  43. package/src/effect-id.ts +30 -0
  44. package/src/handler.ts +279 -0
  45. package/src/index.ts +30 -0
  46. package/src/pipe.ts +93 -0
  47. package/src/race.ts +183 -0
  48. package/src/recursive.ts +112 -0
  49. package/src/run.ts +181 -0
  50. package/src/schema.ts +118 -0
  51. package/src/try-catch.ts +53 -0
  52. package/src/worker.ts +56 -0
  53. package/README.md +0 -19
  54. package/barnum-config-schema.json +0 -408
  55. package/cli.js +0 -20
  56. package/index.js +0 -23
package/src/chain.ts ADDED
@@ -0,0 +1,17 @@
1
+ import {
2
+ type Action,
3
+ type Pipeable,
4
+ type TypedAction,
5
+ typedAction,
6
+ } from "./ast.js";
7
+
8
+ export function chain<T1, T2, T3>(
9
+ first: Pipeable<T1, T2>,
10
+ rest: Pipeable<T2, T3>,
11
+ ): TypedAction<T1, T3> {
12
+ return typedAction({
13
+ kind: "Chain",
14
+ first: first as Action,
15
+ rest: rest as Action,
16
+ });
17
+ }
@@ -0,0 +1,30 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared effect ID counter for gensym'd effect identifiers
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /** Branded ID for resume-style effect handlers. */
6
+ export type ResumeHandlerId = number & {
7
+ readonly __resumeHandlerBrand: unique symbol;
8
+ };
9
+
10
+ /** Branded ID for restart-style effect handlers. */
11
+ export type RestartHandlerId = number & {
12
+ readonly __restartHandlerBrand: unique symbol;
13
+ };
14
+
15
+ let nextId = 0;
16
+
17
+ /** Allocate a fresh, unique resume handler ID. */
18
+ export function allocateResumeHandlerId(): ResumeHandlerId {
19
+ return nextId++ as ResumeHandlerId;
20
+ }
21
+
22
+ /** Allocate a fresh, unique restart handler ID. */
23
+ export function allocateRestartHandlerId(): RestartHandlerId {
24
+ return nextId++ as RestartHandlerId;
25
+ }
26
+
27
+ /** Reset the ID counter. For test isolation only. */
28
+ export function resetEffectIdCounter(): void {
29
+ nextId = 0;
30
+ }
package/src/handler.ts ADDED
@@ -0,0 +1,279 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import type { JSONSchema7 } from "json-schema";
3
+ import type { z } from "zod";
4
+ import { type TypedAction, typedAction } from "./ast.js";
5
+ import { zodToCheckedJsonSchema } from "./schema.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // HandlerDefinition — the user's handle function + optional validators
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface HandlerDefinition<
12
+ TValue = unknown,
13
+ TOutput = unknown,
14
+ TStepConfig = unknown,
15
+ > {
16
+ inputValidator?: z.ZodType<TValue>;
17
+ outputValidator?: z.ZodType<TOutput>;
18
+ stepConfigValidator?: z.ZodType<TStepConfig>;
19
+ handle: (context: {
20
+ value: TValue;
21
+ stepConfig: TStepConfig;
22
+ }) => Promise<TOutput>;
23
+ }
24
+
25
+ /** Runtime-only handler definition shape — erases generic type info. */
26
+ interface UntypedHandlerDefinition {
27
+ inputValidator?: z.ZodType;
28
+ outputValidator?: z.ZodType;
29
+ stepConfigValidator?: z.ZodType;
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ handle: (...args: any[]) => Promise<unknown>;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Handler — opaque typed handler reference
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const HANDLER_BRAND = Symbol.for("barnum:handler");
39
+
40
+ /**
41
+ * Opaque handler reference with typed metadata. The `__definition` property
42
+ * is non-enumerable — invisible to `JSON.stringify`, visible to the worker.
43
+ */
44
+ export type Handler<TValue = unknown, TOutput = unknown> = TypedAction<
45
+ TValue,
46
+ TOutput
47
+ > & {
48
+ readonly [HANDLER_BRAND]: true;
49
+ readonly __definition: UntypedHandlerDefinition;
50
+ };
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // getCallerFilePath
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Deduces the caller's file path from the V8 stack trace API.
58
+ * Frame 0 = getCallerFilePath, Frame 1 = createHandler, Frame 2 = the caller.
59
+ */
60
+ function getCallerFilePath(): string {
61
+ const original = Error.prepareStackTrace;
62
+ let callerFile: string | undefined;
63
+
64
+ Error.prepareStackTrace = (_err, stack): string => {
65
+ const frame = stack[2];
66
+ callerFile = frame?.getFileName() ?? undefined;
67
+ return "";
68
+ };
69
+
70
+ const err = new Error("stack trace capture");
71
+ void err.stack;
72
+ Error.prepareStackTrace = original;
73
+
74
+ if (!callerFile) {
75
+ throw new Error(
76
+ "createHandler: could not determine caller file path from stack trace.",
77
+ );
78
+ }
79
+
80
+ if (callerFile.startsWith("file://")) {
81
+ return fileURLToPath(callerFile);
82
+ }
83
+ return callerFile;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // HandlerOutput — maps void → never so fire-and-forget handlers compose
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Handlers that return `Promise<void>` produce `never` output. This means
92
+ * they naturally compose in pipes without needing `.drop()` — a handler
93
+ * that returns nothing produces a value no one can observe.
94
+ */
95
+ type HandlerOutput<TOutput> = [TOutput] extends [void] ? never : TOutput;
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // createHandler — single overload, validators optional
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export function createHandler<TValue = never, TOutput = unknown>(
102
+ definition: {
103
+ inputValidator?: z.ZodType<TValue>;
104
+ outputValidator?: z.ZodType<NoInfer<TOutput>>;
105
+ handle: (context: { value: TValue }) => Promise<TOutput>;
106
+ },
107
+ exportName?: string,
108
+ ): Handler<TValue, HandlerOutput<TOutput>>;
109
+
110
+ // Implementation
111
+ export function createHandler(
112
+ definition: UntypedHandlerDefinition,
113
+ exportName?: string,
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ ): any {
116
+ const filePath = getCallerFilePath();
117
+ const funcName = exportName ?? "default";
118
+
119
+ const inputSchema = definition.inputValidator
120
+ ? zodToCheckedJsonSchema(
121
+ definition.inputValidator,
122
+ `${filePath}:${funcName} input`,
123
+ )
124
+ : undefined;
125
+ const outputSchema = definition.outputValidator
126
+ ? zodToCheckedJsonSchema(
127
+ definition.outputValidator,
128
+ `${filePath}:${funcName} output`,
129
+ )
130
+ : undefined;
131
+
132
+ const action = typedAction({
133
+ kind: "Invoke",
134
+ handler: {
135
+ kind: "TypeScript",
136
+ module: filePath,
137
+ func: funcName,
138
+ ...(inputSchema && { input_schema: inputSchema }),
139
+ ...(outputSchema && { output_schema: outputSchema }),
140
+ },
141
+ });
142
+
143
+ // Non-enumerable: invisible to JSON.stringify, visible to the worker
144
+ Object.defineProperty(action, HANDLER_BRAND, {
145
+ value: true,
146
+ enumerable: false,
147
+ });
148
+ Object.defineProperty(action, "__definition", {
149
+ value: definition,
150
+ enumerable: false,
151
+ });
152
+
153
+ return action;
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // createHandlerWithConfig — single overload, validators optional
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export function createHandlerWithConfig<
161
+ TValue = never,
162
+ TOutput = unknown,
163
+ TStepConfig = unknown,
164
+ >(
165
+ definition: {
166
+ inputValidator?: z.ZodType<TValue>;
167
+ outputValidator?: z.ZodType<NoInfer<TOutput>>;
168
+ stepConfigValidator?: z.ZodType<TStepConfig>;
169
+ handle: (context: {
170
+ value: TValue;
171
+ stepConfig: TStepConfig;
172
+ }) => Promise<TOutput>;
173
+ },
174
+ exportName?: string,
175
+ ): (config: TStepConfig) => TypedAction<TValue, HandlerOutput<TOutput>>;
176
+
177
+ // Implementation
178
+ export function createHandlerWithConfig(
179
+ definition: UntypedHandlerDefinition,
180
+ exportName?: string,
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ ): any {
183
+ const filePath = getCallerFilePath();
184
+ const funcName = exportName ?? "default";
185
+
186
+ // The invoke receives [value, config] from All(Identity, Constant(config)).
187
+ // Build a tuple schema manually — the Rust engine doesn't support draft-07
188
+ // array-form `items` for tuples, so use `prefixItems` (2020-12 style).
189
+ const valueSchema = definition.inputValidator
190
+ ? zodToCheckedJsonSchema(
191
+ definition.inputValidator,
192
+ `${filePath}:${funcName} input`,
193
+ )
194
+ : {};
195
+ const configSchema = definition.stepConfigValidator
196
+ ? zodToCheckedJsonSchema(
197
+ definition.stepConfigValidator,
198
+ `${filePath}:${funcName} stepConfig`,
199
+ )
200
+ : {};
201
+ const inputSchema: JSONSchema7 = {
202
+ type: "array",
203
+ prefixItems: [valueSchema, configSchema],
204
+ items: false,
205
+ minItems: 2,
206
+ maxItems: 2,
207
+ } as JSONSchema7;
208
+ const outputSchema = definition.outputValidator
209
+ ? zodToCheckedJsonSchema(
210
+ definition.outputValidator,
211
+ `${filePath}:${funcName} output`,
212
+ )
213
+ : undefined;
214
+
215
+ // Internal handle that unpacks the [value, config] tuple from All
216
+ const internalDefinition: UntypedHandlerDefinition = {
217
+ handle: ({ value }: { value: unknown }) => {
218
+ const [pipelineValue, config] = value as [unknown, unknown];
219
+ return definition.handle({ value: pipelineValue, stepConfig: config });
220
+ },
221
+ };
222
+
223
+ const invokeAction = typedAction({
224
+ kind: "Invoke",
225
+ handler: {
226
+ kind: "TypeScript",
227
+ module: filePath,
228
+ func: funcName,
229
+ input_schema: inputSchema,
230
+ ...(outputSchema && { output_schema: outputSchema }),
231
+ },
232
+ });
233
+
234
+ // Non-enumerable: invisible to JSON.stringify, visible to the worker
235
+ Object.defineProperty(invokeAction, HANDLER_BRAND, {
236
+ value: true,
237
+ enumerable: false,
238
+ });
239
+ Object.defineProperty(invokeAction, "__definition", {
240
+ value: internalDefinition,
241
+ enumerable: false,
242
+ });
243
+
244
+ // The factory function is the module export, so it must also carry
245
+ // __definition for the worker to find (the worker imports the module
246
+ // and accesses the named export, which is this function).
247
+ const factory = (config: unknown): TypedAction =>
248
+ typedAction({
249
+ kind: "Chain",
250
+ first: {
251
+ kind: "All",
252
+ actions: [
253
+ {
254
+ kind: "Invoke",
255
+ handler: { kind: "Builtin", builtin: { kind: "Identity" } },
256
+ },
257
+ {
258
+ kind: "Invoke",
259
+ handler: {
260
+ kind: "Builtin",
261
+ builtin: { kind: "Constant", value: config },
262
+ },
263
+ },
264
+ ],
265
+ },
266
+ rest: invokeAction,
267
+ });
268
+
269
+ Object.defineProperty(factory, HANDLER_BRAND, {
270
+ value: true,
271
+ enumerable: false,
272
+ });
273
+ Object.defineProperty(factory, "__definition", {
274
+ value: internalDefinition,
275
+ enumerable: false,
276
+ });
277
+
278
+ return factory;
279
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { TaggedUnion, OptionDef, ResultDef } from "./ast.js";
2
+
3
+ export * from "./ast.js";
4
+ export {
5
+ constant,
6
+ identity,
7
+ drop,
8
+ tag,
9
+ merge,
10
+ flatten,
11
+ extractField,
12
+ extractIndex,
13
+ pick,
14
+ dropResult,
15
+ withResource,
16
+ augment,
17
+ tap,
18
+ range,
19
+ Option,
20
+ Result,
21
+ } from "./builtins.js";
22
+ export * from "./handler.js";
23
+ export { runPipeline } from "./run.js";
24
+ export { zodToCheckedJsonSchema } from "./schema.js";
25
+
26
+ // Declaration merge: the explicit value exports of Option/Result from builtins
27
+ // shadow the type-only exports from ast's `export *`. Re-declare the generic
28
+ // type aliases here so consumers get both the type and value under one name.
29
+ export type Option<T> = TaggedUnion<OptionDef<T>>;
30
+ export type Result<TValue, TError> = TaggedUnion<ResultDef<TValue, TError>>;
package/src/pipe.ts ADDED
@@ -0,0 +1,93 @@
1
+ import {
2
+ type Action,
3
+ type PipeIn,
4
+ type Pipeable,
5
+ type TypedAction,
6
+ typedAction,
7
+ } from "./ast.js";
8
+ import { identity } from "./builtins.js";
9
+
10
+ export function pipe<T1, T2>(a1: Pipeable<T1, T2>): TypedAction<PipeIn<T1>, T2>;
11
+ export function pipe<T1, T2, T3>(
12
+ a1: Pipeable<T1, T2>,
13
+ a2: Pipeable<T2, T3>,
14
+ ): TypedAction<PipeIn<T1>, T3>;
15
+ export function pipe<T1, T2, T3, T4>(
16
+ a1: Pipeable<T1, T2>,
17
+ a2: Pipeable<T2, T3>,
18
+ a3: Pipeable<T3, T4>,
19
+ ): TypedAction<PipeIn<T1>, T4>;
20
+ export function pipe<T1, T2, T3, T4, T5>(
21
+ a1: Pipeable<T1, T2>,
22
+ a2: Pipeable<T2, T3>,
23
+ a3: Pipeable<T3, T4>,
24
+ a4: Pipeable<T4, T5>,
25
+ ): TypedAction<PipeIn<T1>, T5>;
26
+ export function pipe<T1, T2, T3, T4, T5, T6>(
27
+ a1: Pipeable<T1, T2>,
28
+ a2: Pipeable<T2, T3>,
29
+ a3: Pipeable<T3, T4>,
30
+ a4: Pipeable<T4, T5>,
31
+ a5: Pipeable<T5, T6>,
32
+ ): TypedAction<PipeIn<T1>, T6>;
33
+ export function pipe<T1, T2, T3, T4, T5, T6, T7>(
34
+ a1: Pipeable<T1, T2>,
35
+ a2: Pipeable<T2, T3>,
36
+ a3: Pipeable<T3, T4>,
37
+ a4: Pipeable<T4, T5>,
38
+ a5: Pipeable<T5, T6>,
39
+ a6: Pipeable<T6, T7>,
40
+ ): TypedAction<PipeIn<T1>, T7>;
41
+ export function pipe<T1, T2, T3, T4, T5, T6, T7, T8>(
42
+ a1: Pipeable<T1, T2>,
43
+ a2: Pipeable<T2, T3>,
44
+ a3: Pipeable<T3, T4>,
45
+ a4: Pipeable<T4, T5>,
46
+ a5: Pipeable<T5, T6>,
47
+ a6: Pipeable<T6, T7>,
48
+ a7: Pipeable<T7, T8>,
49
+ ): TypedAction<PipeIn<T1>, T8>;
50
+ export function pipe<T1, T2, T3, T4, T5, T6, T7, T8, T9>(
51
+ a1: Pipeable<T1, T2>,
52
+ a2: Pipeable<T2, T3>,
53
+ a3: Pipeable<T3, T4>,
54
+ a4: Pipeable<T4, T5>,
55
+ a5: Pipeable<T5, T6>,
56
+ a6: Pipeable<T6, T7>,
57
+ a7: Pipeable<T7, T8>,
58
+ a8: Pipeable<T8, T9>,
59
+ ): TypedAction<PipeIn<T1>, T9>;
60
+ export function pipe<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
61
+ a1: Pipeable<T1, T2>,
62
+ a2: Pipeable<T2, T3>,
63
+ a3: Pipeable<T3, T4>,
64
+ a4: Pipeable<T4, T5>,
65
+ a5: Pipeable<T5, T6>,
66
+ a6: Pipeable<T6, T7>,
67
+ a7: Pipeable<T7, T8>,
68
+ a8: Pipeable<T8, T9>,
69
+ a9: Pipeable<T9, T10>,
70
+ ): TypedAction<PipeIn<T1>, T10>;
71
+ export function pipe<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(
72
+ a1: Pipeable<T1, T2>,
73
+ a2: Pipeable<T2, T3>,
74
+ a3: Pipeable<T3, T4>,
75
+ a4: Pipeable<T4, T5>,
76
+ a5: Pipeable<T5, T6>,
77
+ a6: Pipeable<T6, T7>,
78
+ a7: Pipeable<T7, T8>,
79
+ a8: Pipeable<T8, T9>,
80
+ a9: Pipeable<T9, T10>,
81
+ a10: Pipeable<T10, T11>,
82
+ ): TypedAction<PipeIn<T1>, T11>;
83
+ export function pipe(...actions: Action[]): Action {
84
+ if (actions.length === 0) {
85
+ return identity;
86
+ }
87
+ if (actions.length === 1) {
88
+ return actions[0];
89
+ }
90
+ return actions.reduceRight(
91
+ (rest, first) => typedAction({ kind: "Chain", first, rest }) as Action,
92
+ );
93
+ }
package/src/race.ts ADDED
@@ -0,0 +1,183 @@
1
+ import {
2
+ type Action,
3
+ type Pipeable,
4
+ type Result,
5
+ type TypedAction,
6
+ typedAction,
7
+ buildRestartBranchAction,
8
+ TAG_BREAK,
9
+ IDENTITY,
10
+ } from "./ast.js";
11
+ import {
12
+ allocateRestartHandlerId,
13
+ type RestartHandlerId,
14
+ } from "./effect-id.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Shared AST fragments
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const TAG_OK: Action = {
21
+ kind: "Invoke",
22
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
23
+ };
24
+
25
+ const TAG_ERR: Action = {
26
+ kind: "Invoke",
27
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Err" } },
28
+ };
29
+
30
+ /**
31
+ * `Chain(Tag("Break"), RestartPerform(id))` — shared by race branches.
32
+ * The winning branch tags its result as Break, then performs. The handler
33
+ * restarts the body; Branch takes the Break arm (identity), `RestartHandle` exits.
34
+ */
35
+ function breakPerform(restartHandlerId: RestartHandlerId): Action {
36
+ return {
37
+ kind: "Chain",
38
+ first: TAG_BREAK,
39
+ rest: { kind: "RestartPerform", restart_handler_id: restartHandlerId },
40
+ };
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // race — first branch to complete wins, losers cancelled
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Run multiple actions concurrently. The first to complete wins; losers
49
+ * are cancelled during `RestartHandle` frame teardown.
50
+ *
51
+ * All branches must have the same input and output type (since either
52
+ * could win).
53
+ *
54
+ * Compiled form (restart+Branch, same substrate as loop/earlyReturn):
55
+ * `Chain(Tag("Continue"),`
56
+ * `RestartHandle(id, ExtractIndex(0),`
57
+ * `Branch({`
58
+ * `Continue: All(Chain(a, breakPerform), Chain(b, breakPerform), ...),`
59
+ * `Break: identity,`
60
+ * `})))`
61
+ *
62
+ * First branch to complete tags Break → `RestartPerform` → handler restarts →
63
+ * Branch takes Break arm → identity → `RestartHandle` exits with winner's value.
64
+ */
65
+ export function race<TIn, TOut>(
66
+ ...actions: Pipeable<TIn, TOut>[]
67
+ ): TypedAction<TIn, TOut> {
68
+ const restartHandlerId = allocateRestartHandlerId();
69
+ const perform = breakPerform(restartHandlerId);
70
+
71
+ const branches = actions.map((action) => ({
72
+ kind: "Chain" as const,
73
+ first: action as Action,
74
+ rest: perform,
75
+ }));
76
+
77
+ const allAction: Action = { kind: "All", actions: branches };
78
+
79
+ return typedAction(
80
+ buildRestartBranchAction(restartHandlerId, allAction, IDENTITY),
81
+ );
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // sleep — Rust builtin that delays for a fixed duration (passthrough)
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Sleep for a fixed duration, ignoring input and returning void.
90
+ *
91
+ * `ms` is baked into the AST at construction time. Executed by the Rust
92
+ * scheduler via `tokio::time::sleep` — no subprocess spawned.
93
+ *
94
+ * To preserve data across a sleep, use `bindInput`.
95
+ */
96
+ export function sleep(ms: number): TypedAction<any, never> {
97
+ return typedAction<any, never>({
98
+ kind: "Invoke",
99
+ handler: { kind: "Builtin", builtin: { kind: "Sleep", value: ms } },
100
+ });
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // dynamicSleep — TypeScript handler for withTimeout (takes ms as input)
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /** The raw Invoke node for the dynamic sleep handler. */
108
+ const DYNAMIC_SLEEP_INVOKE: Action = {
109
+ kind: "Invoke",
110
+ handler: {
111
+ kind: "TypeScript",
112
+ module: import.meta.url,
113
+ func: "dynamicSleep",
114
+ },
115
+ };
116
+
117
+ /**
118
+ * @internal TypeScript handler that takes ms as pipeline input and returns
119
+ * void after the timer fires. Used by `withTimeout` where the duration
120
+ * comes from a runtime pipeline, not a build-time constant.
121
+ */
122
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
123
+ export function dynamicSleep(): void {}
124
+ Object.defineProperty(dynamicSleep, "__definition", {
125
+ value: {
126
+ handle: ({ value }: { value: number }) =>
127
+ new Promise<void>((resolve) => setTimeout(resolve, value)),
128
+ },
129
+ enumerable: false,
130
+ });
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // withTimeout — race body against sleep, return Result
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Race the body against a sleep timer. Returns `Result<TOut, void>`:
138
+ * - Ok(value) if the body completed first
139
+ * - Err(void) if the timeout fired first
140
+ *
141
+ * The `ms` parameter is an AST node that evaluates to the timeout duration
142
+ * in milliseconds. Use `constant(5000)` for a fixed timeout, or any action
143
+ * that computes a duration from the pipeline input.
144
+ *
145
+ * Built as raw AST rather than through `race()` because each branch wraps
146
+ * its result differently (Ok vs Err) before the Break+Perform. `race()`
147
+ * requires homogeneous output types, but withTimeout needs heterogeneous
148
+ * tagging.
149
+ *
150
+ * Same restart+Branch substrate as race: each branch tags Break after
151
+ * wrapping its result as Ok or Err.
152
+ */
153
+ export function withTimeout<TIn, TOut>(
154
+ ms: Pipeable<TIn, number>,
155
+ body: Pipeable<TIn, TOut>,
156
+ ): TypedAction<TIn, Result<TOut, void>> {
157
+ const restartHandlerId = allocateRestartHandlerId();
158
+ const perform = breakPerform(restartHandlerId);
159
+
160
+ // Branch 1: body → Tag("Ok") → Tag("Break") → RestartPerform
161
+ const bodyBranch: Action = {
162
+ kind: "Chain",
163
+ first: { kind: "Chain", first: body as Action, rest: TAG_OK },
164
+ rest: perform,
165
+ };
166
+
167
+ // Branch 2: ms → sleep() → Tag("Err") → Tag("Break") → RestartPerform
168
+ const sleepBranch: Action = {
169
+ kind: "Chain",
170
+ first: {
171
+ kind: "Chain",
172
+ first: { kind: "Chain", first: ms as Action, rest: DYNAMIC_SLEEP_INVOKE },
173
+ rest: TAG_ERR,
174
+ },
175
+ rest: perform,
176
+ };
177
+
178
+ const allAction: Action = { kind: "All", actions: [bodyBranch, sleepBranch] };
179
+
180
+ return typedAction(
181
+ buildRestartBranchAction(restartHandlerId, allAction, IDENTITY),
182
+ );
183
+ }