@aikirun/workflow 0.16.0 → 0.18.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/README.md CHANGED
@@ -32,6 +32,7 @@ Run with a client:
32
32
  ```typescript
33
33
  import { client } from "@aikirun/client";
34
34
 
35
+ // Set AIKI_API_KEY env variable or pass apiKey option
35
36
  const aikiClient = client({
36
37
  url: "http://localhost:9850",
37
38
  redis: { host: "localhost", port: 6379 },
package/dist/index.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { WorkflowName, 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, TerminalWorkflowRunStatus, WorkflowRunState, WorkflowRunId, WorkflowStartOptions, WorkflowDefinitionOptions } from '@aikirun/types/workflow-run';
4
+ import { WorkflowRun, TerminalWorkflowRunStatus, WorkflowRunState, WorkflowRunId, WorkflowRunAddress, ChildWorkflowRunInfo, WorkflowStartOptions, WorkflowDefinitionOptions } from '@aikirun/types/workflow-run';
5
5
  import { StandardSchemaV1 } from '@standard-schema/spec';
6
6
  import { DurationObject, Duration } from '@aikirun/types/duration';
7
7
  import { SleepResult } from '@aikirun/types/sleep';
8
- import { EventSendOptions, EventWaitOptions, EventWaitState } from '@aikirun/types/event';
8
+ import { EventSendOptions, EventWaitOptions, EventWaitResult } from '@aikirun/types/event';
9
9
  import { Serializable } from '@aikirun/types/serializable';
10
10
  import { DistributiveOmit, RequireAtLeastOneProp } from '@aikirun/types/utils';
11
- import { TaskId } from '@aikirun/types/task';
11
+ import { TaskInfo, TaskAddress } from '@aikirun/types/task';
12
12
  import { WorkflowRunStateRequest, WorkflowRunTransitionTaskStateRequestV1 } from '@aikirun/types/workflow-run-api';
13
- import { OverlapPolicy, ScheduleActivateOptions, ScheduleId } from '@aikirun/types/schedule';
13
+ import { ScheduleOverlapPolicy, ScheduleActivateOptions, ScheduleId } from '@aikirun/types/schedule';
14
14
 
15
15
  type NonEmptyArray<T> = [T, ...T[]];
16
16
 
@@ -19,10 +19,7 @@ type IsSubtype<SubT, SuperT> = SubT extends SuperT ? true : false;
19
19
  type And<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? false extends First ? false : Rest extends NonEmptyArray<boolean> ? And<Rest> : true : never;
20
20
  type Or<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? true extends First ? true : Rest extends NonEmptyArray<boolean> ? Or<Rest> : false : never;
21
21
  type PathFromObject<T, IncludeArrayKeys extends boolean = false> = T extends T ? PathFromObjectInternal<T, IncludeArrayKeys> : never;
22
- type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[
23
- IsSubtype<T, object>,
24
- Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>
25
- ]> extends true ? {
22
+ type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[IsSubtype<T, object>, Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>]> extends true ? {
26
23
  [K in Exclude<keyof T, symbol>]-?: And<[
27
24
  IsSubtype<NonNullable<T[K]>, object>,
28
25
  Or<[IncludeArrayKeys, NonArrayObject<NonNullable<T[K]>> extends never ? false : true]>
@@ -93,9 +90,7 @@ interface WorkflowRunHandle<Input, Output, AppContext, TEvents extends EventsDef
93
90
  [INTERNAL]: {
94
91
  client: Client<AppContext>;
95
92
  transitionState: (state: WorkflowRunStateRequest) => Promise<void>;
96
- transitionTaskState: (request: DistributiveOmit<WorkflowRunTransitionTaskStateRequestV1, "id" | "expectedRevision">) => Promise<{
97
- taskId: TaskId;
98
- }>;
93
+ transitionTaskState: (request: DistributiveOmit<WorkflowRunTransitionTaskStateRequestV1, "id" | "expectedWorkflowRunRevision">) => Promise<TaskInfo>;
99
94
  assertExecutionAllowed: () => void;
100
95
  };
101
96
  }
@@ -151,8 +146,8 @@ type EventWaiters<TEvents extends EventsDefinition> = {
151
146
  [K in keyof TEvents]: EventWaiter<EventData<TEvents[K]>>;
152
147
  };
153
148
  interface EventWaiter<Data> {
154
- wait(options?: EventWaitOptions<false>): Promise<EventWaitState<Data, false>>;
155
- wait(options: EventWaitOptions<true>): Promise<EventWaitState<Data, true>>;
149
+ wait(options?: EventWaitOptions<false>): Promise<EventWaitResult<Data, false>>;
150
+ wait(options: EventWaitOptions<true>): Promise<EventWaitResult<Data, true>>;
156
151
  }
157
152
  type EventSenders<TEvents extends EventsDefinition> = {
158
153
  [K in keyof TEvents]: EventSender<EventData<TEvents[K]>>;
@@ -171,13 +166,29 @@ type EventMulticasters<TEvents extends EventsDefinition> = {
171
166
  interface EventMulticaster<Data> {
172
167
  with(): EventMulticasterBuilder<Data>;
173
168
  send: <AppContext>(client: Client<AppContext>, runId: string | string[], ...args: Data extends void ? [] : [Data]) => Promise<void>;
169
+ sendByReferenceId: <AppContext>(client: Client<AppContext>, referenceId: string | string[], ...args: Data extends void ? [] : [Data]) => Promise<void>;
174
170
  }
175
171
  interface EventMulticasterBuilder<Data> {
176
172
  opt<Path extends PathFromObject<EventSendOptions>>(path: Path, value: TypeOfValueAtPath<EventSendOptions, Path>): EventMulticasterBuilder<Data>;
177
173
  send: <AppContext>(client: Client<AppContext>, runId: string | string[], ...args: Data extends void ? [] : [Data]) => Promise<void>;
174
+ sendByReferenceId: <AppContext>(client: Client<AppContext>, referenceId: string | string[], ...args: Data extends void ? [] : [Data]) => Promise<void>;
178
175
  }
179
176
  declare function createEventWaiters<TEvents extends EventsDefinition>(handle: WorkflowRunHandle<unknown, unknown, unknown, TEvents>, eventsDefinition: TEvents, logger: Logger): EventWaiters<TEvents>;
180
- declare function createEventSenders<TEvents extends EventsDefinition>(api: ApiClient, workflowRunId: string, eventsDefinition: TEvents, logger: Logger, onSend: (run: WorkflowRun<unknown, unknown>) => void): EventSenders<TEvents>;
177
+ declare function createEventSenders<TEvents extends EventsDefinition>(api: ApiClient, workflowRunId: string, eventsDefinition: TEvents, logger: Logger): EventSenders<TEvents>;
178
+
179
+ /** biome-ignore-all lint/style/noNonNullAssertion: Manifest boundaries are tracked, hence, we never exceed array boundaries */
180
+
181
+ interface ReplayManifest {
182
+ consumeNextTask(address: TaskAddress): TaskInfo | undefined;
183
+ consumeNextChildWorkflowRun(address: WorkflowRunAddress): ChildWorkflowRunInfo | undefined;
184
+ hasUnconsumedEntries(): boolean;
185
+ getUnconsumedEntries(): UnconsumedManifestEntries;
186
+ }
187
+ interface UnconsumedManifestEntries {
188
+ taskIds: string[];
189
+ childWorkflowRunIds: string[];
190
+ }
191
+ declare function createReplayManifest(run: WorkflowRun): ReplayManifest;
181
192
 
182
193
  interface WorkflowRunContext<Input, AppContext, TEvents extends EventsDefinition> {
183
194
  id: WorkflowRunId;
@@ -189,6 +200,7 @@ interface WorkflowRunContext<Input, AppContext, TEvents extends EventsDefinition
189
200
  events: EventWaiters<TEvents>;
190
201
  [INTERNAL]: {
191
202
  handle: WorkflowRunHandle<Input, unknown, AppContext, TEvents>;
203
+ replayManifest: ReplayManifest;
192
204
  options: {
193
205
  spinThresholdMs: number;
194
206
  };
@@ -275,7 +287,7 @@ declare class WorkflowVersionImpl<Input, Output, AppContext, TEvents extends Eve
275
287
  startWithOpts(client: Client<AppContext>, startOpts: WorkflowStartOptions, ...args: Input extends void ? [] : [Input]): Promise<WorkflowRunHandle<Input, Output, AppContext, TEvents>>;
276
288
  startAsChild(parentRun: WorkflowRunContext<unknown, AppContext, EventsDefinition>, ...args: Input extends void ? [] : [Input]): Promise<ChildWorkflowRunHandle<Input, Output, AppContext, TEvents>>;
277
289
  startAsChildWithOpts(parentRun: WorkflowRunContext<unknown, AppContext, EventsDefinition>, startOpts: WorkflowStartOptions, ...args: Input extends void ? [] : [Input]): Promise<ChildWorkflowRunHandle<Input, Output, AppContext, TEvents>>;
278
- private assertUniqueChildRunReferenceId;
290
+ private throwNonDeterminismError;
279
291
  getHandleById(client: Client<AppContext>, runId: string): Promise<WorkflowRunHandle<Input, Output, AppContext, TEvents>>;
280
292
  getHandleByReferenceId(client: Client<AppContext>, referenceId: string): Promise<WorkflowRunHandle<Input, Output, AppContext, TEvents>>;
281
293
  private handler;
@@ -309,12 +321,12 @@ interface CronScheduleParams {
309
321
  type: "cron";
310
322
  expression: string;
311
323
  timezone?: string;
312
- overlapPolicy?: OverlapPolicy;
324
+ overlapPolicy?: ScheduleOverlapPolicy;
313
325
  }
314
326
  interface IntervalScheduleParams {
315
327
  type: "interval";
316
328
  every: DurationObject;
317
- overlapPolicy?: OverlapPolicy;
329
+ overlapPolicy?: ScheduleOverlapPolicy;
318
330
  }
319
331
  type ScheduleParams = CronScheduleParams | IntervalScheduleParams;
320
332
  interface ScheduleHandle {
@@ -393,4 +405,4 @@ interface Workflow {
393
405
  };
394
406
  }
395
407
 
396
- export { type EventMulticaster, type EventMulticasters, type EventSender, type EventSenders, type EventWaiter, type EventWaiters, type ScheduleDefinition, type ScheduleHandle, type ScheduleParams, type Workflow, type WorkflowParams, type WorkflowRegistry, type WorkflowRunContext, type WorkflowRunHandle, type WorkflowRunWaitOptions, type WorkflowVersion, WorkflowVersionImpl, type WorkflowVersionParams, createEventSenders, createEventWaiters, createSleeper, event, schedule, workflow, workflowRegistry, workflowRunHandle };
408
+ export { type EventDefinition, type EventMulticaster, type EventMulticasters, type EventSender, type EventSenders, type EventWaiter, type EventWaiters, type ReplayManifest, type ScheduleDefinition, type ScheduleHandle, type ScheduleParams, type Workflow, type WorkflowParams, type WorkflowRegistry, type WorkflowRunContext, type WorkflowRunHandle, type WorkflowRunWaitOptions, type WorkflowVersion, WorkflowVersionImpl, type WorkflowVersionParams, createEventSenders, createEventWaiters, createReplayManifest, createSleeper, event, schedule, workflow, workflowRegistry, workflowRunHandle };
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ var WorkflowRegistryImpl = class {
11
11
  return this;
12
12
  }
13
13
  if (workflows.has(workflow2.versionId)) {
14
- throw new Error(`Workflow "${workflow2.name}/${workflow2.versionId}" is already registered`);
14
+ throw new Error(`Workflow "${workflow2.name}:${workflow2.versionId}" is already registered`);
15
15
  }
16
16
  workflows.set(workflow2.versionId, workflow2);
17
17
  return this;
@@ -55,7 +55,7 @@ var WorkflowRegistryImpl = class {
55
55
 
56
56
  // ../../lib/array/utils.ts
57
57
  function isNonEmptyArray(value) {
58
- return value.length > 0;
58
+ return value !== void 0 && value.length > 0;
59
59
  }
60
60
 
61
61
  // ../../lib/async/delay.ts
@@ -77,6 +77,9 @@ function delay(ms, options) {
77
77
  });
78
78
  }
79
79
 
80
+ // ../../lib/crypto/hash.ts
81
+ import { createHash } from "crypto";
82
+
80
83
  // ../../lib/json/stable-stringify.ts
81
84
  function stableStringify(value) {
82
85
  return stringifyValue(value);
@@ -317,20 +320,20 @@ function createEventWaiters(handle, eventsDefinition, logger) {
317
320
  return waiters;
318
321
  }
319
322
  function createEventWaiter(handle, eventName, schema, logger) {
320
- let nextEventIndex = 0;
323
+ let nextIndex = 0;
321
324
  async function wait(options) {
322
325
  await handle.refresh();
323
- const events = handle.run.eventsQueue[eventName]?.events ?? [];
324
- const event2 = events[nextEventIndex];
325
- if (event2) {
326
- nextEventIndex++;
327
- if (event2.status === "timeout") {
326
+ const eventWaits = handle.run.eventWaitQueues[eventName]?.eventWaits ?? [];
327
+ const existingEventWait = eventWaits[nextIndex];
328
+ if (existingEventWait) {
329
+ nextIndex++;
330
+ if (existingEventWait.status === "timeout") {
328
331
  logger.debug("Timed out waiting for event");
329
332
  return { timeout: true };
330
333
  }
331
- let data = event2.data;
334
+ let data = existingEventWait.data;
332
335
  if (schema) {
333
- const schemaValidation = schema["~standard"].validate(event2.data);
336
+ const schemaValidation = schema["~standard"].validate(existingEventWait.data);
334
337
  const schemaValidationResult = schemaValidation instanceof Promise ? await schemaValidation : schemaValidation;
335
338
  if (!schemaValidationResult.issues) {
336
339
  data = schemaValidationResult.value;
@@ -370,7 +373,7 @@ function createEventWaiter(handle, eventName, schema, logger) {
370
373
  }
371
374
  return { wait };
372
375
  }
373
- function createEventSenders(api, workflowRunId, eventsDefinition, logger, onSend) {
376
+ function createEventSenders(api, workflowRunId, eventsDefinition, logger) {
374
377
  const senders = {};
375
378
  for (const [eventName, eventDefinition] of Object.entries(eventsDefinition)) {
376
379
  const sender = createEventSender(
@@ -378,18 +381,17 @@ function createEventSenders(api, workflowRunId, eventsDefinition, logger, onSend
378
381
  workflowRunId,
379
382
  eventName,
380
383
  eventDefinition.schema,
381
- logger.child({ "aiki.eventName": eventName }),
382
- onSend
384
+ logger.child({ "aiki.eventName": eventName })
383
385
  );
384
386
  senders[eventName] = sender;
385
387
  }
386
388
  return senders;
387
389
  }
388
- function createEventSender(api, workflowRunId, eventName, schema, logger, onSend, options) {
390
+ function createEventSender(api, workflowRunId, eventName, schema, logger, options) {
389
391
  const optsOverrider = objectOverrider(options ?? {});
390
392
  const createBuilder = (optsBuilder) => ({
391
393
  opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
392
- send: (...args) => createEventSender(api, workflowRunId, eventName, schema, logger, onSend, optsBuilder.build()).send(...args)
394
+ send: (...args) => createEventSender(api, workflowRunId, eventName, schema, logger, optsBuilder.build()).send(...args)
393
395
  });
394
396
  async function send(...args) {
395
397
  let data = args[0];
@@ -402,13 +404,12 @@ function createEventSender(api, workflowRunId, eventName, schema, logger, onSend
402
404
  }
403
405
  data = schemaValidationResult.value;
404
406
  }
405
- const { run } = await api.workflowRun.sendEventV1({
407
+ await api.workflowRun.sendEventV1({
406
408
  id: workflowRunId,
407
409
  eventName,
408
410
  data,
409
411
  options
410
412
  });
411
- onSend(run);
412
413
  logger.info("Sent event to workflow", {
413
414
  ...options?.reference ? { "aiki.referenceId": options.reference.id } : {}
414
415
  });
@@ -439,6 +440,11 @@ function createEventMulticaster(workflowName, workflowVersionId, eventName, sche
439
440
  client,
440
441
  runId,
441
442
  ...args
443
+ ),
444
+ sendByReferenceId: (client, referenceId, ...args) => createEventMulticaster(workflowName, workflowVersionId, eventName, schema, optsBuilder.build()).sendByReferenceId(
445
+ client,
446
+ referenceId,
447
+ ...args
442
448
  )
443
449
  });
444
450
  async function send(client, runId, ...args) {
@@ -472,12 +478,51 @@ function createEventMulticaster(workflowName, workflowVersionId, eventName, sche
472
478
  "aiki.workflowVersionId": workflowVersionId,
473
479
  "aiki.workflowRunIds": runIds,
474
480
  "aiki.eventName": eventName,
475
- ...options?.reference ? { "aiki.referenceId": options.reference.id } : {}
481
+ ...options?.reference ? { "aiki.eventReferenceId": options.reference.id } : {}
482
+ });
483
+ }
484
+ async function sendByReferenceId(client, referenceId, ...args) {
485
+ let data = args[0];
486
+ if (schema) {
487
+ const schemaValidation = schema["~standard"].validate(data);
488
+ const schemaValidationResult = schemaValidation instanceof Promise ? await schemaValidation : schemaValidation;
489
+ if (schemaValidationResult.issues) {
490
+ client.logger.error("Invalid event data", {
491
+ "aiki.workflowName": workflowName,
492
+ "aiki.workflowVersionId": workflowVersionId,
493
+ "aiki.eventName": eventName,
494
+ "aiki.issues": schemaValidationResult.issues
495
+ });
496
+ throw new SchemaValidationError("Invalid event data", schemaValidationResult.issues);
497
+ }
498
+ data = schemaValidationResult.value;
499
+ }
500
+ const referenceIds = Array.isArray(referenceId) ? referenceId : [referenceId];
501
+ if (!isNonEmptyArray(referenceIds)) {
502
+ return;
503
+ }
504
+ await client.api.workflowRun.multicastEventByReferenceV1({
505
+ references: referenceIds.map((referenceId2) => ({
506
+ name: workflowName,
507
+ versionId: workflowVersionId,
508
+ referenceId: referenceId2
509
+ })),
510
+ eventName,
511
+ data,
512
+ options
513
+ });
514
+ client.logger.info("Multicasted event by reference", {
515
+ "aiki.workflowName": workflowName,
516
+ "aiki.workflowVersionId": workflowVersionId,
517
+ "aiki.referenceIds": referenceIds,
518
+ "aiki.eventName": eventName,
519
+ ...options?.reference ? { "aiki.eventReferenceId": options.reference.id } : {}
476
520
  });
477
521
  }
478
522
  return {
479
523
  with: () => createBuilder(optsOverrider()),
480
- send
524
+ send,
525
+ sendByReferenceId
481
526
  };
482
527
  }
483
528
 
@@ -506,9 +551,7 @@ var WorkflowRunHandleImpl = class {
506
551
  this._run = _run;
507
552
  this.logger = logger;
508
553
  this.api = client.api;
509
- this.events = createEventSenders(client.api, this._run.id, eventsDefinition, this.logger, (run) => {
510
- this._run = run;
511
- });
554
+ this.events = createEventSenders(client.api, this._run.id, eventsDefinition, this.logger);
512
555
  this[INTERNAL2] = {
513
556
  client,
514
557
  transitionState: this.transitionState.bind(this),
@@ -604,24 +647,26 @@ var WorkflowRunHandleImpl = class {
604
647
  }
605
648
  async transitionState(targetState) {
606
649
  try {
650
+ let response;
607
651
  if (targetState.status === "scheduled" && (targetState.reason === "new" || targetState.reason === "resume" || targetState.reason === "awake_early") || targetState.status === "paused" || targetState.status === "cancelled") {
608
- const { run: run2 } = await this.api.workflowRun.transitionStateV1({
652
+ response = await this.api.workflowRun.transitionStateV1({
609
653
  type: "pessimistic",
610
654
  id: this.run.id,
611
655
  state: targetState
612
656
  });
613
- this._run = run2;
614
- return;
657
+ } else {
658
+ response = await this.api.workflowRun.transitionStateV1({
659
+ type: "optimistic",
660
+ id: this.run.id,
661
+ state: targetState,
662
+ expectedRevision: this.run.revision
663
+ });
615
664
  }
616
- const { run } = await this.api.workflowRun.transitionStateV1({
617
- type: "optimistic",
618
- id: this.run.id,
619
- state: targetState,
620
- expectedRevision: this.run.revision
621
- });
622
- this._run = run;
665
+ this._run.revision = response.revision;
666
+ this._run.state = response.state;
667
+ this._run.attempts = response.attempts;
623
668
  } catch (error) {
624
- if (isConflictError(error)) {
669
+ if (isWorkflowRunRevisionConflictError(error)) {
625
670
  throw new WorkflowRunRevisionConflictError2(this.run.id);
626
671
  }
627
672
  throw error;
@@ -629,15 +674,14 @@ var WorkflowRunHandleImpl = class {
629
674
  }
630
675
  async transitionTaskState(request) {
631
676
  try {
632
- const { run, taskId } = await this.api.workflowRun.transitionTaskStateV1({
677
+ const { taskInfo } = await this.api.workflowRun.transitionTaskStateV1({
633
678
  ...request,
634
679
  id: this.run.id,
635
- expectedRevision: this.run.revision
680
+ expectedWorkflowRunRevision: this.run.revision
636
681
  });
637
- this._run = run;
638
- return { taskId };
682
+ return taskInfo;
639
683
  } catch (error) {
640
- if (isConflictError(error)) {
684
+ if (isWorkflowRunRevisionConflictError(error)) {
641
685
  throw new WorkflowRunRevisionConflictError2(this.run.id);
642
686
  }
643
687
  throw error;
@@ -650,8 +694,73 @@ var WorkflowRunHandleImpl = class {
650
694
  }
651
695
  }
652
696
  };
653
- function isConflictError(error) {
654
- return error != null && typeof error === "object" && "code" in error && error.code === "CONFLICT";
697
+ function isWorkflowRunRevisionConflictError(error) {
698
+ return error != null && typeof error === "object" && "code" in error && error.code === "WORKFLOW_RUN_REVISION_CONFLICT";
699
+ }
700
+
701
+ // run/replay-manifest.ts
702
+ function createReplayManifest(run) {
703
+ const { taskQueues, childWorkflowRunQueues } = run;
704
+ let totalEntries = 0;
705
+ const taskCountByAddress = {};
706
+ const childWorkflowRunCountByAddress = {};
707
+ for (const [address, queue] of Object.entries(taskQueues)) {
708
+ taskCountByAddress[address] = queue.tasks.length;
709
+ totalEntries += queue.tasks.length;
710
+ }
711
+ for (const [address, queue] of Object.entries(childWorkflowRunQueues)) {
712
+ childWorkflowRunCountByAddress[address] = queue.childWorkflowRuns.length;
713
+ totalEntries += queue.childWorkflowRuns.length;
714
+ }
715
+ const nextTaskIndexByAddress = {};
716
+ const nextChildWorkflowRunIndexByAddress = {};
717
+ let consumedEntries = 0;
718
+ return {
719
+ consumeNextTask(address) {
720
+ const taskCount = taskCountByAddress[address] ?? 0;
721
+ const nextIndex = nextTaskIndexByAddress[address] ?? 0;
722
+ if (nextIndex >= taskCount) {
723
+ return void 0;
724
+ }
725
+ const task = taskQueues[address].tasks[nextIndex];
726
+ nextTaskIndexByAddress[address] = nextIndex + 1;
727
+ consumedEntries++;
728
+ return task;
729
+ },
730
+ consumeNextChildWorkflowRun(address) {
731
+ const childWorkflowRunCount = childWorkflowRunCountByAddress[address] ?? 0;
732
+ const nextIndex = nextChildWorkflowRunIndexByAddress[address] ?? 0;
733
+ if (nextIndex >= childWorkflowRunCount) {
734
+ return void 0;
735
+ }
736
+ const childWorkflowRun = childWorkflowRunQueues[address].childWorkflowRuns[nextIndex];
737
+ nextChildWorkflowRunIndexByAddress[address] = nextIndex + 1;
738
+ consumedEntries++;
739
+ return childWorkflowRun;
740
+ },
741
+ hasUnconsumedEntries() {
742
+ return consumedEntries < totalEntries;
743
+ },
744
+ getUnconsumedEntries() {
745
+ const taskIds = [];
746
+ const childWorkflowRunIds = [];
747
+ for (const [address, taskCount] of Object.entries(taskCountByAddress)) {
748
+ const tasks = taskQueues[address].tasks;
749
+ const nextIndex = nextTaskIndexByAddress[address] ?? 0;
750
+ for (let i = nextIndex; i < taskCount; i++) {
751
+ taskIds.push(tasks[i].id);
752
+ }
753
+ }
754
+ for (const [address, childWorkflowRunCount] of Object.entries(childWorkflowRunCountByAddress)) {
755
+ const childWorkflowRuns = childWorkflowRunQueues[address].childWorkflowRuns;
756
+ const nextIndex = nextChildWorkflowRunIndexByAddress[address] ?? 0;
757
+ for (let i = nextIndex; i < childWorkflowRunCount; i++) {
758
+ childWorkflowRunIds.push(childWorkflowRuns[i].id);
759
+ }
760
+ }
761
+ return { taskIds, childWorkflowRunIds };
762
+ }
763
+ };
655
764
  }
656
765
 
657
766
  // run/sleeper.ts
@@ -663,17 +772,17 @@ import {
663
772
  var MAX_SLEEP_YEARS = 10;
664
773
  var MAX_SLEEP_MS = MAX_SLEEP_YEARS * 365 * 24 * 60 * 60 * 1e3;
665
774
  function createSleeper(handle, logger) {
666
- const nextSleepIndexByName = {};
775
+ const nextIndexBySleepName = {};
667
776
  return async (name, duration) => {
668
777
  const sleepName = name;
669
778
  let durationMs = toMilliseconds(duration);
670
779
  if (durationMs > MAX_SLEEP_MS) {
671
780
  throw new Error(`Sleep duration ${durationMs}ms exceeds maximum of ${MAX_SLEEP_YEARS} years`);
672
781
  }
673
- const nextSleepIndex = nextSleepIndexByName[sleepName] ?? 0;
674
- const sleepQueue = handle.run.sleepsQueue[sleepName] ?? { sleeps: [] };
675
- const sleepState = sleepQueue.sleeps[nextSleepIndex];
676
- if (!sleepState) {
782
+ const nextIndex = nextIndexBySleepName[sleepName] ?? 0;
783
+ const sleepQueue = handle.run.sleepQueues[sleepName] ?? { sleeps: [] };
784
+ const existingSleep = sleepQueue.sleeps[nextIndex];
785
+ if (!existingSleep) {
677
786
  try {
678
787
  await handle[INTERNAL3].transitionState({ status: "sleeping", sleepName, durationMs });
679
788
  logger.info("Going to sleep", {
@@ -688,37 +797,37 @@ function createSleeper(handle, logger) {
688
797
  }
689
798
  throw new WorkflowRunSuspendedError2(handle.run.id);
690
799
  }
691
- if (sleepState.status === "sleeping") {
800
+ if (existingSleep.status === "sleeping") {
692
801
  logger.debug("Already sleeping", {
693
802
  "aiki.sleepName": sleepName,
694
- "aiki.awakeAt": sleepState.awakeAt
803
+ "aiki.awakeAt": existingSleep.awakeAt
695
804
  });
696
805
  throw new WorkflowRunSuspendedError2(handle.run.id);
697
806
  }
698
- sleepState.status;
699
- nextSleepIndexByName[sleepName] = nextSleepIndex + 1;
700
- if (sleepState.status === "cancelled") {
807
+ existingSleep.status;
808
+ nextIndexBySleepName[sleepName] = nextIndex + 1;
809
+ if (existingSleep.status === "cancelled") {
701
810
  logger.debug("Sleep cancelled", {
702
811
  "aiki.sleepName": sleepName,
703
- "aiki.cancelledAt": sleepState.cancelledAt
812
+ "aiki.cancelledAt": existingSleep.cancelledAt
704
813
  });
705
814
  return { cancelled: true };
706
815
  }
707
- if (durationMs === sleepState.durationMs) {
816
+ if (durationMs === existingSleep.durationMs) {
708
817
  logger.debug("Sleep completed", {
709
818
  "aiki.sleepName": sleepName,
710
819
  "aiki.durationMs": durationMs,
711
- "aiki.completedAt": sleepState.completedAt
820
+ "aiki.completedAt": existingSleep.completedAt
712
821
  });
713
822
  return { cancelled: false };
714
823
  }
715
- if (durationMs > sleepState.durationMs) {
824
+ if (durationMs > existingSleep.durationMs) {
716
825
  logger.warn("Higher sleep duration encountered during replay. Sleeping for remaining duration", {
717
826
  "aiki.sleepName": sleepName,
718
- "aiki.historicDurationMs": sleepState.durationMs,
827
+ "aiki.historicDurationMs": existingSleep.durationMs,
719
828
  "aiki.latestDurationMs": durationMs
720
829
  });
721
- durationMs -= sleepState.durationMs;
830
+ durationMs -= existingSleep.durationMs;
722
831
  } else {
723
832
  return { cancelled: false };
724
833
  }
@@ -804,7 +913,7 @@ import { INTERNAL as INTERNAL6 } from "@aikirun/types/symbols";
804
913
 
805
914
  // ../../lib/address/index.ts
806
915
  function getWorkflowRunAddress(name, versionId, referenceId) {
807
- return `${name}/${versionId}/${referenceId}`;
916
+ return `${name}:${versionId}:${referenceId}`;
808
917
  }
809
918
 
810
919
  // workflow-version.ts
@@ -812,6 +921,7 @@ import { INTERNAL as INTERNAL5 } from "@aikirun/types/symbols";
812
921
  import { TaskFailedError } from "@aikirun/types/task";
813
922
  import { SchemaValidationError as SchemaValidationError2 } from "@aikirun/types/validator";
814
923
  import {
924
+ NonDeterminismError,
815
925
  WorkflowRunFailedError as WorkflowRunFailedError2,
816
926
  WorkflowRunRevisionConflictError as WorkflowRunRevisionConflictError5,
817
927
  WorkflowRunSuspendedError as WorkflowRunSuspendedError4
@@ -824,13 +934,13 @@ import {
824
934
  WorkflowRunRevisionConflictError as WorkflowRunRevisionConflictError4,
825
935
  WorkflowRunSuspendedError as WorkflowRunSuspendedError3
826
936
  } from "@aikirun/types/workflow-run";
827
- async function childWorkflowRunHandle(client, run, parentRun, logger, eventsDefinition) {
937
+ async function childWorkflowRunHandle(client, run, parentRun, childWorkflowRunWaitQueues, logger, eventsDefinition) {
828
938
  const handle = await workflowRunHandle(client, run, eventsDefinition, logger);
829
939
  return {
830
940
  run: handle.run,
831
941
  events: handle.events,
832
942
  refresh: handle.refresh.bind(handle),
833
- waitForStatus: createStatusWaiter(handle, parentRun, logger),
943
+ waitForStatus: createStatusWaiter(handle, parentRun, childWorkflowRunWaitQueues, logger),
834
944
  cancel: handle.cancel.bind(handle),
835
945
  pause: handle.pause.bind(handle),
836
946
  resume: handle.resume.bind(handle),
@@ -838,15 +948,21 @@ async function childWorkflowRunHandle(client, run, parentRun, logger, eventsDefi
838
948
  [INTERNAL4]: handle[INTERNAL4]
839
949
  };
840
950
  }
841
- function createStatusWaiter(handle, parentRun, logger) {
842
- let nextWaitIndex = 0;
951
+ function createStatusWaiter(handle, parentRun, childWorkflowRunWaitQueues, logger) {
952
+ const nextIndexByStatus = {
953
+ cancelled: 0,
954
+ completed: 0,
955
+ failed: 0
956
+ };
843
957
  async function waitForStatus(expectedStatus, options) {
844
958
  const parentRunHandle = parentRun[INTERNAL4].handle;
845
- const waitResults = parentRunHandle.run.childWorkflowRuns[handle.run.address]?.statusWaitResults ?? [];
846
- const waitResult = waitResults[nextWaitIndex];
847
- if (waitResult) {
848
- nextWaitIndex++;
849
- if (waitResult.status === "timeout") {
959
+ const nextIndex = nextIndexByStatus[expectedStatus];
960
+ const { run } = handle;
961
+ const childWorkflowRunWaits = childWorkflowRunWaitQueues[expectedStatus].childWorkflowRunWaits;
962
+ const existingChildWorkflowRunWait = childWorkflowRunWaits[nextIndex];
963
+ if (existingChildWorkflowRunWait) {
964
+ nextIndexByStatus[expectedStatus] = nextIndex + 1;
965
+ if (existingChildWorkflowRunWait.status === "timeout") {
850
966
  logger.debug("Timed out waiting for child workflow status", {
851
967
  "aiki.childWorkflowExpectedStatus": expectedStatus
852
968
  });
@@ -855,43 +971,29 @@ function createStatusWaiter(handle, parentRun, logger) {
855
971
  cause: "timeout"
856
972
  };
857
973
  }
858
- if (waitResult.childWorkflowRunState.status === expectedStatus) {
974
+ const childWorkflowRunStatus = existingChildWorkflowRunWait.childWorkflowRunState.status;
975
+ if (childWorkflowRunStatus === expectedStatus) {
859
976
  return {
860
977
  success: true,
861
- state: waitResult.childWorkflowRunState
978
+ state: existingChildWorkflowRunWait.childWorkflowRunState
862
979
  };
863
980
  }
864
- if (isTerminalWorkflowRunStatus2(waitResult.childWorkflowRunState.status)) {
981
+ if (isTerminalWorkflowRunStatus2(childWorkflowRunStatus)) {
865
982
  logger.debug("Child workflow run reached termnial state", {
866
- "aiki.childWorkflowTerminalStatus": waitResult.childWorkflowRunState.status
983
+ "aiki.childWorkflowTerminalStatus": childWorkflowRunStatus
867
984
  });
868
985
  return {
869
986
  success: false,
870
987
  cause: "run_terminated"
871
988
  };
872
989
  }
873
- }
874
- const { state } = handle.run;
875
- if (state.status === expectedStatus) {
876
- return {
877
- success: true,
878
- state
879
- };
880
- }
881
- if (isTerminalWorkflowRunStatus2(state.status)) {
882
- logger.debug("Child workflow run reached termnial state", {
883
- "aiki.childWorkflowTerminalStatus": state.status
884
- });
885
- return {
886
- success: false,
887
- cause: "run_terminated"
888
- };
990
+ childWorkflowRunStatus;
889
991
  }
890
992
  const timeoutInMs = options?.timeout && toMilliseconds(options.timeout);
891
993
  try {
892
994
  await parentRunHandle[INTERNAL4].transitionState({
893
995
  status: "awaiting_child_workflow",
894
- childWorkflowRunId: handle.run.id,
996
+ childWorkflowRunId: run.id,
895
997
  childWorkflowRunStatus: expectedStatus,
896
998
  timeoutInMs
897
999
  });
@@ -945,7 +1047,7 @@ var WorkflowVersionImpl = class {
945
1047
  }
946
1048
  input = schemaValidationResult.value;
947
1049
  }
948
- const { run } = await client.api.workflowRun.createV1({
1050
+ const { id } = await client.api.workflowRun.createV1({
949
1051
  name: this.name,
950
1052
  versionId: this.versionId,
951
1053
  input,
@@ -954,9 +1056,9 @@ var WorkflowVersionImpl = class {
954
1056
  client.logger.info("Created workflow", {
955
1057
  "aiki.workflowName": this.name,
956
1058
  "aiki.workflowVersionId": this.versionId,
957
- "aiki.workflowRunId": run.id
1059
+ "aiki.workflowRunId": id
958
1060
  });
959
- return workflowRunHandle(client, run, this[INTERNAL5].eventsDefinition);
1061
+ return workflowRunHandle(client, id, this[INTERNAL5].eventsDefinition);
960
1062
  }
961
1063
  async startAsChild(parentRun, ...args) {
962
1064
  return this.startAsChildWithOpts(parentRun, this.params.opts ?? {}, ...args);
@@ -968,48 +1070,41 @@ var WorkflowVersionImpl = class {
968
1070
  const inputRaw = args[0];
969
1071
  const input = await this.parse(parentRunHandle, this.params.schema?.input, inputRaw, parentRun.logger);
970
1072
  const inputHash = await hashInput(input);
971
- const reference = startOpts.reference;
972
- const address = getWorkflowRunAddress(this.name, this.versionId, reference?.id ?? inputHash);
973
- const existingRunInfo = parentRunHandle.run.childWorkflowRuns[address];
974
- if (existingRunInfo) {
975
- await this.assertUniqueChildRunReferenceId(
976
- parentRunHandle,
977
- existingRunInfo,
978
- inputHash,
979
- reference,
980
- parentRun.logger
981
- );
982
- const { run: existingRun } = await client.api.workflowRun.getByIdV1({ id: existingRunInfo.id });
983
- if (existingRun.state.status === "completed") {
984
- await this.parse(parentRunHandle, this.params.schema?.output, existingRun.state.output, parentRun.logger);
1073
+ const referenceId = startOpts.reference?.id;
1074
+ const address = getWorkflowRunAddress(this.name, this.versionId, referenceId ?? inputHash);
1075
+ const replayManifest = parentRun[INTERNAL5].replayManifest;
1076
+ if (replayManifest.hasUnconsumedEntries()) {
1077
+ const existingRunInfo = replayManifest.consumeNextChildWorkflowRun(address);
1078
+ if (existingRunInfo) {
1079
+ const { run: existingRun } = await client.api.workflowRun.getByIdV1({ id: existingRunInfo.id });
1080
+ if (existingRun.state.status === "completed") {
1081
+ await this.parse(parentRunHandle, this.params.schema?.output, existingRun.state.output, parentRun.logger);
1082
+ }
1083
+ const logger2 = parentRun.logger.child({
1084
+ "aiki.childWorkflowName": existingRun.name,
1085
+ "aiki.childWorkflowVersionId": existingRun.versionId,
1086
+ "aiki.childWorkflowRunId": existingRun.id
1087
+ });
1088
+ return childWorkflowRunHandle(
1089
+ client,
1090
+ existingRun,
1091
+ parentRun,
1092
+ existingRunInfo.childWorkflowRunWaitQueues,
1093
+ logger2,
1094
+ this[INTERNAL5].eventsDefinition
1095
+ );
985
1096
  }
986
- const logger2 = parentRun.logger.child({
987
- "aiki.childWorkflowName": existingRun.name,
988
- "aiki.childWorkflowVersionId": existingRun.versionId,
989
- "aiki.childWorkflowRunId": existingRun.id
990
- });
991
- return childWorkflowRunHandle(
992
- client,
993
- existingRun,
994
- parentRun,
995
- logger2,
996
- this[INTERNAL5].eventsDefinition
997
- );
1097
+ await this.throwNonDeterminismError(parentRun, parentRunHandle, inputHash, referenceId, replayManifest);
998
1098
  }
999
- const { run: newRun } = await client.api.workflowRun.createV1({
1099
+ const shard = parentRun.options.shard;
1100
+ const { id: newRunId } = await client.api.workflowRun.createV1({
1000
1101
  name: this.name,
1001
1102
  versionId: this.versionId,
1002
1103
  input,
1003
1104
  parentWorkflowRunId: parentRun.id,
1004
- options: startOpts
1105
+ options: shard === void 0 ? startOpts : { ...startOpts, shard }
1005
1106
  });
1006
- parentRunHandle.run.childWorkflowRuns[address] = {
1007
- id: newRun.id,
1008
- name: newRun.name,
1009
- versionId: newRun.versionId,
1010
- inputHash,
1011
- statusWaitResults: []
1012
- };
1107
+ const { run: newRun } = await client.api.workflowRun.getByIdV1({ id: newRunId });
1013
1108
  const logger = parentRun.logger.child({
1014
1109
  "aiki.childWorkflowName": newRun.name,
1015
1110
  "aiki.childWorkflowVersionId": newRun.versionId,
@@ -1020,32 +1115,33 @@ var WorkflowVersionImpl = class {
1020
1115
  client,
1021
1116
  newRun,
1022
1117
  parentRun,
1118
+ {
1119
+ cancelled: { childWorkflowRunWaits: [] },
1120
+ completed: { childWorkflowRunWaits: [] },
1121
+ failed: { childWorkflowRunWaits: [] }
1122
+ },
1023
1123
  logger,
1024
1124
  this[INTERNAL5].eventsDefinition
1025
1125
  );
1026
1126
  }
1027
- async assertUniqueChildRunReferenceId(parentRunHandle, existingRunInfo, inputHash, reference, logger) {
1028
- if (existingRunInfo.inputHash !== inputHash && reference) {
1029
- const conflictPolicy = reference.conflictPolicy ?? "error";
1030
- if (conflictPolicy !== "error") {
1031
- return;
1032
- }
1033
- logger.error("Reference ID already used by another child workflow", {
1034
- "aiki.referenceId": reference.id,
1035
- "aiki.existingChildWorkflowRunId": existingRunInfo.id
1036
- });
1037
- const error = new WorkflowRunFailedError2(
1038
- parentRunHandle.run.id,
1039
- parentRunHandle.run.attempts,
1040
- `Reference ID "${reference.id}" already used by another child workflow run ${existingRunInfo.id}`
1041
- );
1042
- await parentRunHandle[INTERNAL5].transitionState({
1043
- status: "failed",
1044
- cause: "self",
1045
- error: createSerializableError(error)
1046
- });
1047
- throw error;
1127
+ async throwNonDeterminismError(parentRun, parentRunHandle, inputHash, referenceId, manifest) {
1128
+ const unconsumedManifestEntries = manifest.getUnconsumedEntries();
1129
+ const logMeta = {
1130
+ "aiki.workflowName": this.name,
1131
+ "aiki.inputHash": inputHash,
1132
+ "aiki.unconsumedManifestEntries": unconsumedManifestEntries
1133
+ };
1134
+ if (referenceId !== void 0) {
1135
+ logMeta["aiki.referenceId"] = referenceId;
1048
1136
  }
1137
+ parentRun.logger.error("Replay divergence", logMeta);
1138
+ const error = new NonDeterminismError(parentRun.id, parentRunHandle.run.attempts, unconsumedManifestEntries);
1139
+ await parentRunHandle[INTERNAL5].transitionState({
1140
+ status: "failed",
1141
+ cause: "self",
1142
+ error: createSerializableError(error)
1143
+ });
1144
+ throw error;
1049
1145
  }
1050
1146
  async getHandleById(client, runId) {
1051
1147
  return workflowRunHandle(client, runId, this[INTERNAL5].eventsDefinition);
@@ -1081,7 +1177,7 @@ var WorkflowVersionImpl = class {
1081
1177
  const output = await this.parse(handle, this.params.schema?.output, outputRaw, run.logger);
1082
1178
  return output;
1083
1179
  } catch (error) {
1084
- if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2 || error instanceof WorkflowRunRevisionConflictError5) {
1180
+ if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2 || error instanceof WorkflowRunRevisionConflictError5 || error instanceof NonDeterminismError) {
1085
1181
  throw error;
1086
1182
  }
1087
1183
  const attempts = handle.run.attempts;
@@ -1211,7 +1307,7 @@ var WorkflowImpl = class {
1211
1307
  }
1212
1308
  v(versionId, params) {
1213
1309
  if (this.workflowVersions.has(versionId)) {
1214
- throw new Error(`Workflow "${this.name}/${versionId}" already exists`);
1310
+ throw new Error(`Workflow "${this.name}:${versionId}" already exists`);
1215
1311
  }
1216
1312
  const workflowVersion = new WorkflowVersionImpl(this.name, versionId, params);
1217
1313
  this.workflowVersions.set(
@@ -1231,6 +1327,7 @@ export {
1231
1327
  WorkflowVersionImpl,
1232
1328
  createEventSenders,
1233
1329
  createEventWaiters,
1330
+ createReplayManifest,
1234
1331
  createSleeper,
1235
1332
  event,
1236
1333
  schedule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/workflow",
3
- "version": "0.16.0",
3
+ "version": "0.18.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.16.0",
21
+ "@aikirun/types": "0.18.0",
22
22
  "@standard-schema/spec": "^1.1.0"
23
23
  },
24
24
  "publishConfig": {