@aikirun/workflow 0.17.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,14 +1,14 @@
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
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
13
  import { ScheduleOverlapPolicy, ScheduleActivateOptions, ScheduleId } from '@aikirun/types/schedule';
14
14
 
@@ -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
  }
@@ -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;
@@ -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
@@ -320,20 +320,20 @@ function createEventWaiters(handle, eventsDefinition, logger) {
320
320
  return waiters;
321
321
  }
322
322
  function createEventWaiter(handle, eventName, schema, logger) {
323
- let nextEventIndex = 0;
323
+ let nextIndex = 0;
324
324
  async function wait(options) {
325
325
  await handle.refresh();
326
326
  const eventWaits = handle.run.eventWaitQueues[eventName]?.eventWaits ?? [];
327
- const eventWait = eventWaits[nextEventIndex];
328
- if (eventWait) {
329
- nextEventIndex++;
330
- if (eventWait.status === "timeout") {
327
+ const existingEventWait = eventWaits[nextIndex];
328
+ if (existingEventWait) {
329
+ nextIndex++;
330
+ if (existingEventWait.status === "timeout") {
331
331
  logger.debug("Timed out waiting for event");
332
332
  return { timeout: true };
333
333
  }
334
- let data = eventWait.data;
334
+ let data = existingEventWait.data;
335
335
  if (schema) {
336
- const schemaValidation = schema["~standard"].validate(eventWait.data);
336
+ const schemaValidation = schema["~standard"].validate(existingEventWait.data);
337
337
  const schemaValidationResult = schemaValidation instanceof Promise ? await schemaValidation : schemaValidation;
338
338
  if (!schemaValidationResult.issues) {
339
339
  data = schemaValidationResult.value;
@@ -373,7 +373,7 @@ function createEventWaiter(handle, eventName, schema, logger) {
373
373
  }
374
374
  return { wait };
375
375
  }
376
- function createEventSenders(api, workflowRunId, eventsDefinition, logger, onSend) {
376
+ function createEventSenders(api, workflowRunId, eventsDefinition, logger) {
377
377
  const senders = {};
378
378
  for (const [eventName, eventDefinition] of Object.entries(eventsDefinition)) {
379
379
  const sender = createEventSender(
@@ -381,18 +381,17 @@ function createEventSenders(api, workflowRunId, eventsDefinition, logger, onSend
381
381
  workflowRunId,
382
382
  eventName,
383
383
  eventDefinition.schema,
384
- logger.child({ "aiki.eventName": eventName }),
385
- onSend
384
+ logger.child({ "aiki.eventName": eventName })
386
385
  );
387
386
  senders[eventName] = sender;
388
387
  }
389
388
  return senders;
390
389
  }
391
- function createEventSender(api, workflowRunId, eventName, schema, logger, onSend, options) {
390
+ function createEventSender(api, workflowRunId, eventName, schema, logger, options) {
392
391
  const optsOverrider = objectOverrider(options ?? {});
393
392
  const createBuilder = (optsBuilder) => ({
394
393
  opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
395
- 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)
396
395
  });
397
396
  async function send(...args) {
398
397
  let data = args[0];
@@ -405,13 +404,12 @@ function createEventSender(api, workflowRunId, eventName, schema, logger, onSend
405
404
  }
406
405
  data = schemaValidationResult.value;
407
406
  }
408
- const { run } = await api.workflowRun.sendEventV1({
407
+ await api.workflowRun.sendEventV1({
409
408
  id: workflowRunId,
410
409
  eventName,
411
410
  data,
412
411
  options
413
412
  });
414
- onSend(run);
415
413
  logger.info("Sent event to workflow", {
416
414
  ...options?.reference ? { "aiki.referenceId": options.reference.id } : {}
417
415
  });
@@ -442,6 +440,11 @@ function createEventMulticaster(workflowName, workflowVersionId, eventName, sche
442
440
  client,
443
441
  runId,
444
442
  ...args
443
+ ),
444
+ sendByReferenceId: (client, referenceId, ...args) => createEventMulticaster(workflowName, workflowVersionId, eventName, schema, optsBuilder.build()).sendByReferenceId(
445
+ client,
446
+ referenceId,
447
+ ...args
445
448
  )
446
449
  });
447
450
  async function send(client, runId, ...args) {
@@ -475,12 +478,51 @@ function createEventMulticaster(workflowName, workflowVersionId, eventName, sche
475
478
  "aiki.workflowVersionId": workflowVersionId,
476
479
  "aiki.workflowRunIds": runIds,
477
480
  "aiki.eventName": eventName,
478
- ...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 } : {}
479
520
  });
480
521
  }
481
522
  return {
482
523
  with: () => createBuilder(optsOverrider()),
483
- send
524
+ send,
525
+ sendByReferenceId
484
526
  };
485
527
  }
486
528
 
@@ -509,9 +551,7 @@ var WorkflowRunHandleImpl = class {
509
551
  this._run = _run;
510
552
  this.logger = logger;
511
553
  this.api = client.api;
512
- this.events = createEventSenders(client.api, this._run.id, eventsDefinition, this.logger, (run) => {
513
- this._run = run;
514
- });
554
+ this.events = createEventSenders(client.api, this._run.id, eventsDefinition, this.logger);
515
555
  this[INTERNAL2] = {
516
556
  client,
517
557
  transitionState: this.transitionState.bind(this),
@@ -607,24 +647,26 @@ var WorkflowRunHandleImpl = class {
607
647
  }
608
648
  async transitionState(targetState) {
609
649
  try {
650
+ let response;
610
651
  if (targetState.status === "scheduled" && (targetState.reason === "new" || targetState.reason === "resume" || targetState.reason === "awake_early") || targetState.status === "paused" || targetState.status === "cancelled") {
611
- const { run: run2 } = await this.api.workflowRun.transitionStateV1({
652
+ response = await this.api.workflowRun.transitionStateV1({
612
653
  type: "pessimistic",
613
654
  id: this.run.id,
614
655
  state: targetState
615
656
  });
616
- this._run = run2;
617
- 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
+ });
618
664
  }
619
- const { run } = await this.api.workflowRun.transitionStateV1({
620
- type: "optimistic",
621
- id: this.run.id,
622
- state: targetState,
623
- expectedRevision: this.run.revision
624
- });
625
- this._run = run;
665
+ this._run.revision = response.revision;
666
+ this._run.state = response.state;
667
+ this._run.attempts = response.attempts;
626
668
  } catch (error) {
627
- if (isConflictError(error)) {
669
+ if (isWorkflowRunRevisionConflictError(error)) {
628
670
  throw new WorkflowRunRevisionConflictError2(this.run.id);
629
671
  }
630
672
  throw error;
@@ -632,15 +674,14 @@ var WorkflowRunHandleImpl = class {
632
674
  }
633
675
  async transitionTaskState(request) {
634
676
  try {
635
- const { run, taskId } = await this.api.workflowRun.transitionTaskStateV1({
677
+ const { taskInfo } = await this.api.workflowRun.transitionTaskStateV1({
636
678
  ...request,
637
679
  id: this.run.id,
638
- expectedRevision: this.run.revision
680
+ expectedWorkflowRunRevision: this.run.revision
639
681
  });
640
- this._run = run;
641
- return { taskId };
682
+ return taskInfo;
642
683
  } catch (error) {
643
- if (isConflictError(error)) {
684
+ if (isWorkflowRunRevisionConflictError(error)) {
644
685
  throw new WorkflowRunRevisionConflictError2(this.run.id);
645
686
  }
646
687
  throw error;
@@ -653,8 +694,73 @@ var WorkflowRunHandleImpl = class {
653
694
  }
654
695
  }
655
696
  };
656
- function isConflictError(error) {
657
- 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
+ };
658
764
  }
659
765
 
660
766
  // run/sleeper.ts
@@ -666,17 +772,17 @@ import {
666
772
  var MAX_SLEEP_YEARS = 10;
667
773
  var MAX_SLEEP_MS = MAX_SLEEP_YEARS * 365 * 24 * 60 * 60 * 1e3;
668
774
  function createSleeper(handle, logger) {
669
- const nextSleepIndexByName = {};
775
+ const nextIndexBySleepName = {};
670
776
  return async (name, duration) => {
671
777
  const sleepName = name;
672
778
  let durationMs = toMilliseconds(duration);
673
779
  if (durationMs > MAX_SLEEP_MS) {
674
780
  throw new Error(`Sleep duration ${durationMs}ms exceeds maximum of ${MAX_SLEEP_YEARS} years`);
675
781
  }
676
- const nextSleepIndex = nextSleepIndexByName[sleepName] ?? 0;
677
- const sleepQueue = handle.run.sleepsQueue[sleepName] ?? { sleeps: [] };
678
- const sleepState = sleepQueue.sleeps[nextSleepIndex];
679
- 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) {
680
786
  try {
681
787
  await handle[INTERNAL3].transitionState({ status: "sleeping", sleepName, durationMs });
682
788
  logger.info("Going to sleep", {
@@ -691,37 +797,37 @@ function createSleeper(handle, logger) {
691
797
  }
692
798
  throw new WorkflowRunSuspendedError2(handle.run.id);
693
799
  }
694
- if (sleepState.status === "sleeping") {
800
+ if (existingSleep.status === "sleeping") {
695
801
  logger.debug("Already sleeping", {
696
802
  "aiki.sleepName": sleepName,
697
- "aiki.awakeAt": sleepState.awakeAt
803
+ "aiki.awakeAt": existingSleep.awakeAt
698
804
  });
699
805
  throw new WorkflowRunSuspendedError2(handle.run.id);
700
806
  }
701
- sleepState.status;
702
- nextSleepIndexByName[sleepName] = nextSleepIndex + 1;
703
- if (sleepState.status === "cancelled") {
807
+ existingSleep.status;
808
+ nextIndexBySleepName[sleepName] = nextIndex + 1;
809
+ if (existingSleep.status === "cancelled") {
704
810
  logger.debug("Sleep cancelled", {
705
811
  "aiki.sleepName": sleepName,
706
- "aiki.cancelledAt": sleepState.cancelledAt
812
+ "aiki.cancelledAt": existingSleep.cancelledAt
707
813
  });
708
814
  return { cancelled: true };
709
815
  }
710
- if (durationMs === sleepState.durationMs) {
816
+ if (durationMs === existingSleep.durationMs) {
711
817
  logger.debug("Sleep completed", {
712
818
  "aiki.sleepName": sleepName,
713
819
  "aiki.durationMs": durationMs,
714
- "aiki.completedAt": sleepState.completedAt
820
+ "aiki.completedAt": existingSleep.completedAt
715
821
  });
716
822
  return { cancelled: false };
717
823
  }
718
- if (durationMs > sleepState.durationMs) {
824
+ if (durationMs > existingSleep.durationMs) {
719
825
  logger.warn("Higher sleep duration encountered during replay. Sleeping for remaining duration", {
720
826
  "aiki.sleepName": sleepName,
721
- "aiki.historicDurationMs": sleepState.durationMs,
827
+ "aiki.historicDurationMs": existingSleep.durationMs,
722
828
  "aiki.latestDurationMs": durationMs
723
829
  });
724
- durationMs -= sleepState.durationMs;
830
+ durationMs -= existingSleep.durationMs;
725
831
  } else {
726
832
  return { cancelled: false };
727
833
  }
@@ -815,6 +921,7 @@ import { INTERNAL as INTERNAL5 } from "@aikirun/types/symbols";
815
921
  import { TaskFailedError } from "@aikirun/types/task";
816
922
  import { SchemaValidationError as SchemaValidationError2 } from "@aikirun/types/validator";
817
923
  import {
924
+ NonDeterminismError,
818
925
  WorkflowRunFailedError as WorkflowRunFailedError2,
819
926
  WorkflowRunRevisionConflictError as WorkflowRunRevisionConflictError5,
820
927
  WorkflowRunSuspendedError as WorkflowRunSuspendedError4
@@ -827,13 +934,13 @@ import {
827
934
  WorkflowRunRevisionConflictError as WorkflowRunRevisionConflictError4,
828
935
  WorkflowRunSuspendedError as WorkflowRunSuspendedError3
829
936
  } from "@aikirun/types/workflow-run";
830
- async function childWorkflowRunHandle(client, run, parentRun, logger, eventsDefinition) {
937
+ async function childWorkflowRunHandle(client, run, parentRun, childWorkflowRunWaitQueues, logger, eventsDefinition) {
831
938
  const handle = await workflowRunHandle(client, run, eventsDefinition, logger);
832
939
  return {
833
940
  run: handle.run,
834
941
  events: handle.events,
835
942
  refresh: handle.refresh.bind(handle),
836
- waitForStatus: createStatusWaiter(handle, parentRun, logger),
943
+ waitForStatus: createStatusWaiter(handle, parentRun, childWorkflowRunWaitQueues, logger),
837
944
  cancel: handle.cancel.bind(handle),
838
945
  pause: handle.pause.bind(handle),
839
946
  resume: handle.resume.bind(handle),
@@ -841,15 +948,21 @@ async function childWorkflowRunHandle(client, run, parentRun, logger, eventsDefi
841
948
  [INTERNAL4]: handle[INTERNAL4]
842
949
  };
843
950
  }
844
- function createStatusWaiter(handle, parentRun, logger) {
845
- let nextWaitIndex = 0;
951
+ function createStatusWaiter(handle, parentRun, childWorkflowRunWaitQueues, logger) {
952
+ const nextIndexByStatus = {
953
+ cancelled: 0,
954
+ completed: 0,
955
+ failed: 0
956
+ };
846
957
  async function waitForStatus(expectedStatus, options) {
847
958
  const parentRunHandle = parentRun[INTERNAL4].handle;
848
- const waitResults = parentRunHandle.run.childWorkflowRuns[handle.run.address]?.statusWaitResults ?? [];
849
- const waitResult = waitResults[nextWaitIndex];
850
- if (waitResult) {
851
- nextWaitIndex++;
852
- 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") {
853
966
  logger.debug("Timed out waiting for child workflow status", {
854
967
  "aiki.childWorkflowExpectedStatus": expectedStatus
855
968
  });
@@ -858,43 +971,29 @@ function createStatusWaiter(handle, parentRun, logger) {
858
971
  cause: "timeout"
859
972
  };
860
973
  }
861
- if (waitResult.childWorkflowRunState.status === expectedStatus) {
974
+ const childWorkflowRunStatus = existingChildWorkflowRunWait.childWorkflowRunState.status;
975
+ if (childWorkflowRunStatus === expectedStatus) {
862
976
  return {
863
977
  success: true,
864
- state: waitResult.childWorkflowRunState
978
+ state: existingChildWorkflowRunWait.childWorkflowRunState
865
979
  };
866
980
  }
867
- if (isTerminalWorkflowRunStatus2(waitResult.childWorkflowRunState.status)) {
981
+ if (isTerminalWorkflowRunStatus2(childWorkflowRunStatus)) {
868
982
  logger.debug("Child workflow run reached termnial state", {
869
- "aiki.childWorkflowTerminalStatus": waitResult.childWorkflowRunState.status
983
+ "aiki.childWorkflowTerminalStatus": childWorkflowRunStatus
870
984
  });
871
985
  return {
872
986
  success: false,
873
987
  cause: "run_terminated"
874
988
  };
875
989
  }
876
- }
877
- const { state } = handle.run;
878
- if (state.status === expectedStatus) {
879
- return {
880
- success: true,
881
- state
882
- };
883
- }
884
- if (isTerminalWorkflowRunStatus2(state.status)) {
885
- logger.debug("Child workflow run reached termnial state", {
886
- "aiki.childWorkflowTerminalStatus": state.status
887
- });
888
- return {
889
- success: false,
890
- cause: "run_terminated"
891
- };
990
+ childWorkflowRunStatus;
892
991
  }
893
992
  const timeoutInMs = options?.timeout && toMilliseconds(options.timeout);
894
993
  try {
895
994
  await parentRunHandle[INTERNAL4].transitionState({
896
995
  status: "awaiting_child_workflow",
897
- childWorkflowRunId: handle.run.id,
996
+ childWorkflowRunId: run.id,
898
997
  childWorkflowRunStatus: expectedStatus,
899
998
  timeoutInMs
900
999
  });
@@ -948,7 +1047,7 @@ var WorkflowVersionImpl = class {
948
1047
  }
949
1048
  input = schemaValidationResult.value;
950
1049
  }
951
- const { run } = await client.api.workflowRun.createV1({
1050
+ const { id } = await client.api.workflowRun.createV1({
952
1051
  name: this.name,
953
1052
  versionId: this.versionId,
954
1053
  input,
@@ -957,9 +1056,9 @@ var WorkflowVersionImpl = class {
957
1056
  client.logger.info("Created workflow", {
958
1057
  "aiki.workflowName": this.name,
959
1058
  "aiki.workflowVersionId": this.versionId,
960
- "aiki.workflowRunId": run.id
1059
+ "aiki.workflowRunId": id
961
1060
  });
962
- return workflowRunHandle(client, run, this[INTERNAL5].eventsDefinition);
1061
+ return workflowRunHandle(client, id, this[INTERNAL5].eventsDefinition);
963
1062
  }
964
1063
  async startAsChild(parentRun, ...args) {
965
1064
  return this.startAsChildWithOpts(parentRun, this.params.opts ?? {}, ...args);
@@ -971,48 +1070,41 @@ var WorkflowVersionImpl = class {
971
1070
  const inputRaw = args[0];
972
1071
  const input = await this.parse(parentRunHandle, this.params.schema?.input, inputRaw, parentRun.logger);
973
1072
  const inputHash = await hashInput(input);
974
- const reference = startOpts.reference;
975
- const address = getWorkflowRunAddress(this.name, this.versionId, reference?.id ?? inputHash);
976
- const existingRunInfo = parentRunHandle.run.childWorkflowRuns[address];
977
- if (existingRunInfo) {
978
- await this.assertUniqueChildRunReferenceId(
979
- parentRunHandle,
980
- existingRunInfo,
981
- inputHash,
982
- reference,
983
- parentRun.logger
984
- );
985
- const { run: existingRun } = await client.api.workflowRun.getByIdV1({ id: existingRunInfo.id });
986
- if (existingRun.state.status === "completed") {
987
- 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
+ );
988
1096
  }
989
- const logger2 = parentRun.logger.child({
990
- "aiki.childWorkflowName": existingRun.name,
991
- "aiki.childWorkflowVersionId": existingRun.versionId,
992
- "aiki.childWorkflowRunId": existingRun.id
993
- });
994
- return childWorkflowRunHandle(
995
- client,
996
- existingRun,
997
- parentRun,
998
- logger2,
999
- this[INTERNAL5].eventsDefinition
1000
- );
1097
+ await this.throwNonDeterminismError(parentRun, parentRunHandle, inputHash, referenceId, replayManifest);
1001
1098
  }
1002
- const { run: newRun } = await client.api.workflowRun.createV1({
1099
+ const shard = parentRun.options.shard;
1100
+ const { id: newRunId } = await client.api.workflowRun.createV1({
1003
1101
  name: this.name,
1004
1102
  versionId: this.versionId,
1005
1103
  input,
1006
1104
  parentWorkflowRunId: parentRun.id,
1007
- options: startOpts
1105
+ options: shard === void 0 ? startOpts : { ...startOpts, shard }
1008
1106
  });
1009
- parentRunHandle.run.childWorkflowRuns[address] = {
1010
- id: newRun.id,
1011
- name: newRun.name,
1012
- versionId: newRun.versionId,
1013
- inputHash,
1014
- statusWaitResults: []
1015
- };
1107
+ const { run: newRun } = await client.api.workflowRun.getByIdV1({ id: newRunId });
1016
1108
  const logger = parentRun.logger.child({
1017
1109
  "aiki.childWorkflowName": newRun.name,
1018
1110
  "aiki.childWorkflowVersionId": newRun.versionId,
@@ -1023,32 +1115,33 @@ var WorkflowVersionImpl = class {
1023
1115
  client,
1024
1116
  newRun,
1025
1117
  parentRun,
1118
+ {
1119
+ cancelled: { childWorkflowRunWaits: [] },
1120
+ completed: { childWorkflowRunWaits: [] },
1121
+ failed: { childWorkflowRunWaits: [] }
1122
+ },
1026
1123
  logger,
1027
1124
  this[INTERNAL5].eventsDefinition
1028
1125
  );
1029
1126
  }
1030
- async assertUniqueChildRunReferenceId(parentRunHandle, existingRunInfo, inputHash, reference, logger) {
1031
- if (existingRunInfo.inputHash !== inputHash && reference) {
1032
- const conflictPolicy = reference.conflictPolicy ?? "error";
1033
- if (conflictPolicy !== "error") {
1034
- return;
1035
- }
1036
- logger.error("Reference ID already used by another child workflow", {
1037
- "aiki.referenceId": reference.id,
1038
- "aiki.existingChildWorkflowRunId": existingRunInfo.id
1039
- });
1040
- const error = new WorkflowRunFailedError2(
1041
- parentRunHandle.run.id,
1042
- parentRunHandle.run.attempts,
1043
- `Reference ID "${reference.id}" already used by another child workflow run ${existingRunInfo.id}`
1044
- );
1045
- await parentRunHandle[INTERNAL5].transitionState({
1046
- status: "failed",
1047
- cause: "self",
1048
- error: createSerializableError(error)
1049
- });
1050
- 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;
1051
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;
1052
1145
  }
1053
1146
  async getHandleById(client, runId) {
1054
1147
  return workflowRunHandle(client, runId, this[INTERNAL5].eventsDefinition);
@@ -1084,7 +1177,7 @@ var WorkflowVersionImpl = class {
1084
1177
  const output = await this.parse(handle, this.params.schema?.output, outputRaw, run.logger);
1085
1178
  return output;
1086
1179
  } catch (error) {
1087
- if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2 || error instanceof WorkflowRunRevisionConflictError5) {
1180
+ if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2 || error instanceof WorkflowRunRevisionConflictError5 || error instanceof NonDeterminismError) {
1088
1181
  throw error;
1089
1182
  }
1090
1183
  const attempts = handle.run.attempts;
@@ -1214,7 +1307,7 @@ var WorkflowImpl = class {
1214
1307
  }
1215
1308
  v(versionId, params) {
1216
1309
  if (this.workflowVersions.has(versionId)) {
1217
- throw new Error(`Workflow "${this.name}/${versionId}" already exists`);
1310
+ throw new Error(`Workflow "${this.name}:${versionId}" already exists`);
1218
1311
  }
1219
1312
  const workflowVersion = new WorkflowVersionImpl(this.name, versionId, params);
1220
1313
  this.workflowVersions.set(
@@ -1234,6 +1327,7 @@ export {
1234
1327
  WorkflowVersionImpl,
1235
1328
  createEventSenders,
1236
1329
  createEventWaiters,
1330
+ createReplayManifest,
1237
1331
  createSleeper,
1238
1332
  event,
1239
1333
  schedule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/workflow",
3
- "version": "0.17.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.17.0",
21
+ "@aikirun/types": "0.18.0",
22
22
  "@standard-schema/spec": "^1.1.0"
23
23
  },
24
24
  "publishConfig": {