@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/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
|
+
}
|
package/src/effect-id.ts
ADDED
|
@@ -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
|
+
}
|