@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
@@ -0,0 +1,146 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { typedAction } from "./ast.js";
3
+ import { zodToCheckedJsonSchema } from "./schema.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Handler — opaque typed handler reference
6
+ // ---------------------------------------------------------------------------
7
+ const HANDLER_BRAND = Symbol.for("barnum:handler");
8
+ // ---------------------------------------------------------------------------
9
+ // getCallerFilePath
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Deduces the caller's file path from the V8 stack trace API.
13
+ * Frame 0 = getCallerFilePath, Frame 1 = createHandler, Frame 2 = the caller.
14
+ */
15
+ function getCallerFilePath() {
16
+ const original = Error.prepareStackTrace;
17
+ let callerFile;
18
+ Error.prepareStackTrace = (_err, stack) => {
19
+ const frame = stack[2];
20
+ callerFile = frame?.getFileName() ?? undefined;
21
+ return "";
22
+ };
23
+ const err = new Error("stack trace capture");
24
+ void err.stack;
25
+ Error.prepareStackTrace = original;
26
+ if (!callerFile) {
27
+ throw new Error("createHandler: could not determine caller file path from stack trace.");
28
+ }
29
+ if (callerFile.startsWith("file://")) {
30
+ return fileURLToPath(callerFile);
31
+ }
32
+ return callerFile;
33
+ }
34
+ // Implementation
35
+ export function createHandler(definition, exportName) {
36
+ const filePath = getCallerFilePath();
37
+ const funcName = exportName ?? "default";
38
+ const inputSchema = definition.inputValidator
39
+ ? zodToCheckedJsonSchema(definition.inputValidator, `${filePath}:${funcName} input`)
40
+ : undefined;
41
+ const outputSchema = definition.outputValidator
42
+ ? zodToCheckedJsonSchema(definition.outputValidator, `${filePath}:${funcName} output`)
43
+ : undefined;
44
+ const action = typedAction({
45
+ kind: "Invoke",
46
+ handler: {
47
+ kind: "TypeScript",
48
+ module: filePath,
49
+ func: funcName,
50
+ ...(inputSchema && { input_schema: inputSchema }),
51
+ ...(outputSchema && { output_schema: outputSchema }),
52
+ },
53
+ });
54
+ // Non-enumerable: invisible to JSON.stringify, visible to the worker
55
+ Object.defineProperty(action, HANDLER_BRAND, {
56
+ value: true,
57
+ enumerable: false,
58
+ });
59
+ Object.defineProperty(action, "__definition", {
60
+ value: definition,
61
+ enumerable: false,
62
+ });
63
+ return action;
64
+ }
65
+ // Implementation
66
+ export function createHandlerWithConfig(definition, exportName) {
67
+ const filePath = getCallerFilePath();
68
+ const funcName = exportName ?? "default";
69
+ // The invoke receives [value, config] from All(Identity, Constant(config)).
70
+ // Build a tuple schema manually — the Rust engine doesn't support draft-07
71
+ // array-form `items` for tuples, so use `prefixItems` (2020-12 style).
72
+ const valueSchema = definition.inputValidator
73
+ ? zodToCheckedJsonSchema(definition.inputValidator, `${filePath}:${funcName} input`)
74
+ : {};
75
+ const configSchema = definition.stepConfigValidator
76
+ ? zodToCheckedJsonSchema(definition.stepConfigValidator, `${filePath}:${funcName} stepConfig`)
77
+ : {};
78
+ const inputSchema = {
79
+ type: "array",
80
+ prefixItems: [valueSchema, configSchema],
81
+ items: false,
82
+ minItems: 2,
83
+ maxItems: 2,
84
+ };
85
+ const outputSchema = definition.outputValidator
86
+ ? zodToCheckedJsonSchema(definition.outputValidator, `${filePath}:${funcName} output`)
87
+ : undefined;
88
+ // Internal handle that unpacks the [value, config] tuple from All
89
+ const internalDefinition = {
90
+ handle: ({ value }) => {
91
+ const [pipelineValue, config] = value;
92
+ return definition.handle({ value: pipelineValue, stepConfig: config });
93
+ },
94
+ };
95
+ const invokeAction = typedAction({
96
+ kind: "Invoke",
97
+ handler: {
98
+ kind: "TypeScript",
99
+ module: filePath,
100
+ func: funcName,
101
+ input_schema: inputSchema,
102
+ ...(outputSchema && { output_schema: outputSchema }),
103
+ },
104
+ });
105
+ // Non-enumerable: invisible to JSON.stringify, visible to the worker
106
+ Object.defineProperty(invokeAction, HANDLER_BRAND, {
107
+ value: true,
108
+ enumerable: false,
109
+ });
110
+ Object.defineProperty(invokeAction, "__definition", {
111
+ value: internalDefinition,
112
+ enumerable: false,
113
+ });
114
+ // The factory function is the module export, so it must also carry
115
+ // __definition for the worker to find (the worker imports the module
116
+ // and accesses the named export, which is this function).
117
+ const factory = (config) => typedAction({
118
+ kind: "Chain",
119
+ first: {
120
+ kind: "All",
121
+ actions: [
122
+ {
123
+ kind: "Invoke",
124
+ handler: { kind: "Builtin", builtin: { kind: "Identity" } },
125
+ },
126
+ {
127
+ kind: "Invoke",
128
+ handler: {
129
+ kind: "Builtin",
130
+ builtin: { kind: "Constant", value: config },
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ rest: invokeAction,
136
+ });
137
+ Object.defineProperty(factory, HANDLER_BRAND, {
138
+ value: true,
139
+ enumerable: false,
140
+ });
141
+ Object.defineProperty(factory, "__definition", {
142
+ value: internalDefinition,
143
+ enumerable: false,
144
+ });
145
+ return factory;
146
+ }
@@ -0,0 +1,8 @@
1
+ import type { TaggedUnion, OptionDef, ResultDef } from "./ast.js";
2
+ export * from "./ast.js";
3
+ export { constant, identity, drop, tag, merge, flatten, extractField, extractIndex, pick, dropResult, withResource, augment, tap, range, Option, Result, } from "./builtins.js";
4
+ export * from "./handler.js";
5
+ export { runPipeline } from "./run.js";
6
+ export { zodToCheckedJsonSchema } from "./schema.js";
7
+ export type Option<T> = TaggedUnion<OptionDef<T>>;
8
+ export type Result<TValue, TError> = TaggedUnion<ResultDef<TValue, TError>>;
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./ast.js";
2
+ export { constant, identity, drop, tag, merge, flatten, extractField, extractIndex, pick, dropResult, withResource, augment, tap, range, Option, Result, } from "./builtins.js";
3
+ export * from "./handler.js";
4
+ export { runPipeline } from "./run.js";
5
+ export { zodToCheckedJsonSchema } from "./schema.js";
package/dist/pipe.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { type PipeIn, type Pipeable, type TypedAction } from "./ast.js";
2
+ export declare function pipe<T1, T2>(a1: Pipeable<T1, T2>): TypedAction<PipeIn<T1>, T2>;
3
+ export declare function pipe<T1, T2, T3>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>): TypedAction<PipeIn<T1>, T3>;
4
+ export declare function pipe<T1, T2, T3, T4>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>): TypedAction<PipeIn<T1>, T4>;
5
+ export declare function pipe<T1, T2, T3, T4, T5>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>): TypedAction<PipeIn<T1>, T5>;
6
+ export declare function pipe<T1, T2, T3, T4, T5, T6>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>, a5: Pipeable<T5, T6>): TypedAction<PipeIn<T1>, T6>;
7
+ export declare function pipe<T1, T2, T3, T4, T5, T6, T7>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>, a5: Pipeable<T5, T6>, a6: Pipeable<T6, T7>): TypedAction<PipeIn<T1>, T7>;
8
+ export declare function pipe<T1, T2, T3, T4, T5, T6, T7, T8>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>, a5: Pipeable<T5, T6>, a6: Pipeable<T6, T7>, a7: Pipeable<T7, T8>): TypedAction<PipeIn<T1>, T8>;
9
+ export declare function pipe<T1, T2, T3, T4, T5, T6, T7, T8, T9>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>, a5: Pipeable<T5, T6>, a6: Pipeable<T6, T7>, a7: Pipeable<T7, T8>, a8: Pipeable<T8, T9>): TypedAction<PipeIn<T1>, T9>;
10
+ export declare function pipe<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>, a5: Pipeable<T5, T6>, a6: Pipeable<T6, T7>, a7: Pipeable<T7, T8>, a8: Pipeable<T8, T9>, a9: Pipeable<T9, T10>): TypedAction<PipeIn<T1>, T10>;
11
+ export declare function pipe<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(a1: Pipeable<T1, T2>, a2: Pipeable<T2, T3>, a3: Pipeable<T3, T4>, a4: Pipeable<T4, T5>, a5: Pipeable<T5, T6>, a6: Pipeable<T6, T7>, a7: Pipeable<T7, T8>, a8: Pipeable<T8, T9>, a9: Pipeable<T9, T10>, a10: Pipeable<T10, T11>): TypedAction<PipeIn<T1>, T11>;
package/dist/pipe.js ADDED
@@ -0,0 +1,11 @@
1
+ import { typedAction, } from "./ast.js";
2
+ import { identity } from "./builtins.js";
3
+ export function pipe(...actions) {
4
+ if (actions.length === 0) {
5
+ return identity;
6
+ }
7
+ if (actions.length === 1) {
8
+ return actions[0];
9
+ }
10
+ return actions.reduceRight((rest, first) => typedAction({ kind: "Chain", first, rest }));
11
+ }
package/dist/race.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { type Pipeable, type Result, type TypedAction } from "./ast.js";
2
+ /**
3
+ * Run multiple actions concurrently. The first to complete wins; losers
4
+ * are cancelled during `RestartHandle` frame teardown.
5
+ *
6
+ * All branches must have the same input and output type (since either
7
+ * could win).
8
+ *
9
+ * Compiled form (restart+Branch, same substrate as loop/earlyReturn):
10
+ * `Chain(Tag("Continue"),`
11
+ * `RestartHandle(id, ExtractIndex(0),`
12
+ * `Branch({`
13
+ * `Continue: All(Chain(a, breakPerform), Chain(b, breakPerform), ...),`
14
+ * `Break: identity,`
15
+ * `})))`
16
+ *
17
+ * First branch to complete tags Break → `RestartPerform` → handler restarts →
18
+ * Branch takes Break arm → identity → `RestartHandle` exits with winner's value.
19
+ */
20
+ export declare function race<TIn, TOut>(...actions: Pipeable<TIn, TOut>[]): TypedAction<TIn, TOut>;
21
+ /**
22
+ * Sleep for a fixed duration, ignoring input and returning void.
23
+ *
24
+ * `ms` is baked into the AST at construction time. Executed by the Rust
25
+ * scheduler via `tokio::time::sleep` — no subprocess spawned.
26
+ *
27
+ * To preserve data across a sleep, use `bindInput`.
28
+ */
29
+ export declare function sleep(ms: number): TypedAction<any, never>;
30
+ /**
31
+ * @internal TypeScript handler that takes ms as pipeline input and returns
32
+ * void after the timer fires. Used by `withTimeout` where the duration
33
+ * comes from a runtime pipeline, not a build-time constant.
34
+ */
35
+ export declare function dynamicSleep(): void;
36
+ /**
37
+ * Race the body against a sleep timer. Returns `Result<TOut, void>`:
38
+ * - Ok(value) if the body completed first
39
+ * - Err(void) if the timeout fired first
40
+ *
41
+ * The `ms` parameter is an AST node that evaluates to the timeout duration
42
+ * in milliseconds. Use `constant(5000)` for a fixed timeout, or any action
43
+ * that computes a duration from the pipeline input.
44
+ *
45
+ * Built as raw AST rather than through `race()` because each branch wraps
46
+ * its result differently (Ok vs Err) before the Break+Perform. `race()`
47
+ * requires homogeneous output types, but withTimeout needs heterogeneous
48
+ * tagging.
49
+ *
50
+ * Same restart+Branch substrate as race: each branch tags Break after
51
+ * wrapping its result as Ok or Err.
52
+ */
53
+ export declare function withTimeout<TIn, TOut>(ms: Pipeable<TIn, number>, body: Pipeable<TIn, TOut>): TypedAction<TIn, Result<TOut, void>>;
package/dist/race.js ADDED
@@ -0,0 +1,141 @@
1
+ import { typedAction, buildRestartBranchAction, TAG_BREAK, IDENTITY, } from "./ast.js";
2
+ import { allocateRestartHandlerId, } from "./effect-id.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Shared AST fragments
5
+ // ---------------------------------------------------------------------------
6
+ const TAG_OK = {
7
+ kind: "Invoke",
8
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
9
+ };
10
+ const TAG_ERR = {
11
+ kind: "Invoke",
12
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Err" } },
13
+ };
14
+ /**
15
+ * `Chain(Tag("Break"), RestartPerform(id))` — shared by race branches.
16
+ * The winning branch tags its result as Break, then performs. The handler
17
+ * restarts the body; Branch takes the Break arm (identity), `RestartHandle` exits.
18
+ */
19
+ function breakPerform(restartHandlerId) {
20
+ return {
21
+ kind: "Chain",
22
+ first: TAG_BREAK,
23
+ rest: { kind: "RestartPerform", restart_handler_id: restartHandlerId },
24
+ };
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // race — first branch to complete wins, losers cancelled
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * Run multiple actions concurrently. The first to complete wins; losers
31
+ * are cancelled during `RestartHandle` frame teardown.
32
+ *
33
+ * All branches must have the same input and output type (since either
34
+ * could win).
35
+ *
36
+ * Compiled form (restart+Branch, same substrate as loop/earlyReturn):
37
+ * `Chain(Tag("Continue"),`
38
+ * `RestartHandle(id, ExtractIndex(0),`
39
+ * `Branch({`
40
+ * `Continue: All(Chain(a, breakPerform), Chain(b, breakPerform), ...),`
41
+ * `Break: identity,`
42
+ * `})))`
43
+ *
44
+ * First branch to complete tags Break → `RestartPerform` → handler restarts →
45
+ * Branch takes Break arm → identity → `RestartHandle` exits with winner's value.
46
+ */
47
+ export function race(...actions) {
48
+ const restartHandlerId = allocateRestartHandlerId();
49
+ const perform = breakPerform(restartHandlerId);
50
+ const branches = actions.map((action) => ({
51
+ kind: "Chain",
52
+ first: action,
53
+ rest: perform,
54
+ }));
55
+ const allAction = { kind: "All", actions: branches };
56
+ return typedAction(buildRestartBranchAction(restartHandlerId, allAction, IDENTITY));
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // sleep — Rust builtin that delays for a fixed duration (passthrough)
60
+ // ---------------------------------------------------------------------------
61
+ /**
62
+ * Sleep for a fixed duration, ignoring input and returning void.
63
+ *
64
+ * `ms` is baked into the AST at construction time. Executed by the Rust
65
+ * scheduler via `tokio::time::sleep` — no subprocess spawned.
66
+ *
67
+ * To preserve data across a sleep, use `bindInput`.
68
+ */
69
+ export function sleep(ms) {
70
+ return typedAction({
71
+ kind: "Invoke",
72
+ handler: { kind: "Builtin", builtin: { kind: "Sleep", value: ms } },
73
+ });
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // dynamicSleep — TypeScript handler for withTimeout (takes ms as input)
77
+ // ---------------------------------------------------------------------------
78
+ /** The raw Invoke node for the dynamic sleep handler. */
79
+ const DYNAMIC_SLEEP_INVOKE = {
80
+ kind: "Invoke",
81
+ handler: {
82
+ kind: "TypeScript",
83
+ module: import.meta.url,
84
+ func: "dynamicSleep",
85
+ },
86
+ };
87
+ /**
88
+ * @internal TypeScript handler that takes ms as pipeline input and returns
89
+ * void after the timer fires. Used by `withTimeout` where the duration
90
+ * comes from a runtime pipeline, not a build-time constant.
91
+ */
92
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
93
+ export function dynamicSleep() { }
94
+ Object.defineProperty(dynamicSleep, "__definition", {
95
+ value: {
96
+ handle: ({ value }) => new Promise((resolve) => setTimeout(resolve, value)),
97
+ },
98
+ enumerable: false,
99
+ });
100
+ // ---------------------------------------------------------------------------
101
+ // withTimeout — race body against sleep, return Result
102
+ // ---------------------------------------------------------------------------
103
+ /**
104
+ * Race the body against a sleep timer. Returns `Result<TOut, void>`:
105
+ * - Ok(value) if the body completed first
106
+ * - Err(void) if the timeout fired first
107
+ *
108
+ * The `ms` parameter is an AST node that evaluates to the timeout duration
109
+ * in milliseconds. Use `constant(5000)` for a fixed timeout, or any action
110
+ * that computes a duration from the pipeline input.
111
+ *
112
+ * Built as raw AST rather than through `race()` because each branch wraps
113
+ * its result differently (Ok vs Err) before the Break+Perform. `race()`
114
+ * requires homogeneous output types, but withTimeout needs heterogeneous
115
+ * tagging.
116
+ *
117
+ * Same restart+Branch substrate as race: each branch tags Break after
118
+ * wrapping its result as Ok or Err.
119
+ */
120
+ export function withTimeout(ms, body) {
121
+ const restartHandlerId = allocateRestartHandlerId();
122
+ const perform = breakPerform(restartHandlerId);
123
+ // Branch 1: body → Tag("Ok") → Tag("Break") → RestartPerform
124
+ const bodyBranch = {
125
+ kind: "Chain",
126
+ first: { kind: "Chain", first: body, rest: TAG_OK },
127
+ rest: perform,
128
+ };
129
+ // Branch 2: ms → sleep() → Tag("Err") → Tag("Break") → RestartPerform
130
+ const sleepBranch = {
131
+ kind: "Chain",
132
+ first: {
133
+ kind: "Chain",
134
+ first: { kind: "Chain", first: ms, rest: DYNAMIC_SLEEP_INVOKE },
135
+ rest: TAG_ERR,
136
+ },
137
+ rest: perform,
138
+ };
139
+ const allAction = { kind: "All", actions: [bodyBranch, sleepBranch] };
140
+ return typedAction(buildRestartBranchAction(restartHandlerId, allAction, IDENTITY));
141
+ }
@@ -0,0 +1,34 @@
1
+ import { type Action, type Pipeable, type TypedAction } from "./ast.js";
2
+ type FunctionDef = [input: unknown, output: unknown];
3
+ type FunctionRefs<TDefs extends FunctionDef[]> = {
4
+ [K in keyof TDefs]: TypedAction<TDefs[K][0], TDefs[K][1]>;
5
+ };
6
+ /**
7
+ * Constraint for the entry-point callback return type. Only requires the
8
+ * output phantom fields — omits __in and __in_co so that actions with
9
+ * In = never (e.g. pipelines starting from a call token) are assignable.
10
+ */
11
+ type BodyResult<TOut> = Action & {
12
+ __out?: () => TOut;
13
+ __out_contra?: (output: TOut) => void;
14
+ };
15
+ /**
16
+ * Define mutually recursive functions that can call each other.
17
+ *
18
+ * The type parameter is an array of [In, Out] tuples — one per function.
19
+ * TypeScript can't infer these from circular definitions, so they must be
20
+ * explicit.
21
+ *
22
+ * Returns a curried combinator: the first callback defines function bodies,
23
+ * the second receives the same call tokens and returns the workflow entry
24
+ * point.
25
+ *
26
+ * Desugars to a ResumeHandle with a Branch-based handler. Each call token
27
+ * is Chain(Tag("CallN"), ResumePerform(id)). The handler dispatches to the
28
+ * correct function body by tag. The caller's pipeline is preserved as a
29
+ * ResumePerformFrame across each call.
30
+ */
31
+ export declare function defineRecursiveFunctions<TDefs extends FunctionDef[]>(bodiesFn: (...fns: FunctionRefs<TDefs>) => {
32
+ [K in keyof TDefs]: Pipeable<TDefs[K][0], TDefs[K][1]>;
33
+ }): <TOut>(entryFn: (...fns: FunctionRefs<TDefs>) => BodyResult<TOut>) => TypedAction<any, TOut>;
34
+ export {};
@@ -0,0 +1,53 @@
1
+ import { typedAction, branch, } from "./ast.js";
2
+ import { all } from "./all.js";
3
+ import { chain } from "./chain.js";
4
+ import { constant, identity, extractField, extractIndex, tag, } from "./builtins.js";
5
+ import { allocateResumeHandlerId } from "./effect-id.js";
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const UNUSED_STATE = undefined;
8
+ // ---------------------------------------------------------------------------
9
+ // defineRecursiveFunctions
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Define mutually recursive functions that can call each other.
13
+ *
14
+ * The type parameter is an array of [In, Out] tuples — one per function.
15
+ * TypeScript can't infer these from circular definitions, so they must be
16
+ * explicit.
17
+ *
18
+ * Returns a curried combinator: the first callback defines function bodies,
19
+ * the second receives the same call tokens and returns the workflow entry
20
+ * point.
21
+ *
22
+ * Desugars to a ResumeHandle with a Branch-based handler. Each call token
23
+ * is Chain(Tag("CallN"), ResumePerform(id)). The handler dispatches to the
24
+ * correct function body by tag. The caller's pipeline is preserved as a
25
+ * ResumePerformFrame across each call.
26
+ */
27
+ export function defineRecursiveFunctions(bodiesFn) {
28
+ const resumeHandlerId = allocateResumeHandlerId();
29
+ const resumePerform = {
30
+ kind: "ResumePerform",
31
+ resume_handler_id: resumeHandlerId,
32
+ };
33
+ // Call tokens: Chain(Tag("CallN"), ResumePerform(resumeHandlerId))
34
+ const fnCount = bodiesFn.length;
35
+ const callTokens = Array.from({ length: fnCount }, (_, i) => typedAction(chain(tag(`Call${i}`), resumePerform)));
36
+ // Get function body ASTs
37
+ const bodyActions = bodiesFn(...callTokens);
38
+ // Branch cases: CallN → ExtractField("value") → bodyN
39
+ const cases = {};
40
+ for (let i = 0; i < bodyActions.length; i++) {
41
+ cases[`Call${i}`] = chain(extractField("value"), bodyActions[i]);
42
+ }
43
+ // Return curried entry-point combinator
44
+ return (entryFn) => {
45
+ const userBody = entryFn(...callTokens);
46
+ return typedAction(chain(all(identity, constant(UNUSED_STATE)), {
47
+ kind: "ResumeHandle",
48
+ resume_handler_id: resumeHandlerId,
49
+ body: chain(extractIndex(0), userBody),
50
+ handler: all(chain(extractIndex(0), branch(cases)), constant(UNUSED_STATE)),
51
+ }));
52
+ };
53
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Workflow execution: resolves the barnum binary, tsx executor, and worker
3
+ * script, then spawns the workflow as a subprocess.
4
+ */
5
+ import type { Action, ExtractOutput } from "./ast.js";
6
+ /** Run a pipeline to completion. Returns the workflow's final output value. */
7
+ export declare function runPipeline<TPipeline extends Action>(pipeline: TPipeline, input?: unknown): Promise<ExtractOutput<TPipeline>>;
package/dist/run.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Workflow execution: resolves the barnum binary, tsx executor, and worker
3
+ * script, then spawns the workflow as a subprocess.
4
+ */
5
+ import { execFileSync, spawn as nodeSpawn } from "node:child_process";
6
+ import { createRequire } from "node:module";
7
+ import { existsSync } from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { chain } from "./chain.js";
11
+ import { constant } from "./builtins.js";
12
+ const __dirname = import.meta.dirname;
13
+ /** Resolve the TypeScript executor. Uses bun if the workflow was launched with bun, otherwise tsx. */
14
+ function resolveExecutor() {
15
+ if (process.versions.bun) {
16
+ return "bun";
17
+ }
18
+ const callerRequire = createRequire(process.argv[1] || import.meta.url);
19
+ const tsxPath = callerRequire.resolve("tsx/cli");
20
+ return `node ${tsxPath}`;
21
+ }
22
+ /** Resolve the platform-specific binary from the @barnum/barnum package artifacts. */
23
+ function resolveInstalledBinary() {
24
+ const platform = os.platform();
25
+ const arch = os.arch();
26
+ let artifactDir;
27
+ let binaryName = "barnum";
28
+ if (platform === "darwin" && arch === "arm64") {
29
+ artifactDir = "macos-arm64";
30
+ }
31
+ else if (platform === "darwin") {
32
+ artifactDir = "macos-x64";
33
+ }
34
+ else if (platform === "linux" && arch === "arm64") {
35
+ artifactDir = "linux-arm64";
36
+ }
37
+ else if (platform === "linux") {
38
+ artifactDir = "linux-x64";
39
+ }
40
+ else if (platform === "win32") {
41
+ artifactDir = "win-x64";
42
+ binaryName = "barnum.exe";
43
+ }
44
+ else {
45
+ return undefined;
46
+ }
47
+ const callerRequire = createRequire(process.argv[1] || import.meta.url);
48
+ try {
49
+ const packageDir = path.dirname(callerRequire.resolve("@barnum/barnum/package.json"));
50
+ const binaryPath = path.join(packageDir, "artifacts", artifactDir, binaryName);
51
+ if (existsSync(binaryPath)) {
52
+ return binaryPath;
53
+ }
54
+ }
55
+ catch {
56
+ // Package not installed
57
+ }
58
+ return undefined;
59
+ }
60
+ /** Resolve the barnum binary. Checks: BARNUM env var, local repo, node_modules. */
61
+ function resolveBinary() {
62
+ if (process.env.BARNUM) {
63
+ return { kind: "Env", path: process.env.BARNUM };
64
+ }
65
+ const repoRoot = path.resolve(__dirname, "../../..");
66
+ if (existsSync(path.join(repoRoot, "Cargo.toml"))) {
67
+ return {
68
+ kind: "Local",
69
+ path: path.join(repoRoot, "target/debug/barnum"),
70
+ };
71
+ }
72
+ const installedBinaryPath = resolveInstalledBinary();
73
+ if (installedBinaryPath) {
74
+ return { kind: "NodeModules", path: installedBinaryPath };
75
+ }
76
+ throw new Error("Could not find barnum binary. Set BARNUM env var or install @barnum/barnum.");
77
+ }
78
+ /** Resolve worker.ts relative to this package. */
79
+ function resolveWorker() {
80
+ return path.resolve(__dirname, "../src/worker.ts");
81
+ }
82
+ /** Build the barnum binary if using the local dev path. */
83
+ function buildBinary() {
84
+ const repoRoot = path.resolve(__dirname, "../../..");
85
+ execFileSync("cargo", ["build", "-p", "barnum_cli"], {
86
+ cwd: repoRoot,
87
+ stdio: "ignore",
88
+ });
89
+ }
90
+ /** Run a pipeline to completion. Returns the workflow's final output value. */
91
+ export function runPipeline(pipeline, input) {
92
+ const workflow = input === undefined
93
+ ? pipeline
94
+ : chain(constant(input), pipeline);
95
+ return spawnBarnum({ workflow });
96
+ }
97
+ /** Spawn the barnum CLI with the given config. Returns the parsed final value from stdout. */
98
+ function spawnBarnum(config) {
99
+ const binaryResolution = resolveBinary();
100
+ if (binaryResolution.kind === "Local") {
101
+ buildBinary();
102
+ }
103
+ const executor = resolveExecutor();
104
+ const worker = resolveWorker();
105
+ const configJson = JSON.stringify(config);
106
+ return new Promise((resolve, reject) => {
107
+ const child = nodeSpawn(binaryResolution.path, [
108
+ "run",
109
+ "--config",
110
+ configJson,
111
+ "--executor",
112
+ executor,
113
+ "--worker",
114
+ worker,
115
+ ], {
116
+ stdio: ["inherit", "pipe", "inherit"],
117
+ });
118
+ const stdoutChunks = [];
119
+ child.stdout?.on("data", (chunk) => {
120
+ stdoutChunks.push(chunk);
121
+ });
122
+ child.on("error", (error) => {
123
+ reject(new Error(`Failed to spawn barnum: ${error.message}`));
124
+ });
125
+ child.on("close", (code) => {
126
+ if (code !== 0) {
127
+ reject(new Error(`barnum exited with code ${code}`));
128
+ return;
129
+ }
130
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
131
+ if (!stdout) {
132
+ resolve(undefined);
133
+ return;
134
+ }
135
+ try {
136
+ resolve(JSON.parse(stdout));
137
+ }
138
+ catch {
139
+ reject(new Error(`barnum produced non-JSON output on stdout: ${stdout}`));
140
+ }
141
+ });
142
+ });
143
+ }
@@ -0,0 +1,8 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ import { type z } from "zod";
3
+ /**
4
+ * Convert a Zod schema to a JSON Schema document suitable for embedding
5
+ * in the serialized AST. Throws if the schema contains types that can't
6
+ * survive the TS → JSON → Rust boundary.
7
+ */
8
+ export declare function zodToCheckedJsonSchema(schema: z.ZodType, label: string): JSONSchema7;