@aikirun/workflow 0.6.0 → 0.7.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 (3) hide show
  1. package/dist/index.d.ts +144 -45
  2. package/dist/index.js +288 -82
  3. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { WorkflowId, WorkflowVersionId } from '@aikirun/types/workflow';
2
2
  import { Client, Logger, ApiClient } from '@aikirun/types/client';
3
3
  import { INTERNAL } from '@aikirun/types/symbols';
4
- import { WorkflowRun, WorkflowRunStatus, WorkflowRunStateCompleted, WorkflowRunStateInComplete, WorkflowRunState, WorkflowRunId, WorkflowOptions } from '@aikirun/types/workflow-run';
4
+ import { WorkflowRun, TerminalWorkflowRunStatus, WorkflowRunState, WorkflowRunId, WorkflowOptions } from '@aikirun/types/workflow-run';
5
5
  import { SleepParams, SleepResult } from '@aikirun/types/sleep';
6
- import { DurationObject } from '@aikirun/types/duration';
7
6
  import { EventSendOptions, EventWaitOptions, EventWaitState } from '@aikirun/types/event';
7
+ import { DurationObject } from '@aikirun/types/duration';
8
8
  import { TaskPath } from '@aikirun/types/task';
9
9
  import { WorkflowRunStateRequest, TaskStateRequest } from '@aikirun/types/workflow-run-api';
10
- import { SerializableInput } from '@aikirun/types/error';
10
+ import { Serializable } from '@aikirun/types/error';
11
11
 
12
12
  type NonEmptyArray<T> = [T, ...T[]];
13
13
 
@@ -28,46 +28,87 @@ type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[
28
28
  type ExtractObjectType<T> = T extends object ? T : never;
29
29
  type TypeOfValueAtPath<T extends object, Path extends PathFromObject<T>> = Path extends keyof T ? T[Path] : Path extends `${infer First}.${infer Rest}` ? First extends keyof T ? undefined extends T[First] ? Rest extends PathFromObject<ExtractObjectType<T[First]>> ? TypeOfValueAtPath<ExtractObjectType<T[First]>, Rest> | undefined : never : Rest extends PathFromObject<ExtractObjectType<T[First]>> ? TypeOfValueAtPath<ExtractObjectType<T[First]>, Rest> : never : never : never;
30
30
 
31
- declare function workflowRunHandle<Input, Output, TEventsDefinition extends EventsDefinition>(client: Client, id: WorkflowRunId, eventsDefinition?: TEventsDefinition, logger?: Logger): Promise<WorkflowRunHandle<Input, Output, TEventsDefinition>>;
32
- declare function workflowRunHandle<Input, Output, TEventsDefinition extends EventsDefinition>(client: Client, run: WorkflowRun<Input, Output>, eventsDefinition?: TEventsDefinition, logger?: Logger): Promise<WorkflowRunHandle<Input, Output, TEventsDefinition>>;
33
- interface WorkflowRunHandle<Input, Output, TEventsDefinition extends EventsDefinition = EventsDefinition> {
31
+ declare function workflowRunHandle<Input, Output, AppContext, TEventsDefinition extends EventsDefinition>(client: Client<AppContext>, id: WorkflowRunId, eventsDefinition?: TEventsDefinition, logger?: Logger): Promise<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
32
+ declare function workflowRunHandle<Input, Output, AppContext, TEventsDefinition extends EventsDefinition>(client: Client<AppContext>, run: WorkflowRun<Input, Output>, eventsDefinition?: TEventsDefinition, logger?: Logger): Promise<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
33
+ interface WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition extends EventsDefinition = EventsDefinition> {
34
34
  run: Readonly<WorkflowRun<Input, Output>>;
35
35
  events: EventSenders<TEventsDefinition>;
36
36
  refresh: () => Promise<void>;
37
- wait<S extends WorkflowRunStatus>(condition: {
38
- type: "status";
39
- status: S;
40
- }, options: WorkflowRunWaitOptions): Promise<{
41
- success: false;
42
- cause: "timeout" | "aborted";
43
- } | {
44
- success: true;
45
- state: S extends "completed" ? WorkflowRunStateCompleted<Output> : WorkflowRunStateInComplete;
46
- }>;
47
- wait(condition: {
48
- type: "event";
49
- event: string;
50
- }, options: WorkflowRunWaitOptions): Promise<{
51
- success: false;
52
- cause: "timeout" | "aborted";
53
- } | {
54
- success: true;
55
- state: WorkflowRunState<Output>;
56
- }>;
37
+ /**
38
+ * Waits for the child workflow run to reach a terminal status by polling.
39
+ *
40
+ * Returns a result object:
41
+ * - `{ success: true, state }` - workflow reached the expected status
42
+ * - `{ success: false, cause }` - workflow did not reach status
43
+ *
44
+ * Possible failure causes:
45
+ * - `"run_terminated"` - workflow reached a terminal state other than expected
46
+ * - `"timeout"` - timeout elapsed (only when timeout option provided)
47
+ * - `"aborted"` - abort signal triggered (only when abortSignal option provided)
48
+ *
49
+ * @param status - The target status to wait for
50
+ * @param options - Optional configuration for polling interval, timeout, and abort signal
51
+ *
52
+ * @example
53
+ * // Wait indefinitely until completed or the workflow reaches another terminal state
54
+ * const result = await handle.waitForStatus("completed");
55
+ * if (result.success) {
56
+ * console.log(result.state.output);
57
+ * } else {
58
+ * console.log(`Workflow terminated: ${result.cause}`);
59
+ * }
60
+ *
61
+ * @example
62
+ * // Wait with a timeout
63
+ * const result = await handle.waitForStatus("completed", {
64
+ * timeout: { seconds: 30 }
65
+ * });
66
+ * if (result.success) {
67
+ * console.log(result.state.output);
68
+ * } else if (result.cause === "timeout") {
69
+ * console.log("Timed out waiting for completion");
70
+ * }
71
+ *
72
+ * @example
73
+ * // Wait with an abort signal
74
+ * const controller = new AbortController();
75
+ * const result = await handle.waitForStatus("completed", {
76
+ * abortSignal: controller.signal
77
+ * });
78
+ * if (!result.success) {
79
+ * console.log(`Wait ended: ${result.cause}`);
80
+ * }
81
+ */
82
+ waitForStatus<Status extends TerminalWorkflowRunStatus>(status: Status, options?: WorkflowRunWaitOptions<false, false>): Promise<WorkflowRunWaitResult<Status, Output, false, false>>;
83
+ waitForStatus<Status extends TerminalWorkflowRunStatus>(status: Status, options: WorkflowRunWaitOptions<true, false>): Promise<WorkflowRunWaitResult<Status, Output, true, false>>;
84
+ waitForStatus<Status extends TerminalWorkflowRunStatus>(status: Status, options: WorkflowRunWaitOptions<false, true>): Promise<WorkflowRunWaitResult<Status, Output, false, true>>;
85
+ waitForStatus<Status extends TerminalWorkflowRunStatus>(status: Status, options: WorkflowRunWaitOptions<true, true>): Promise<WorkflowRunWaitResult<Status, Output, true, true>>;
57
86
  cancel: (reason?: string) => Promise<void>;
58
87
  pause: () => Promise<void>;
59
88
  resume: () => Promise<void>;
89
+ awake: () => Promise<void>;
60
90
  [INTERNAL]: {
91
+ client: Client<AppContext>;
61
92
  transitionState: (state: WorkflowRunStateRequest) => Promise<void>;
62
93
  transitionTaskState: (taskPath: TaskPath, taskState: TaskStateRequest) => Promise<void>;
63
94
  assertExecutionAllowed: () => void;
64
95
  };
65
96
  }
66
- interface WorkflowRunWaitOptions {
67
- maxDurationMs: number;
68
- pollIntervalMs?: number;
69
- abortSignal?: AbortSignal;
97
+ interface WorkflowRunWaitOptions<Timed extends boolean, Abortable extends boolean> {
98
+ interval?: DurationObject;
99
+ timeout?: Timed extends true ? DurationObject : never;
100
+ abortSignal?: Abortable extends true ? AbortSignal : never;
70
101
  }
102
+ type WorkflowRunWaitResultSuccess<Status extends TerminalWorkflowRunStatus, Output> = Extract<WorkflowRunState<Output>, {
103
+ status: Status;
104
+ }>;
105
+ type WorkflowRunWaitResult<Status extends TerminalWorkflowRunStatus, Output, Timed extends boolean, Abortable extends boolean> = {
106
+ success: false;
107
+ cause: "run_terminated" | (Timed extends true ? "timeout" : never) | (Abortable extends true ? "aborted" : never);
108
+ } | {
109
+ success: true;
110
+ state: WorkflowRunWaitResultSuccess<Status, Output>;
111
+ };
71
112
 
72
113
  /**
73
114
  * Defines an event type that can be sent to and waited for by workflows.
@@ -90,6 +131,7 @@ interface WorkflowRunWaitOptions {
90
131
  * });
91
132
  * ```
92
133
  */
134
+ declare function event(): EventDefinition<undefined>;
93
135
  declare function event<Data>(params?: EventParams<Data>): EventDefinition<Data>;
94
136
  interface EventParams<Data> {
95
137
  schema?: Schema<Data>;
@@ -107,19 +149,25 @@ type EventWaiters<TEventsDefinition extends EventsDefinition> = {
107
149
  [K in keyof TEventsDefinition]: EventWaiter<EventData<TEventsDefinition[K]>>;
108
150
  };
109
151
  interface EventWaiter<Data> {
110
- wait(options?: EventWaitOptions<undefined>): Promise<EventWaitState<Data, false>>;
111
- wait(options: EventWaitOptions<DurationObject>): Promise<EventWaitState<Data, true>>;
152
+ wait(options?: EventWaitOptions<false>): Promise<EventWaitState<Data, false>>;
153
+ wait(options: EventWaitOptions<true>): Promise<EventWaitState<Data, true>>;
112
154
  }
113
155
  type EventSenders<TEventsDefinition extends EventsDefinition> = {
114
156
  [K in keyof TEventsDefinition]: EventSender<EventData<TEventsDefinition[K]>>;
115
157
  };
116
158
  interface EventSender<Data> {
117
- send: (data: Data, options?: EventSendOptions) => Promise<void>;
159
+ send: (...args: Data extends undefined ? [data?: Data, options?: EventSendOptions] : [data: Data, options?: EventSendOptions]) => Promise<void>;
160
+ }
161
+ type EventMulticasters<TEventsDefinition extends EventsDefinition> = {
162
+ [K in keyof TEventsDefinition]: EventMulticaster<EventData<TEventsDefinition[K]>>;
163
+ };
164
+ interface EventMulticaster<Data> {
165
+ send: <AppContext>(client: Client<AppContext>, runId: string | string[], data: Data, options?: EventSendOptions) => Promise<void>;
118
166
  }
119
- declare function createEventWaiters<TEventsDefinition extends EventsDefinition>(handle: WorkflowRunHandle<unknown, unknown, TEventsDefinition>, eventsDefinition: TEventsDefinition, logger: Logger): EventWaiters<TEventsDefinition>;
167
+ declare function createEventWaiters<TEventsDefinition extends EventsDefinition>(handle: WorkflowRunHandle<unknown, unknown, unknown, TEventsDefinition>, eventsDefinition: TEventsDefinition, logger: Logger): EventWaiters<TEventsDefinition>;
120
168
  declare function createEventSenders<TEventsDefinition extends EventsDefinition>(api: ApiClient, workflowRunId: string, eventsDefinition: TEventsDefinition, logger: Logger, onSend: (run: WorkflowRun<unknown, unknown>) => void): EventSenders<TEventsDefinition>;
121
169
 
122
- interface WorkflowRunContext<Input, Output, TEventDefinition extends EventsDefinition> {
170
+ interface WorkflowRunContext<Input, AppContext, TEventDefinition extends EventsDefinition> {
123
171
  id: WorkflowRunId;
124
172
  workflowId: WorkflowId;
125
173
  workflowVersionId: WorkflowVersionId;
@@ -128,47 +176,98 @@ interface WorkflowRunContext<Input, Output, TEventDefinition extends EventsDefin
128
176
  sleep: (params: SleepParams) => Promise<SleepResult>;
129
177
  events: EventWaiters<TEventDefinition>;
130
178
  [INTERNAL]: {
131
- handle: WorkflowRunHandle<Input, Output, TEventDefinition>;
179
+ handle: WorkflowRunHandle<Input, unknown, AppContext, TEventDefinition>;
132
180
  options: {
133
181
  spinThresholdMs: number;
134
182
  };
135
183
  };
136
184
  }
137
185
 
186
+ type ChildWorkflowRunHandle<Input, Output, AppContext, TEventsDefinition extends EventsDefinition = EventsDefinition> = Omit<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>, "waitForStatus"> & {
187
+ /**
188
+ * Waits for the child workflow run to reach a terminal status.
189
+ *
190
+ * This method suspends the parent workflow until the child reaches the expected terminal status
191
+ * or the optional timeout elapses.
192
+ *
193
+ * When the parent resumes, the result is deterministically replayed from stored wait results.
194
+ *
195
+ * Returns a result object:
196
+ * - `{ success: true, state }` - child reached the expected status
197
+ * - `{ success: false, cause }` - child did not reach status
198
+ *
199
+ * Possible failure causes:
200
+ * - `"run_terminated"` - child reached a different terminal state than expected
201
+ * - `"timeout"` - timeout elapsed (only when timeout option provided)
202
+ *
203
+ * @param status - The target terminal status to wait for
204
+ * @param options - Optional configuration with timeout
205
+ *
206
+ * @example
207
+ * // Wait indefinitely for child to complete
208
+ * const result = await childHandle.waitForStatus("completed");
209
+ * if (result.success) {
210
+ * console.log(result.state.output);
211
+ * } else {
212
+ * console.log(`Child terminated: ${result.cause}`);
213
+ * }
214
+ *
215
+ * @example
216
+ * // Wait with a timeout
217
+ * const result = await childHandle.waitForStatus("completed", {
218
+ * timeout: { minutes: 5 }
219
+ * });
220
+ * if (!result.success && result.cause === "timeout") {
221
+ * console.log("Child workflow took too long");
222
+ * }
223
+ */
224
+ waitForStatus<Status extends TerminalWorkflowRunStatus>(status: Status, options?: ChildWorkflowRunWaitOptions<false>): Promise<WorkflowRunWaitResult<Status, Output, false, false>>;
225
+ waitForStatus<Status extends TerminalWorkflowRunStatus>(status: Status, options: ChildWorkflowRunWaitOptions<true>): Promise<WorkflowRunWaitResult<Status, Output, true, false>>;
226
+ };
227
+ interface ChildWorkflowRunWaitOptions<Timed extends boolean> {
228
+ timeout?: Timed extends true ? DurationObject : never;
229
+ }
230
+
138
231
  interface WorkflowVersionParams<Input, Output, AppContext, TEventsDefinition extends EventsDefinition> {
232
+ handler: (input: Input, run: Readonly<WorkflowRunContext<Input, AppContext, TEventsDefinition>>, context: AppContext) => Promise<Output>;
139
233
  events?: TEventsDefinition;
140
234
  opts?: WorkflowOptions;
141
- handler: (input: Input, run: Readonly<WorkflowRunContext<Input, Output, TEventsDefinition>>, context: AppContext) => Promise<Output>;
142
235
  }
143
236
  interface WorkflowBuilder<Input, Output, AppContext, TEventsDefinition extends EventsDefinition> {
144
237
  opt<Path extends PathFromObject<WorkflowOptions>>(path: Path, value: TypeOfValueAtPath<WorkflowOptions, Path>): WorkflowBuilder<Input, Output, AppContext, TEventsDefinition>;
145
238
  start: WorkflowVersion<Input, Output, AppContext, TEventsDefinition>["start"];
239
+ startAsChild: WorkflowVersion<Input, Output, AppContext, TEventsDefinition>["startAsChild"];
146
240
  }
147
241
  interface WorkflowVersion<Input, Output, AppContext, TEventsDefinition extends EventsDefinition = EventsDefinition> {
148
242
  id: WorkflowId;
149
243
  versionId: WorkflowVersionId;
244
+ events: EventMulticasters<TEventsDefinition>;
150
245
  with(): WorkflowBuilder<Input, Output, AppContext, TEventsDefinition>;
151
- start: (client: Client<AppContext>, ...args: Input extends null ? [] : [Input]) => Promise<WorkflowRunHandle<Input, Output, TEventsDefinition>>;
152
- getHandle: (client: Client<AppContext>, runId: WorkflowRunId) => Promise<WorkflowRunHandle<Input, Output, TEventsDefinition>>;
246
+ start: (client: Client<AppContext>, ...args: Input extends null ? [] : [Input]) => Promise<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
247
+ startAsChild: <ParentInput, ParentEventsDefinition extends EventsDefinition>(parentRun: WorkflowRunContext<ParentInput, AppContext, ParentEventsDefinition>, ...args: Input extends null ? [] : [Input]) => Promise<ChildWorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
248
+ getHandle: (client: Client<AppContext>, runId: WorkflowRunId) => Promise<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
153
249
  [INTERNAL]: {
154
250
  eventsDefinition: TEventsDefinition;
155
- handler: (input: Input, run: WorkflowRunContext<Input, Output, TEventsDefinition>, context: AppContext) => Promise<void>;
251
+ handler: (input: Input, run: WorkflowRunContext<Input, AppContext, TEventsDefinition>, context: AppContext) => Promise<void>;
156
252
  };
157
253
  }
158
254
  declare class WorkflowVersionImpl<Input, Output, AppContext, TEventsDefinition extends EventsDefinition> implements WorkflowVersion<Input, Output, AppContext, TEventsDefinition> {
159
255
  readonly id: WorkflowId;
160
256
  readonly versionId: WorkflowVersionId;
161
257
  private readonly params;
258
+ readonly events: EventMulticasters<TEventsDefinition>;
162
259
  readonly [INTERNAL]: WorkflowVersion<Input, Output, AppContext, TEventsDefinition>[typeof INTERNAL];
163
260
  constructor(id: WorkflowId, versionId: WorkflowVersionId, params: WorkflowVersionParams<Input, Output, AppContext, TEventsDefinition>);
164
261
  with(): WorkflowBuilder<Input, Output, AppContext, TEventsDefinition>;
165
- start(client: Client<AppContext>, ...args: Input extends null ? [] : [Input]): Promise<WorkflowRunHandle<Input, Output, TEventsDefinition>>;
166
- getHandle(client: Client<AppContext>, runId: WorkflowRunId): Promise<WorkflowRunHandle<Input, Output, TEventsDefinition>>;
262
+ start(client: Client<AppContext>, ...args: Input extends null ? [] : [Input]): Promise<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
263
+ startAsChild<ParentInput>(parentRun: WorkflowRunContext<ParentInput, AppContext, EventsDefinition>, ...args: Input extends null ? [] : [Input]): Promise<ChildWorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
264
+ getHandle(client: Client<AppContext>, runId: WorkflowRunId): Promise<WorkflowRunHandle<Input, Output, AppContext, TEventsDefinition>>;
167
265
  private handler;
168
266
  private tryExecuteWorkflow;
169
267
  private assertRetryAllowed;
170
268
  private createFailedState;
171
269
  private createAwaitingRetryState;
270
+ private getPath;
172
271
  }
173
272
 
174
273
  declare function workflowRegistry(): WorkflowRegistry;
@@ -186,7 +285,7 @@ interface WorkflowRegistry {
186
285
  interface SleeperOptions {
187
286
  spinThresholdMs: number;
188
287
  }
189
- declare function createSleeper(handle: WorkflowRunHandle<unknown, unknown>, logger: Logger, options: SleeperOptions): (params: SleepParams) => Promise<SleepResult>;
288
+ declare function createSleeper(handle: WorkflowRunHandle<unknown, unknown, unknown>, logger: Logger, options: SleeperOptions): (params: SleepParams) => Promise<SleepResult>;
190
289
 
191
290
  /**
192
291
  * Defines a durable workflow with versioning and multiple task execution.
@@ -241,11 +340,11 @@ interface WorkflowParams {
241
340
  }
242
341
  interface Workflow {
243
342
  id: WorkflowId;
244
- v: <Input extends SerializableInput = null, Output = void, AppContext = null, TEventsDefinition extends EventsDefinition = Record<string, never>>(versionId: string, params: WorkflowVersionParams<Input, Output, AppContext, TEventsDefinition>) => WorkflowVersion<Input, Output, AppContext, TEventsDefinition>;
343
+ v: <Input extends Serializable, Output extends Serializable, AppContext = null, TEventsDefinition extends EventsDefinition = Record<string, never>>(versionId: string, params: WorkflowVersionParams<Input, Output, AppContext, TEventsDefinition>) => WorkflowVersion<Input, Output, AppContext, TEventsDefinition>;
245
344
  [INTERNAL]: {
246
345
  getAllVersions: () => WorkflowVersion<unknown, unknown, unknown>[];
247
346
  getVersion: (versionId: WorkflowVersionId) => WorkflowVersion<unknown, unknown, unknown> | undefined;
248
347
  };
249
348
  }
250
349
 
251
- export { type EventDefinition, type EventSender, type EventSenders, type EventWaiter, type EventWaiters, type Workflow, type WorkflowParams, type WorkflowRegistry, type WorkflowRunContext, type WorkflowRunHandle, type WorkflowRunWaitOptions, type WorkflowVersion, WorkflowVersionImpl, type WorkflowVersionParams, createEventSenders, createEventWaiters, createSleeper, event, workflow, workflowRegistry, workflowRunHandle };
350
+ export { type EventMulticaster, type EventMulticasters, type EventSender, type EventSenders, type EventWaiter, type EventWaiters, type Workflow, type WorkflowParams, type WorkflowRegistry, type WorkflowRunContext, type WorkflowRunHandle, type WorkflowRunWaitOptions, type WorkflowVersion, WorkflowVersionImpl, type WorkflowVersionParams, createEventSenders, createEventWaiters, createSleeper, event, workflow, workflowRegistry, workflowRunHandle };
package/dist/index.js CHANGED
@@ -77,6 +77,14 @@ function delay(ms, options) {
77
77
  });
78
78
  }
79
79
 
80
+ // ../../lib/crypto/hash.ts
81
+ async function sha256(input) {
82
+ const data = new TextEncoder().encode(input);
83
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
84
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
85
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
86
+ }
87
+
80
88
  // ../../lib/duration/convert.ts
81
89
  var MS_PER_SECOND = 1e3;
82
90
  var MS_PER_MINUTE = 60 * MS_PER_SECOND;
@@ -136,6 +144,25 @@ function createSerializableError(error) {
136
144
  };
137
145
  }
138
146
 
147
+ // ../../lib/json/stable-stringify.ts
148
+ function stableStringify(value) {
149
+ if (value === null || value === void 0) {
150
+ return JSON.stringify(value);
151
+ }
152
+ if (typeof value !== "object") {
153
+ return JSON.stringify(value);
154
+ }
155
+ if (Array.isArray(value)) {
156
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
157
+ }
158
+ const keys = Object.keys(value).sort();
159
+ const pairs = keys.map((key) => {
160
+ const val = value[key];
161
+ return `${JSON.stringify(key)}:${stableStringify(val)}`;
162
+ });
163
+ return `{${pairs.join(",")}}`;
164
+ }
165
+
139
166
  // ../../lib/object/overrider.ts
140
167
  function set(obj, path, value) {
141
168
  const keys = path.split(".");
@@ -279,9 +306,10 @@ function createEventWaiters(handle, eventsDefinition, logger) {
279
306
  function createEventWaiter(handle, eventId, schema, logger) {
280
307
  let nextEventIndex = 0;
281
308
  async function wait(options) {
309
+ await handle.refresh();
282
310
  const events = handle.run.eventsQueue[eventId]?.events ?? [];
283
- if (nextEventIndex < events.length) {
284
- const event2 = events[nextEventIndex];
311
+ const event2 = events[nextEventIndex];
312
+ if (event2) {
285
313
  nextEventIndex++;
286
314
  if (event2.status === "timeout") {
287
315
  logger.debug("Timed out waiting for event");
@@ -333,7 +361,8 @@ function createEventSenders(api, workflowRunId, eventsDefinition, logger, onSend
333
361
  }
334
362
  function createEventSender(api, workflowRunId, eventId, schema, logger, onSend) {
335
363
  return {
336
- async send(data, options) {
364
+ async send(...args) {
365
+ const [data, options] = args;
337
366
  if (schema) {
338
367
  schema.parse(data);
339
368
  }
@@ -350,35 +379,66 @@ function createEventSender(api, workflowRunId, eventId, schema, logger, onSend)
350
379
  }
351
380
  };
352
381
  }
382
+ function createEventMulticasters(eventsDefinition) {
383
+ const senders = {};
384
+ for (const [eventId, eventDefinition] of Object.entries(eventsDefinition)) {
385
+ const sender = createEventMulticaster(eventId, eventDefinition.schema);
386
+ senders[eventId] = sender;
387
+ }
388
+ return senders;
389
+ }
390
+ function createEventMulticaster(eventId, schema) {
391
+ return {
392
+ async send(client, runId, data, options) {
393
+ if (schema) {
394
+ schema.parse(data);
395
+ }
396
+ const runIds = Array.isArray(runId) ? runId : [runId];
397
+ await client.api.workflowRun.multicastEventV1({
398
+ ids: runIds,
399
+ eventId,
400
+ data,
401
+ options
402
+ });
403
+ }
404
+ };
405
+ }
353
406
 
354
407
  // run/handle.ts
355
408
  import { INTERNAL as INTERNAL2 } from "@aikirun/types/symbols";
356
409
  import {
410
+ isTerminalWorkflowRunStatus,
357
411
  WorkflowRunNotExecutableError
358
412
  } from "@aikirun/types/workflow-run";
359
413
  async function workflowRunHandle(client, runOrId, eventsDefinition, logger) {
360
414
  const run = typeof runOrId !== "string" ? runOrId : (await client.api.workflowRun.getByIdV1({ id: runOrId })).run;
361
415
  return new WorkflowRunHandleImpl(
362
- client.api,
416
+ client,
363
417
  run,
364
418
  eventsDefinition ?? {},
365
- logger ?? client.logger.child({ "aiki.workflowRunId": run.id })
419
+ logger ?? client.logger.child({
420
+ "aiki.workflowId": run.workflowId,
421
+ "aiki.workflowVersionId": run.workflowVersionId,
422
+ "aiki.workflowRunId": run.id
423
+ })
366
424
  );
367
425
  }
368
426
  var WorkflowRunHandleImpl = class {
369
- constructor(api, _run, eventsDefinition, logger) {
370
- this.api = api;
427
+ constructor(client, _run, eventsDefinition, logger) {
371
428
  this._run = _run;
372
429
  this.logger = logger;
373
- this.events = createEventSenders(this.api, this._run.id, eventsDefinition, this.logger, (run) => {
430
+ this.api = client.api;
431
+ this.events = createEventSenders(client.api, this._run.id, eventsDefinition, this.logger, (run) => {
374
432
  this._run = run;
375
433
  });
376
434
  this[INTERNAL2] = {
435
+ client,
377
436
  transitionState: this.transitionState.bind(this),
378
437
  transitionTaskState: this.transitionTaskState.bind(this),
379
438
  assertExecutionAllowed: this.assertExecutionAllowed.bind(this)
380
439
  };
381
440
  }
441
+ api;
382
442
  events;
383
443
  [INTERNAL2];
384
444
  get run() {
@@ -388,59 +448,62 @@ var WorkflowRunHandleImpl = class {
388
448
  const { run: currentRun } = await this.api.workflowRun.getByIdV1({ id: this.run.id });
389
449
  this._run = currentRun;
390
450
  }
391
- // TODO: instead polling the current state, use the transition history
392
- // because it is entirely possible for a workflow to flash though a state
451
+ // TODO: instead checking the current state, use the transition history
452
+ // because it is possible for a workflow to flash though a state
393
453
  // and the handle will never know that the workflow hit that state
394
- async wait(condition, options) {
395
- if (options.abortSignal?.aborted) {
396
- throw new Error("Wait operation aborted");
397
- }
398
- const delayMs = options.pollIntervalMs ?? 1e3;
399
- const maxAttempts = Math.ceil(options.maxDurationMs / delayMs);
400
- switch (condition.type) {
401
- case "status": {
402
- if (options.abortSignal !== void 0) {
403
- const maybeResult2 = await withRetry(
404
- async () => {
405
- await this.refresh();
406
- return this.run.state;
407
- },
408
- { type: "fixed", maxAttempts, delayMs },
409
- {
410
- abortSignal: options.abortSignal,
411
- shouldRetryOnResult: (state) => Promise.resolve(state.status !== condition.status)
412
- }
413
- ).run();
414
- if (maybeResult2.state === "timeout" || maybeResult2.state === "aborted") {
415
- return { success: false, cause: maybeResult2.state };
416
- }
417
- return {
418
- success: true,
419
- state: maybeResult2.result
420
- };
421
- }
422
- const maybeResult = await withRetry(
423
- async () => {
424
- await this.refresh();
425
- return this.run.state;
426
- },
427
- { type: "fixed", maxAttempts, delayMs },
428
- { shouldRetryOnResult: (state) => Promise.resolve(state.status !== condition.status) }
429
- ).run();
430
- if (maybeResult.state === "timeout") {
431
- return { success: false, cause: maybeResult.state };
432
- }
454
+ async waitForStatus(status, options) {
455
+ return this.waitForStatusByPolling(status, options);
456
+ }
457
+ async waitForStatusByPolling(expectedStatus, options) {
458
+ if (options?.abortSignal?.aborted) {
459
+ return {
460
+ success: false,
461
+ cause: "aborted"
462
+ };
463
+ }
464
+ const delayMs = options?.interval ? toMilliseconds(options.interval) : 1e3;
465
+ const maxAttempts = options?.timeout ? Math.ceil(toMilliseconds(options.timeout) / delayMs) : Number.POSITIVE_INFINITY;
466
+ const retryStrategy = { type: "fixed", maxAttempts, delayMs };
467
+ const loadState = async () => {
468
+ await this.refresh();
469
+ return this.run.state;
470
+ };
471
+ const isNeitherExpectedNorTerminal = (state) => Promise.resolve(state.status !== expectedStatus && !isTerminalWorkflowRunStatus(state.status));
472
+ if (!Number.isFinite(maxAttempts) && !options?.abortSignal) {
473
+ const maybeResult2 = await withRetry(loadState, retryStrategy, {
474
+ shouldRetryOnResult: isNeitherExpectedNorTerminal
475
+ }).run();
476
+ if (maybeResult2.state === "timeout") {
477
+ throw new Error("Something's wrong, this should've never timed out");
478
+ }
479
+ if (await isNeitherExpectedNorTerminal(maybeResult2.result)) {
433
480
  return {
434
- success: true,
435
- state: maybeResult.result
481
+ success: false,
482
+ cause: "run_terminated"
436
483
  };
437
484
  }
438
- case "event": {
439
- throw new Error("Event-based waiting is not yet implemented");
485
+ return {
486
+ success: true,
487
+ state: maybeResult2.result
488
+ };
489
+ }
490
+ const maybeResult = options?.abortSignal ? await withRetry(loadState, retryStrategy, {
491
+ abortSignal: options.abortSignal,
492
+ shouldRetryOnResult: isNeitherExpectedNorTerminal
493
+ }).run() : await withRetry(loadState, retryStrategy, { shouldRetryOnResult: isNeitherExpectedNorTerminal }).run();
494
+ if (maybeResult.state === "completed") {
495
+ if (await isNeitherExpectedNorTerminal(maybeResult.result)) {
496
+ return {
497
+ success: false,
498
+ cause: "run_terminated"
499
+ };
440
500
  }
441
- default:
442
- return condition;
501
+ return {
502
+ success: true,
503
+ state: maybeResult.result
504
+ };
443
505
  }
506
+ return { success: false, cause: maybeResult.state };
444
507
  }
445
508
  async cancel(reason) {
446
509
  return this.transitionState({ status: "cancelled", reason });
@@ -451,8 +514,11 @@ var WorkflowRunHandleImpl = class {
451
514
  async resume() {
452
515
  return this.transitionState({ status: "scheduled", scheduledInMs: 0, reason: "resume" });
453
516
  }
517
+ async awake() {
518
+ return this.transitionState({ status: "scheduled", scheduledInMs: 0, reason: "awake" });
519
+ }
454
520
  async transitionState(targetState) {
455
- if (targetState.status === "scheduled" && (targetState.reason === "new" || targetState.reason === "resume") || targetState.status === "paused" || targetState.status === "cancelled") {
521
+ if (targetState.status === "scheduled" && (targetState.reason === "new" || targetState.reason === "resume" || targetState.reason === "awake") || targetState.status === "paused" || targetState.status === "cancelled") {
456
522
  const { run: run2 } = await this.api.workflowRun.transitionStateV1({
457
523
  type: "pessimistic",
458
524
  id: this.run.id,
@@ -540,35 +606,121 @@ function createSleeper(handle, logger, options) {
540
606
  }
541
607
 
542
608
  // workflow.ts
543
- import { INTERNAL as INTERNAL5 } from "@aikirun/types/symbols";
609
+ import { INTERNAL as INTERNAL6 } from "@aikirun/types/symbols";
544
610
 
545
611
  // workflow-version.ts
546
- import { INTERNAL as INTERNAL4 } from "@aikirun/types/symbols";
612
+ import { INTERNAL as INTERNAL5 } from "@aikirun/types/symbols";
547
613
  import { TaskFailedError } from "@aikirun/types/task";
548
614
  import {
549
615
  WorkflowRunFailedError as WorkflowRunFailedError2,
616
+ WorkflowRunSuspendedError as WorkflowRunSuspendedError4
617
+ } from "@aikirun/types/workflow-run";
618
+
619
+ // run/handle-child.ts
620
+ import { INTERNAL as INTERNAL4 } from "@aikirun/types/symbols";
621
+ import {
622
+ isTerminalWorkflowRunStatus as isTerminalWorkflowRunStatus2,
550
623
  WorkflowRunSuspendedError as WorkflowRunSuspendedError3
551
624
  } from "@aikirun/types/workflow-run";
625
+ async function childWorkflowRunHandle(client, path, run, parentRun, logger, eventsDefinition) {
626
+ const handle = await workflowRunHandle(client, run, eventsDefinition, logger);
627
+ return {
628
+ run: handle.run,
629
+ events: handle.events,
630
+ refresh: handle.refresh.bind(handle),
631
+ waitForStatus: createStatusWaiter(path, handle, parentRun, logger),
632
+ cancel: handle.cancel.bind(handle),
633
+ pause: handle.pause.bind(handle),
634
+ resume: handle.resume.bind(handle),
635
+ awake: handle.awake.bind(handle),
636
+ [INTERNAL4]: handle[INTERNAL4]
637
+ };
638
+ }
639
+ function createStatusWaiter(path, handle, parentRun, logger) {
640
+ let nextWaitIndex = 0;
641
+ async function waitForStatus(expectedStatus, options) {
642
+ const parentRunHandle = parentRun[INTERNAL4].handle;
643
+ const waitResults = parentRunHandle.run.childWorkflowRuns[path]?.statusWaitResults ?? [];
644
+ const waitResult = waitResults[nextWaitIndex];
645
+ if (waitResult) {
646
+ nextWaitIndex++;
647
+ if (waitResult.status === "timeout") {
648
+ logger.debug("Timed out waiting for child workflow status", { "aiki.expectedStatus": expectedStatus });
649
+ return {
650
+ success: false,
651
+ cause: "timeout"
652
+ };
653
+ }
654
+ if (waitResult.childWorkflowRunState.status === expectedStatus) {
655
+ return {
656
+ success: true,
657
+ state: waitResult.childWorkflowRunState
658
+ };
659
+ }
660
+ if (isTerminalWorkflowRunStatus2(waitResult.childWorkflowRunState.status)) {
661
+ logger.debug("Child workflow run reached termnial state");
662
+ return {
663
+ success: false,
664
+ cause: "run_terminated"
665
+ };
666
+ }
667
+ }
668
+ const { state } = handle.run;
669
+ if (state.status === expectedStatus) {
670
+ return {
671
+ success: true,
672
+ state
673
+ };
674
+ }
675
+ if (isTerminalWorkflowRunStatus2(state.status)) {
676
+ logger.debug("Child workflow run reached termnial state");
677
+ return {
678
+ success: false,
679
+ cause: "run_terminated"
680
+ };
681
+ }
682
+ const timeoutInMs = options?.timeout && toMilliseconds(options.timeout);
683
+ await parentRunHandle[INTERNAL4].transitionState({
684
+ status: "awaiting_child_workflow",
685
+ childWorkflowRunPath: path,
686
+ childWorkflowRunStatus: expectedStatus,
687
+ timeoutInMs
688
+ });
689
+ throw new WorkflowRunSuspendedError3(parentRun.id);
690
+ }
691
+ return waitForStatus;
692
+ }
693
+
694
+ // workflow-version.ts
552
695
  var WorkflowVersionImpl = class _WorkflowVersionImpl {
553
696
  constructor(id, versionId, params) {
554
697
  this.id = id;
555
698
  this.versionId = versionId;
556
699
  this.params = params;
557
- this[INTERNAL4] = {
558
- eventsDefinition: this.params.events ?? {},
700
+ const eventsDefinition = this.params.events ?? {};
701
+ this.events = createEventMulticasters(eventsDefinition);
702
+ this[INTERNAL5] = {
703
+ eventsDefinition,
559
704
  handler: this.handler.bind(this)
560
705
  };
561
706
  }
562
- [INTERNAL4];
707
+ events;
708
+ [INTERNAL5];
563
709
  with() {
564
710
  const optsOverrider = objectOverrider(this.params.opts ?? {});
565
- const createBuilder = (optsBuilder) => ({
566
- opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
567
- start: (client, ...args) => new _WorkflowVersionImpl(this.id, this.versionId, { ...this.params, opts: optsBuilder.build() }).start(
568
- client,
569
- ...args
570
- )
571
- });
711
+ const createBuilder = (optsBuilder) => {
712
+ return {
713
+ opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
714
+ start: (client, ...args) => new _WorkflowVersionImpl(this.id, this.versionId, {
715
+ ...this.params,
716
+ opts: optsBuilder.build()
717
+ }).start(client, ...args),
718
+ startAsChild: (parentRun, ...args) => new _WorkflowVersionImpl(this.id, this.versionId, {
719
+ ...this.params,
720
+ opts: optsBuilder.build()
721
+ }).startAsChild(parentRun, ...args)
722
+ };
723
+ };
572
724
  return createBuilder(optsOverrider());
573
725
  }
574
726
  async start(client, ...args) {
@@ -578,24 +730,73 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
578
730
  input: isNonEmptyArray(args) ? args[0] : null,
579
731
  options: this.params.opts
580
732
  });
581
- return workflowRunHandle(client, run, this[INTERNAL4].eventsDefinition);
733
+ return workflowRunHandle(client, run, this[INTERNAL5].eventsDefinition);
734
+ }
735
+ async startAsChild(parentRun, ...args) {
736
+ const parentRunHandle = parentRun[INTERNAL5].handle;
737
+ parentRunHandle[INTERNAL5].assertExecutionAllowed();
738
+ const { client } = parentRunHandle[INTERNAL5];
739
+ const input = isNonEmptyArray(args) ? args[0] : null;
740
+ const childRunPath = await this.getPath(input);
741
+ const existingChildRunId = parentRunHandle.run.childWorkflowRuns[childRunPath]?.id;
742
+ if (existingChildRunId) {
743
+ const { run: existingChildRun } = await client.api.workflowRun.getByIdV1({ id: existingChildRunId });
744
+ const logger2 = parentRun.logger.child({
745
+ "aiki.childWorkflowId": existingChildRun.workflowId,
746
+ "aiki.childWorkflowVersionId": existingChildRun.workflowVersionId,
747
+ "aiki.childWorkflowRunId": existingChildRun.id
748
+ });
749
+ return childWorkflowRunHandle(
750
+ client,
751
+ childRunPath,
752
+ existingChildRun,
753
+ parentRun,
754
+ logger2,
755
+ this[INTERNAL5].eventsDefinition
756
+ );
757
+ }
758
+ const { run: newChildRun } = await client.api.workflowRun.createV1({
759
+ workflowId: this.id,
760
+ workflowVersionId: this.versionId,
761
+ input,
762
+ path: childRunPath,
763
+ parentWorkflowRunId: parentRun.id,
764
+ options: {
765
+ ...this.params.opts,
766
+ idempotencyKey: childRunPath
767
+ }
768
+ });
769
+ parentRunHandle.run.childWorkflowRuns[childRunPath] = { id: newChildRun.id, statusWaitResults: [] };
770
+ const logger = parentRun.logger.child({
771
+ "aiki.childWorkflowId": newChildRun.workflowId,
772
+ "aiki.childWorkflowVersionId": newChildRun.workflowVersionId,
773
+ "aiki.childWorkflowRunId": newChildRun.id
774
+ });
775
+ return childWorkflowRunHandle(
776
+ client,
777
+ childRunPath,
778
+ newChildRun,
779
+ parentRun,
780
+ logger,
781
+ this[INTERNAL5].eventsDefinition
782
+ );
582
783
  }
583
784
  async getHandle(client, runId) {
584
- return workflowRunHandle(client, runId, this[INTERNAL4].eventsDefinition);
785
+ return workflowRunHandle(client, runId, this[INTERNAL5].eventsDefinition);
585
786
  }
586
787
  async handler(input, run, context) {
587
788
  const { logger } = run;
588
- const { handle } = run[INTERNAL4];
589
- handle[INTERNAL4].assertExecutionAllowed();
789
+ const { handle } = run[INTERNAL5];
790
+ handle[INTERNAL5].assertExecutionAllowed();
590
791
  const retryStrategy = this.params.opts?.retry ?? { type: "never" };
591
792
  const state = handle.run.state;
592
793
  if (state.status === "queued" && state.reason === "retry") {
593
794
  await this.assertRetryAllowed(handle, retryStrategy, logger);
594
795
  }
595
796
  logger.info("Starting workflow");
596
- await handle[INTERNAL4].transitionState({ status: "running" });
797
+ await handle[INTERNAL5].transitionState({ status: "running" });
597
798
  const output = await this.tryExecuteWorkflow(input, run, context, retryStrategy);
598
- await handle[INTERNAL4].transitionState({ status: "completed", output });
799
+ await handle[INTERNAL5].transitionState({ status: "completed", output });
599
800
  logger.info("Workflow complete");
600
801
  }
601
802
  async tryExecuteWorkflow(input, run, context, retryStrategy) {
@@ -603,15 +804,15 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
603
804
  try {
604
805
  return await this.params.handler(input, run, context);
605
806
  } catch (error) {
606
- if (error instanceof WorkflowRunSuspendedError3) {
807
+ if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2) {
607
808
  throw error;
608
809
  }
609
- const { handle } = run[INTERNAL4];
810
+ const { handle } = run[INTERNAL5];
610
811
  const attempts = handle.run.attempts;
611
812
  const retryParams = getRetryParams(attempts, retryStrategy);
612
813
  if (!retryParams.retriesLeft) {
613
814
  const failedState = this.createFailedState(error);
614
- await handle[INTERNAL4].transitionState(failedState);
815
+ await handle[INTERNAL5].transitionState(failedState);
615
816
  const logMeta2 = {};
616
817
  for (const [key, value] of Object.entries(failedState)) {
617
818
  logMeta2[`aiki.${key}`] = value;
@@ -623,7 +824,7 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
623
824
  throw new WorkflowRunFailedError2(run.id, attempts);
624
825
  }
625
826
  const awaitingRetryState = this.createAwaitingRetryState(error, retryParams.delayMs);
626
- await handle[INTERNAL4].transitionState(awaitingRetryState);
827
+ await handle[INTERNAL5].transitionState(awaitingRetryState);
627
828
  const logMeta = {};
628
829
  for (const [key, value] of Object.entries(awaitingRetryState)) {
629
830
  logMeta[`aiki.${key}`] = value;
@@ -633,7 +834,7 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
633
834
  "aiki.delayMs": retryParams.delayMs,
634
835
  ...logMeta
635
836
  });
636
- throw new WorkflowRunSuspendedError3(run.id);
837
+ throw new WorkflowRunSuspendedError4(run.id);
637
838
  }
638
839
  }
639
840
  }
@@ -644,7 +845,7 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
644
845
  logger.error("Workflow retry not allowed", { "aiki.attempts": attempts });
645
846
  const error = new WorkflowRunFailedError2(id, attempts);
646
847
  const serializableError = createSerializableError(error);
647
- await handle[INTERNAL4].transitionState({
848
+ await handle[INTERNAL5].transitionState({
648
849
  status: "failed",
649
850
  cause: "self",
650
851
  error: serializableError
@@ -684,6 +885,11 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
684
885
  error: serializableError
685
886
  };
686
887
  }
888
+ async getPath(input) {
889
+ const inputHash = await sha256(stableStringify(input));
890
+ const path = this.params.opts?.idempotencyKey ? `${this.id}/${this.versionId}/${inputHash}/${this.params.opts.idempotencyKey}` : `${this.id}/${this.versionId}/${inputHash}`;
891
+ return path;
892
+ }
687
893
  };
688
894
 
689
895
  // workflow.ts
@@ -692,11 +898,11 @@ function workflow(params) {
692
898
  }
693
899
  var WorkflowImpl = class {
694
900
  id;
695
- [INTERNAL5];
901
+ [INTERNAL6];
696
902
  workflowVersions = /* @__PURE__ */ new Map();
697
903
  constructor(params) {
698
904
  this.id = params.id;
699
- this[INTERNAL5] = {
905
+ this[INTERNAL6] = {
700
906
  getAllVersions: this.getAllVersions.bind(this),
701
907
  getVersion: this.getVersion.bind(this)
702
908
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/workflow",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Workflow SDK for Aiki - define durable workflows with tasks, sleeps, waits, and event handling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "build": "tsup"
19
19
  },
20
20
  "dependencies": {
21
- "@aikirun/types": "0.6.0"
21
+ "@aikirun/types": "0.7.0"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"