@barnum/barnum 0.0.0-main-ef6df91f → 0.0.0-main-e8b82cff

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/src/handler.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import type { z } from "zod";
3
3
  import { type TypedAction, typedAction } from "./ast.js";
4
+ import { zodToCheckedJsonSchema } from "./schema.js";
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // HandlerDefinition — the user's handle function + optional validators
@@ -12,6 +13,7 @@ export interface HandlerDefinition<
12
13
  TStepConfig = unknown,
13
14
  > {
14
15
  inputValidator?: z.ZodType<TValue>;
16
+ outputValidator?: z.ZodType<TOutput>;
15
17
  stepConfigValidator?: z.ZodType<TStepConfig>;
16
18
  handle: (context: {
17
19
  value: TValue;
@@ -22,6 +24,7 @@ export interface HandlerDefinition<
22
24
  /** Runtime-only handler definition shape — erases generic type info. */
23
25
  interface UntypedHandlerDefinition {
24
26
  inputValidator?: z.ZodType;
27
+ outputValidator?: z.ZodType;
25
28
  stepConfigValidator?: z.ZodType;
26
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
30
  handle: (...args: any[]) => Promise<unknown>;
@@ -37,7 +40,10 @@ const HANDLER_BRAND = Symbol.for("barnum:handler");
37
40
  * Opaque handler reference with typed metadata. The `__definition` property
38
41
  * is non-enumerable — invisible to `JSON.stringify`, visible to the worker.
39
42
  */
40
- export type Handler<TValue = unknown, TOutput = unknown> = TypedAction<TValue, TOutput> & {
43
+ export type Handler<TValue = unknown, TOutput = unknown> = TypedAction<
44
+ TValue,
45
+ TOutput
46
+ > & {
41
47
  readonly [HANDLER_BRAND]: true;
42
48
  readonly __definition: UntypedHandlerDefinition;
43
49
  };
@@ -88,26 +94,18 @@ function getCallerFilePath(): string {
88
94
  type HandlerOutput<TOutput> = [TOutput] extends [void] ? never : TOutput;
89
95
 
90
96
  // ---------------------------------------------------------------------------
91
- // createHandler — handlers with no config, returns TypedAction directly
97
+ // createHandler — single overload, validators optional
92
98
  // ---------------------------------------------------------------------------
93
99
 
94
- // With inputValidator: handler accepts typed pipeline input.
95
- export function createHandler<TValue, TOutput>(
100
+ export function createHandler<TValue = never, TOutput = unknown>(
96
101
  definition: {
97
- inputValidator: z.ZodType<TValue>;
102
+ inputValidator?: z.ZodType<TValue>;
103
+ outputValidator?: z.ZodType<NoInfer<TOutput>>;
98
104
  handle: (context: { value: TValue }) => Promise<TOutput>;
99
105
  },
100
106
  exportName?: string,
101
107
  ): Handler<TValue, HandlerOutput<TOutput>>;
102
108
 
103
- // Without inputValidator: handler takes no pipeline input.
104
- export function createHandler<TOutput>(
105
- definition: {
106
- handle: () => Promise<TOutput>;
107
- },
108
- exportName?: string,
109
- ): Handler<never, HandlerOutput<TOutput>>;
110
-
111
109
  // Implementation
112
110
  export function createHandler(
113
111
  definition: UntypedHandlerDefinition,
@@ -117,9 +115,28 @@ export function createHandler(
117
115
  const filePath = getCallerFilePath();
118
116
  const funcName = exportName ?? "default";
119
117
 
118
+ const inputSchema = definition.inputValidator
119
+ ? zodToCheckedJsonSchema(
120
+ definition.inputValidator,
121
+ `${filePath}:${funcName} input`,
122
+ )
123
+ : undefined;
124
+ const outputSchema = definition.outputValidator
125
+ ? zodToCheckedJsonSchema(
126
+ definition.outputValidator,
127
+ `${filePath}:${funcName} output`,
128
+ )
129
+ : undefined;
130
+
120
131
  const action = typedAction({
121
132
  kind: "Invoke",
122
- handler: { kind: "TypeScript", module: filePath, func: funcName },
133
+ handler: {
134
+ kind: "TypeScript",
135
+ module: filePath,
136
+ func: funcName,
137
+ ...(inputSchema && { input_schema: inputSchema }),
138
+ ...(outputSchema && { output_schema: outputSchema }),
139
+ },
123
140
  });
124
141
 
125
142
  // Non-enumerable: invisible to JSON.stringify, visible to the worker
@@ -136,28 +153,26 @@ export function createHandler(
136
153
  }
137
154
 
138
155
  // ---------------------------------------------------------------------------
139
- // createHandlerWithConfig — handlers that need static config
156
+ // createHandlerWithConfig — single overload, validators optional
140
157
  // ---------------------------------------------------------------------------
141
158
 
142
- // With inputValidator: handler accepts typed pipeline input + config.
143
- export function createHandlerWithConfig<TValue, TOutput, TStepConfig>(
159
+ export function createHandlerWithConfig<
160
+ TValue = never,
161
+ TOutput = unknown,
162
+ TStepConfig = unknown,
163
+ >(
144
164
  definition: {
145
- inputValidator: z.ZodType<TValue>;
146
- stepConfigValidator: z.ZodType<TStepConfig>;
147
- handle: (context: { value: TValue; stepConfig: TStepConfig }) => Promise<TOutput>;
165
+ inputValidator?: z.ZodType<TValue>;
166
+ outputValidator?: z.ZodType<NoInfer<TOutput>>;
167
+ stepConfigValidator?: z.ZodType<TStepConfig>;
168
+ handle: (context: {
169
+ value: TValue;
170
+ stepConfig: TStepConfig;
171
+ }) => Promise<TOutput>;
148
172
  },
149
173
  exportName?: string,
150
174
  ): (config: TStepConfig) => TypedAction<TValue, HandlerOutput<TOutput>>;
151
175
 
152
- // Without inputValidator: handler takes no pipeline input, has config.
153
- export function createHandlerWithConfig<TOutput, TStepConfig>(
154
- definition: {
155
- stepConfigValidator: z.ZodType<TStepConfig>;
156
- handle: (context: { stepConfig: TStepConfig }) => Promise<TOutput>;
157
- },
158
- exportName?: string,
159
- ): (config: TStepConfig) => TypedAction<never, HandlerOutput<TOutput>>;
160
-
161
176
  // Implementation
162
177
  export function createHandlerWithConfig(
163
178
  definition: UntypedHandlerDefinition,
@@ -167,6 +182,19 @@ export function createHandlerWithConfig(
167
182
  const filePath = getCallerFilePath();
168
183
  const funcName = exportName ?? "default";
169
184
 
185
+ const inputSchema = definition.inputValidator
186
+ ? zodToCheckedJsonSchema(
187
+ definition.inputValidator,
188
+ `${filePath}:${funcName} input`,
189
+ )
190
+ : undefined;
191
+ const outputSchema = definition.outputValidator
192
+ ? zodToCheckedJsonSchema(
193
+ definition.outputValidator,
194
+ `${filePath}:${funcName} output`,
195
+ )
196
+ : undefined;
197
+
170
198
  // Internal handle that unpacks the [value, config] tuple from All
171
199
  const internalDefinition: UntypedHandlerDefinition = {
172
200
  handle: ({ value }: { value: unknown }) => {
@@ -177,7 +205,13 @@ export function createHandlerWithConfig(
177
205
 
178
206
  const invokeAction = typedAction({
179
207
  kind: "Invoke",
180
- handler: { kind: "TypeScript", module: filePath, func: funcName },
208
+ handler: {
209
+ kind: "TypeScript",
210
+ module: filePath,
211
+ func: funcName,
212
+ ...(inputSchema && { input_schema: inputSchema }),
213
+ ...(outputSchema && { output_schema: outputSchema }),
214
+ },
181
215
  });
182
216
 
183
217
  // Non-enumerable: invisible to JSON.stringify, visible to the worker
@@ -199,8 +233,17 @@ export function createHandlerWithConfig(
199
233
  first: {
200
234
  kind: "All",
201
235
  actions: [
202
- { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Identity" } } },
203
- { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Constant", value: config } } },
236
+ {
237
+ kind: "Invoke",
238
+ handler: { kind: "Builtin", builtin: { kind: "Identity" } },
239
+ },
240
+ {
241
+ kind: "Invoke",
242
+ handler: {
243
+ kind: "Builtin",
244
+ builtin: { kind: "Constant", value: config },
245
+ },
246
+ },
204
247
  ],
205
248
  },
206
249
  rest: invokeAction,
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { TaggedUnion, OptionDef, ResultDef } from "./ast.js";
2
+
1
3
  export * from "./ast.js";
2
4
  export {
3
5
  constant,
@@ -17,6 +19,12 @@ export {
17
19
  Option,
18
20
  Result,
19
21
  } from "./builtins.js";
20
- export { createHandler, createHandlerWithConfig } from "./handler.js";
21
- export type { HandlerDefinition, Handler } from "./handler.js";
22
- export { run } from "./run.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 CHANGED
@@ -1,4 +1,10 @@
1
- import { type Action, type PipeIn, type Pipeable, type TypedAction, typedAction } from "./ast.js";
1
+ import {
2
+ type Action,
3
+ type PipeIn,
4
+ type Pipeable,
5
+ type TypedAction,
6
+ typedAction,
7
+ } from "./ast.js";
2
8
  import { identity } from "./builtins.js";
3
9
 
4
10
  export function pipe<T1, T2, R1 extends string>(
@@ -9,16 +15,28 @@ export function pipe<T1, T2, T3, R1 extends string, R2 extends string>(
9
15
  a2: Pipeable<T2, T3, R2>,
10
16
  ): TypedAction<PipeIn<T1>, T3, R1 | R2>;
11
17
  export function pipe<
12
- T1, T2, T3, T4,
13
- R1 extends string, R2 extends string, R3 extends string,
18
+ T1,
19
+ T2,
20
+ T3,
21
+ T4,
22
+ R1 extends string,
23
+ R2 extends string,
24
+ R3 extends string,
14
25
  >(
15
26
  a1: Pipeable<T1, T2, R1>,
16
27
  a2: Pipeable<T2, T3, R2>,
17
28
  a3: Pipeable<T3, T4, R3>,
18
29
  ): TypedAction<PipeIn<T1>, T4, R1 | R2 | R3>;
19
30
  export function pipe<
20
- T1, T2, T3, T4, T5,
21
- R1 extends string, R2 extends string, R3 extends string, R4 extends string,
31
+ T1,
32
+ T2,
33
+ T3,
34
+ T4,
35
+ T5,
36
+ R1 extends string,
37
+ R2 extends string,
38
+ R3 extends string,
39
+ R4 extends string,
22
40
  >(
23
41
  a1: Pipeable<T1, T2, R1>,
24
42
  a2: Pipeable<T2, T3, R2>,
@@ -26,9 +44,17 @@ export function pipe<
26
44
  a4: Pipeable<T4, T5, R4>,
27
45
  ): TypedAction<PipeIn<T1>, T5, R1 | R2 | R3 | R4>;
28
46
  export function pipe<
29
- T1, T2, T3, T4, T5, T6,
30
- R1 extends string, R2 extends string, R3 extends string,
31
- R4 extends string, R5 extends string,
47
+ T1,
48
+ T2,
49
+ T3,
50
+ T4,
51
+ T5,
52
+ T6,
53
+ R1 extends string,
54
+ R2 extends string,
55
+ R3 extends string,
56
+ R4 extends string,
57
+ R5 extends string,
32
58
  >(
33
59
  a1: Pipeable<T1, T2, R1>,
34
60
  a2: Pipeable<T2, T3, R2>,
@@ -37,9 +63,19 @@ export function pipe<
37
63
  a5: Pipeable<T5, T6, R5>,
38
64
  ): TypedAction<PipeIn<T1>, T6, R1 | R2 | R3 | R4 | R5>;
39
65
  export function pipe<
40
- T1, T2, T3, T4, T5, T6, T7,
41
- R1 extends string, R2 extends string, R3 extends string,
42
- R4 extends string, R5 extends string, R6 extends string,
66
+ T1,
67
+ T2,
68
+ T3,
69
+ T4,
70
+ T5,
71
+ T6,
72
+ T7,
73
+ R1 extends string,
74
+ R2 extends string,
75
+ R3 extends string,
76
+ R4 extends string,
77
+ R5 extends string,
78
+ R6 extends string,
43
79
  >(
44
80
  a1: Pipeable<T1, T2, R1>,
45
81
  a2: Pipeable<T2, T3, R2>,
@@ -49,9 +85,20 @@ export function pipe<
49
85
  a6: Pipeable<T6, T7, R6>,
50
86
  ): TypedAction<PipeIn<T1>, T7, R1 | R2 | R3 | R4 | R5 | R6>;
51
87
  export function pipe<
52
- T1, T2, T3, T4, T5, T6, T7, T8,
53
- R1 extends string, R2 extends string, R3 extends string,
54
- R4 extends string, R5 extends string, R6 extends string,
88
+ T1,
89
+ T2,
90
+ T3,
91
+ T4,
92
+ T5,
93
+ T6,
94
+ T7,
95
+ T8,
96
+ R1 extends string,
97
+ R2 extends string,
98
+ R3 extends string,
99
+ R4 extends string,
100
+ R5 extends string,
101
+ R6 extends string,
55
102
  R7 extends string,
56
103
  >(
57
104
  a1: Pipeable<T1, T2, R1>,
@@ -63,10 +110,23 @@ export function pipe<
63
110
  a7: Pipeable<T7, T8, R7>,
64
111
  ): TypedAction<PipeIn<T1>, T8, R1 | R2 | R3 | R4 | R5 | R6 | R7>;
65
112
  export function pipe<
66
- T1, T2, T3, T4, T5, T6, T7, T8, T9,
67
- R1 extends string, R2 extends string, R3 extends string,
68
- R4 extends string, R5 extends string, R6 extends string,
69
- R7 extends string, R8 extends string,
113
+ T1,
114
+ T2,
115
+ T3,
116
+ T4,
117
+ T5,
118
+ T6,
119
+ T7,
120
+ T8,
121
+ T9,
122
+ R1 extends string,
123
+ R2 extends string,
124
+ R3 extends string,
125
+ R4 extends string,
126
+ R5 extends string,
127
+ R6 extends string,
128
+ R7 extends string,
129
+ R8 extends string,
70
130
  >(
71
131
  a1: Pipeable<T1, T2, R1>,
72
132
  a2: Pipeable<T2, T3, R2>,
@@ -78,10 +138,25 @@ export function pipe<
78
138
  a8: Pipeable<T8, T9, R8>,
79
139
  ): TypedAction<PipeIn<T1>, T9, R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8>;
80
140
  export function pipe<
81
- T1, T2, T3, T4, T5, T6, T7, T8, T9, T10,
82
- R1 extends string, R2 extends string, R3 extends string,
83
- R4 extends string, R5 extends string, R6 extends string,
84
- R7 extends string, R8 extends string, R9 extends string,
141
+ T1,
142
+ T2,
143
+ T3,
144
+ T4,
145
+ T5,
146
+ T6,
147
+ T7,
148
+ T8,
149
+ T9,
150
+ T10,
151
+ R1 extends string,
152
+ R2 extends string,
153
+ R3 extends string,
154
+ R4 extends string,
155
+ R5 extends string,
156
+ R6 extends string,
157
+ R7 extends string,
158
+ R8 extends string,
159
+ R9 extends string,
85
160
  >(
86
161
  a1: Pipeable<T1, T2, R1>,
87
162
  a2: Pipeable<T2, T3, R2>,
@@ -94,10 +169,26 @@ export function pipe<
94
169
  a9: Pipeable<T9, T10, R9>,
95
170
  ): TypedAction<PipeIn<T1>, T10, R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9>;
96
171
  export function pipe<
97
- T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11,
98
- R1 extends string, R2 extends string, R3 extends string,
99
- R4 extends string, R5 extends string, R6 extends string,
100
- R7 extends string, R8 extends string, R9 extends string,
172
+ T1,
173
+ T2,
174
+ T3,
175
+ T4,
176
+ T5,
177
+ T6,
178
+ T7,
179
+ T8,
180
+ T9,
181
+ T10,
182
+ T11,
183
+ R1 extends string,
184
+ R2 extends string,
185
+ R3 extends string,
186
+ R4 extends string,
187
+ R5 extends string,
188
+ R6 extends string,
189
+ R7 extends string,
190
+ R8 extends string,
191
+ R9 extends string,
101
192
  R10 extends string,
102
193
  >(
103
194
  a1: Pipeable<T1, T2, R1>,
@@ -110,7 +201,11 @@ export function pipe<
110
201
  a8: Pipeable<T8, T9, R8>,
111
202
  a9: Pipeable<T9, T10, R9>,
112
203
  a10: Pipeable<T10, T11, R10>,
113
- ): TypedAction<PipeIn<T1>, T11, R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10>;
204
+ ): TypedAction<
205
+ PipeIn<T1>,
206
+ T11,
207
+ R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10
208
+ >;
114
209
  export function pipe(...actions: Action[]): Action {
115
210
  if (actions.length === 0) {
116
211
  return identity;
@@ -118,7 +213,7 @@ export function pipe(...actions: Action[]): Action {
118
213
  if (actions.length === 1) {
119
214
  return actions[0];
120
215
  }
121
- return actions.reduceRight((rest, first) =>
122
- typedAction({ kind: "Chain", first, rest }) as Action,
216
+ return actions.reduceRight(
217
+ (rest, first) => typedAction({ kind: "Chain", first, rest }) as Action,
123
218
  );
124
219
  }
package/src/race.ts CHANGED
@@ -1,20 +1,22 @@
1
- import { type Action, type Pipeable, type Result, type TypedAction, typedAction } from "./ast.js";
2
- import { allocateEffectId } from "./effect-id.js";
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";
3
15
 
4
16
  // ---------------------------------------------------------------------------
5
17
  // Shared AST fragments
6
18
  // ---------------------------------------------------------------------------
7
19
 
8
- const EXTRACT_PAYLOAD: Action = {
9
- kind: "Invoke",
10
- handler: { kind: "Builtin", builtin: { kind: "ExtractField", value: "payload" } },
11
- };
12
-
13
- const TAG_DISCARD: Action = {
14
- kind: "Invoke",
15
- handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Discard" } },
16
- };
17
-
18
20
  const TAG_OK: Action = {
19
21
  kind: "Invoke",
20
22
  handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
@@ -25,12 +27,18 @@ const TAG_ERR: Action = {
25
27
  handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Err" } },
26
28
  };
27
29
 
28
- /** Handler DAG shared by race and withTimeout: extract payload, tag Discard. */
29
- const RACE_HANDLER: Action = {
30
- kind: "Chain",
31
- first: EXTRACT_PAYLOAD,
32
- rest: TAG_DISCARD,
33
- };
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
+ }
34
42
 
35
43
  // ---------------------------------------------------------------------------
36
44
  // race — first branch to complete wins, losers cancelled
@@ -38,26 +46,27 @@ const RACE_HANDLER: Action = {
38
46
 
39
47
  /**
40
48
  * Run multiple actions concurrently. The first to complete wins; losers
41
- * are cancelled during Handle frame teardown.
49
+ * are cancelled during `RestartHandle` frame teardown.
42
50
  *
43
51
  * All branches must have the same input and output type (since either
44
52
  * could win).
45
53
  *
46
- * Compiles to:
47
- * Handle(effectId, Chain(ExtractField("payload"), Tag("Discard")),
48
- * All(
49
- * Chain(action1, Perform(effectId)),
50
- * Chain(action2, Perform(effectId)),
51
- * ...
52
- * ),
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.
54
64
  */
55
65
  export function race<TIn, TOut>(
56
66
  ...actions: Pipeable<TIn, TOut>[]
57
67
  ): TypedAction<TIn, TOut> {
58
- const effectId = allocateEffectId();
59
-
60
- const perform: Action = { kind: "Perform", effect_id: effectId };
68
+ const restartHandlerId = allocateRestartHandlerId();
69
+ const perform = breakPerform(restartHandlerId);
61
70
 
62
71
  const branches = actions.map((action) => ({
63
72
  kind: "Chain" as const,
@@ -65,12 +74,11 @@ export function race<TIn, TOut>(
65
74
  rest: perform,
66
75
  }));
67
76
 
68
- return typedAction({
69
- kind: "Handle",
70
- effect_id: effectId,
71
- handler: RACE_HANDLER,
72
- body: { kind: "All", actions: branches },
73
- });
77
+ const allAction: Action = { kind: "All", actions: branches };
78
+
79
+ return typedAction(
80
+ buildRestartBranchAction(restartHandlerId, allAction, IDENTITY),
81
+ );
74
82
  }
75
83
 
76
84
  // ---------------------------------------------------------------------------
@@ -129,28 +137,28 @@ Object.defineProperty(sleep, "__definition", {
129
137
  * that computes a duration from the pipeline input.
130
138
  *
131
139
  * Built as raw AST rather than through `race()` because each branch wraps
132
- * its result differently (Ok vs Err) before Perform. `race()` requires
133
- * homogeneous output types, but withTimeout needs heterogeneous tagging.
140
+ * its result differently (Ok vs Err) before the Break+Perform. `race()`
141
+ * requires homogeneous output types, but withTimeout needs heterogeneous
142
+ * tagging.
134
143
  *
135
- * Compiles to the same Handle/All/Perform structure as race, with each
136
- * branch wrapping its result as Ok or Err before Perform.
144
+ * Same restart+Branch substrate as race: each branch tags Break after
145
+ * wrapping its result as Ok or Err.
137
146
  */
138
147
  export function withTimeout<TIn, TOut>(
139
148
  ms: Pipeable<TIn, number>,
140
149
  body: Pipeable<TIn, TOut>,
141
150
  ): TypedAction<TIn, Result<TOut, void>> {
142
- const effectId = allocateEffectId();
143
-
144
- const perform: Action = { kind: "Perform", effect_id: effectId };
151
+ const restartHandlerId = allocateRestartHandlerId();
152
+ const perform = breakPerform(restartHandlerId);
145
153
 
146
- // Branch 1: body → Tag("Ok") → Perform
154
+ // Branch 1: body → Tag("Ok") → Tag("Break") → RestartPerform
147
155
  const bodyBranch: Action = {
148
156
  kind: "Chain",
149
157
  first: { kind: "Chain", first: body as Action, rest: TAG_OK },
150
158
  rest: perform,
151
159
  };
152
160
 
153
- // Branch 2: ms → sleep() → Tag("Err") → Perform
161
+ // Branch 2: ms → sleep() → Tag("Err") → Tag("Break") → RestartPerform
154
162
  const sleepBranch: Action = {
155
163
  kind: "Chain",
156
164
  first: {
@@ -161,11 +169,9 @@ export function withTimeout<TIn, TOut>(
161
169
  rest: perform,
162
170
  };
163
171
 
164
- return typedAction({
165
- kind: "Handle",
166
- effect_id: effectId,
167
- handler: RACE_HANDLER,
168
- body: { kind: "All", actions: [bodyBranch, sleepBranch] },
169
- });
170
- }
172
+ const allAction: Action = { kind: "All", actions: [bodyBranch, sleepBranch] };
171
173
 
174
+ return typedAction(
175
+ buildRestartBranchAction(restartHandlerId, allAction, IDENTITY),
176
+ );
177
+ }