@aikirun/workflow 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -3,15 +3,15 @@ function workflowRegistry() {
3
3
  return new WorkflowRegistryImpl();
4
4
  }
5
5
  var WorkflowRegistryImpl = class {
6
- workflowsById = /* @__PURE__ */ new Map();
6
+ workflowsByName = /* @__PURE__ */ new Map();
7
7
  add(workflow2) {
8
- const workflows = this.workflowsById.get(workflow2.id);
8
+ const workflows = this.workflowsByName.get(workflow2.name);
9
9
  if (!workflows) {
10
- this.workflowsById.set(workflow2.id, /* @__PURE__ */ new Map([[workflow2.versionId, workflow2]]));
10
+ this.workflowsByName.set(workflow2.name, /* @__PURE__ */ new Map([[workflow2.versionId, workflow2]]));
11
11
  return this;
12
12
  }
13
13
  if (workflows.has(workflow2.versionId)) {
14
- throw new Error(`Workflow "${workflow2.id}/${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;
@@ -23,7 +23,7 @@ var WorkflowRegistryImpl = class {
23
23
  return this;
24
24
  }
25
25
  remove(workflow2) {
26
- const workflowVersinos = this.workflowsById.get(workflow2.id);
26
+ const workflowVersinos = this.workflowsByName.get(workflow2.name);
27
27
  if (workflowVersinos) {
28
28
  workflowVersinos.delete(workflow2.versionId);
29
29
  }
@@ -36,20 +36,20 @@ var WorkflowRegistryImpl = class {
36
36
  return this;
37
37
  }
38
38
  removeAll() {
39
- this.workflowsById.clear();
39
+ this.workflowsByName.clear();
40
40
  return this;
41
41
  }
42
42
  getAll() {
43
43
  const workflows = [];
44
- for (const workflowVersions of this.workflowsById.values()) {
44
+ for (const workflowVersions of this.workflowsByName.values()) {
45
45
  for (const workflow2 of workflowVersions.values()) {
46
46
  workflows.push(workflow2);
47
47
  }
48
48
  }
49
49
  return workflows;
50
50
  }
51
- get(id, versionId) {
52
- return this.workflowsById.get(id)?.get(versionId);
51
+ get(name, versionId) {
52
+ return this.workflowsByName.get(name)?.get(versionId);
53
53
  }
54
54
  };
55
55
 
@@ -77,6 +77,42 @@ function delay(ms, options) {
77
77
  });
78
78
  }
79
79
 
80
+ // ../../lib/json/stable-stringify.ts
81
+ function stableStringify(value) {
82
+ return stringifyValue(value);
83
+ }
84
+ function stringifyValue(value) {
85
+ if (value === null || value === void 0) {
86
+ return "null";
87
+ }
88
+ if (typeof value !== "object") {
89
+ return JSON.stringify(value);
90
+ }
91
+ if (Array.isArray(value)) {
92
+ return `[${value.map(stringifyValue).join(",")}]`;
93
+ }
94
+ const keys = Object.keys(value).sort();
95
+ const pairs = [];
96
+ for (const key of keys) {
97
+ const keyValue = value[key];
98
+ if (keyValue !== void 0) {
99
+ pairs.push(`${JSON.stringify(key)}:${stringifyValue(keyValue)}`);
100
+ }
101
+ }
102
+ return `{${pairs.join(",")}}`;
103
+ }
104
+
105
+ // ../../lib/crypto/hash.ts
106
+ async function sha256(input) {
107
+ const data = new TextEncoder().encode(input);
108
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
109
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
110
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
111
+ }
112
+ async function hashInput(input) {
113
+ return sha256(stableStringify({ input }));
114
+ }
115
+
80
116
  // ../../lib/duration/convert.ts
81
117
  var MS_PER_SECOND = 1e3;
82
118
  var MS_PER_MINUTE = 60 * MS_PER_SECOND;
@@ -254,6 +290,7 @@ function getRetryParams(attempts, strategy) {
254
290
  // run/event.ts
255
291
  import { INTERNAL } from "@aikirun/types/symbols";
256
292
  import {
293
+ WorkflowRunConflictError,
257
294
  WorkflowRunFailedError,
258
295
  WorkflowRunSuspendedError
259
296
  } from "@aikirun/types/workflow-run";
@@ -265,23 +302,24 @@ function event(params) {
265
302
  }
266
303
  function createEventWaiters(handle, eventsDefinition, logger) {
267
304
  const waiters = {};
268
- for (const [eventId, eventDefinition] of Object.entries(eventsDefinition)) {
305
+ for (const [eventName, eventDefinition] of Object.entries(eventsDefinition)) {
269
306
  const waiter = createEventWaiter(
270
307
  handle,
271
- eventId,
308
+ eventName,
272
309
  eventDefinition.schema,
273
- logger.child({ "aiki.eventId": eventId })
310
+ logger.child({ "aiki.eventName": eventName })
274
311
  );
275
- waiters[eventId] = waiter;
312
+ waiters[eventName] = waiter;
276
313
  }
277
314
  return waiters;
278
315
  }
279
- function createEventWaiter(handle, eventId, schema, logger) {
316
+ function createEventWaiter(handle, eventName, schema, logger) {
280
317
  let nextEventIndex = 0;
281
318
  async function wait(options) {
282
- const events = handle.run.eventsQueue[eventId]?.events ?? [];
283
- if (nextEventIndex < events.length) {
284
- const event2 = events[nextEventIndex];
319
+ await handle.refresh();
320
+ const events = handle.run.eventsQueue[eventName]?.events ?? [];
321
+ const event2 = events[nextEventIndex];
322
+ if (event2) {
285
323
  nextEventIndex++;
286
324
  if (event2.status === "timeout") {
287
325
  logger.debug("Timed out waiting for event");
@@ -292,11 +330,10 @@ function createEventWaiter(handle, eventId, schema, logger) {
292
330
  data = schema ? schema.parse(event2.data) : event2.data;
293
331
  } catch (error) {
294
332
  logger.error("Invalid event data", { data: event2.data, error });
295
- const serializableError = createSerializableError(error);
296
333
  await handle[INTERNAL].transitionState({
297
334
  status: "failed",
298
335
  cause: "self",
299
- error: serializableError
336
+ error: createSerializableError(error)
300
337
  });
301
338
  throw new WorkflowRunFailedError(handle.run.id, handle.run.attempts);
302
339
  }
@@ -304,81 +341,160 @@ function createEventWaiter(handle, eventId, schema, logger) {
304
341
  return { timeout: false, data };
305
342
  }
306
343
  const timeoutInMs = options?.timeout && toMilliseconds(options.timeout);
307
- logger.info("Waiting for event", {
308
- ...timeoutInMs !== void 0 ? { "aiki.timeoutInMs": timeoutInMs } : {}
309
- });
310
- await handle[INTERNAL].transitionState({
311
- status: "awaiting_event",
312
- eventId,
313
- timeoutInMs
314
- });
344
+ try {
345
+ await handle[INTERNAL].transitionState({
346
+ status: "awaiting_event",
347
+ eventName,
348
+ timeoutInMs
349
+ });
350
+ logger.info("Waiting for event", {
351
+ ...timeoutInMs !== void 0 ? { "aiki.timeoutInMs": timeoutInMs } : {}
352
+ });
353
+ } catch (error) {
354
+ if (error instanceof WorkflowRunConflictError) {
355
+ throw new WorkflowRunSuspendedError(handle.run.id);
356
+ }
357
+ throw error;
358
+ }
315
359
  throw new WorkflowRunSuspendedError(handle.run.id);
316
360
  }
317
361
  return { wait };
318
362
  }
319
363
  function createEventSenders(api, workflowRunId, eventsDefinition, logger, onSend) {
320
364
  const senders = {};
321
- for (const [eventId, eventDefinition] of Object.entries(eventsDefinition)) {
365
+ for (const [eventName, eventDefinition] of Object.entries(eventsDefinition)) {
322
366
  const sender = createEventSender(
323
367
  api,
324
368
  workflowRunId,
325
- eventId,
369
+ eventName,
326
370
  eventDefinition.schema,
327
- logger.child({ "aiki.eventId": eventId }),
371
+ logger.child({ "aiki.eventName": eventName }),
328
372
  onSend
329
373
  );
330
- senders[eventId] = sender;
374
+ senders[eventName] = sender;
331
375
  }
332
376
  return senders;
333
377
  }
334
- function createEventSender(api, workflowRunId, eventId, schema, logger, onSend) {
378
+ function createEventSender(api, workflowRunId, eventName, schema, logger, onSend, options) {
379
+ const optsOverrider = objectOverrider(options ?? {});
380
+ const createBuilder = (optsBuilder) => ({
381
+ opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
382
+ send: (...args) => createEventSender(api, workflowRunId, eventName, schema, logger, onSend, optsBuilder.build()).send(...args)
383
+ });
384
+ async function send(...args) {
385
+ const data = isNonEmptyArray(args) ? args[0] : void 0;
386
+ if (schema) {
387
+ schema.parse(data);
388
+ }
389
+ const { run } = await api.workflowRun.sendEventV1({
390
+ id: workflowRunId,
391
+ eventName,
392
+ data,
393
+ options
394
+ });
395
+ onSend(run);
396
+ logger.info("Sent event to workflow", {
397
+ ...options?.reference ? { "aiki.referenceId": options.reference.id } : {}
398
+ });
399
+ }
335
400
  return {
336
- async send(data, options) {
337
- if (schema) {
338
- schema.parse(data);
339
- }
340
- logger.debug("Sending event", {
341
- ...options?.idempotencyKey ? { "aiki.idempotencyKey": options.idempotencyKey } : {}
342
- });
343
- const { run } = await api.workflowRun.sendEventV1({
344
- id: workflowRunId,
345
- eventId,
346
- data,
347
- options
348
- });
349
- onSend(run);
401
+ with: () => createBuilder(optsOverrider()),
402
+ send
403
+ };
404
+ }
405
+ function createEventMulticasters(workflowName, workflowVersionId, eventsDefinition) {
406
+ const senders = {};
407
+ for (const [eventName, eventDefinition] of Object.entries(eventsDefinition)) {
408
+ const sender = createEventMulticaster(
409
+ workflowName,
410
+ workflowVersionId,
411
+ eventName,
412
+ eventDefinition.schema
413
+ );
414
+ senders[eventName] = sender;
415
+ }
416
+ return senders;
417
+ }
418
+ function createEventMulticaster(workflowName, workflowVersionId, eventName, schema, options) {
419
+ const optsOverrider = objectOverrider(options ?? {});
420
+ const createBuilder = (optsBuilder) => ({
421
+ opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
422
+ send: (client, runId, ...args) => createEventMulticaster(workflowName, workflowVersionId, eventName, schema, optsBuilder.build()).send(
423
+ client,
424
+ runId,
425
+ ...args
426
+ )
427
+ });
428
+ async function send(client, runId, ...args) {
429
+ const data = isNonEmptyArray(args) ? args[0] : void 0;
430
+ if (schema) {
431
+ schema.parse(data);
350
432
  }
433
+ const runIds = Array.isArray(runId) ? runId : [runId];
434
+ if (!isNonEmptyArray(runIds)) {
435
+ return;
436
+ }
437
+ const logger = client.logger.child({
438
+ "aiki.workflowName": workflowName,
439
+ "aiki.workflowVersionId": workflowVersionId,
440
+ "aiki.eventName": eventName
441
+ });
442
+ await client.api.workflowRun.multicastEventV1({
443
+ ids: runIds,
444
+ eventName,
445
+ data,
446
+ options
447
+ });
448
+ logger.info("Multicasted event to workflows", {
449
+ "aiki.workflowName": workflowName,
450
+ "aiki.workflowVersionId": workflowVersionId,
451
+ "aiki.workflowRunIds": runIds,
452
+ "aiki.eventName": eventName,
453
+ ...options?.reference ? { "aiki.referenceId": options.reference.id } : {}
454
+ });
455
+ }
456
+ return {
457
+ with: () => createBuilder(optsOverrider()),
458
+ send
351
459
  };
352
460
  }
353
461
 
354
462
  // run/handle.ts
355
463
  import { INTERNAL as INTERNAL2 } from "@aikirun/types/symbols";
356
464
  import {
465
+ isTerminalWorkflowRunStatus,
466
+ WorkflowRunConflictError as WorkflowRunConflictError2,
357
467
  WorkflowRunNotExecutableError
358
468
  } from "@aikirun/types/workflow-run";
359
469
  async function workflowRunHandle(client, runOrId, eventsDefinition, logger) {
360
470
  const run = typeof runOrId !== "string" ? runOrId : (await client.api.workflowRun.getByIdV1({ id: runOrId })).run;
361
471
  return new WorkflowRunHandleImpl(
362
- client.api,
472
+ client,
363
473
  run,
364
474
  eventsDefinition ?? {},
365
- logger ?? client.logger.child({ "aiki.workflowRunId": run.id })
475
+ logger ?? client.logger.child({
476
+ "aiki.workflowName": run.name,
477
+ "aiki.workflowVersionId": run.versionId,
478
+ "aiki.workflowRunId": run.id
479
+ })
366
480
  );
367
481
  }
368
482
  var WorkflowRunHandleImpl = class {
369
- constructor(api, _run, eventsDefinition, logger) {
370
- this.api = api;
483
+ constructor(client, _run, eventsDefinition, logger) {
371
484
  this._run = _run;
372
485
  this.logger = logger;
373
- this.events = createEventSenders(this.api, this._run.id, eventsDefinition, this.logger, (run) => {
486
+ this.api = client.api;
487
+ this.events = createEventSenders(client.api, this._run.id, eventsDefinition, this.logger, (run) => {
374
488
  this._run = run;
375
489
  });
376
490
  this[INTERNAL2] = {
491
+ client,
377
492
  transitionState: this.transitionState.bind(this),
378
493
  transitionTaskState: this.transitionTaskState.bind(this),
379
494
  assertExecutionAllowed: this.assertExecutionAllowed.bind(this)
380
495
  };
381
496
  }
497
+ api;
382
498
  events;
383
499
  [INTERNAL2];
384
500
  get run() {
@@ -388,95 +504,119 @@ var WorkflowRunHandleImpl = class {
388
504
  const { run: currentRun } = await this.api.workflowRun.getByIdV1({ id: this.run.id });
389
505
  this._run = currentRun;
390
506
  }
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
507
+ // TODO: instead checking the current state, use the transition history
508
+ // because it is possible for a workflow to flash though a state
393
509
  // 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
- }
510
+ async waitForStatus(status, options) {
511
+ return this.waitForStatusByPolling(status, options);
512
+ }
513
+ async waitForStatusByPolling(expectedStatus, options) {
514
+ if (options?.abortSignal?.aborted) {
515
+ return {
516
+ success: false,
517
+ cause: "aborted"
518
+ };
519
+ }
520
+ const delayMs = options?.interval ? toMilliseconds(options.interval) : 1e3;
521
+ const maxAttempts = options?.timeout ? Math.ceil(toMilliseconds(options.timeout) / delayMs) : Number.POSITIVE_INFINITY;
522
+ const retryStrategy = { type: "fixed", maxAttempts, delayMs };
523
+ const loadState = async () => {
524
+ await this.refresh();
525
+ return this.run.state;
526
+ };
527
+ const isNeitherExpectedNorTerminal = (state) => Promise.resolve(state.status !== expectedStatus && !isTerminalWorkflowRunStatus(state.status));
528
+ if (!Number.isFinite(maxAttempts) && !options?.abortSignal) {
529
+ const maybeResult2 = await withRetry(loadState, retryStrategy, {
530
+ shouldRetryOnResult: isNeitherExpectedNorTerminal
531
+ }).run();
532
+ if (maybeResult2.state === "timeout") {
533
+ throw new Error("Something's wrong, this should've never timed out");
534
+ }
535
+ if (await isNeitherExpectedNorTerminal(maybeResult2.result)) {
433
536
  return {
434
- success: true,
435
- state: maybeResult.result
537
+ success: false,
538
+ cause: "run_terminated"
436
539
  };
437
540
  }
438
- case "event": {
439
- throw new Error("Event-based waiting is not yet implemented");
541
+ return {
542
+ success: true,
543
+ state: maybeResult2.result
544
+ };
545
+ }
546
+ const maybeResult = options?.abortSignal ? await withRetry(loadState, retryStrategy, {
547
+ abortSignal: options.abortSignal,
548
+ shouldRetryOnResult: isNeitherExpectedNorTerminal
549
+ }).run() : await withRetry(loadState, retryStrategy, { shouldRetryOnResult: isNeitherExpectedNorTerminal }).run();
550
+ if (maybeResult.state === "completed") {
551
+ if (await isNeitherExpectedNorTerminal(maybeResult.result)) {
552
+ return {
553
+ success: false,
554
+ cause: "run_terminated"
555
+ };
440
556
  }
441
- default:
442
- return condition;
557
+ return {
558
+ success: true,
559
+ state: maybeResult.result
560
+ };
443
561
  }
562
+ return { success: false, cause: maybeResult.state };
444
563
  }
445
564
  async cancel(reason) {
446
- return this.transitionState({ status: "cancelled", reason });
565
+ await this.transitionState({ status: "cancelled", reason });
566
+ this.logger.info("Workflow cancelled");
447
567
  }
448
568
  async pause() {
449
- return this.transitionState({ status: "paused" });
569
+ await this.transitionState({ status: "paused" });
570
+ this.logger.info("Workflow paused");
450
571
  }
451
572
  async resume() {
452
- return this.transitionState({ status: "scheduled", scheduledInMs: 0, reason: "resume" });
573
+ await this.transitionState({ status: "scheduled", scheduledInMs: 0, reason: "resume" });
574
+ this.logger.info("Workflow resumed");
575
+ }
576
+ async awake() {
577
+ await this.transitionState({ status: "scheduled", scheduledInMs: 0, reason: "awake_early" });
578
+ this.logger.info("Workflow awoken");
453
579
  }
454
580
  async transitionState(targetState) {
455
- if (targetState.status === "scheduled" && (targetState.reason === "new" || targetState.reason === "resume") || targetState.status === "paused" || targetState.status === "cancelled") {
456
- const { run: run2 } = await this.api.workflowRun.transitionStateV1({
457
- type: "pessimistic",
581
+ try {
582
+ if (targetState.status === "scheduled" && (targetState.reason === "new" || targetState.reason === "resume" || targetState.reason === "awake_early") || targetState.status === "paused" || targetState.status === "cancelled") {
583
+ const { run: run2 } = await this.api.workflowRun.transitionStateV1({
584
+ type: "pessimistic",
585
+ id: this.run.id,
586
+ state: targetState
587
+ });
588
+ this._run = run2;
589
+ return;
590
+ }
591
+ const { run } = await this.api.workflowRun.transitionStateV1({
592
+ type: "optimistic",
458
593
  id: this.run.id,
459
- state: targetState
594
+ state: targetState,
595
+ expectedRevision: this.run.revision
460
596
  });
461
- this._run = run2;
462
- return;
597
+ this._run = run;
598
+ } catch (error) {
599
+ if (isConflictError(error)) {
600
+ throw new WorkflowRunConflictError2(this.run.id);
601
+ }
602
+ throw error;
603
+ }
604
+ }
605
+ async transitionTaskState(request) {
606
+ try {
607
+ const { run, taskId } = await this.api.workflowRun.transitionTaskStateV1({
608
+ ...request,
609
+ id: this.run.id,
610
+ expectedRevision: this.run.revision
611
+ });
612
+ this._run = run;
613
+ return { taskId };
614
+ } catch (error) {
615
+ if (isConflictError(error)) {
616
+ throw new WorkflowRunConflictError2(this.run.id);
617
+ }
618
+ throw error;
463
619
  }
464
- const { run } = await this.api.workflowRun.transitionStateV1({
465
- type: "optimistic",
466
- id: this.run.id,
467
- state: targetState,
468
- expectedRevision: this.run.revision
469
- });
470
- this._run = run;
471
- }
472
- async transitionTaskState(taskPath, taskState) {
473
- const { run } = await this.api.workflowRun.transitionTaskStateV1({
474
- id: this.run.id,
475
- taskPath,
476
- taskState,
477
- expectedRevision: this.run.revision
478
- });
479
- this._run = run;
480
620
  }
481
621
  assertExecutionAllowed() {
482
622
  const status = this.run.state.status;
@@ -485,133 +625,394 @@ var WorkflowRunHandleImpl = class {
485
625
  }
486
626
  }
487
627
  };
628
+ function isConflictError(error) {
629
+ return error != null && typeof error === "object" && "code" in error && error.code === "CONFLICT";
630
+ }
488
631
 
489
632
  // run/sleeper.ts
490
633
  import { INTERNAL as INTERNAL3 } from "@aikirun/types/symbols";
491
- import { WorkflowRunSuspendedError as WorkflowRunSuspendedError2 } from "@aikirun/types/workflow-run";
634
+ import { WorkflowRunConflictError as WorkflowRunConflictError3, WorkflowRunSuspendedError as WorkflowRunSuspendedError2 } from "@aikirun/types/workflow-run";
492
635
  var MAX_SLEEP_YEARS = 10;
493
636
  var MAX_SLEEP_MS = MAX_SLEEP_YEARS * 365 * 24 * 60 * 60 * 1e3;
494
- function createSleeper(handle, logger, options) {
495
- return async (params) => {
496
- const { id: sleepId, ...durationFields } = params;
497
- const durationMs = toMilliseconds(durationFields);
637
+ function createSleeper(handle, logger) {
638
+ const nextSleepIndexByName = {};
639
+ return async (name, duration) => {
640
+ const sleepName = name;
641
+ let durationMs = toMilliseconds(duration);
498
642
  if (durationMs > MAX_SLEEP_MS) {
499
643
  throw new Error(`Sleep duration ${durationMs}ms exceeds maximum of ${MAX_SLEEP_YEARS} years`);
500
644
  }
501
- const sleepPath = `${sleepId}/${durationMs}`;
502
- const sleepState = handle.run.sleepsState[sleepPath] ?? { status: "none" };
503
- if (sleepState.status === "completed") {
504
- logger.debug("Sleep completed", {
505
- "aiki.sleepId": sleepId,
506
- "aiki.durationMs": durationMs
645
+ const nextSleepIndex = nextSleepIndexByName[sleepName] ?? 0;
646
+ const sleepQueue = handle.run.sleepsQueue[sleepName] ?? { sleeps: [] };
647
+ const sleepState = sleepQueue.sleeps[nextSleepIndex];
648
+ if (!sleepState) {
649
+ try {
650
+ await handle[INTERNAL3].transitionState({ status: "sleeping", sleepName, durationMs });
651
+ logger.info("Sleeping", {
652
+ "aiki.sleepName": sleepName,
653
+ "aiki.durationMs": durationMs
654
+ });
655
+ } catch (error) {
656
+ if (error instanceof WorkflowRunConflictError3) {
657
+ throw new WorkflowRunSuspendedError2(handle.run.id);
658
+ }
659
+ throw error;
660
+ }
661
+ throw new WorkflowRunSuspendedError2(handle.run.id);
662
+ }
663
+ if (sleepState.status === "sleeping") {
664
+ logger.debug("Already sleeping", {
665
+ "aiki.sleepName": sleepName,
666
+ "aiki.awakeAt": sleepState.awakeAt
507
667
  });
508
- return { cancelled: false };
668
+ throw new WorkflowRunSuspendedError2(handle.run.id);
509
669
  }
670
+ sleepState.status;
671
+ nextSleepIndexByName[sleepName] = nextSleepIndex + 1;
510
672
  if (sleepState.status === "cancelled") {
511
673
  logger.debug("Sleep cancelled", {
512
- "aiki.sleepId": sleepId,
513
- "aiki.durationMs": durationMs
674
+ "aiki.sleepName": sleepName,
675
+ "aiki.cancelledAt": sleepState.cancelledAt
514
676
  });
515
677
  return { cancelled: true };
516
678
  }
517
- if (sleepState.status === "sleeping") {
518
- logger.debug("Already sleeping", {
519
- "aiki.sleepId": sleepId,
520
- "aiki.durationMs": durationMs
679
+ if (durationMs === sleepState.durationMs) {
680
+ logger.debug("Sleep completed", {
681
+ "aiki.sleepName": sleepName,
682
+ "aiki.durationMs": durationMs,
683
+ "aiki.completedAt": sleepState.completedAt
521
684
  });
522
- throw new WorkflowRunSuspendedError2(handle.run.id);
685
+ return { cancelled: false };
523
686
  }
524
- sleepState;
525
- if (durationMs <= options.spinThresholdMs) {
526
- logger.debug("Spinning for short sleep", {
527
- "aiki.sleepId": sleepId,
528
- "aiki.durationMs": durationMs
687
+ if (durationMs > sleepState.durationMs) {
688
+ logger.warn("Higher sleep duration encountered during replay. Sleeping for remaining duration", {
689
+ "aiki.sleepName": sleepName,
690
+ "aiki.historicDurationMs": sleepState.durationMs,
691
+ "aiki.latestDurationMs": durationMs
692
+ });
693
+ durationMs -= sleepState.durationMs;
694
+ } else {
695
+ logger.warn("Lower sleep duration encountered during replay. Already slept enough", {
696
+ "aiki.sleepName": sleepName,
697
+ "aiki.historicDurationMs": sleepState.durationMs,
698
+ "aiki.latestDurationMs": durationMs
529
699
  });
530
- await delay(durationMs);
531
700
  return { cancelled: false };
532
701
  }
533
- await handle[INTERNAL3].transitionState({ status: "sleeping", sleepPath, durationMs });
534
- logger.info("Workflow going to sleep", {
535
- "aiki.sleepId": sleepId,
536
- "aiki.durationMs": durationMs
537
- });
702
+ try {
703
+ await handle[INTERNAL3].transitionState({ status: "sleeping", sleepName, durationMs });
704
+ logger.info("Sleeping", {
705
+ "aiki.sleepName": sleepName,
706
+ "aiki.durationMs": durationMs
707
+ });
708
+ } catch (error) {
709
+ if (error instanceof WorkflowRunConflictError3) {
710
+ throw new WorkflowRunSuspendedError2(handle.run.id);
711
+ }
712
+ throw error;
713
+ }
538
714
  throw new WorkflowRunSuspendedError2(handle.run.id);
539
715
  };
540
716
  }
541
717
 
542
718
  // workflow.ts
543
- import { INTERNAL as INTERNAL5 } from "@aikirun/types/symbols";
719
+ import { INTERNAL as INTERNAL6 } from "@aikirun/types/symbols";
720
+
721
+ // ../../lib/path/index.ts
722
+ function getWorkflowRunPath(name, versionId, referenceId) {
723
+ return `${name}/${versionId}/${referenceId}`;
724
+ }
544
725
 
545
726
  // workflow-version.ts
546
- import { INTERNAL as INTERNAL4 } from "@aikirun/types/symbols";
727
+ import { INTERNAL as INTERNAL5 } from "@aikirun/types/symbols";
547
728
  import { TaskFailedError } from "@aikirun/types/task";
548
729
  import {
730
+ WorkflowRunConflictError as WorkflowRunConflictError5,
549
731
  WorkflowRunFailedError as WorkflowRunFailedError2,
732
+ WorkflowRunSuspendedError as WorkflowRunSuspendedError4
733
+ } from "@aikirun/types/workflow-run";
734
+
735
+ // run/handle-child.ts
736
+ import { INTERNAL as INTERNAL4 } from "@aikirun/types/symbols";
737
+ import {
738
+ isTerminalWorkflowRunStatus as isTerminalWorkflowRunStatus2,
739
+ WorkflowRunConflictError as WorkflowRunConflictError4,
550
740
  WorkflowRunSuspendedError as WorkflowRunSuspendedError3
551
741
  } from "@aikirun/types/workflow-run";
742
+ async function childWorkflowRunHandle(client, run, parentRun, logger, eventsDefinition) {
743
+ const handle = await workflowRunHandle(client, run, eventsDefinition, logger);
744
+ return {
745
+ run: handle.run,
746
+ events: handle.events,
747
+ refresh: handle.refresh.bind(handle),
748
+ waitForStatus: createStatusWaiter(handle, parentRun, logger),
749
+ cancel: handle.cancel.bind(handle),
750
+ pause: handle.pause.bind(handle),
751
+ resume: handle.resume.bind(handle),
752
+ awake: handle.awake.bind(handle),
753
+ [INTERNAL4]: handle[INTERNAL4]
754
+ };
755
+ }
756
+ function createStatusWaiter(handle, parentRun, logger) {
757
+ let nextWaitIndex = 0;
758
+ async function waitForStatus(expectedStatus, options) {
759
+ const parentRunHandle = parentRun[INTERNAL4].handle;
760
+ const waitResults = parentRunHandle.run.childWorkflowRuns[handle.run.path]?.statusWaitResults ?? [];
761
+ const waitResult = waitResults[nextWaitIndex];
762
+ if (waitResult) {
763
+ nextWaitIndex++;
764
+ if (waitResult.status === "timeout") {
765
+ logger.debug("Timed out waiting for child workflow status", {
766
+ "aiki.childWorkflowExpectedStatus": expectedStatus
767
+ });
768
+ return {
769
+ success: false,
770
+ cause: "timeout"
771
+ };
772
+ }
773
+ if (waitResult.childWorkflowRunState.status === expectedStatus) {
774
+ return {
775
+ success: true,
776
+ state: waitResult.childWorkflowRunState
777
+ };
778
+ }
779
+ if (isTerminalWorkflowRunStatus2(waitResult.childWorkflowRunState.status)) {
780
+ logger.debug("Child workflow run reached termnial state", {
781
+ "aiki.childWorkflowTerminalStatus": waitResult.childWorkflowRunState.status
782
+ });
783
+ return {
784
+ success: false,
785
+ cause: "run_terminated"
786
+ };
787
+ }
788
+ }
789
+ const { state } = handle.run;
790
+ if (state.status === expectedStatus) {
791
+ return {
792
+ success: true,
793
+ state
794
+ };
795
+ }
796
+ if (isTerminalWorkflowRunStatus2(state.status)) {
797
+ logger.debug("Child workflow run reached termnial state", {
798
+ "aiki.childWorkflowTerminalStatus": state.status
799
+ });
800
+ return {
801
+ success: false,
802
+ cause: "run_terminated"
803
+ };
804
+ }
805
+ const timeoutInMs = options?.timeout && toMilliseconds(options.timeout);
806
+ try {
807
+ await parentRunHandle[INTERNAL4].transitionState({
808
+ status: "awaiting_child_workflow",
809
+ childWorkflowRunId: handle.run.id,
810
+ childWorkflowRunStatus: expectedStatus,
811
+ timeoutInMs
812
+ });
813
+ logger.info("Waiting for child Workflow", {
814
+ "aiki.childWorkflowExpectedStatus": expectedStatus,
815
+ ...timeoutInMs !== void 0 ? { "aiki.timeoutInMs": timeoutInMs } : {}
816
+ });
817
+ } catch (error) {
818
+ if (error instanceof WorkflowRunConflictError4) {
819
+ throw new WorkflowRunSuspendedError3(parentRun.id);
820
+ }
821
+ throw error;
822
+ }
823
+ throw new WorkflowRunSuspendedError3(parentRun.id);
824
+ }
825
+ return waitForStatus;
826
+ }
827
+
828
+ // workflow-version.ts
552
829
  var WorkflowVersionImpl = class _WorkflowVersionImpl {
553
- constructor(id, versionId, params) {
554
- this.id = id;
830
+ constructor(name, versionId, params) {
831
+ this.name = name;
555
832
  this.versionId = versionId;
556
833
  this.params = params;
557
- this[INTERNAL4] = {
558
- eventsDefinition: this.params.events ?? {},
834
+ const eventsDefinition = this.params.events ?? {};
835
+ this.events = createEventMulticasters(this.name, this.versionId, eventsDefinition);
836
+ this[INTERNAL5] = {
837
+ eventsDefinition,
559
838
  handler: this.handler.bind(this)
560
839
  };
561
840
  }
562
- [INTERNAL4];
841
+ events;
842
+ [INTERNAL5];
563
843
  with() {
564
844
  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
- });
845
+ const createBuilder = (optsBuilder) => {
846
+ return {
847
+ opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
848
+ start: (client, ...args) => new _WorkflowVersionImpl(this.name, this.versionId, {
849
+ ...this.params,
850
+ opts: optsBuilder.build()
851
+ }).start(client, ...args),
852
+ startAsChild: (parentRun, ...args) => new _WorkflowVersionImpl(this.name, this.versionId, {
853
+ ...this.params,
854
+ opts: optsBuilder.build()
855
+ }).startAsChild(parentRun, ...args)
856
+ };
857
+ };
572
858
  return createBuilder(optsOverrider());
573
859
  }
574
860
  async start(client, ...args) {
861
+ const inputRaw = isNonEmptyArray(args) ? args[0] : void 0;
862
+ const input = this.params.schema?.input ? this.params.schema.input.parse(inputRaw) : inputRaw;
575
863
  const { run } = await client.api.workflowRun.createV1({
576
- workflowId: this.id,
577
- workflowVersionId: this.versionId,
578
- input: isNonEmptyArray(args) ? args[0] : null,
864
+ name: this.name,
865
+ versionId: this.versionId,
866
+ input,
867
+ options: this.params.opts
868
+ });
869
+ client.logger.info("Created workflow", {
870
+ "aiki.workflowName": this.name,
871
+ "aiki.workflowVersionId": this.versionId,
872
+ "aiki.workflowRunId": run.id
873
+ });
874
+ return workflowRunHandle(client, run, this[INTERNAL5].eventsDefinition);
875
+ }
876
+ async startAsChild(parentRun, ...args) {
877
+ const parentRunHandle = parentRun[INTERNAL5].handle;
878
+ parentRunHandle[INTERNAL5].assertExecutionAllowed();
879
+ const { client } = parentRunHandle[INTERNAL5];
880
+ const inputRaw = isNonEmptyArray(args) ? args[0] : void 0;
881
+ let input = inputRaw;
882
+ if (this.params.schema?.input) {
883
+ try {
884
+ input = this.params.schema.input.parse(inputRaw);
885
+ } catch (error) {
886
+ await parentRunHandle[INTERNAL5].transitionState({
887
+ status: "failed",
888
+ cause: "self",
889
+ error: createSerializableError(error)
890
+ });
891
+ throw new WorkflowRunFailedError2(parentRun.id, parentRunHandle.run.attempts);
892
+ }
893
+ }
894
+ const inputHash = await hashInput(input);
895
+ const reference = this.params.opts?.reference;
896
+ const path = getWorkflowRunPath(this.name, this.versionId, reference?.id ?? inputHash);
897
+ const existingRunInfo = parentRunHandle.run.childWorkflowRuns[path];
898
+ if (existingRunInfo) {
899
+ await this.assertUniqueChildRunReferenceId(
900
+ parentRunHandle,
901
+ existingRunInfo,
902
+ inputHash,
903
+ reference,
904
+ parentRun.logger
905
+ );
906
+ const { run: existingRun } = await client.api.workflowRun.getByIdV1({ id: existingRunInfo.id });
907
+ const logger2 = parentRun.logger.child({
908
+ "aiki.childWorkflowName": existingRun.name,
909
+ "aiki.childWorkflowVersionId": existingRun.versionId,
910
+ "aiki.childWorkflowRunId": existingRun.id
911
+ });
912
+ return childWorkflowRunHandle(
913
+ client,
914
+ existingRun,
915
+ parentRun,
916
+ logger2,
917
+ this[INTERNAL5].eventsDefinition
918
+ );
919
+ }
920
+ const { run: newRun } = await client.api.workflowRun.createV1({
921
+ name: this.name,
922
+ versionId: this.versionId,
923
+ input,
924
+ parentWorkflowRunId: parentRun.id,
579
925
  options: this.params.opts
580
926
  });
581
- return workflowRunHandle(client, run, this[INTERNAL4].eventsDefinition);
927
+ parentRunHandle.run.childWorkflowRuns[path] = {
928
+ id: newRun.id,
929
+ inputHash,
930
+ statusWaitResults: []
931
+ };
932
+ const logger = parentRun.logger.child({
933
+ "aiki.childWorkflowName": newRun.name,
934
+ "aiki.childWorkflowVersionId": newRun.versionId,
935
+ "aiki.childWorkflowRunId": newRun.id
936
+ });
937
+ logger.info("Created child workflow");
938
+ return childWorkflowRunHandle(
939
+ client,
940
+ newRun,
941
+ parentRun,
942
+ logger,
943
+ this[INTERNAL5].eventsDefinition
944
+ );
945
+ }
946
+ async assertUniqueChildRunReferenceId(parentRunHandle, existingRunInfo, inputHash, reference, logger) {
947
+ if (existingRunInfo.inputHash !== inputHash && reference) {
948
+ const onConflict = reference.onConflict ?? "error";
949
+ if (onConflict !== "error") {
950
+ return;
951
+ }
952
+ logger.error("Reference ID already used by another child workflow", {
953
+ "aiki.referenceId": reference.id,
954
+ "aiki.existingChildWorkflowRunId": existingRunInfo.id
955
+ });
956
+ const error = new WorkflowRunFailedError2(
957
+ parentRunHandle.run.id,
958
+ parentRunHandle.run.attempts,
959
+ `Reference ID "${reference.id}" already used by another child workflow run ${existingRunInfo.id}`
960
+ );
961
+ await parentRunHandle[INTERNAL5].transitionState({
962
+ status: "failed",
963
+ cause: "self",
964
+ error: createSerializableError(error)
965
+ });
966
+ throw error;
967
+ }
582
968
  }
583
969
  async getHandle(client, runId) {
584
- return workflowRunHandle(client, runId, this[INTERNAL4].eventsDefinition);
970
+ return workflowRunHandle(client, runId, this[INTERNAL5].eventsDefinition);
585
971
  }
586
- async handler(input, run, context) {
972
+ async handler(run, input, context) {
587
973
  const { logger } = run;
588
- const { handle } = run[INTERNAL4];
589
- handle[INTERNAL4].assertExecutionAllowed();
974
+ const { handle } = run[INTERNAL5];
975
+ handle[INTERNAL5].assertExecutionAllowed();
590
976
  const retryStrategy = this.params.opts?.retry ?? { type: "never" };
591
977
  const state = handle.run.state;
592
978
  if (state.status === "queued" && state.reason === "retry") {
593
979
  await this.assertRetryAllowed(handle, retryStrategy, logger);
594
980
  }
595
981
  logger.info("Starting workflow");
596
- await handle[INTERNAL4].transitionState({ status: "running" });
982
+ await handle[INTERNAL5].transitionState({ status: "running" });
597
983
  const output = await this.tryExecuteWorkflow(input, run, context, retryStrategy);
598
- await handle[INTERNAL4].transitionState({ status: "completed", output });
984
+ await handle[INTERNAL5].transitionState({ status: "completed", output });
599
985
  logger.info("Workflow complete");
600
986
  }
601
987
  async tryExecuteWorkflow(input, run, context, retryStrategy) {
602
988
  while (true) {
603
989
  try {
604
- return await this.params.handler(input, run, context);
990
+ const outputRaw = await this.params.handler(run, input, context);
991
+ let output = outputRaw;
992
+ if (this.params.schema?.output) {
993
+ try {
994
+ output = this.params.schema.output.parse(outputRaw);
995
+ } catch (error) {
996
+ const { handle } = run[INTERNAL5];
997
+ await handle[INTERNAL5].transitionState({
998
+ status: "failed",
999
+ cause: "self",
1000
+ error: createSerializableError(error)
1001
+ });
1002
+ throw new WorkflowRunFailedError2(run.id, handle.run.attempts);
1003
+ }
1004
+ }
1005
+ return output;
605
1006
  } catch (error) {
606
- if (error instanceof WorkflowRunSuspendedError3) {
1007
+ if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2 || error instanceof WorkflowRunConflictError5) {
607
1008
  throw error;
608
1009
  }
609
- const { handle } = run[INTERNAL4];
1010
+ const { handle } = run[INTERNAL5];
610
1011
  const attempts = handle.run.attempts;
611
1012
  const retryParams = getRetryParams(attempts, retryStrategy);
612
1013
  if (!retryParams.retriesLeft) {
613
1014
  const failedState = this.createFailedState(error);
614
- await handle[INTERNAL4].transitionState(failedState);
1015
+ await handle[INTERNAL5].transitionState(failedState);
615
1016
  const logMeta2 = {};
616
1017
  for (const [key, value] of Object.entries(failedState)) {
617
1018
  logMeta2[`aiki.${key}`] = value;
@@ -623,7 +1024,7 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
623
1024
  throw new WorkflowRunFailedError2(run.id, attempts);
624
1025
  }
625
1026
  const awaitingRetryState = this.createAwaitingRetryState(error, retryParams.delayMs);
626
- await handle[INTERNAL4].transitionState(awaitingRetryState);
1027
+ await handle[INTERNAL5].transitionState(awaitingRetryState);
627
1028
  const logMeta = {};
628
1029
  for (const [key, value] of Object.entries(awaitingRetryState)) {
629
1030
  logMeta[`aiki.${key}`] = value;
@@ -633,7 +1034,7 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
633
1034
  "aiki.delayMs": retryParams.delayMs,
634
1035
  ...logMeta
635
1036
  });
636
- throw new WorkflowRunSuspendedError3(run.id);
1037
+ throw new WorkflowRunSuspendedError4(run.id);
637
1038
  }
638
1039
  }
639
1040
  }
@@ -643,11 +1044,10 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
643
1044
  if (!retryParams.retriesLeft) {
644
1045
  logger.error("Workflow retry not allowed", { "aiki.attempts": attempts });
645
1046
  const error = new WorkflowRunFailedError2(id, attempts);
646
- const serializableError = createSerializableError(error);
647
- await handle[INTERNAL4].transitionState({
1047
+ await handle[INTERNAL5].transitionState({
648
1048
  status: "failed",
649
1049
  cause: "self",
650
- error: serializableError
1050
+ error: createSerializableError(error)
651
1051
  });
652
1052
  throw error;
653
1053
  }
@@ -657,14 +1057,13 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
657
1057
  return {
658
1058
  status: "failed",
659
1059
  cause: "task",
660
- taskPath: error.taskPath
1060
+ taskId: error.taskId
661
1061
  };
662
1062
  }
663
- const serializableError = createSerializableError(error);
664
1063
  return {
665
1064
  status: "failed",
666
1065
  cause: "self",
667
- error: serializableError
1066
+ error: createSerializableError(error)
668
1067
  };
669
1068
  }
670
1069
  createAwaitingRetryState(error, nextAttemptInMs) {
@@ -673,15 +1072,14 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
673
1072
  status: "awaiting_retry",
674
1073
  cause: "task",
675
1074
  nextAttemptInMs,
676
- taskPath: error.taskPath
1075
+ taskId: error.taskId
677
1076
  };
678
1077
  }
679
- const serializableError = createSerializableError(error);
680
1078
  return {
681
1079
  status: "awaiting_retry",
682
1080
  cause: "self",
683
1081
  nextAttemptInMs,
684
- error: serializableError
1082
+ error: createSerializableError(error)
685
1083
  };
686
1084
  }
687
1085
  };
@@ -691,21 +1089,21 @@ function workflow(params) {
691
1089
  return new WorkflowImpl(params);
692
1090
  }
693
1091
  var WorkflowImpl = class {
694
- id;
695
- [INTERNAL5];
1092
+ name;
1093
+ [INTERNAL6];
696
1094
  workflowVersions = /* @__PURE__ */ new Map();
697
1095
  constructor(params) {
698
- this.id = params.id;
699
- this[INTERNAL5] = {
1096
+ this.name = params.name;
1097
+ this[INTERNAL6] = {
700
1098
  getAllVersions: this.getAllVersions.bind(this),
701
1099
  getVersion: this.getVersion.bind(this)
702
1100
  };
703
1101
  }
704
1102
  v(versionId, params) {
705
1103
  if (this.workflowVersions.has(versionId)) {
706
- throw new Error(`Workflow "${this.id}/${versionId}" already exists`);
1104
+ throw new Error(`Workflow "${this.name}/${versionId}" already exists`);
707
1105
  }
708
- const workflowVersion = new WorkflowVersionImpl(this.id, versionId, params);
1106
+ const workflowVersion = new WorkflowVersionImpl(this.name, versionId, params);
709
1107
  this.workflowVersions.set(
710
1108
  versionId,
711
1109
  workflowVersion