@barnum/barnum 0.2.3 → 0.4.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 +43 -0
- package/dist/all.d.ts.map +1 -0
- package/dist/all.js +8 -0
- package/dist/ast.d.ts +476 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +419 -0
- package/dist/bind.d.ts +59 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +69 -0
- package/dist/builtins/array.d.ts +36 -0
- package/dist/builtins/array.d.ts.map +1 -0
- package/dist/builtins/array.js +93 -0
- package/dist/builtins/index.d.ts +6 -0
- package/dist/builtins/index.d.ts.map +1 -0
- package/dist/builtins/index.js +5 -0
- package/dist/builtins/scalar.d.ts +12 -0
- package/dist/builtins/scalar.d.ts.map +1 -0
- package/dist/builtins/scalar.js +41 -0
- package/dist/builtins/struct.d.ts +25 -0
- package/dist/builtins/struct.d.ts.map +1 -0
- package/dist/builtins/struct.js +67 -0
- package/dist/builtins/tagged-union.d.ts +54 -0
- package/dist/builtins/tagged-union.d.ts.map +1 -0
- package/dist/builtins/tagged-union.js +81 -0
- package/dist/builtins/with-resource.d.ts +23 -0
- package/dist/builtins/with-resource.d.ts.map +1 -0
- package/dist/builtins/with-resource.js +35 -0
- package/dist/chain.d.ts +3 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +8 -0
- package/dist/effect-id.d.ts +15 -0
- package/dist/effect-id.d.ts.map +1 -0
- package/dist/effect-id.js +16 -0
- package/dist/handler.d.ts +51 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +130 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/iterator.d.ts +32 -0
- package/dist/iterator.d.ts.map +1 -0
- package/dist/iterator.js +123 -0
- package/dist/option.d.ts +74 -0
- package/dist/option.d.ts.map +1 -0
- package/dist/option.js +141 -0
- package/dist/pipe.d.ts +12 -0
- package/dist/pipe.d.ts.map +1 -0
- package/dist/pipe.js +12 -0
- package/dist/race.d.ts +54 -0
- package/dist/race.d.ts.map +1 -0
- package/dist/race.js +116 -0
- package/dist/recursive.d.ts +40 -0
- package/dist/recursive.d.ts.map +1 -0
- package/dist/recursive.js +58 -0
- package/dist/result.d.ts +50 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +117 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +160 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +7 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +95 -0
- package/dist/schemas.d.ts +5 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +13 -0
- package/dist/try-catch.d.ts +24 -0
- package/dist/try-catch.d.ts.map +1 -0
- package/dist/try-catch.js +37 -0
- package/dist/values.d.ts +6 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +12 -0
- package/dist/worker.d.ts +15 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +58 -0
- package/package.json +42 -16
- package/src/all.ts +133 -0
- package/src/ast.ts +1301 -0
- package/src/bind.ts +162 -0
- package/src/builtins/array.ts +121 -0
- package/src/builtins/index.ts +17 -0
- package/src/builtins/scalar.ts +49 -0
- package/src/builtins/struct.ts +111 -0
- package/src/builtins/tagged-union.ts +142 -0
- package/src/builtins/with-resource.ts +69 -0
- package/src/chain.ts +17 -0
- package/src/effect-id.ts +30 -0
- package/src/handler.ts +263 -0
- package/src/index.ts +37 -0
- package/src/iterator.ts +243 -0
- package/src/option.ts +199 -0
- package/src/pipe.ts +138 -0
- package/src/race.ts +173 -0
- package/src/recursive.ts +129 -0
- package/src/result.ts +168 -0
- package/src/run.ts +209 -0
- package/src/runtime.ts +16 -0
- package/src/schema.ts +118 -0
- package/src/schemas.ts +21 -0
- package/src/try-catch.ts +57 -0
- package/src/values.ts +21 -0
- package/src/worker.ts +71 -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/result.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Option as OptionT,
|
|
3
|
+
type Pipeable,
|
|
4
|
+
type Result as ResultT,
|
|
5
|
+
type ResultDef,
|
|
6
|
+
type TypedAction,
|
|
7
|
+
branch,
|
|
8
|
+
} from "./ast.js";
|
|
9
|
+
import { chain } from "./chain.js";
|
|
10
|
+
import { constant, drop, identity, panic, tag } from "./builtins/index.js";
|
|
11
|
+
import { Option } from "./option.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Result namespace — combinators for Result<TValue, TError> tagged unions
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export const Result = {
|
|
18
|
+
/** Tag combinator: wrap value as `Result.Ok`. `TValue → Result<TValue, TError>` */
|
|
19
|
+
ok<TValue, TError = never>(): TypedAction<TValue, ResultT<TValue, TError>> {
|
|
20
|
+
return tag<"Result", ResultDef<TValue, TError>, "Ok">("Ok", "Result");
|
|
21
|
+
},
|
|
22
|
+
/** Tag combinator: wrap value as `Result.Err`. `TError → Result<TValue, TError>` */
|
|
23
|
+
err<TValue = never, TError = unknown>(): TypedAction<
|
|
24
|
+
TError,
|
|
25
|
+
ResultT<TValue, TError>
|
|
26
|
+
> {
|
|
27
|
+
return tag<"Result", ResultDef<TValue, TError>, "Err">("Err", "Result");
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/** Transform the Ok value. `Result<TValue, TError> → Result<TOut, TError>` */
|
|
31
|
+
map<TValue, TOut, TError>(
|
|
32
|
+
action: Pipeable<TValue, TOut>,
|
|
33
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>> {
|
|
34
|
+
return branch({
|
|
35
|
+
Ok: chain(action, Result.ok<TOut, TError>()),
|
|
36
|
+
Err: Result.err<TOut, TError>(),
|
|
37
|
+
}) as TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>>;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/** Transform the Err value. `Result<TValue, TError> → Result<TValue, TErrorOut>` */
|
|
41
|
+
mapErr<TValue, TError, TErrorOut>(
|
|
42
|
+
action: Pipeable<TError, TErrorOut>,
|
|
43
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TValue, TErrorOut>> {
|
|
44
|
+
return branch({
|
|
45
|
+
Ok: Result.ok<TValue, TErrorOut>(),
|
|
46
|
+
Err: chain(action, Result.err<TValue, TErrorOut>()),
|
|
47
|
+
}) as TypedAction<ResultT<TValue, TError>, ResultT<TValue, TErrorOut>>;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Monadic bind (flatMap) for Ok. If Ok, pass value to action which
|
|
52
|
+
* returns Result<TOut, TError>. If Err, propagate.
|
|
53
|
+
*/
|
|
54
|
+
andThen<TValue, TOut, TError>(
|
|
55
|
+
action: Pipeable<TValue, ResultT<TOut, TError>>,
|
|
56
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>> {
|
|
57
|
+
return branch({
|
|
58
|
+
Ok: action,
|
|
59
|
+
Err: Result.err<TOut, TError>(),
|
|
60
|
+
}) as TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>>;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/** Fallback on Err. If Ok, keep it. If Err, pass error to fallback. */
|
|
64
|
+
or<TValue, TError, TErrorOut>(
|
|
65
|
+
fallback: Pipeable<TError, ResultT<TValue, TErrorOut>>,
|
|
66
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TValue, TErrorOut>> {
|
|
67
|
+
return branch({
|
|
68
|
+
Ok: Result.ok<TValue, TErrorOut>(),
|
|
69
|
+
Err: fallback,
|
|
70
|
+
}) as TypedAction<ResultT<TValue, TError>, ResultT<TValue, TErrorOut>>;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract the Ok value or panic. `Result<TValue, TError> → TValue`
|
|
75
|
+
*
|
|
76
|
+
* Panics (fatal, not caught by tryCatch) if the value is Err.
|
|
77
|
+
*/
|
|
78
|
+
unwrap<TValue, TError>(): TypedAction<ResultT<TValue, TError>, TValue> {
|
|
79
|
+
return branch({
|
|
80
|
+
Ok: identity<TValue>(),
|
|
81
|
+
Err: panic("called unwrap on Err"),
|
|
82
|
+
}) as TypedAction<ResultT<TValue, TError>, TValue>;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract Ok or compute default from Err. `Result<TValue, TError> → TValue`
|
|
87
|
+
*/
|
|
88
|
+
unwrapOr<TValue, TError>(
|
|
89
|
+
defaultAction: Pipeable<TError, TValue>,
|
|
90
|
+
): TypedAction<ResultT<TValue, TError>, TValue> {
|
|
91
|
+
return branch({
|
|
92
|
+
Ok: identity<TValue>(),
|
|
93
|
+
Err: defaultAction,
|
|
94
|
+
}) as TypedAction<ResultT<TValue, TError>, TValue>;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Convert Ok to Some, Err to None. `Result<TValue, TError> → Option<TValue>`
|
|
99
|
+
*/
|
|
100
|
+
asOkOption<TValue, TError>(): TypedAction<
|
|
101
|
+
ResultT<TValue, TError>,
|
|
102
|
+
OptionT<TValue>
|
|
103
|
+
> {
|
|
104
|
+
return branch({
|
|
105
|
+
Ok: Option.some<TValue>(),
|
|
106
|
+
Err: chain(drop, Option.none<TValue>()),
|
|
107
|
+
}) as TypedAction<ResultT<TValue, TError>, OptionT<TValue>>;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert Err to Some, Ok to None. `Result<TValue, TError> → Option<TError>`
|
|
112
|
+
*/
|
|
113
|
+
asErrOption<TValue, TError>(): TypedAction<
|
|
114
|
+
ResultT<TValue, TError>,
|
|
115
|
+
OptionT<TError>
|
|
116
|
+
> {
|
|
117
|
+
return branch({
|
|
118
|
+
Ok: chain(drop, Option.none<TError>()),
|
|
119
|
+
Err: Option.some<TError>(),
|
|
120
|
+
}) as TypedAction<ResultT<TValue, TError>, OptionT<TError>>;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Swap Result/Option nesting.
|
|
125
|
+
* `Result<Option<TValue>, TError> → Option<Result<TValue, TError>>`
|
|
126
|
+
*/
|
|
127
|
+
transpose<TValue, TError>(): TypedAction<
|
|
128
|
+
ResultT<OptionT<TValue>, TError>,
|
|
129
|
+
OptionT<ResultT<TValue, TError>>
|
|
130
|
+
> {
|
|
131
|
+
return branch({
|
|
132
|
+
Ok: branch({
|
|
133
|
+
Some: chain(
|
|
134
|
+
Result.ok<TValue, TError>(),
|
|
135
|
+
Option.some<ResultT<TValue, TError>>(),
|
|
136
|
+
),
|
|
137
|
+
None: chain(drop, Option.none<ResultT<TValue, TError>>()),
|
|
138
|
+
}),
|
|
139
|
+
Err: chain(
|
|
140
|
+
Result.err<TValue, TError>(),
|
|
141
|
+
Option.some<ResultT<TValue, TError>>(),
|
|
142
|
+
),
|
|
143
|
+
}) as TypedAction<
|
|
144
|
+
ResultT<OptionT<TValue>, TError>,
|
|
145
|
+
OptionT<ResultT<TValue, TError>>
|
|
146
|
+
>;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Test if the value is Ok. `Result<TValue, TError> → boolean`
|
|
151
|
+
*/
|
|
152
|
+
isOk<TValue, TError>(): TypedAction<ResultT<TValue, TError>, boolean> {
|
|
153
|
+
return branch({
|
|
154
|
+
Ok: constant<boolean>(true),
|
|
155
|
+
Err: constant<boolean>(false),
|
|
156
|
+
}) as TypedAction<ResultT<TValue, TError>, boolean>;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Test if the value is Err. `Result<TValue, TError> → boolean`
|
|
161
|
+
*/
|
|
162
|
+
isErr<TValue, TError>(): TypedAction<ResultT<TValue, TError>, boolean> {
|
|
163
|
+
return branch({
|
|
164
|
+
Ok: constant<boolean>(false),
|
|
165
|
+
Err: constant<boolean>(true),
|
|
166
|
+
}) as TypedAction<ResultT<TValue, TError>, boolean>;
|
|
167
|
+
},
|
|
168
|
+
} as const;
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow execution: resolves the barnum binary, tsx executor, and worker
|
|
3
|
+
* script, then spawns the workflow as a subprocess.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFileSync, spawn as nodeSpawn } from "node:child_process";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
type Action,
|
|
13
|
+
type Config,
|
|
14
|
+
type ExtractOutput,
|
|
15
|
+
toAction,
|
|
16
|
+
} from "./ast.js";
|
|
17
|
+
import { chain } from "./chain.js";
|
|
18
|
+
import { constant } from "./builtins/index.js";
|
|
19
|
+
|
|
20
|
+
/** Log verbosity for the barnum engine runtime. Passed to the CLI's `--log-level`. */
|
|
21
|
+
export type LogLevel = "off" | "error" | "warn" | "info" | "debug" | "trace";
|
|
22
|
+
|
|
23
|
+
export interface RunPipelineOptions {
|
|
24
|
+
/** Engine log verbosity. Default: "off" (only handler stderr is visible). */
|
|
25
|
+
logLevel?: LogLevel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const __dirname = import.meta.dirname;
|
|
29
|
+
|
|
30
|
+
/** Resolve the TypeScript executor. Uses bun if the workflow was launched with bun, otherwise tsx. */
|
|
31
|
+
function resolveExecutor(): string {
|
|
32
|
+
if (process.versions.bun) {
|
|
33
|
+
return "bun";
|
|
34
|
+
}
|
|
35
|
+
const callerRequire = createRequire(process.argv[1] || import.meta.url);
|
|
36
|
+
const tsxPath = callerRequire.resolve("tsx/cli");
|
|
37
|
+
return `node ${tsxPath}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolve the platform-specific binary from the @barnum/barnum package artifacts. */
|
|
41
|
+
function resolveInstalledBinary(): string | undefined {
|
|
42
|
+
const platform = os.platform();
|
|
43
|
+
const arch = os.arch();
|
|
44
|
+
|
|
45
|
+
let artifactDir: string;
|
|
46
|
+
let binaryName = "barnum";
|
|
47
|
+
|
|
48
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
49
|
+
artifactDir = "macos-arm64";
|
|
50
|
+
} else if (platform === "darwin") {
|
|
51
|
+
artifactDir = "macos-x64";
|
|
52
|
+
} else if (platform === "linux" && arch === "arm64") {
|
|
53
|
+
artifactDir = "linux-arm64";
|
|
54
|
+
} else if (platform === "linux") {
|
|
55
|
+
artifactDir = "linux-x64";
|
|
56
|
+
} else if (platform === "win32") {
|
|
57
|
+
artifactDir = "win-x64";
|
|
58
|
+
binaryName = "barnum.exe";
|
|
59
|
+
} else {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const callerRequire = createRequire(process.argv[1] || import.meta.url);
|
|
64
|
+
try {
|
|
65
|
+
const packageDir = path.dirname(
|
|
66
|
+
callerRequire.resolve("@barnum/barnum/package.json"),
|
|
67
|
+
);
|
|
68
|
+
const binaryPath = path.join(
|
|
69
|
+
packageDir,
|
|
70
|
+
"artifacts",
|
|
71
|
+
artifactDir,
|
|
72
|
+
binaryName,
|
|
73
|
+
);
|
|
74
|
+
if (existsSync(binaryPath)) {
|
|
75
|
+
return binaryPath;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Package not installed
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type BinaryResolution =
|
|
84
|
+
| { kind: "Env"; path: string }
|
|
85
|
+
| { kind: "NodeModules"; path: string }
|
|
86
|
+
| { kind: "Local"; path: string };
|
|
87
|
+
|
|
88
|
+
/** Resolve the barnum binary. Checks: BARNUM env var, local repo, node_modules. */
|
|
89
|
+
function resolveBinary(): BinaryResolution {
|
|
90
|
+
if (process.env.BARNUM) {
|
|
91
|
+
return { kind: "Env", path: process.env.BARNUM };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const repoRoot = path.resolve(__dirname, "../../..");
|
|
95
|
+
if (existsSync(path.join(repoRoot, "Cargo.toml"))) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "Local",
|
|
98
|
+
path: path.join(repoRoot, "target/debug/barnum"),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const installedBinaryPath = resolveInstalledBinary();
|
|
103
|
+
if (installedBinaryPath) {
|
|
104
|
+
return { kind: "NodeModules", path: installedBinaryPath };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error(
|
|
108
|
+
"Could not find barnum binary. Set BARNUM env var or install @barnum/barnum.",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Resolve worker.ts relative to this package. */
|
|
113
|
+
function resolveWorker(): string {
|
|
114
|
+
return path.resolve(__dirname, "../src/worker.ts");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Build the barnum binary if using the local dev path. Skips if binary already exists. */
|
|
118
|
+
function buildBinaryIfNeeded(binaryPath: string): void {
|
|
119
|
+
if (existsSync(binaryPath)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const repoRoot = path.resolve(__dirname, "../../..");
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.error("[barnum] building CLI binary (cargo build -p barnum_cli)...");
|
|
125
|
+
execFileSync("cargo", ["build", "-p", "barnum_cli"], {
|
|
126
|
+
cwd: repoRoot,
|
|
127
|
+
stdio: "inherit",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Run a pipeline to completion. Returns the workflow's final output value. */
|
|
132
|
+
export function runPipeline<TPipeline extends Action>(
|
|
133
|
+
pipeline: TPipeline,
|
|
134
|
+
input?: unknown,
|
|
135
|
+
options?: RunPipelineOptions,
|
|
136
|
+
): Promise<ExtractOutput<TPipeline>> {
|
|
137
|
+
const workflow =
|
|
138
|
+
input === undefined
|
|
139
|
+
? pipeline
|
|
140
|
+
: toAction(chain(toAction(constant(input)), toAction(pipeline)));
|
|
141
|
+
return spawnBarnum({ workflow }, options?.logLevel);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Spawn the barnum CLI with the given config. Returns the parsed final value from stdout. */
|
|
145
|
+
function spawnBarnum<TOut>(config: Config, logLevel?: LogLevel): Promise<TOut> {
|
|
146
|
+
const binaryResolution = resolveBinary();
|
|
147
|
+
if (binaryResolution.kind === "Local") {
|
|
148
|
+
buildBinaryIfNeeded(binaryResolution.path);
|
|
149
|
+
}
|
|
150
|
+
const executor = resolveExecutor();
|
|
151
|
+
const worker = resolveWorker();
|
|
152
|
+
const configJson = JSON.stringify(config);
|
|
153
|
+
|
|
154
|
+
const cliArgs = [
|
|
155
|
+
"run",
|
|
156
|
+
"--config",
|
|
157
|
+
configJson,
|
|
158
|
+
"--executor",
|
|
159
|
+
executor,
|
|
160
|
+
"--worker",
|
|
161
|
+
worker,
|
|
162
|
+
];
|
|
163
|
+
if (logLevel) {
|
|
164
|
+
cliArgs.push("--log-level", logLevel);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new Promise<TOut>((resolve, reject) => {
|
|
168
|
+
const child = nodeSpawn(binaryResolution.path, cliArgs, {
|
|
169
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const stdoutChunks: Buffer[] = [];
|
|
173
|
+
const stderrChunks: Buffer[] = [];
|
|
174
|
+
|
|
175
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
176
|
+
stdoutChunks.push(chunk);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
180
|
+
stderrChunks.push(chunk);
|
|
181
|
+
process.stderr.write(chunk);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
child.on("error", (error) => {
|
|
185
|
+
reject(new Error(`Failed to spawn barnum: ${error.message}`));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
child.on("close", (code) => {
|
|
189
|
+
if (code !== 0) {
|
|
190
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
191
|
+
const detail = stderr ? `\n${stderr}` : "";
|
|
192
|
+
reject(new Error(`barnum exited with code ${code}${detail}`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
196
|
+
if (!stdout) {
|
|
197
|
+
resolve(undefined as TOut);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
resolve(JSON.parse(stdout) as TOut);
|
|
202
|
+
} catch {
|
|
203
|
+
reject(
|
|
204
|
+
new Error(`barnum produced non-JSON output on stdout: ${stdout}`),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Runtime value constructors
|
|
2
|
+
export { ok, err, some, none } from "./values.js";
|
|
3
|
+
|
|
4
|
+
// Handler creation
|
|
5
|
+
export {
|
|
6
|
+
createHandler,
|
|
7
|
+
createHandlerWithConfig,
|
|
8
|
+
type Handler,
|
|
9
|
+
} from "./handler.js";
|
|
10
|
+
|
|
11
|
+
// Schema builders
|
|
12
|
+
export { resultSchema, optionSchema } from "./schemas.js";
|
|
13
|
+
export { taggedUnionSchema } from "./builtins/index.js";
|
|
14
|
+
|
|
15
|
+
// Types only
|
|
16
|
+
export type { Result, Option, TaggedUnion } from "./ast.js";
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { type z, toJSONSchema } from "zod";
|
|
3
|
+
|
|
4
|
+
// Zod v4 schema def types that have child schemas.
|
|
5
|
+
// Verified against Zod 4.3.6 internals — every compound type's def
|
|
6
|
+
// shape and child property name is listed here.
|
|
7
|
+
const CHILD_ACCESSORS: Record<string, (def: any) => z.ZodType[]> = {
|
|
8
|
+
object: (def) => Object.values(def.shape),
|
|
9
|
+
array: (def) => [def.element],
|
|
10
|
+
tuple: (def) => [...def.items, ...(def.rest ? [def.rest] : [])],
|
|
11
|
+
union: (def) => def.options,
|
|
12
|
+
intersection: (def) => [def.left, def.right],
|
|
13
|
+
record: (def) => [def.keyType, def.valueType],
|
|
14
|
+
// Wrappers with a single inner type
|
|
15
|
+
nullable: (def) => [def.innerType],
|
|
16
|
+
optional: (def) => [def.innerType],
|
|
17
|
+
nonoptional: (def) => [def.innerType],
|
|
18
|
+
default: (def) => [def.innerType],
|
|
19
|
+
catch: (def) => [def.innerType],
|
|
20
|
+
readonly: (def) => [def.innerType],
|
|
21
|
+
promise: (def) => [def.innerType],
|
|
22
|
+
// Pipe has two children
|
|
23
|
+
pipe: (def) => [def.in, def.out],
|
|
24
|
+
// Lazy resolves to inner schema
|
|
25
|
+
lazy: (def) => [def.getter()],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Walk the Zod schema tree and reject patterns that `toJSONSchema()`
|
|
30
|
+
* handles incorrectly or silently drops:
|
|
31
|
+
*
|
|
32
|
+
* - `z.intersection()` — produces `allOf` with `additionalProperties: false`
|
|
33
|
+
* on both sides (from `io: "output"`), making the intersection unmatchable
|
|
34
|
+
* on Draft 7.
|
|
35
|
+
*
|
|
36
|
+
* - `.refine()` / `.superRefine()` — silently stripped from JSON Schema
|
|
37
|
+
* output, so the Rust side would accept values that fail the refinement.
|
|
38
|
+
* Detected by checking for custom checks (`check._zod.def.check === "custom"`)
|
|
39
|
+
* in the schema's checks array.
|
|
40
|
+
*/
|
|
41
|
+
function assertNoUnsupportedPatterns(
|
|
42
|
+
schema: z.ZodType,
|
|
43
|
+
label: string,
|
|
44
|
+
visited = new WeakSet<z.ZodType>(),
|
|
45
|
+
): void {
|
|
46
|
+
if (visited.has(schema)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
visited.add(schema);
|
|
50
|
+
|
|
51
|
+
const def = (schema as any)._zod.def;
|
|
52
|
+
|
|
53
|
+
// Reject intersections
|
|
54
|
+
if (def.type === "intersection") {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Handler "${label}": z.intersection() is not supported. ` +
|
|
57
|
+
`It produces broken JSON Schema on Draft 7 because both sides ` +
|
|
58
|
+
`get additionalProperties: false. Use z.object().extend() or ` +
|
|
59
|
+
`z.object().merge() instead.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reject custom checks (from .refine() and .superRefine())
|
|
64
|
+
const checks: any[] | undefined = def.checks;
|
|
65
|
+
if (checks) {
|
|
66
|
+
for (const check of checks) {
|
|
67
|
+
if (check._zod.def.check === "custom") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Handler "${label}": .refine() and .superRefine() are not ` +
|
|
70
|
+
`supported. Custom validations cannot be expressed in JSON ` +
|
|
71
|
+
`Schema and would be silently dropped.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Recurse into children
|
|
78
|
+
const getChildren = CHILD_ACCESSORS[def.type];
|
|
79
|
+
if (getChildren) {
|
|
80
|
+
for (const child of getChildren(def)) {
|
|
81
|
+
assertNoUnsupportedPatterns(child, label, visited);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert a Zod schema to a JSON Schema document suitable for embedding
|
|
88
|
+
* in the serialized AST. Throws if the schema contains types that can't
|
|
89
|
+
* survive the TS → JSON → Rust boundary.
|
|
90
|
+
*/
|
|
91
|
+
export function zodToCheckedJsonSchema(
|
|
92
|
+
schema: z.ZodType,
|
|
93
|
+
label: string,
|
|
94
|
+
): JSONSchema7 {
|
|
95
|
+
// Pre-validate: catch patterns that toJSONSchema() handles incorrectly
|
|
96
|
+
assertNoUnsupportedPatterns(schema, label);
|
|
97
|
+
|
|
98
|
+
let raw: Record<string, unknown>;
|
|
99
|
+
try {
|
|
100
|
+
raw = toJSONSchema(schema, {
|
|
101
|
+
target: "draft-07",
|
|
102
|
+
unrepresentable: "throw",
|
|
103
|
+
io: "output",
|
|
104
|
+
cycles: "throw",
|
|
105
|
+
reused: "inline",
|
|
106
|
+
}) as Record<string, unknown>;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Handler "${label}": Zod schema cannot be converted to JSON Schema: ${message}`,
|
|
111
|
+
{ cause: error },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Strip $schema — embedded schemas don't need the draft URI.
|
|
116
|
+
const { $schema: _, ...rest } = raw;
|
|
117
|
+
return rest as JSONSchema7;
|
|
118
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Result, Option } from "./ast.js";
|
|
3
|
+
|
|
4
|
+
export function resultSchema<TValue, TError>(
|
|
5
|
+
okSchema: z.ZodType<TValue>,
|
|
6
|
+
errSchema: z.ZodType<TError>,
|
|
7
|
+
): z.ZodType<Result<TValue, TError>> {
|
|
8
|
+
return z.discriminatedUnion("kind", [
|
|
9
|
+
z.object({ kind: z.literal("Result.Ok"), value: okSchema }),
|
|
10
|
+
z.object({ kind: z.literal("Result.Err"), value: errSchema }),
|
|
11
|
+
]) as z.ZodType<Result<TValue, TError>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function optionSchema<TValue>(
|
|
15
|
+
valueSchema: z.ZodType<TValue>,
|
|
16
|
+
): z.ZodType<Option<TValue>> {
|
|
17
|
+
return z.discriminatedUnion("kind", [
|
|
18
|
+
z.object({ kind: z.literal("Option.Some"), value: valueSchema }),
|
|
19
|
+
z.object({ kind: z.literal("Option.None"), value: z.null() }),
|
|
20
|
+
]) as z.ZodType<Option<TValue>>;
|
|
21
|
+
}
|
package/src/try-catch.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Pipeable,
|
|
3
|
+
type TypedAction,
|
|
4
|
+
toAction,
|
|
5
|
+
typedAction,
|
|
6
|
+
buildRestartBranchAction,
|
|
7
|
+
} from "./ast.js";
|
|
8
|
+
import { chain } from "./chain.js";
|
|
9
|
+
import { tag } from "./builtins/index.js";
|
|
10
|
+
import { allocateRestartHandlerId } from "./effect-id.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// tryCatch — type-level error handling via restart+Branch
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* HOAS combinator for type-level error handling. The body callback receives
|
|
18
|
+
* a `throwError` token — a `TypedAction<TError, never>` that, when placed
|
|
19
|
+
* in the pipeline, tags the error as Break, performs to the handler, which
|
|
20
|
+
* restarts the body. The body-level Branch routes to the recovery arm.
|
|
21
|
+
*
|
|
22
|
+
* This handles **type-level errors only** — values returned by handlers via
|
|
23
|
+
* the `Result` type. If a handler panics, throws a JavaScript exception, or
|
|
24
|
+
* the runtime crashes, the existing error propagation path handles it.
|
|
25
|
+
* tryCatch does not catch those. Analogous to Rust's `Result` vs `panic!`.
|
|
26
|
+
*
|
|
27
|
+
* Compiled form (restart+Branch, same substrate as loop/earlyReturn):
|
|
28
|
+
* `Chain(Tag("Continue"),`
|
|
29
|
+
* `RestartHandle(id, GetIndex(0),`
|
|
30
|
+
* `Branch({ Continue: body, Break: recovery })))`
|
|
31
|
+
*
|
|
32
|
+
* throwError = `Chain(Tag("Break"), RestartPerform(id))`
|
|
33
|
+
*
|
|
34
|
+
* When throwError fires: error tagged Break → `RestartPerform` → handler extracts
|
|
35
|
+
* payload → body restarts → Branch takes Break arm → recovery receives error.
|
|
36
|
+
*/
|
|
37
|
+
export function tryCatch<TIn, TOut, TError>(
|
|
38
|
+
body: (throwError: TypedAction<TError, never>) => Pipeable<TIn, TOut>,
|
|
39
|
+
recovery: Pipeable<TError, TOut>,
|
|
40
|
+
): TypedAction<TIn, TOut> {
|
|
41
|
+
const restartHandlerId = allocateRestartHandlerId();
|
|
42
|
+
|
|
43
|
+
const throwError = typedAction<TError, never>(
|
|
44
|
+
toAction(
|
|
45
|
+
chain(toAction(tag("Break", "LoopResult")), {
|
|
46
|
+
kind: "RestartPerform",
|
|
47
|
+
restart_handler_id: restartHandlerId,
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const bodyAction = toAction(body(throwError));
|
|
53
|
+
|
|
54
|
+
return typedAction(
|
|
55
|
+
buildRestartBranchAction(restartHandlerId, bodyAction, toAction(recovery)),
|
|
56
|
+
);
|
|
57
|
+
}
|
package/src/values.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Result, Option } from "./ast.js";
|
|
2
|
+
|
|
3
|
+
export function ok<TValue, TError = unknown>(
|
|
4
|
+
value: TValue,
|
|
5
|
+
): Result<TValue, TError> {
|
|
6
|
+
return { kind: "Result.Ok", value } as Result<TValue, TError>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function err<TValue = unknown, TError = never>(
|
|
10
|
+
error: TError,
|
|
11
|
+
): Result<TValue, TError> {
|
|
12
|
+
return { kind: "Result.Err", value: error } as Result<TValue, TError>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function some<T>(value: T): Option<T> {
|
|
16
|
+
return { kind: "Option.Some", value } as Option<T>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function none<T = unknown>(): Option<T> {
|
|
20
|
+
return { kind: "Option.None", value: null } as Option<T>;
|
|
21
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker script: invoked as `tsx worker.ts <module> <export>`.
|
|
3
|
+
*
|
|
4
|
+
* Protocol:
|
|
5
|
+
* Rust → stdin: JSON `{ "value": <any> }`
|
|
6
|
+
* stdout → Rust: JSON result (handler return value)
|
|
7
|
+
*
|
|
8
|
+
* stdout is reserved for the protocol. All console output is redirected to
|
|
9
|
+
* stderr so that handler code can freely use console.log for debugging.
|
|
10
|
+
*
|
|
11
|
+
* If the handler throws or the module/export can't be resolved, the
|
|
12
|
+
* process exits non-zero. Rust interprets that as a fatal workflow error.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Redirect all console output to stderr — stdout is the protocol channel.
|
|
16
|
+
// This must happen before any handler code is imported or executed.
|
|
17
|
+
import { Console } from "node:console";
|
|
18
|
+
|
|
19
|
+
const stderrConsole = new Console({ stdout: process.stderr });
|
|
20
|
+
globalThis.console = stderrConsole;
|
|
21
|
+
|
|
22
|
+
// Suppress EPIPE — when the Rust binary exits (e.g., a race was resolved),
|
|
23
|
+
// orphan workers get broken pipe on stdout. This is expected, not an error.
|
|
24
|
+
process.stdout.on("error", (error: NodeJS.ErrnoException) => {
|
|
25
|
+
if (error.code === "EPIPE") {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function main(): Promise<void> {
|
|
32
|
+
const [modulePath, exportName = "default"] = process.argv.slice(2);
|
|
33
|
+
|
|
34
|
+
if (!modulePath) {
|
|
35
|
+
process.stderr.write("worker: missing module path argument\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Read entire stdin
|
|
40
|
+
const chunks: Buffer[] = [];
|
|
41
|
+
for await (const chunk of process.stdin) {
|
|
42
|
+
chunks.push(chunk);
|
|
43
|
+
}
|
|
44
|
+
const input = JSON.parse(Buffer.concat(chunks).toString());
|
|
45
|
+
|
|
46
|
+
// Import handler, call it
|
|
47
|
+
const mod = await import(modulePath);
|
|
48
|
+
const handler = mod[exportName];
|
|
49
|
+
|
|
50
|
+
if (!handler?.__definition?.handle) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`worker: ${modulePath}:${exportName} is not a barnum handler\n`,
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = await handler.__definition.handle({ value: input.value });
|
|
58
|
+
|
|
59
|
+
// Write result to stdout, then exit. Explicit exit is required because
|
|
60
|
+
// importing the handler module may leave open handles (timers, servers,
|
|
61
|
+
// etc.) that keep the Node event loop alive indefinitely.
|
|
62
|
+
const json = JSON.stringify(result) ?? "null";
|
|
63
|
+
process.stdout.write(json, () => {
|
|
64
|
+
process.exit(0);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch((error) => {
|
|
69
|
+
process.stderr.write(`worker: ${error}\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|