awaitly 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/di-Biw_plBn.d.cts +15 -0
  2. package/dist/di-DxyH2N3i.d.ts +15 -0
  3. package/dist/durable.cjs +4670 -0
  4. package/dist/durable.cjs.map +1 -0
  5. package/dist/durable.d.cts +9 -0
  6. package/dist/durable.d.ts +9 -0
  7. package/dist/durable.js +4610 -0
  8. package/dist/durable.js.map +1 -0
  9. package/dist/{di-BbFFfO8y.d.ts → duration-BjVn3QpB.d.cts} +1 -15
  10. package/dist/{di-BDlT7InM.d.cts → duration-BjVn3QpB.d.ts} +1 -15
  11. package/dist/engine.cjs +4501 -0
  12. package/dist/engine.cjs.map +1 -0
  13. package/dist/engine.d.cts +113 -0
  14. package/dist/engine.d.ts +113 -0
  15. package/dist/engine.js +4474 -0
  16. package/dist/engine.js.map +1 -0
  17. package/dist/{errors-DtXvrCiO.d.cts → errors-sDSeaZBO.d.cts} +1 -1
  18. package/dist/{errors-DtXvrCiO.d.ts → errors-sDSeaZBO.d.ts} +1 -1
  19. package/dist/guards-CwQZro1F.d.ts +72 -0
  20. package/dist/guards-DhhOJZda.d.cts +72 -0
  21. package/dist/hitl-BnnnLzh2.d.cts +468 -0
  22. package/dist/hitl-pYfkQMv_.d.ts +468 -0
  23. package/dist/hitl.cjs +646 -0
  24. package/dist/hitl.cjs.map +1 -0
  25. package/dist/hitl.d.cts +440 -0
  26. package/dist/hitl.d.ts +440 -0
  27. package/dist/hitl.js +605 -0
  28. package/dist/hitl.js.map +1 -0
  29. package/dist/index-Bew12SZ8.d.ts +417 -0
  30. package/dist/index-Bwt-iz50.d.cts +417 -0
  31. package/dist/{types-B8NfNRGX.d.ts → index-Cv4K_3sZ.d.ts} +2 -1162
  32. package/dist/{types-BZ2f4MRR.d.cts → index-ICmfQi08.d.cts} +2 -1162
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +11 -1527
  35. package/dist/index.d.ts +11 -1527
  36. package/dist/persistence.cjs +552 -0
  37. package/dist/persistence.cjs.map +1 -0
  38. package/dist/persistence.d.cts +254 -0
  39. package/dist/persistence.d.ts +254 -0
  40. package/dist/persistence.js +499 -0
  41. package/dist/persistence.js.map +1 -0
  42. package/dist/reliability.cjs +1651 -0
  43. package/dist/reliability.cjs.map +1 -0
  44. package/dist/reliability.d.cts +1442 -0
  45. package/dist/reliability.d.ts +1442 -0
  46. package/dist/reliability.js +1589 -0
  47. package/dist/reliability.js.map +1 -0
  48. package/dist/result.d.cts +1 -1
  49. package/dist/result.d.ts +1 -1
  50. package/dist/run.cjs +2283 -0
  51. package/dist/run.cjs.map +1 -0
  52. package/dist/run.d.cts +90 -0
  53. package/dist/run.d.ts +90 -0
  54. package/dist/run.js +2247 -0
  55. package/dist/run.js.map +1 -0
  56. package/dist/saga.cjs +3699 -0
  57. package/dist/saga.cjs.map +1 -0
  58. package/dist/saga.d.cts +162 -0
  59. package/dist/saga.d.ts +162 -0
  60. package/dist/saga.js +3670 -0
  61. package/dist/saga.js.map +1 -0
  62. package/dist/store-contract-BI98VYmX.d.ts +66 -0
  63. package/dist/store-contract-C2HyR_o6.d.cts +66 -0
  64. package/dist/streaming.cjs +895 -0
  65. package/dist/streaming.cjs.map +1 -0
  66. package/dist/streaming.d.cts +594 -0
  67. package/dist/streaming.d.ts +594 -0
  68. package/dist/streaming.js +832 -0
  69. package/dist/streaming.js.map +1 -0
  70. package/dist/testing.d.cts +4 -2
  71. package/dist/testing.d.ts +4 -2
  72. package/dist/types-BDTxgKKc.d.ts +845 -0
  73. package/dist/types-C9Y71dua.d.cts +845 -0
  74. package/dist/types-d8q8iQlk.d.cts +323 -0
  75. package/dist/types-sObbY4mX.d.ts +323 -0
  76. package/dist/webhook.cjs +461 -0
  77. package/dist/webhook.cjs.map +1 -0
  78. package/dist/webhook.d.cts +497 -0
  79. package/dist/webhook.d.ts +497 -0
  80. package/dist/webhook.js +422 -0
  81. package/dist/webhook.js.map +1 -0
  82. package/dist/workflow.cjs +2 -2614
  83. package/dist/workflow.cjs.map +1 -1
  84. package/dist/workflow.d.cts +12 -3023
  85. package/dist/workflow.d.ts +12 -3023
  86. package/dist/workflow.js +2 -2535
  87. package/dist/workflow.js.map +1 -1
  88. package/package.json +46 -1
package/dist/workflow.cjs CHANGED
@@ -23,66 +23,26 @@ __export(workflow_entry_exports, {
23
23
  Duration: () => Duration,
24
24
  HOOK_STEP_KEY_PREFIX: () => HOOK_STEP_KEY_PREFIX2,
25
25
  STEP_TIMEOUT_MARKER: () => STEP_TIMEOUT_MARKER,
26
- STREAM_BACKPRESSURE_ERROR: () => STREAM_BACKPRESSURE_ERROR,
27
- STREAM_CLOSE_ERROR: () => STREAM_CLOSE_ERROR,
28
- STREAM_ENDED: () => STREAM_ENDED,
29
- STREAM_READ_ERROR: () => STREAM_READ_ERROR,
30
- STREAM_STORE_ERROR: () => STREAM_STORE_ERROR,
31
- STREAM_WRITE_ERROR: () => STREAM_WRITE_ERROR,
32
26
  SnapshotDecodeError: () => SnapshotDecodeError,
33
27
  SnapshotFormatError: () => SnapshotFormatError,
34
28
  SnapshotMismatchError: () => SnapshotMismatchError,
35
29
  UnexpectedError: () => UnexpectedError,
36
30
  assertValidSnapshot: () => assertValidSnapshot,
37
31
  batchPresets: () => batchPresets,
38
- chunk: () => chunk,
39
32
  clearStep: () => clearStep,
40
- collect: () => collect,
41
- composeMigrations: () => composeMigrations,
42
- composeValidators: () => composeValidators,
43
- createApprovalChecker: () => createApprovalChecker,
44
33
  createApprovalStateCollector: () => createApprovalStateCollector,
45
34
  createApprovalStep: () => createApprovalStep,
46
- createApprovalWebhookHandler: () => createApprovalWebhookHandler,
47
- createBackpressureController: () => createBackpressureController,
48
- createEngine: () => createEngine,
49
- createEventHandler: () => createEventHandler,
50
- createExpressHandler: () => createExpressHandler,
51
- createFileStreamStore: () => createFileStreamStore,
52
- createHITLOrchestrator: () => createHITLOrchestrator,
53
35
  createHook: () => createHook,
54
- createKeyRemoveMigration: () => createKeyRemoveMigration,
55
- createKeyRenameMigration: () => createKeyRenameMigration,
56
- createMemoryApprovalStore: () => createMemoryApprovalStore,
57
- createMemoryCache: () => createMemoryCache,
58
- createMemoryStreamStore: () => createMemoryStreamStore,
59
- createMemoryWorkflowStateStore: () => createMemoryWorkflowStateStore,
60
36
  createResource: () => createResource,
61
37
  createResourceScope: () => createResourceScope,
62
- createResultMapper: () => createResultMapper,
63
38
  createResumeStateCollector: () => createResumeStateCollector,
64
- createSagaWorkflow: () => createSagaWorkflow,
65
- createSimpleHandler: () => createSimpleHandler,
66
- createTestableMemoryStreamStore: () => createTestableMemoryStreamStore,
67
- createValueTransformMigration: () => createValueTransformMigration,
68
- createVersionedState: () => createVersionedState,
69
- createVersionedStateLoader: () => createVersionedStateLoader,
70
- createWebhookHandler: () => createWebhookHandler,
71
39
  createWorkflow: () => createWorkflow,
72
40
  days: () => days,
73
- defaultUnexpectedErrorMapper: () => defaultUnexpectedErrorMapper,
74
- defaultValidationErrorMapper: () => defaultValidationErrorMapper,
75
- deserializeCauseNew: () => deserializeCauseNew,
76
41
  deserializeResumeState: () => deserializeResumeState,
77
- durable: () => durable,
78
- filter: () => filter,
79
- flatMap: () => flatMap,
80
- flatMapAsync: () => flatMapAsync,
81
42
  gatedStep: () => gatedStep,
82
43
  getPendingApprovals: () => getPendingApprovals,
83
44
  getPendingHooks: () => getPendingHooks,
84
45
  getStepTimeoutMeta: () => getStepTimeoutMeta,
85
- getStreamReader: () => getStreamReader,
86
46
  hasPendingApproval: () => hasPendingApproval,
87
47
  hasPendingHook: () => hasPendingHook,
88
48
  hours: () => hours,
@@ -90,75 +50,36 @@ __export(workflow_entry_exports, {
90
50
  injectHook: () => injectHook,
91
51
  isApprovalRejected: () => isApprovalRejected,
92
52
  isBatchProcessingError: () => isBatchProcessingError,
93
- isConcurrentExecution: () => isConcurrentExecution,
94
53
  isDuration: () => isDuration,
95
- isIdempotencyConflict: () => isIdempotencyConflict,
96
54
  isInputValidationError: () => isInputValidationError,
97
55
  isInvalidBatchConfigError: () => isInvalidBatchConfigError,
98
- isLeaseExpired: () => isLeaseExpired,
99
- isMigrationError: () => isMigrationError,
100
56
  isPendingApproval: () => isPendingApproval,
101
57
  isPendingHook: () => isPendingHook,
102
- isPersistenceError: () => isPersistenceError,
103
58
  isResourceCleanupError: () => isResourceCleanupError,
104
59
  isResumeState: () => isResumeState,
105
- isSagaCompensationError: () => isSagaCompensationError,
106
60
  isSerializedResumeState: () => isSerializedResumeState,
107
61
  isStepComplete: () => isStepComplete,
108
62
  isStepTimeoutError: () => isStepTimeoutError,
109
- isStreamBackpressureError: () => isStreamBackpressureError,
110
- isStreamEnded: () => isStreamEnded,
111
- isStreamReadError: () => isStreamReadError,
112
- isStreamStoreError: () => isStreamStoreError,
113
- isStreamWriteError: () => isStreamWriteError,
114
- isValidationError: () => isValidationError,
115
- isVersionIncompatibleError: () => isVersionIncompatibleError,
116
- isVersionMismatch: () => isVersionMismatch,
117
63
  isWorkflowCancelled: () => isWorkflowCancelled,
118
64
  isWorkflowSnapshot: () => isWorkflowSnapshot,
119
65
  looksLikeWorkflowSnapshot: () => looksLikeWorkflowSnapshot,
120
- map: () => map,
121
- mapAsync: () => mapAsync,
122
66
  mergeSnapshots: () => mergeSnapshots,
123
67
  millis: () => millis,
124
68
  minutes: () => minutes,
125
- parseVersionedState: () => parseVersionedState,
126
69
  pendingApproval: () => pendingApproval,
127
70
  pendingHook: () => pendingHook,
128
- pipe: () => pipe,
129
71
  processInBatches: () => processInBatches,
130
- reduce: () => reduce,
131
- requireFields: () => requireFields,
132
72
  run: () => run,
133
- runSaga: () => runSaga,
134
73
  seconds: () => seconds,
135
- sendWebhookResponse: () => sendWebhookResponse,
136
- serializeError: () => serializeError,
137
74
  serializeResumeState: () => serializeResumeState,
138
- serializeThrown: () => serializeThrown,
139
- shouldApplyBackpressure: () => shouldApplyBackpressure,
140
- skip: () => skip,
141
- skipWhile: () => skipWhile,
142
- streamBackpressureError: () => streamBackpressureError,
143
- streamCloseError: () => streamCloseError,
144
- streamEnded: () => streamEnded,
145
- streamReadError: () => streamReadError,
146
- streamStoreError: () => streamStoreError,
147
- streamWriteError: () => streamWriteError,
148
- stringifyVersionedState: () => stringifyVersionedState,
149
- take: () => take,
150
- takeWhile: () => takeWhile,
151
- toAsyncIterable: () => toAsyncIterable,
152
75
  toDays: () => toDays,
153
76
  toHours: () => toHours,
154
77
  toMillis: () => toMillis,
155
78
  toMinutes: () => toMinutes,
156
79
  toResumeState: () => toResumeState,
157
80
  toSeconds: () => toSeconds,
158
- toWebhookRequest: () => toWebhookRequest,
159
81
  validateInput: () => validateInput,
160
82
  validateSnapshot: () => validateSnapshot,
161
- validationError: () => validationError,
162
83
  withDeps: () => withDeps,
163
84
  withScope: () => withScope
164
85
  });
@@ -551,7 +472,6 @@ function err(error, options) {
551
472
  const cause = options?.cause;
552
473
  return { ok: false, error, ...cause !== void 0 ? { cause } : {} };
553
474
  }
554
- var isOk = (r) => r.ok;
555
475
  var isUnexpectedError = (e) => e instanceof UnexpectedError || typeof e === "object" && e !== null && "_tag" in e && e._tag === "UnexpectedError";
556
476
  function extractErrorTag(error) {
557
477
  if (error == null) return "unknown";
@@ -2670,69 +2590,6 @@ function deserializeCauseNew(serialized) {
2670
2590
  }
2671
2591
  return serialized.value !== void 0 ? serialized.value : serialized.stringRepresentation;
2672
2592
  }
2673
- function createMemoryCache(options = {}) {
2674
- const { maxSize, ttl } = options;
2675
- const cache = /* @__PURE__ */ new Map();
2676
- const isExpired = (entry) => {
2677
- const effectiveTtl = entry.entryTtl ?? ttl;
2678
- if (!effectiveTtl) return false;
2679
- return Date.now() - entry.timestamp > effectiveTtl;
2680
- };
2681
- const evictExpired = () => {
2682
- for (const [key, entry] of cache) {
2683
- if (isExpired(entry)) {
2684
- cache.delete(key);
2685
- }
2686
- }
2687
- };
2688
- const evictOldest = () => {
2689
- if (!maxSize || cache.size < maxSize) return;
2690
- let oldestKey;
2691
- let oldestTime = Infinity;
2692
- for (const [key, entry] of cache) {
2693
- if (entry.timestamp < oldestTime) {
2694
- oldestTime = entry.timestamp;
2695
- oldestKey = key;
2696
- }
2697
- }
2698
- if (oldestKey) {
2699
- cache.delete(oldestKey);
2700
- }
2701
- };
2702
- return {
2703
- get(key) {
2704
- evictExpired();
2705
- const entry = cache.get(key);
2706
- if (!entry) return void 0;
2707
- if (isExpired(entry)) {
2708
- cache.delete(key);
2709
- return void 0;
2710
- }
2711
- return entry.result;
2712
- },
2713
- set(key, result, options2) {
2714
- evictExpired();
2715
- evictOldest();
2716
- cache.set(key, { result, timestamp: Date.now(), entryTtl: options2?.ttl });
2717
- },
2718
- has(key) {
2719
- evictExpired();
2720
- const entry = cache.get(key);
2721
- if (!entry) return false;
2722
- if (isExpired(entry)) {
2723
- cache.delete(key);
2724
- return false;
2725
- }
2726
- return true;
2727
- },
2728
- delete(key) {
2729
- return cache.delete(key);
2730
- },
2731
- clear() {
2732
- cache.clear();
2733
- }
2734
- };
2735
- }
2736
2593
 
2737
2594
  // src/workflow/cache-encoding.ts
2738
2595
  function isCachedErrorCause(cause) {
@@ -2981,24 +2838,7 @@ function createHook() {
2981
2838
  var STREAM_WRITE_ERROR = "STREAM_WRITE_ERROR";
2982
2839
  var STREAM_READ_ERROR = "STREAM_READ_ERROR";
2983
2840
  var STREAM_CLOSE_ERROR = "STREAM_CLOSE_ERROR";
2984
- var STREAM_STORE_ERROR = "STREAM_STORE_ERROR";
2985
2841
  var STREAM_ENDED = "STREAM_ENDED";
2986
- var STREAM_BACKPRESSURE_ERROR = "STREAM_BACKPRESSURE_ERROR";
2987
- function isStreamEnded(error) {
2988
- return typeof error === "object" && error !== null && error.type === STREAM_ENDED;
2989
- }
2990
- function isStreamWriteError(error) {
2991
- return typeof error === "object" && error !== null && error.type === STREAM_WRITE_ERROR;
2992
- }
2993
- function isStreamReadError(error) {
2994
- return typeof error === "object" && error !== null && error.type === STREAM_READ_ERROR;
2995
- }
2996
- function isStreamStoreError(error) {
2997
- return typeof error === "object" && error !== null && error.type === STREAM_STORE_ERROR;
2998
- }
2999
- function isStreamBackpressureError(error) {
3000
- return typeof error === "object" && error !== null && error.type === STREAM_BACKPRESSURE_ERROR;
3001
- }
3002
2842
  function streamWriteError(reason, message, cause) {
3003
2843
  return {
3004
2844
  type: STREAM_WRITE_ERROR,
@@ -3023,27 +2863,12 @@ function streamCloseError(reason, message, cause) {
3023
2863
  ...cause !== void 0 ? { cause } : {}
3024
2864
  };
3025
2865
  }
3026
- function streamStoreError(reason, message, cause) {
3027
- return {
3028
- type: STREAM_STORE_ERROR,
3029
- reason,
3030
- message,
3031
- ...cause !== void 0 ? { cause } : {}
3032
- };
3033
- }
3034
2866
  function streamEnded(finalPosition) {
3035
2867
  return {
3036
2868
  type: STREAM_ENDED,
3037
2869
  finalPosition
3038
2870
  };
3039
2871
  }
3040
- function streamBackpressureError(bufferedCount, highWaterMark) {
3041
- return {
3042
- type: STREAM_BACKPRESSURE_ERROR,
3043
- bufferedCount,
3044
- highWaterMark
3045
- };
3046
- }
3047
2872
 
3048
2873
  // src/streaming/backpressure.ts
3049
2874
  function createBackpressureController(options = {}) {
@@ -3115,9 +2940,6 @@ function createBackpressureController(options = {}) {
3115
2940
  }
3116
2941
  };
3117
2942
  }
3118
- function shouldApplyBackpressure(controller) {
3119
- return controller.state === "paused";
3120
- }
3121
2943
 
3122
2944
  // src/workflow/validation.ts
3123
2945
  function isInputValidationError(e) {
@@ -3997,7 +3819,7 @@ Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow'
3997
3819
  const results = [];
3998
3820
  let processedCount = 0;
3999
3821
  let lastPosition = -1;
4000
- const isStreamReader2 = (s) => {
3822
+ const isStreamReader = (s) => {
4001
3823
  return typeof s === "object" && s !== null && "read" in s && typeof s.read === "function";
4002
3824
  };
4003
3825
  const processItem = async (item, itemIndex, itemPosition, namespace) => {
@@ -4011,7 +3833,7 @@ Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow'
4011
3833
  );
4012
3834
  return { index: itemIndex, position: itemPosition, result: stepResult };
4013
3835
  };
4014
- if (isStreamReader2(source)) {
3836
+ if (isStreamReader(source)) {
4015
3837
  if (concurrency <= 1) {
4016
3838
  let itemPosition = source.position;
4017
3839
  let readResult = await source.read();
@@ -4312,2361 +4134,6 @@ Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow'
4312
4134
  return workflow;
4313
4135
  }
4314
4136
 
4315
- // src/durable/index.ts
4316
- var defaultStore;
4317
- function createMemorySnapshotStore() {
4318
- const store = /* @__PURE__ */ new Map();
4319
- return {
4320
- async save(id, snapshot) {
4321
- store.set(id, { snapshot, updatedAt: /* @__PURE__ */ new Date() });
4322
- },
4323
- async load(id) {
4324
- const entry = store.get(id);
4325
- return entry?.snapshot ?? null;
4326
- },
4327
- async delete(id) {
4328
- store.delete(id);
4329
- },
4330
- async list(options) {
4331
- const prefix = options?.prefix ?? "";
4332
- const limit = options?.limit ?? 100;
4333
- const results = [];
4334
- for (const [id, entry] of store.entries()) {
4335
- if (prefix && !id.startsWith(prefix)) continue;
4336
- results.push({ id, updatedAt: entry.updatedAt.toISOString() });
4337
- if (results.length >= limit) break;
4338
- }
4339
- results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
4340
- return results;
4341
- },
4342
- async close() {
4343
- }
4344
- };
4345
- }
4346
- function getDefaultStore() {
4347
- if (defaultStore === void 0) {
4348
- defaultStore = createMemorySnapshotStore();
4349
- }
4350
- return defaultStore;
4351
- }
4352
- function isVersionMismatch(error) {
4353
- return typeof error === "object" && error !== null && error.type === "VERSION_MISMATCH";
4354
- }
4355
- function isConcurrentExecution(error) {
4356
- return typeof error === "object" && error !== null && error.type === "CONCURRENT_EXECUTION";
4357
- }
4358
- function isPersistenceError(error) {
4359
- return typeof error === "object" && error !== null && error.type === "PERSISTENCE_ERROR";
4360
- }
4361
- function isLeaseExpired(error) {
4362
- return typeof error === "object" && error !== null && error.type === "LEASE_EXPIRED";
4363
- }
4364
- function isIdempotencyConflict(error) {
4365
- return typeof error === "object" && error !== null && error.type === "IDEMPOTENCY_CONFLICT";
4366
- }
4367
- function hasWorkflowLock(store) {
4368
- return typeof store.tryAcquire === "function" && typeof store.release === "function";
4369
- }
4370
- var activeWorkflows = /* @__PURE__ */ new Set();
4371
- var pendingIdempotencyRuns = /* @__PURE__ */ new Map();
4372
- var durable = {
4373
- /**
4374
- * Execute a workflow with automatic state persistence.
4375
- *
4376
- * Features:
4377
- * - **Automatic checkpointing**: State is saved after each keyed step
4378
- * - **Crash recovery**: Resume from the last completed step on restart
4379
- * - **Version checking**: Reject resume if workflow logic version changed
4380
- * - **Concurrency control**: Prevent duplicate executions of the same workflow ID
4381
- * - **Cancellation support**: Integrates with AbortSignal for graceful shutdown
4382
- *
4383
- * ## How It Works
4384
- *
4385
- * 1. On start: Load existing state from store (if any)
4386
- * 2. Check version compatibility (reject if mismatch)
4387
- * 3. Pre-populate cache from loaded state (skip completed steps)
4388
- * 4. Execute workflow, persisting state after each keyed step
4389
- * 5. On completion: Delete stored state (clean up)
4390
- * 6. On error/cancellation: State remains for future resume
4391
- *
4392
- * ## Important Notes
4393
- *
4394
- * - **Only keyed steps are durable**: Use `{ key: 'step-name' }` option
4395
- * - **Steps should be idempotent**: They may be retried on resume
4396
- * - **Serialization**: State is JSON-serialized; complex objects may lose fidelity
4397
- *
4398
- * @param deps - Workflow dependencies (Result-returning functions)
4399
- * @param fn - Workflow function receiving ({ step, deps, ctx })
4400
- * @param options - Durable execution options
4401
- * @returns AsyncResult with workflow result or error
4402
- *
4403
- * @example
4404
- * ```typescript
4405
- * import { durable } from 'awaitly/workflow';
4406
- *
4407
- * // Zero-config: uses in-memory store (per process)
4408
- * const result = await durable.run(
4409
- * { fetchUser, createOrder, sendEmail },
4410
- * async ({ step, deps: { fetchUser, createOrder, sendEmail } }) => {
4411
- * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
4412
- * const order = await step(() => createOrder(user), { key: 'create-order' });
4413
- * await step(() => sendEmail(order), { key: 'send-email' });
4414
- * return order;
4415
- * },
4416
- * { id: 'checkout-123' }
4417
- * );
4418
- *
4419
- * // Persistence across restarts: pass a store adapter
4420
- * import { postgres } from 'awaitly-postgres'; // or awaitly-mongo, awaitly-libsql
4421
- * const store = postgres(process.env.DATABASE_URL);
4422
- * await durable.run(deps, fn, { id: 'checkout-123', store });
4423
- *
4424
- * if (result.ok) {
4425
- * console.log('Order completed:', result.value);
4426
- * } else if (isWorkflowCancelled(result.error)) {
4427
- * console.log('Workflow cancelled at:', result.error.lastStepKey);
4428
- * }
4429
- * ```
4430
- */
4431
- async run(deps, fn, options) {
4432
- const {
4433
- id,
4434
- store: storeOption,
4435
- version = 1,
4436
- allowConcurrent = false,
4437
- lockTtlMs = 6e4,
4438
- heartbeatIntervalMs,
4439
- abortOnLeaseLoss,
4440
- metadata,
4441
- signal,
4442
- createContext,
4443
- onEvent,
4444
- onError,
4445
- onVersionMismatch,
4446
- idempotencyKey,
4447
- input
4448
- } = options;
4449
- const effectiveStore = storeOption ?? getDefaultStore();
4450
- let resolveIdempotencyRun;
4451
- if (idempotencyKey) {
4452
- const idemId = `idem:${idempotencyKey}`;
4453
- const pending = pendingIdempotencyRuns.get(idemId);
4454
- if (pending) {
4455
- return await pending;
4456
- }
4457
- pendingIdempotencyRuns.set(idemId, new Promise((r) => {
4458
- resolveIdempotencyRun = r;
4459
- }));
4460
- try {
4461
- const idemSnapshot = await effectiveStore.load(idemId);
4462
- if (idemSnapshot) {
4463
- if (input !== void 0 && idemSnapshot.metadata?.input !== void 0) {
4464
- const storedInput = JSON.stringify(idemSnapshot.metadata.input);
4465
- const currentInput = JSON.stringify(input);
4466
- if (storedInput !== currentInput) {
4467
- const result = err({
4468
- type: "IDEMPOTENCY_CONFLICT",
4469
- idempotencyKey,
4470
- workflowId: id,
4471
- message: `Idempotency key '${idempotencyKey}' already used with different input for workflow '${id}'.`
4472
- });
4473
- resolveIdempotencyRun(result);
4474
- pendingIdempotencyRuns.delete(idemId);
4475
- resolveIdempotencyRun = void 0;
4476
- return result;
4477
- }
4478
- }
4479
- if (idemSnapshot.execution.status === "completed" && idemSnapshot.metadata?.finalResult !== void 0) {
4480
- const result = idemSnapshot.metadata.finalResult;
4481
- resolveIdempotencyRun(result);
4482
- pendingIdempotencyRuns.delete(idemId);
4483
- resolveIdempotencyRun = void 0;
4484
- return result;
4485
- }
4486
- if (idemSnapshot.execution.status === "running") {
4487
- const result = err({
4488
- type: "CONCURRENT_EXECUTION",
4489
- workflowId: id,
4490
- message: `Workflow '${id}' with idempotency key '${idempotencyKey}' is already running.`,
4491
- reason: "cross-process"
4492
- });
4493
- resolveIdempotencyRun(result);
4494
- pendingIdempotencyRuns.delete(idemId);
4495
- resolveIdempotencyRun = void 0;
4496
- return result;
4497
- }
4498
- }
4499
- } catch {
4500
- }
4501
- try {
4502
- await effectiveStore.save(idemId, {
4503
- formatVersion: 1,
4504
- steps: {},
4505
- execution: {
4506
- status: "running",
4507
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4508
- },
4509
- metadata: {
4510
- workflowId: id,
4511
- idempotencyKey,
4512
- input
4513
- }
4514
- });
4515
- } catch {
4516
- }
4517
- }
4518
- if (!allowConcurrent && activeWorkflows.has(id)) {
4519
- const error = {
4520
- type: "CONCURRENT_EXECUTION",
4521
- workflowId: id,
4522
- message: `Workflow '${id}' is already running. Set allowConcurrent: true to allow parallel executions.`,
4523
- reason: "in-process"
4524
- };
4525
- return err(error);
4526
- }
4527
- let leaseOwnerToken = null;
4528
- if (!allowConcurrent && hasWorkflowLock(effectiveStore)) {
4529
- let lease;
4530
- try {
4531
- lease = await effectiveStore.tryAcquire(id, { ttlMs: lockTtlMs });
4532
- } catch (lockError) {
4533
- const error = {
4534
- type: "PERSISTENCE_ERROR",
4535
- operation: "load",
4536
- workflowId: id,
4537
- cause: lockError,
4538
- message: `Failed to acquire lock for workflow '${id}': ${lockError instanceof Error ? lockError.message : String(lockError)}`
4539
- };
4540
- return err(error);
4541
- }
4542
- if (lease === null) {
4543
- const error = {
4544
- type: "CONCURRENT_EXECUTION",
4545
- workflowId: id,
4546
- message: `Workflow '${id}' is already running (lease held by another process). Set allowConcurrent: true to allow parallel executions.`,
4547
- reason: "cross-process"
4548
- };
4549
- return err(error);
4550
- }
4551
- leaseOwnerToken = lease.ownerToken;
4552
- }
4553
- let heartbeatTimer;
4554
- let leaseAbortController;
4555
- const lockStore = effectiveStore;
4556
- if (leaseOwnerToken && typeof lockStore.renew === "function") {
4557
- const heartbeatMs = heartbeatIntervalMs ?? Math.floor(lockTtlMs / 3);
4558
- leaseAbortController = new AbortController();
4559
- heartbeatTimer = setInterval(async () => {
4560
- try {
4561
- const renewed = await lockStore.renew(id, leaseOwnerToken, { ttlMs: lockTtlMs });
4562
- if (!renewed) {
4563
- leaseAbortController.abort(new Error("Lease expired"));
4564
- }
4565
- } catch {
4566
- leaseAbortController.abort(new Error("Lease renewal failed"));
4567
- }
4568
- }, heartbeatMs);
4569
- }
4570
- activeWorkflows.add(id);
4571
- let durableResult;
4572
- try {
4573
- let existingSnapshot = null;
4574
- try {
4575
- existingSnapshot = await effectiveStore.load(id);
4576
- } catch (loadError) {
4577
- const error = {
4578
- type: "PERSISTENCE_ERROR",
4579
- operation: "load",
4580
- workflowId: id,
4581
- cause: loadError,
4582
- message: `Failed to load state for workflow '${id}': ${loadError instanceof Error ? loadError.message : String(loadError)}`
4583
- };
4584
- durableResult = err(error);
4585
- return err(error);
4586
- }
4587
- if (existingSnapshot) {
4588
- try {
4589
- assertValidSnapshot(existingSnapshot);
4590
- } catch (validationError2) {
4591
- if (validationError2 instanceof SnapshotFormatError) {
4592
- const error = {
4593
- type: "PERSISTENCE_ERROR",
4594
- operation: "load",
4595
- workflowId: id,
4596
- cause: validationError2,
4597
- message: `Invalid snapshot format for workflow '${id}': ${validationError2.message}`
4598
- };
4599
- durableResult = err(error);
4600
- return err(error);
4601
- }
4602
- throw validationError2;
4603
- }
4604
- }
4605
- if (existingSnapshot) {
4606
- const storedVersion = typeof existingSnapshot.metadata?.version === "number" ? existingSnapshot.metadata.version : 1;
4607
- if (storedVersion !== version) {
4608
- const error = {
4609
- type: "VERSION_MISMATCH",
4610
- workflowId: id,
4611
- storedVersion,
4612
- requestedVersion: version,
4613
- currentVersion: version,
4614
- message: `Workflow '${id}' has stored state at version ${storedVersion} but this run requested version ${version}. Migrate the stored state to the new version, or clear state for this id (e.g. durable.deleteState(store, '${id}')) and re-run.`
4615
- };
4616
- if (!onVersionMismatch) {
4617
- durableResult = err(error);
4618
- return err(error);
4619
- }
4620
- const resolution = await Promise.resolve(
4621
- onVersionMismatch({ id, storedVersion, requestedVersion: version })
4622
- );
4623
- if (resolution === "throw") {
4624
- durableResult = err(error);
4625
- return err(error);
4626
- }
4627
- if (resolution === "clear") {
4628
- try {
4629
- await effectiveStore.delete(id);
4630
- } catch {
4631
- }
4632
- existingSnapshot = null;
4633
- } else {
4634
- existingSnapshot = resolution.migratedSnapshot;
4635
- }
4636
- }
4637
- }
4638
- const emitDurableEvent = (event, ctx) => {
4639
- if (onEvent) {
4640
- onEvent(event, ctx);
4641
- }
4642
- };
4643
- const resumeCollector = createResumeStateCollector();
4644
- const workflowOptions = {
4645
- // Restore from existing snapshot
4646
- snapshot: existingSnapshot,
4647
- // Persist after each keyed step
4648
- onAfterStep: async (stepKey, result2, wfId, ctx) => {
4649
- try {
4650
- const collectedState = resumeCollector.getResumeState();
4651
- const steps = {};
4652
- for (const [key, entry] of collectedState.steps) {
4653
- if (entry.result.ok) {
4654
- steps[key] = { ok: true, value: entry.result.value };
4655
- } else {
4656
- const cause = entry.result.cause;
4657
- const serializedCause = cause instanceof Error ? serializeError(cause) : serializeThrown(cause);
4658
- const origin = entry.meta?.origin === "throw" ? "throw" : "result";
4659
- steps[key] = {
4660
- ok: false,
4661
- error: entry.result.error,
4662
- cause: serializedCause,
4663
- meta: { origin }
4664
- };
4665
- }
4666
- }
4667
- const currentSnapshot = {
4668
- formatVersion: 1,
4669
- workflowName: id,
4670
- steps,
4671
- execution: {
4672
- status: "running",
4673
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4674
- currentStepId: stepKey
4675
- },
4676
- metadata: {
4677
- ...existingSnapshot?.metadata ?? {},
4678
- ...metadata,
4679
- version,
4680
- lastStepKey: stepKey
4681
- }
4682
- };
4683
- let snapshotToSave = existingSnapshot ? mergeSnapshots(existingSnapshot, currentSnapshot) : currentSnapshot;
4684
- if (snapshotToSave.warnings && snapshotToSave.warnings.length > 0) {
4685
- const currentStepKeys = new Set(Object.keys(currentSnapshot.steps));
4686
- const filtered = snapshotToSave.warnings.filter(
4687
- (w) => !currentStepKeys.has(w.stepId)
4688
- );
4689
- snapshotToSave = {
4690
- ...snapshotToSave,
4691
- warnings: filtered.length > 0 ? filtered : void 0
4692
- };
4693
- }
4694
- await effectiveStore.save(id, snapshotToSave);
4695
- emitDurableEvent(
4696
- {
4697
- type: "persist_success",
4698
- workflowId: wfId,
4699
- stepKey,
4700
- ts: Date.now(),
4701
- context: ctx
4702
- },
4703
- ctx
4704
- );
4705
- } catch (persistError) {
4706
- emitDurableEvent(
4707
- {
4708
- type: "persist_error",
4709
- workflowId: wfId,
4710
- stepKey,
4711
- error: persistError,
4712
- ts: Date.now(),
4713
- context: ctx
4714
- },
4715
- ctx
4716
- );
4717
- }
4718
- },
4719
- // Forward events and collect step results for snapshot building
4720
- onEvent: (event, ctx) => {
4721
- resumeCollector.handleEvent(event);
4722
- emitDurableEvent(event, ctx);
4723
- },
4724
- onError,
4725
- signal: leaseAbortController && signal ? AbortSignal.any([signal, leaseAbortController.signal]) : leaseAbortController?.signal ?? signal,
4726
- createContext
4727
- };
4728
- let workflowInstance;
4729
- try {
4730
- workflowInstance = createWorkflow(id, deps, workflowOptions);
4731
- } catch (createError) {
4732
- if (createError instanceof SnapshotFormatError) {
4733
- const error = {
4734
- type: "PERSISTENCE_ERROR",
4735
- operation: "load",
4736
- workflowId: id,
4737
- cause: createError,
4738
- message: `Invalid snapshot format for workflow '${id}': ${createError.message}`
4739
- };
4740
- durableResult = err(error);
4741
- return err(error);
4742
- }
4743
- throw createError;
4744
- }
4745
- let result;
4746
- try {
4747
- result = await workflowInstance.run(fn);
4748
- } catch (runError) {
4749
- if (runError instanceof SnapshotFormatError || runError instanceof SnapshotDecodeError) {
4750
- const error = {
4751
- type: "PERSISTENCE_ERROR",
4752
- operation: "load",
4753
- workflowId: id,
4754
- cause: runError,
4755
- message: `Invalid snapshot format for workflow '${id}': ${runError.message}`
4756
- };
4757
- durableResult = err(error);
4758
- return err(error);
4759
- }
4760
- throw runError;
4761
- }
4762
- if (abortOnLeaseLoss !== false && leaseAbortController?.signal.aborted) {
4763
- const leaseErr = err({
4764
- type: "LEASE_EXPIRED",
4765
- workflowId: id,
4766
- message: `Lease expired for workflow '${id}' during execution. The workflow may have been reclaimed by another process.`
4767
- });
4768
- durableResult = leaseErr;
4769
- return leaseErr;
4770
- }
4771
- if (result.ok) {
4772
- try {
4773
- await effectiveStore.delete(id);
4774
- } catch (deleteError) {
4775
- const error = {
4776
- type: "PERSISTENCE_ERROR",
4777
- operation: "delete",
4778
- workflowId: id,
4779
- cause: deleteError,
4780
- message: `Failed to delete state for workflow '${id}': ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
4781
- };
4782
- durableResult = err(error);
4783
- return err(error);
4784
- }
4785
- if (idempotencyKey) {
4786
- const idemId = `idem:${idempotencyKey}`;
4787
- try {
4788
- await effectiveStore.save(idemId, {
4789
- formatVersion: 1,
4790
- steps: {},
4791
- execution: {
4792
- status: "completed",
4793
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4794
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
4795
- },
4796
- metadata: {
4797
- workflowId: id,
4798
- idempotencyKey,
4799
- input,
4800
- finalResult: result
4801
- }
4802
- });
4803
- } catch {
4804
- }
4805
- }
4806
- }
4807
- durableResult = result;
4808
- return result;
4809
- } finally {
4810
- activeWorkflows.delete(id);
4811
- if (resolveIdempotencyRun) {
4812
- resolveIdempotencyRun(durableResult);
4813
- pendingIdempotencyRuns.delete(`idem:${idempotencyKey}`);
4814
- }
4815
- if (heartbeatTimer) {
4816
- clearInterval(heartbeatTimer);
4817
- }
4818
- if (leaseOwnerToken !== null && hasWorkflowLock(effectiveStore)) {
4819
- try {
4820
- await effectiveStore.release(id, leaseOwnerToken);
4821
- } catch {
4822
- }
4823
- }
4824
- }
4825
- },
4826
- /**
4827
- * Clear all persisted workflow state from the store.
4828
- * Use for admin/testing. If the store implements `clear()`, that is used;
4829
- * otherwise clears by listing and deleting in pages.
4830
- *
4831
- * @param store - Snapshot store
4832
- */
4833
- async clearState(store) {
4834
- const storeWithClear = store;
4835
- if (typeof storeWithClear.clear === "function") {
4836
- await storeWithClear.clear();
4837
- return;
4838
- }
4839
- const limit = 100;
4840
- for (; ; ) {
4841
- const entries = await store.list({ limit });
4842
- if (entries.length === 0) break;
4843
- const ids = entries.map((e) => e.id);
4844
- await this.deleteStates(store, ids, { continueOnError: true });
4845
- if (entries.length < limit) break;
4846
- }
4847
- },
4848
- /**
4849
- * Check if a workflow ID has persisted state (can be resumed).
4850
- *
4851
- * @param store - Snapshot store
4852
- * @param id - Workflow execution ID
4853
- * @returns `true` if state exists, `false` otherwise (including on store errors)
4854
- */
4855
- async hasState(store, id) {
4856
- try {
4857
- const snapshot = await store.load(id);
4858
- return snapshot !== null;
4859
- } catch {
4860
- return false;
4861
- }
4862
- },
4863
- /**
4864
- * Delete persisted state for a workflow (cancel resume capability).
4865
- * Deleting is effectively an ack/reset: the workflow can no longer resume from that state.
4866
- * If you delete while a run is in flight, the run continues; on success it may delete again (no-op) or save (recreating state).
4867
- * For multi-worker safety, prefer deleting only when the workflow is not running or when you hold the lock.
4868
- *
4869
- * @param store - Snapshot store
4870
- * @param id - Workflow execution ID
4871
- * @returns `true` on success, `false` on store errors
4872
- */
4873
- async deleteState(store, id) {
4874
- try {
4875
- await store.delete(id);
4876
- return true;
4877
- } catch {
4878
- return false;
4879
- }
4880
- },
4881
- /**
4882
- * Bulk delete persisted state for multiple workflow IDs (best-effort).
4883
- * Use for admin/cleanup. Deletes in a loop with optional concurrency.
4884
- *
4885
- * @param store - Snapshot store
4886
- * @param ids - Workflow execution IDs to delete
4887
- * @param options - Optional concurrency and error handling
4888
- * @returns Count of deleted entries and any errors when continueOnError is true
4889
- */
4890
- async deleteStates(store, ids, options = {}) {
4891
- const { concurrency = 10, continueOnError = true } = options;
4892
- if (ids.length === 0) {
4893
- return { deleted: 0 };
4894
- }
4895
- const errors = [];
4896
- let deleted = 0;
4897
- const run2 = async (id) => {
4898
- try {
4899
- await store.delete(id);
4900
- deleted++;
4901
- } catch (error) {
4902
- if (continueOnError) errors.push({ id, error });
4903
- else throw error;
4904
- }
4905
- };
4906
- const limit = Math.max(1, concurrency);
4907
- for (let i = 0; i < ids.length; i += limit) {
4908
- const batch = ids.slice(i, i + limit);
4909
- await Promise.all(batch.map((id) => run2(id)));
4910
- }
4911
- return errors.length > 0 ? { deleted, errors } : { deleted };
4912
- },
4913
- /**
4914
- * List workflow IDs with persisted state.
4915
- *
4916
- * @param store - Snapshot store
4917
- * @param options - Optional prefix and limit
4918
- * @returns Array of { id, updatedAt } entries
4919
- */
4920
- async listPending(store, options) {
4921
- try {
4922
- return await store.list(options);
4923
- } catch {
4924
- return [];
4925
- }
4926
- }
4927
- };
4928
-
4929
- // src/versioning.ts
4930
- function isMigrationError(error) {
4931
- return typeof error === "object" && error !== null && error.type === "MIGRATION_ERROR";
4932
- }
4933
- function isVersionIncompatibleError(error) {
4934
- return typeof error === "object" && error !== null && error.type === "VERSION_INCOMPATIBLE";
4935
- }
4936
- async function migrateState(state, targetVersion, migrations) {
4937
- let currentState = state.state;
4938
- let currentVersion = state.version;
4939
- if (currentVersion > targetVersion) {
4940
- return err({
4941
- type: "VERSION_INCOMPATIBLE",
4942
- stateVersion: currentVersion,
4943
- currentVersion: targetVersion,
4944
- reason: "State version is higher than current workflow version. Cannot downgrade."
4945
- });
4946
- }
4947
- if (currentVersion === targetVersion) {
4948
- return ok2({ version: currentVersion, state: currentState });
4949
- }
4950
- while (currentVersion < targetVersion) {
4951
- const migration = migrations[currentVersion];
4952
- if (!migration) {
4953
- return err({
4954
- type: "VERSION_INCOMPATIBLE",
4955
- stateVersion: state.version,
4956
- currentVersion: targetVersion,
4957
- reason: `No migration found for version ${currentVersion} to ${currentVersion + 1}`
4958
- });
4959
- }
4960
- try {
4961
- currentState = await migration(currentState);
4962
- currentVersion++;
4963
- } catch (cause) {
4964
- return err({
4965
- type: "MIGRATION_ERROR",
4966
- fromVersion: currentVersion,
4967
- toVersion: currentVersion + 1,
4968
- cause
4969
- });
4970
- }
4971
- }
4972
- return ok2({ version: currentVersion, state: currentState });
4973
- }
4974
- function createVersionedStateLoader(config) {
4975
- const { version, migrations = {}, strictVersioning = true } = config;
4976
- return async (versionedState) => {
4977
- if (!versionedState) {
4978
- return ok2(void 0);
4979
- }
4980
- if (strictVersioning && versionedState.version > version) {
4981
- return err({
4982
- type: "VERSION_INCOMPATIBLE",
4983
- stateVersion: versionedState.version,
4984
- currentVersion: version,
4985
- reason: "Saved state is from a newer workflow version"
4986
- });
4987
- }
4988
- if (versionedState.version === version) {
4989
- return ok2(versionedState.state);
4990
- }
4991
- const result = await migrateState(versionedState, version, migrations);
4992
- if (!result.ok) {
4993
- return result;
4994
- }
4995
- return ok2(result.value.state);
4996
- };
4997
- }
4998
- function createVersionedState(state, version) {
4999
- return { version, state };
5000
- }
5001
- function parseVersionedState(json) {
5002
- if (!json) return null;
5003
- try {
5004
- const parsed = typeof json === "string" ? JSON.parse(json) : json;
5005
- if (typeof parsed !== "object" || parsed === null || !("version" in parsed) || typeof parsed.version !== "number" || !("state" in parsed) || !parsed.state || !Array.isArray(parsed.state.steps)) {
5006
- return null;
5007
- }
5008
- const typedParsed = parsed;
5009
- const steps = new Map(typedParsed.state.steps);
5010
- return {
5011
- version: typedParsed.version,
5012
- state: { steps }
5013
- };
5014
- } catch {
5015
- return null;
5016
- }
5017
- }
5018
- function stringifyVersionedState(state) {
5019
- return JSON.stringify({
5020
- version: state.version,
5021
- state: {
5022
- steps: Array.from(state.state.steps.entries())
5023
- }
5024
- });
5025
- }
5026
- function createKeyRenameMigration(renames) {
5027
- return (state) => {
5028
- const newSteps = /* @__PURE__ */ new Map();
5029
- for (const [key, entry] of state.steps) {
5030
- const newKey = renames[key] ?? key;
5031
- newSteps.set(newKey, entry);
5032
- }
5033
- return { steps: newSteps };
5034
- };
5035
- }
5036
- function createKeyRemoveMigration(keysToRemove) {
5037
- const keysSet = new Set(keysToRemove);
5038
- return (state) => {
5039
- const newSteps = /* @__PURE__ */ new Map();
5040
- for (const [key, entry] of state.steps) {
5041
- if (!keysSet.has(key)) {
5042
- newSteps.set(key, entry);
5043
- }
5044
- }
5045
- return { steps: newSteps };
5046
- };
5047
- }
5048
- function createValueTransformMigration(transforms) {
5049
- return (state) => {
5050
- const newSteps = /* @__PURE__ */ new Map();
5051
- for (const [key, entry] of state.steps) {
5052
- const transform = transforms[key];
5053
- newSteps.set(key, transform ? transform(entry) : entry);
5054
- }
5055
- return { steps: newSteps };
5056
- };
5057
- }
5058
- function composeMigrations(migrations) {
5059
- return async (state) => {
5060
- let currentState = state;
5061
- for (const migration of migrations) {
5062
- currentState = await migration(currentState);
5063
- }
5064
- return currentState;
5065
- };
5066
- }
5067
-
5068
- // src/hitl/index.ts
5069
- function createMemoryApprovalStore() {
5070
- const approvals = /* @__PURE__ */ new Map();
5071
- return {
5072
- async getApproval(key) {
5073
- const approval = approvals.get(key);
5074
- if (!approval) {
5075
- return { status: "pending" };
5076
- }
5077
- if (approval.expiresAt && Date.now() > approval.expiresAt) {
5078
- return { status: "expired", expiredAt: approval.expiresAt };
5079
- }
5080
- return approval;
5081
- },
5082
- async createApproval(key, options) {
5083
- approvals.set(key, {
5084
- status: "pending",
5085
- metadata: options?.metadata,
5086
- expiresAt: options?.expiresAt
5087
- });
5088
- },
5089
- async grantApproval(key, value, options) {
5090
- approvals.set(key, {
5091
- status: "approved",
5092
- value,
5093
- approvedBy: options?.approvedBy,
5094
- approvedAt: Date.now()
5095
- });
5096
- },
5097
- async rejectApproval(key, reason, options) {
5098
- approvals.set(key, {
5099
- status: "rejected",
5100
- reason,
5101
- rejectedBy: options?.rejectedBy,
5102
- rejectedAt: Date.now()
5103
- });
5104
- },
5105
- async editApproval(key, originalValue, editedValue, options) {
5106
- approvals.set(key, {
5107
- status: "edited",
5108
- originalValue,
5109
- editedValue,
5110
- editedBy: options?.editedBy,
5111
- editedAt: Date.now()
5112
- });
5113
- },
5114
- async cancelApproval(key) {
5115
- approvals.delete(key);
5116
- },
5117
- async listPending(options) {
5118
- const pending = [];
5119
- for (const [key, approval] of approvals) {
5120
- if (approval.status === "pending") {
5121
- if (!options?.prefix || key.startsWith(options.prefix)) {
5122
- pending.push(key);
5123
- }
5124
- }
5125
- }
5126
- return pending;
5127
- }
5128
- };
5129
- }
5130
- function createMemoryWorkflowStateStore() {
5131
- const states = /* @__PURE__ */ new Map();
5132
- return {
5133
- async save(state) {
5134
- states.set(state.runId, { ...state, updatedAt: Date.now() });
5135
- },
5136
- async load(runId) {
5137
- return states.get(runId);
5138
- },
5139
- async delete(runId) {
5140
- states.delete(runId);
5141
- },
5142
- async list(options) {
5143
- const results = [];
5144
- for (const [runId, state] of states) {
5145
- if (options?.workflowName && state.workflowName !== options.workflowName) {
5146
- continue;
5147
- }
5148
- if (options?.hasPendingApprovals !== void 0) {
5149
- const hasPending = state.pendingApprovals.length > 0;
5150
- if (options.hasPendingApprovals !== hasPending) {
5151
- continue;
5152
- }
5153
- }
5154
- results.push(runId);
5155
- }
5156
- return results;
5157
- },
5158
- async findByPendingApproval(approvalKey) {
5159
- const results = [];
5160
- for (const [runId, state] of states) {
5161
- if (state.pendingApprovals.includes(approvalKey)) {
5162
- results.push(runId);
5163
- }
5164
- }
5165
- return results;
5166
- }
5167
- };
5168
- }
5169
- function createHITLOrchestrator(options) {
5170
- const {
5171
- approvalStore,
5172
- workflowStateStore,
5173
- defaultExpirationMs = 7 * 24 * 60 * 60 * 1e3,
5174
- // 7 days
5175
- logger = () => {
5176
- },
5177
- notificationChannel
5178
- } = options;
5179
- async function execute(workflowName, workflowFactory, workflowFn, input, opts) {
5180
- const runId = opts?.runId ?? crypto.randomUUID();
5181
- const collector = createApprovalStateCollector();
5182
- const workflow = workflowFactory({
5183
- onEvent: collector.handleEvent
5184
- });
5185
- const args = input;
5186
- const result = await workflow.run(
5187
- async (context) => workflowFn({ ...context, args })
5188
- );
5189
- const pendingApprovals = collector.getPendingApprovals().map((p) => p.stepKey);
5190
- if (pendingApprovals.length > 0) {
5191
- const state = {
5192
- runId,
5193
- workflowName,
5194
- resumeState: collector.getResumeState(),
5195
- pendingApprovals,
5196
- input,
5197
- metadata: opts?.metadata,
5198
- startedAt: Date.now(),
5199
- updatedAt: Date.now()
5200
- };
5201
- await workflowStateStore.save(state);
5202
- let reason;
5203
- let pendingMetadata;
5204
- if (!result.ok && isPendingApproval(result.error)) {
5205
- reason = result.error.reason;
5206
- pendingMetadata = result.error.metadata;
5207
- }
5208
- const expiresAt = Date.now() + defaultExpirationMs;
5209
- for (const key of pendingApprovals) {
5210
- await approvalStore.createApproval(key, {
5211
- metadata: { ...pendingMetadata, runId, workflowName },
5212
- expiresAt
5213
- });
5214
- }
5215
- if (notificationChannel) {
5216
- for (const key of pendingApprovals) {
5217
- try {
5218
- const pendingInfo = collector.getPendingApprovals().find((p) => p.stepKey === key);
5219
- await notificationChannel.onApprovalNeeded({
5220
- approvalKey: key,
5221
- runId,
5222
- workflowName,
5223
- reason,
5224
- metadata: { ...opts?.metadata, ...pendingMetadata },
5225
- expiresAt,
5226
- summary: reason ?? `Approval needed for ${key}`,
5227
- pendingArgs: pendingInfo?.error.metadata
5228
- });
5229
- } catch (notifyError) {
5230
- logger(`Failed to notify for approval ${key}: ${notifyError}`);
5231
- }
5232
- }
5233
- }
5234
- logger(`Workflow ${runId} paused, waiting for: ${pendingApprovals.join(", ")}`);
5235
- return {
5236
- status: "paused",
5237
- runId,
5238
- pendingApprovals,
5239
- reason
5240
- };
5241
- }
5242
- logger(`Workflow ${runId} completed`);
5243
- return { status: "completed", result };
5244
- }
5245
- async function resume(runId, workflowFactory, workflowFn) {
5246
- const savedState = await workflowStateStore.load(runId);
5247
- if (!savedState) {
5248
- throw new Error(`Workflow run not found: ${runId}`);
5249
- }
5250
- for (const key of savedState.pendingApprovals) {
5251
- const status = await approvalStore.getApproval(key);
5252
- if (status.status === "pending") {
5253
- return {
5254
- status: "paused",
5255
- runId,
5256
- pendingApprovals: savedState.pendingApprovals
5257
- };
5258
- }
5259
- if (status.status === "rejected") {
5260
- }
5261
- if (status.status === "expired") {
5262
- throw new Error(`Approval ${key} has expired`);
5263
- }
5264
- }
5265
- let resumeState = savedState.resumeState;
5266
- for (const key of savedState.pendingApprovals) {
5267
- const status = await approvalStore.getApproval(key);
5268
- if (status.status === "approved") {
5269
- resumeState = injectApproval(resumeState, {
5270
- stepKey: key,
5271
- value: status.value
5272
- });
5273
- } else if (status.status === "edited") {
5274
- resumeState = injectApproval(resumeState, {
5275
- stepKey: key,
5276
- value: status.editedValue
5277
- });
5278
- }
5279
- }
5280
- const collector = createApprovalStateCollector();
5281
- const workflow = workflowFactory({
5282
- resumeState,
5283
- onEvent: collector.handleEvent
5284
- });
5285
- const args = savedState.input;
5286
- const result = await workflow.run(
5287
- async (context) => workflowFn({ ...context, args })
5288
- );
5289
- const newPendingApprovals = collector.getPendingApprovals().map((p) => p.stepKey);
5290
- if (newPendingApprovals.length > 0) {
5291
- const updatedState = {
5292
- ...savedState,
5293
- resumeState: collector.getResumeState(),
5294
- pendingApprovals: newPendingApprovals,
5295
- updatedAt: Date.now()
5296
- };
5297
- await workflowStateStore.save(updatedState);
5298
- for (const key of newPendingApprovals) {
5299
- await approvalStore.createApproval(key, {
5300
- metadata: { runId, workflowName: savedState.workflowName },
5301
- expiresAt: Date.now() + defaultExpirationMs
5302
- });
5303
- }
5304
- logger(`Workflow ${runId} paused again, waiting for: ${newPendingApprovals.join(", ")}`);
5305
- let reason;
5306
- if (!result.ok && isPendingApproval(result.error)) {
5307
- reason = result.error.reason;
5308
- }
5309
- return {
5310
- status: "paused",
5311
- runId,
5312
- pendingApprovals: newPendingApprovals,
5313
- reason
5314
- };
5315
- }
5316
- await workflowStateStore.delete(runId);
5317
- logger(`Workflow ${runId} resumed and completed`);
5318
- return { status: "resumed", runId, result };
5319
- }
5320
- async function grantApprovalFn(approvalKey, value, opts) {
5321
- await approvalStore.grantApproval(approvalKey, value, {
5322
- approvedBy: opts?.approvedBy
5323
- });
5324
- const grantedAt = Date.now();
5325
- const resumedWorkflows = [];
5326
- if (opts?.autoResume !== false) {
5327
- const waitingWorkflows = await workflowStateStore.findByPendingApproval(approvalKey);
5328
- for (const runId of waitingWorkflows) {
5329
- resumedWorkflows.push(runId);
5330
- }
5331
- }
5332
- if (notificationChannel?.onApprovalResolved) {
5333
- await notificationChannel.onApprovalResolved({
5334
- approvalKey,
5335
- action: "approved",
5336
- actorId: opts?.approvedBy,
5337
- resolvedAt: grantedAt,
5338
- value
5339
- });
5340
- }
5341
- logger(`Approval ${approvalKey} granted by ${opts?.approvedBy ?? "unknown"}`);
5342
- return { grantedAt, resumedWorkflows };
5343
- }
5344
- async function rejectApprovalFn(approvalKey, reason, opts) {
5345
- await approvalStore.rejectApproval(approvalKey, reason, {
5346
- rejectedBy: opts?.rejectedBy
5347
- });
5348
- if (notificationChannel?.onApprovalResolved) {
5349
- await notificationChannel.onApprovalResolved({
5350
- approvalKey,
5351
- action: "rejected",
5352
- actorId: opts?.rejectedBy,
5353
- resolvedAt: Date.now(),
5354
- reason
5355
- });
5356
- }
5357
- logger(`Approval ${approvalKey} rejected: ${reason}`);
5358
- }
5359
- async function editApprovalFn(approvalKey, originalValue, editedValue, opts) {
5360
- await approvalStore.editApproval(approvalKey, originalValue, editedValue, {
5361
- editedBy: opts?.editedBy
5362
- });
5363
- const editedAt = Date.now();
5364
- if (notificationChannel?.onApprovalResolved) {
5365
- await notificationChannel.onApprovalResolved({
5366
- approvalKey,
5367
- action: "edited",
5368
- actorId: opts?.editedBy,
5369
- resolvedAt: editedAt,
5370
- value: editedValue,
5371
- originalValue
5372
- });
5373
- }
5374
- logger(`Approval ${approvalKey} edited by ${opts?.editedBy ?? "unknown"}`);
5375
- return { editedAt };
5376
- }
5377
- async function pollApproval(approvalKey, opts) {
5378
- const {
5379
- intervalMs = 1e3,
5380
- maxPolls,
5381
- timeoutMs,
5382
- onPollStart,
5383
- onPollComplete
5384
- } = opts ?? {};
5385
- const startTime = Date.now();
5386
- let pollCount = 0;
5387
- while (true) {
5388
- onPollStart?.();
5389
- const status = await approvalStore.getApproval(approvalKey);
5390
- onPollComplete?.(status);
5391
- if (status.status !== "pending") {
5392
- return status;
5393
- }
5394
- pollCount++;
5395
- if (maxPolls !== void 0 && pollCount >= maxPolls) {
5396
- return { status: "pending" };
5397
- }
5398
- if (timeoutMs !== void 0 && Date.now() - startTime >= timeoutMs) {
5399
- return { status: "pending" };
5400
- }
5401
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
5402
- }
5403
- }
5404
- async function getWorkflowStatus(runId) {
5405
- return workflowStateStore.load(runId);
5406
- }
5407
- async function listPendingWorkflows(workflowName) {
5408
- return workflowStateStore.list({
5409
- workflowName,
5410
- hasPendingApprovals: true
5411
- });
5412
- }
5413
- async function cleanup(maxAgeMs) {
5414
- const allWorkflows = await workflowStateStore.list();
5415
- let cleaned = 0;
5416
- const cutoff = Date.now() - maxAgeMs;
5417
- for (const runId of allWorkflows) {
5418
- const state = await workflowStateStore.load(runId);
5419
- if (state && state.updatedAt < cutoff && state.pendingApprovals.length === 0) {
5420
- await workflowStateStore.delete(runId);
5421
- cleaned++;
5422
- }
5423
- }
5424
- logger(`Cleaned up ${cleaned} old workflow states`);
5425
- return cleaned;
5426
- }
5427
- return {
5428
- execute,
5429
- resume,
5430
- grantApproval: grantApprovalFn,
5431
- rejectApproval: rejectApprovalFn,
5432
- editApproval: editApprovalFn,
5433
- pollApproval,
5434
- getWorkflowStatus,
5435
- listPendingWorkflows,
5436
- cleanup
5437
- };
5438
- }
5439
- function createApprovalWebhookHandler(store) {
5440
- return async (request) => {
5441
- const { key, action, value, originalValue, editedValue, reason, actorId } = request;
5442
- try {
5443
- switch (action) {
5444
- case "approve":
5445
- await store.grantApproval(key, value, { approvedBy: actorId });
5446
- return {
5447
- success: true,
5448
- message: `Approval ${key} granted`,
5449
- data: { key, action, timestamp: Date.now() }
5450
- };
5451
- case "reject":
5452
- if (!reason) {
5453
- return { success: false, message: "Reason is required for rejection" };
5454
- }
5455
- await store.rejectApproval(key, reason, { rejectedBy: actorId });
5456
- return {
5457
- success: true,
5458
- message: `Approval ${key} rejected`,
5459
- data: { key, action, timestamp: Date.now() }
5460
- };
5461
- case "edit":
5462
- if (originalValue === void 0 || editedValue === void 0) {
5463
- return { success: false, message: "Both originalValue and editedValue are required for edit" };
5464
- }
5465
- await store.editApproval(key, originalValue, editedValue, { editedBy: actorId });
5466
- return {
5467
- success: true,
5468
- message: `Approval ${key} edited`,
5469
- data: { key, action, timestamp: Date.now() }
5470
- };
5471
- case "cancel":
5472
- await store.cancelApproval(key);
5473
- return {
5474
- success: true,
5475
- message: `Approval ${key} cancelled`,
5476
- data: { key, action, timestamp: Date.now() }
5477
- };
5478
- default:
5479
- return { success: false, message: `Unknown action: ${action}` };
5480
- }
5481
- } catch (error) {
5482
- return {
5483
- success: false,
5484
- message: error instanceof Error ? error.message : String(error)
5485
- };
5486
- }
5487
- };
5488
- }
5489
- function createApprovalChecker(store) {
5490
- return (key) => async () => {
5491
- const status = await store.getApproval(key);
5492
- switch (status.status) {
5493
- case "pending":
5494
- return { status: "pending" };
5495
- case "approved":
5496
- return { status: "approved", value: status.value };
5497
- case "edited":
5498
- return { status: "approved", value: status.editedValue };
5499
- case "rejected":
5500
- return { status: "rejected", reason: status.reason };
5501
- case "expired":
5502
- return { status: "rejected", reason: "Approval request expired" };
5503
- default:
5504
- return { status: "pending" };
5505
- }
5506
- };
5507
- }
5508
-
5509
- // src/saga.ts
5510
- function isSagaCompensationError(error) {
5511
- return typeof error === "object" && error !== null && error.type === "SAGA_COMPENSATION_ERROR";
5512
- }
5513
- function createSagaWorkflow(workflowName, deps, options) {
5514
- return createWorkflow(workflowName, deps, options);
5515
- }
5516
- async function runSaga(fn, options) {
5517
- const sagaId = crypto.randomUUID();
5518
- const startTime = performance.now();
5519
- const compensations = [];
5520
- const emit = (e) => options?.onEvent?.(e);
5521
- emit({ type: "saga_start", sagaId, ts: Date.now() });
5522
- const stepFn = async (name, operation, stepOptions) => {
5523
- if (typeof name !== "string" || name.length === 0) {
5524
- throw new TypeError(
5525
- "step(name, operation, options?): first argument must be a string."
5526
- );
5527
- }
5528
- const result = await operation();
5529
- if (result.ok) {
5530
- if (stepOptions?.compensate) {
5531
- compensations.push({
5532
- name,
5533
- value: result.value,
5534
- compensate: stepOptions.compensate
5535
- });
5536
- }
5537
- return result.value;
5538
- }
5539
- throw createEarlyExit(result.error, {
5540
- origin: "result",
5541
- resultCause: result.cause
5542
- });
5543
- };
5544
- const stepTry = async (name, operation, opts) => {
5545
- if (typeof name !== "string" || name.length === 0) {
5546
- throw new TypeError(
5547
- "step.try(name, operation, options): first argument must be a string."
5548
- );
5549
- }
5550
- const mapToError = "error" in opts ? () => opts.error : opts.onError;
5551
- try {
5552
- const value = await operation();
5553
- if (opts.compensate) {
5554
- compensations.push({
5555
- name,
5556
- value,
5557
- compensate: opts.compensate
5558
- });
5559
- }
5560
- return value;
5561
- } catch (thrown) {
5562
- const mapped = mapToError(thrown);
5563
- throw createEarlyExit(mapped, { origin: "throw", thrown });
5564
- }
5565
- };
5566
- const step = Object.assign(stepFn, { try: stepTry });
5567
- try {
5568
- const value = await fn({ step });
5569
- emit({
5570
- type: "saga_success",
5571
- sagaId,
5572
- ts: Date.now(),
5573
- durationMs: performance.now() - startTime
5574
- });
5575
- return ok2(value);
5576
- } catch (thrown) {
5577
- const durationMs = performance.now() - startTime;
5578
- const originalError = isEarlyExit(thrown) ? thrown.error : thrown;
5579
- emit({ type: "saga_error", sagaId, ts: Date.now(), durationMs, error: originalError });
5580
- emit({
5581
- type: "saga_compensation_start",
5582
- sagaId,
5583
- ts: Date.now(),
5584
- stepCount: compensations.length
5585
- });
5586
- const compensationStart = performance.now();
5587
- const compensationErrors = [];
5588
- for (let i = compensations.length - 1; i >= 0; i--) {
5589
- const comp = compensations[i];
5590
- try {
5591
- await comp.compensate(comp.value);
5592
- emit({
5593
- type: "saga_compensation_step",
5594
- sagaId,
5595
- stepName: comp.name,
5596
- ts: Date.now(),
5597
- success: true
5598
- });
5599
- } catch (error) {
5600
- compensationErrors.push({ stepName: comp.name, error });
5601
- emit({
5602
- type: "saga_compensation_step",
5603
- sagaId,
5604
- stepName: comp.name,
5605
- ts: Date.now(),
5606
- success: false,
5607
- error
5608
- });
5609
- }
5610
- }
5611
- emit({
5612
- type: "saga_compensation_end",
5613
- sagaId,
5614
- ts: Date.now(),
5615
- durationMs: performance.now() - compensationStart,
5616
- success: compensationErrors.length === 0,
5617
- failedCount: compensationErrors.length
5618
- });
5619
- if (compensationErrors.length > 0) {
5620
- const sagaError = {
5621
- type: "SAGA_COMPENSATION_ERROR",
5622
- originalError,
5623
- compensationErrors
5624
- };
5625
- options?.onError?.(sagaError);
5626
- if (options?.throwOnCompensationFailure) throw sagaError;
5627
- return err(sagaError);
5628
- }
5629
- options?.onError?.(originalError);
5630
- if (!isEarlyExit(thrown)) {
5631
- return err(new UnexpectedError({ cause: thrown }));
5632
- }
5633
- return err(originalError);
5634
- }
5635
- }
5636
-
5637
- // src/streaming/stores/memory.ts
5638
- function createMemoryStreamStore(options = {}) {
5639
- const maxItemsPerStream = options.maxItemsPerStream ?? Infinity;
5640
- const streams = /* @__PURE__ */ new Map();
5641
- function getStreamKey(workflowId, namespace) {
5642
- return `${workflowId}:${namespace}`;
5643
- }
5644
- function getOrCreateStream(workflowId, namespace) {
5645
- const key = getStreamKey(workflowId, namespace);
5646
- let data = streams.get(key);
5647
- if (!data) {
5648
- data = {
5649
- metadata: {
5650
- id: key,
5651
- namespace,
5652
- workflowId,
5653
- length: 0,
5654
- closed: false,
5655
- createdAt: Date.now()
5656
- },
5657
- items: [],
5658
- subscribers: /* @__PURE__ */ new Set()
5659
- };
5660
- streams.set(key, data);
5661
- }
5662
- return data;
5663
- }
5664
- return {
5665
- async append(workflowId, namespace, item) {
5666
- try {
5667
- const data = getOrCreateStream(workflowId, namespace);
5668
- if (data.metadata.closed) {
5669
- return err(
5670
- streamStoreError("write_error", "Cannot write to closed stream")
5671
- );
5672
- }
5673
- if (data.items.length >= maxItemsPerStream) {
5674
- return err(
5675
- streamStoreError(
5676
- "write_error",
5677
- `Stream exceeded max items limit (${maxItemsPerStream})`
5678
- )
5679
- );
5680
- }
5681
- data.items.push(item);
5682
- data.metadata.length = data.items.length;
5683
- data.metadata.lastWriteAt = Date.now();
5684
- for (const callback of data.subscribers) {
5685
- try {
5686
- callback(item);
5687
- } catch {
5688
- }
5689
- }
5690
- return ok2(void 0);
5691
- } catch (cause) {
5692
- return err(
5693
- streamStoreError("write_error", "Failed to append item", cause)
5694
- );
5695
- }
5696
- },
5697
- async read(workflowId, namespace, startIndex, limit) {
5698
- try {
5699
- const key = getStreamKey(workflowId, namespace);
5700
- const data = streams.get(key);
5701
- if (!data) {
5702
- return ok2([]);
5703
- }
5704
- const endIndex = limit !== void 0 ? Math.min(startIndex + limit, data.items.length) : data.items.length;
5705
- const items = data.items.slice(startIndex, endIndex);
5706
- return ok2(items);
5707
- } catch (cause) {
5708
- return err(streamStoreError("read_error", "Failed to read items", cause));
5709
- }
5710
- },
5711
- async getMetadata(workflowId, namespace) {
5712
- try {
5713
- const key = getStreamKey(workflowId, namespace);
5714
- const data = streams.get(key);
5715
- return ok2(data?.metadata);
5716
- } catch (cause) {
5717
- return err(
5718
- streamStoreError("metadata_error", "Failed to get metadata", cause)
5719
- );
5720
- }
5721
- },
5722
- async closeStream(workflowId, namespace) {
5723
- try {
5724
- const key = getStreamKey(workflowId, namespace);
5725
- const data = streams.get(key);
5726
- if (data) {
5727
- data.metadata.closed = true;
5728
- data.metadata.closedAt = Date.now();
5729
- }
5730
- return ok2(void 0);
5731
- } catch (cause) {
5732
- return err(
5733
- streamStoreError("close_error", "Failed to close stream", cause)
5734
- );
5735
- }
5736
- },
5737
- subscribe(workflowId, namespace, callback) {
5738
- const data = getOrCreateStream(workflowId, namespace);
5739
- const typedCallback = callback;
5740
- data.subscribers.add(typedCallback);
5741
- return () => {
5742
- data.subscribers.delete(typedCallback);
5743
- };
5744
- }
5745
- };
5746
- }
5747
- function createTestableMemoryStreamStore(options = {}) {
5748
- const streams = /* @__PURE__ */ new Map();
5749
- const maxItemsPerStream = options.maxItemsPerStream ?? Infinity;
5750
- function getStreamKey(workflowId, namespace) {
5751
- return `${workflowId}:${namespace}`;
5752
- }
5753
- function getOrCreateStream(workflowId, namespace) {
5754
- const key = getStreamKey(workflowId, namespace);
5755
- let data = streams.get(key);
5756
- if (!data) {
5757
- data = {
5758
- metadata: {
5759
- id: key,
5760
- namespace,
5761
- workflowId,
5762
- length: 0,
5763
- closed: false,
5764
- createdAt: Date.now()
5765
- },
5766
- items: [],
5767
- subscribers: /* @__PURE__ */ new Set()
5768
- };
5769
- streams.set(key, data);
5770
- }
5771
- return data;
5772
- }
5773
- const store = createMemoryStreamStore(options);
5774
- return {
5775
- ...store,
5776
- // Override to use our local streams map
5777
- async append(workflowId, namespace, item) {
5778
- try {
5779
- const data = getOrCreateStream(workflowId, namespace);
5780
- if (data.metadata.closed) {
5781
- return err(
5782
- streamStoreError("write_error", "Cannot write to closed stream")
5783
- );
5784
- }
5785
- if (data.items.length >= maxItemsPerStream) {
5786
- return err(
5787
- streamStoreError(
5788
- "write_error",
5789
- `Stream exceeded max items limit (${maxItemsPerStream})`
5790
- )
5791
- );
5792
- }
5793
- data.items.push(item);
5794
- data.metadata.length = data.items.length;
5795
- data.metadata.lastWriteAt = Date.now();
5796
- for (const callback of data.subscribers) {
5797
- try {
5798
- callback(item);
5799
- } catch {
5800
- }
5801
- }
5802
- return ok2(void 0);
5803
- } catch (cause) {
5804
- return err(
5805
- streamStoreError("write_error", "Failed to append item", cause)
5806
- );
5807
- }
5808
- },
5809
- async read(workflowId, namespace, startIndex, limit) {
5810
- try {
5811
- const key = getStreamKey(workflowId, namespace);
5812
- const data = streams.get(key);
5813
- if (!data) {
5814
- return ok2([]);
5815
- }
5816
- const endIndex = limit !== void 0 ? Math.min(startIndex + limit, data.items.length) : data.items.length;
5817
- const items = data.items.slice(startIndex, endIndex);
5818
- return ok2(items);
5819
- } catch (cause) {
5820
- return err(streamStoreError("read_error", "Failed to read items", cause));
5821
- }
5822
- },
5823
- async getMetadata(workflowId, namespace) {
5824
- try {
5825
- const key = getStreamKey(workflowId, namespace);
5826
- const data = streams.get(key);
5827
- return ok2(data?.metadata);
5828
- } catch (cause) {
5829
- return err(
5830
- streamStoreError("metadata_error", "Failed to get metadata", cause)
5831
- );
5832
- }
5833
- },
5834
- async closeStream(workflowId, namespace) {
5835
- try {
5836
- const key = getStreamKey(workflowId, namespace);
5837
- const data = streams.get(key);
5838
- if (data) {
5839
- data.metadata.closed = true;
5840
- data.metadata.closedAt = Date.now();
5841
- }
5842
- return ok2(void 0);
5843
- } catch (cause) {
5844
- return err(
5845
- streamStoreError("close_error", "Failed to close stream", cause)
5846
- );
5847
- }
5848
- },
5849
- subscribe(workflowId, namespace, callback) {
5850
- const data = getOrCreateStream(workflowId, namespace);
5851
- const typedCallback = callback;
5852
- data.subscribers.add(typedCallback);
5853
- return () => {
5854
- data.subscribers.delete(typedCallback);
5855
- };
5856
- },
5857
- clear() {
5858
- streams.clear();
5859
- },
5860
- keys() {
5861
- return Array.from(streams.keys());
5862
- },
5863
- has(workflowId, namespace) {
5864
- return streams.has(getStreamKey(workflowId, namespace));
5865
- },
5866
- delete(workflowId, namespace) {
5867
- return streams.delete(getStreamKey(workflowId, namespace));
5868
- }
5869
- };
5870
- }
5871
-
5872
- // src/streaming/stores/file.ts
5873
- function createFileStreamStore(options) {
5874
- const { directory, fs } = options;
5875
- const subscribers = /* @__PURE__ */ new Map();
5876
- function getStreamDir(workflowId, namespace) {
5877
- return `${directory}/${workflowId}/${namespace}`;
5878
- }
5879
- function getMetadataPath(workflowId, namespace) {
5880
- return `${getStreamDir(workflowId, namespace)}/metadata.json`;
5881
- }
5882
- function getItemsPath(workflowId, namespace) {
5883
- return `${getStreamDir(workflowId, namespace)}/items.jsonl`;
5884
- }
5885
- function getSubscriberKey(workflowId, namespace) {
5886
- return `${workflowId}:${namespace}`;
5887
- }
5888
- async function ensureDir(workflowId, namespace) {
5889
- const dir = getStreamDir(workflowId, namespace);
5890
- await fs.mkdir(dir, { recursive: true });
5891
- }
5892
- async function readMetadata(workflowId, namespace) {
5893
- const metaPath = getMetadataPath(workflowId, namespace);
5894
- if (!await fs.exists(metaPath)) {
5895
- return void 0;
5896
- }
5897
- const content = await fs.readFile(metaPath);
5898
- return JSON.parse(content);
5899
- }
5900
- async function writeMetadata(workflowId, namespace, metadata) {
5901
- await ensureDir(workflowId, namespace);
5902
- const metaPath = getMetadataPath(workflowId, namespace);
5903
- await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2));
5904
- }
5905
- async function appendItem(workflowId, namespace, item) {
5906
- await ensureDir(workflowId, namespace);
5907
- const itemsPath = getItemsPath(workflowId, namespace);
5908
- const line = JSON.stringify(item) + "\n";
5909
- let content = "";
5910
- if (await fs.exists(itemsPath)) {
5911
- content = await fs.readFile(itemsPath);
5912
- }
5913
- await fs.writeFile(itemsPath, content + line);
5914
- }
5915
- async function readItems(workflowId, namespace, startIndex, limit) {
5916
- const itemsPath = getItemsPath(workflowId, namespace);
5917
- if (!await fs.exists(itemsPath)) {
5918
- return [];
5919
- }
5920
- const content = await fs.readFile(itemsPath);
5921
- const lines = content.trim().split("\n").filter(Boolean);
5922
- const items = [];
5923
- for (const line of lines) {
5924
- const item = JSON.parse(line);
5925
- if (item.position >= startIndex) {
5926
- items.push(item);
5927
- if (limit !== void 0 && items.length >= limit) {
5928
- break;
5929
- }
5930
- }
5931
- }
5932
- return items;
5933
- }
5934
- return {
5935
- async append(workflowId, namespace, item) {
5936
- try {
5937
- let metadata = await readMetadata(workflowId, namespace);
5938
- if (!metadata) {
5939
- metadata = {
5940
- id: `${workflowId}:${namespace}`,
5941
- namespace,
5942
- workflowId,
5943
- length: 0,
5944
- closed: false,
5945
- createdAt: Date.now()
5946
- };
5947
- }
5948
- if (metadata.closed) {
5949
- return err(
5950
- streamStoreError("write_error", "Cannot write to closed stream")
5951
- );
5952
- }
5953
- const serializedItem = {
5954
- value: item.value,
5955
- position: item.position,
5956
- ts: item.ts
5957
- };
5958
- await appendItem(workflowId, namespace, serializedItem);
5959
- metadata.length++;
5960
- metadata.lastWriteAt = Date.now();
5961
- await writeMetadata(workflowId, namespace, metadata);
5962
- const key = getSubscriberKey(workflowId, namespace);
5963
- const subs = subscribers.get(key);
5964
- if (subs) {
5965
- for (const callback of subs) {
5966
- try {
5967
- callback(item);
5968
- } catch {
5969
- }
5970
- }
5971
- }
5972
- return ok2(void 0);
5973
- } catch (cause) {
5974
- return err(
5975
- streamStoreError("write_error", "Failed to append item", cause)
5976
- );
5977
- }
5978
- },
5979
- async read(workflowId, namespace, startIndex, limit) {
5980
- try {
5981
- const serializedItems = await readItems(
5982
- workflowId,
5983
- namespace,
5984
- startIndex,
5985
- limit
5986
- );
5987
- const items = serializedItems.map((si) => ({
5988
- value: si.value,
5989
- position: si.position,
5990
- ts: si.ts
5991
- }));
5992
- return ok2(items);
5993
- } catch (cause) {
5994
- return err(streamStoreError("read_error", "Failed to read items", cause));
5995
- }
5996
- },
5997
- async getMetadata(workflowId, namespace) {
5998
- try {
5999
- const meta = await readMetadata(workflowId, namespace);
6000
- if (!meta) {
6001
- return ok2(void 0);
6002
- }
6003
- return ok2({
6004
- id: meta.id,
6005
- namespace: meta.namespace,
6006
- workflowId: meta.workflowId,
6007
- length: meta.length,
6008
- closed: meta.closed,
6009
- createdAt: meta.createdAt,
6010
- lastWriteAt: meta.lastWriteAt,
6011
- closedAt: meta.closedAt
6012
- });
6013
- } catch (cause) {
6014
- return err(
6015
- streamStoreError("metadata_error", "Failed to get metadata", cause)
6016
- );
6017
- }
6018
- },
6019
- async closeStream(workflowId, namespace) {
6020
- try {
6021
- const metadata = await readMetadata(workflowId, namespace);
6022
- if (metadata) {
6023
- metadata.closed = true;
6024
- metadata.closedAt = Date.now();
6025
- await writeMetadata(workflowId, namespace, metadata);
6026
- }
6027
- return ok2(void 0);
6028
- } catch (cause) {
6029
- return err(
6030
- streamStoreError("close_error", "Failed to close stream", cause)
6031
- );
6032
- }
6033
- },
6034
- subscribe(workflowId, namespace, callback) {
6035
- const key = getSubscriberKey(workflowId, namespace);
6036
- let subs = subscribers.get(key);
6037
- if (!subs) {
6038
- subs = /* @__PURE__ */ new Set();
6039
- subscribers.set(key, subs);
6040
- }
6041
- const typedCallback = callback;
6042
- subs.add(typedCallback);
6043
- return () => {
6044
- subs?.delete(typedCallback);
6045
- if (subs?.size === 0) {
6046
- subscribers.delete(key);
6047
- }
6048
- };
6049
- }
6050
- };
6051
- }
6052
-
6053
- // src/streaming/transformers.ts
6054
- async function* toAsyncIterable(reader) {
6055
- while (reader.readable) {
6056
- const result = await reader.read();
6057
- if (!result.ok) {
6058
- if (isStreamEnded(result.error)) {
6059
- break;
6060
- }
6061
- throw result.error;
6062
- }
6063
- yield result.value;
6064
- }
6065
- }
6066
- async function* map(source, fn) {
6067
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6068
- let index = 0;
6069
- for await (const item of iterable) {
6070
- yield await fn(item, index++);
6071
- }
6072
- }
6073
- async function* filter(source, predicate) {
6074
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6075
- let index = 0;
6076
- for await (const item of iterable) {
6077
- if (await predicate(item, index++)) {
6078
- yield item;
6079
- }
6080
- }
6081
- }
6082
- async function* chunk(source, size) {
6083
- if (size < 1) {
6084
- throw new Error("Chunk size must be at least 1");
6085
- }
6086
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6087
- let currentChunk = [];
6088
- for await (const item of iterable) {
6089
- currentChunk.push(item);
6090
- if (currentChunk.length >= size) {
6091
- yield currentChunk;
6092
- currentChunk = [];
6093
- }
6094
- }
6095
- if (currentChunk.length > 0) {
6096
- yield currentChunk;
6097
- }
6098
- }
6099
- async function* flatMap(source, fn) {
6100
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6101
- let index = 0;
6102
- for await (const item of iterable) {
6103
- const mapped = fn(item, index++);
6104
- if (Symbol.asyncIterator in mapped) {
6105
- for await (const subItem of mapped) {
6106
- yield subItem;
6107
- }
6108
- } else {
6109
- for (const subItem of mapped) {
6110
- yield subItem;
6111
- }
6112
- }
6113
- }
6114
- }
6115
- async function* flatMapAsync(source, fn) {
6116
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6117
- let index = 0;
6118
- for await (const item of iterable) {
6119
- const result = await fn(item, index++);
6120
- if (!result.ok) {
6121
- yield err(result.error);
6122
- return;
6123
- }
6124
- const mapped = result.value;
6125
- if (Symbol.asyncIterator in mapped) {
6126
- for await (const subItem of mapped) {
6127
- yield ok2(subItem);
6128
- }
6129
- } else {
6130
- for (const subItem of mapped) {
6131
- yield ok2(subItem);
6132
- }
6133
- }
6134
- }
6135
- }
6136
- async function* mapAsync(source, fn) {
6137
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6138
- let index = 0;
6139
- for await (const item of iterable) {
6140
- yield await fn(item, index++);
6141
- }
6142
- }
6143
- async function* take(source, count) {
6144
- if (count <= 0) return;
6145
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6146
- let taken = 0;
6147
- for await (const item of iterable) {
6148
- yield item;
6149
- taken++;
6150
- if (taken >= count) break;
6151
- }
6152
- }
6153
- async function* skip(source, count) {
6154
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6155
- let skipped = 0;
6156
- for await (const item of iterable) {
6157
- if (skipped < count) {
6158
- skipped++;
6159
- continue;
6160
- }
6161
- yield item;
6162
- }
6163
- }
6164
- async function* takeWhile(source, predicate) {
6165
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6166
- let index = 0;
6167
- for await (const item of iterable) {
6168
- if (!await predicate(item, index++)) break;
6169
- yield item;
6170
- }
6171
- }
6172
- async function* skipWhile(source, predicate) {
6173
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6174
- let skipping = true;
6175
- let index = 0;
6176
- for await (const item of iterable) {
6177
- if (skipping && await predicate(item, index++)) {
6178
- continue;
6179
- }
6180
- skipping = false;
6181
- yield item;
6182
- }
6183
- }
6184
- async function collect(source) {
6185
- const items = [];
6186
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6187
- for await (const item of iterable) {
6188
- items.push(item);
6189
- }
6190
- return items;
6191
- }
6192
- async function reduce(source, reducer, initial) {
6193
- let accumulator = initial;
6194
- const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
6195
- let index = 0;
6196
- for await (const item of iterable) {
6197
- accumulator = await reducer(accumulator, item, index++);
6198
- }
6199
- return accumulator;
6200
- }
6201
- function isStreamReader(value) {
6202
- return typeof value === "object" && value !== null && "read" in value && typeof value.read === "function" && "readable" in value;
6203
- }
6204
- function pipe(source, ...transformers) {
6205
- let result = isStreamReader(source) ? toAsyncIterable(source) : source;
6206
- for (const transformer of transformers) {
6207
- result = transformer(result);
6208
- }
6209
- return result;
6210
- }
6211
-
6212
- // src/streaming/index.ts
6213
- function getStreamReader(options) {
6214
- const {
6215
- store,
6216
- workflowId,
6217
- namespace = "default",
6218
- startIndex = 0,
6219
- pollInterval = 100,
6220
- pollTimeout = 3e4
6221
- } = options;
6222
- let position = startIndex;
6223
- let readable = true;
6224
- let closed = false;
6225
- let bufferedItems = [];
6226
- let bufferIndex = 0;
6227
- const reader = {
6228
- async read() {
6229
- if (closed) {
6230
- return err(streamReadError("closed", "Reader is closed"));
6231
- }
6232
- if (bufferIndex < bufferedItems.length) {
6233
- const item = bufferedItems[bufferIndex++];
6234
- position = item.position + 1;
6235
- return ok2(item.value);
6236
- }
6237
- const pollStart = Date.now();
6238
- while (Date.now() - pollStart < pollTimeout) {
6239
- const result = await store.read(workflowId, namespace, position, 100);
6240
- if (!result.ok) {
6241
- return err(streamReadError("store_error", result.error.message, result.error));
6242
- }
6243
- const items = result.value;
6244
- if (items.length > 0) {
6245
- bufferedItems = items;
6246
- bufferIndex = 1;
6247
- const item = items[0];
6248
- position = item.position + 1;
6249
- return ok2(item.value);
6250
- }
6251
- const metaResult = await store.getMetadata(workflowId, namespace);
6252
- if (metaResult.ok && metaResult.value?.closed) {
6253
- readable = false;
6254
- return err(streamEnded(position));
6255
- }
6256
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
6257
- }
6258
- readable = false;
6259
- return err(streamEnded(position));
6260
- },
6261
- close() {
6262
- closed = true;
6263
- readable = false;
6264
- bufferedItems = [];
6265
- },
6266
- get readable() {
6267
- return readable;
6268
- },
6269
- get position() {
6270
- return position;
6271
- },
6272
- get namespace() {
6273
- return namespace;
6274
- }
6275
- };
6276
- return reader;
6277
- }
6278
-
6279
- // src/webhook/index.ts
6280
- function isValidationError(e) {
6281
- return typeof e === "object" && e !== null && e.type === "VALIDATION_ERROR";
6282
- }
6283
- function defaultValidationErrorMapper(error) {
6284
- return {
6285
- status: 400,
6286
- body: {
6287
- error: {
6288
- type: error.type,
6289
- message: error.message,
6290
- details: error.field ? { field: error.field } : error.details
6291
- }
6292
- }
6293
- };
6294
- }
6295
- function defaultUnexpectedErrorMapper(error) {
6296
- return {
6297
- status: 500,
6298
- body: {
6299
- error: {
6300
- type: "INTERNAL_ERROR",
6301
- message: error instanceof Error ? error.message : "An unexpected error occurred"
6302
- }
6303
- }
6304
- };
6305
- }
6306
- function createWebhookHandler(workflow, workflowFn, config) {
6307
- const {
6308
- validateInput: validateInput2,
6309
- mapResult,
6310
- mapValidationError = defaultValidationErrorMapper,
6311
- mapUnexpectedError = defaultUnexpectedErrorMapper,
6312
- beforeValidation,
6313
- afterResponse
6314
- } = config;
6315
- return async (req) => {
6316
- try {
6317
- const processedReq = beforeValidation ? await beforeValidation(req) : req;
6318
- const validationResult = await validateInput2(processedReq);
6319
- if (!isOk(validationResult)) {
6320
- const response2 = mapValidationError(validationResult.error, processedReq);
6321
- return afterResponse ? await afterResponse(response2, processedReq) : response2;
6322
- }
6323
- const args = validationResult.value;
6324
- const workflowResult = await workflow.run(
6325
- async ({ step, deps }) => workflowFn({ step, deps, args })
6326
- );
6327
- const response = mapResult(workflowResult, processedReq);
6328
- return afterResponse ? await afterResponse(response, processedReq) : response;
6329
- } catch (error) {
6330
- const response = mapUnexpectedError(error, req);
6331
- return afterResponse ? await afterResponse(response, req) : response;
6332
- }
6333
- };
6334
- }
6335
- function createSimpleHandler(config) {
6336
- const {
6337
- validateInput: validateInput2,
6338
- handler,
6339
- mapResult,
6340
- mapValidationError = defaultValidationErrorMapper,
6341
- mapUnexpectedError = defaultUnexpectedErrorMapper
6342
- } = config;
6343
- return async (req) => {
6344
- try {
6345
- const validationResult = await validateInput2(req);
6346
- if (!isOk(validationResult)) {
6347
- return mapValidationError(validationResult.error, req);
6348
- }
6349
- const result = await handler(validationResult.value, req);
6350
- return mapResult(result, req);
6351
- } catch (error) {
6352
- return mapUnexpectedError(error, req);
6353
- }
6354
- };
6355
- }
6356
- function createResultMapper(mappings, options = {}) {
6357
- const { defaultStatus = 500, successStatus = 200 } = options;
6358
- const errorMap = /* @__PURE__ */ new Map();
6359
- for (const mapping of mappings) {
6360
- errorMap.set(mapping.error, mapping);
6361
- }
6362
- return (result) => {
6363
- if (result.ok) {
6364
- return {
6365
- status: successStatus,
6366
- body: result.value
6367
- };
6368
- }
6369
- if (isUnexpectedError(result.error)) {
6370
- return {
6371
- status: 500,
6372
- body: {
6373
- error: {
6374
- type: "INTERNAL_ERROR",
6375
- message: result.error instanceof Error ? result.error.message : "An unexpected error occurred"
6376
- }
6377
- }
6378
- };
6379
- }
6380
- const mapping = errorMap.get(result.error);
6381
- if (mapping) {
6382
- return {
6383
- status: mapping.status,
6384
- body: {
6385
- error: {
6386
- type: String(mapping.error),
6387
- message: mapping.message
6388
- }
6389
- }
6390
- };
6391
- }
6392
- return {
6393
- status: defaultStatus,
6394
- body: {
6395
- error: {
6396
- type: String(result.error)
6397
- }
6398
- }
6399
- };
6400
- };
6401
- }
6402
- function toWebhookRequest(req) {
6403
- return {
6404
- method: req.method,
6405
- path: req.path,
6406
- headers: req.headers,
6407
- body: req.body,
6408
- query: req.query,
6409
- params: req.params,
6410
- raw: req
6411
- };
6412
- }
6413
- function sendWebhookResponse(res, response) {
6414
- if (response.headers) {
6415
- res.set(response.headers);
6416
- }
6417
- res.status(response.status).json(response.body);
6418
- }
6419
- function createExpressHandler(handler) {
6420
- return async (req, res) => {
6421
- const webhookReq = toWebhookRequest(req);
6422
- const response = await handler(webhookReq);
6423
- sendWebhookResponse(res, response);
6424
- };
6425
- }
6426
- function validationError(message, field, details) {
6427
- return {
6428
- type: "VALIDATION_ERROR",
6429
- message,
6430
- field,
6431
- details
6432
- };
6433
- }
6434
- function requireFields(fields) {
6435
- return (body) => {
6436
- for (const field of fields) {
6437
- if (body[field] === void 0 || body[field] === null || body[field] === "") {
6438
- return err(validationError(`Missing required field: ${field}`, field));
6439
- }
6440
- }
6441
- return ok2(void 0);
6442
- };
6443
- }
6444
- function composeValidators(...validators) {
6445
- return (input) => {
6446
- for (const validator of validators) {
6447
- const result = validator(input);
6448
- if (!result.ok) return result;
6449
- }
6450
- return ok2(void 0);
6451
- };
6452
- }
6453
- function createEventHandler(workflow, workflowFn, config) {
6454
- const { validatePayload, mapResult } = config;
6455
- return async (event) => {
6456
- try {
6457
- const validationResult = validatePayload(event);
6458
- if (!isOk(validationResult)) {
6459
- return {
6460
- success: false,
6461
- ack: true,
6462
- // Don't retry validation errors
6463
- error: {
6464
- type: validationResult.error.type,
6465
- message: validationResult.error.message,
6466
- retryable: false
6467
- }
6468
- };
6469
- }
6470
- const args = validationResult.value;
6471
- const workflowResult = await workflow.run(
6472
- async ({ step, deps }) => workflowFn({ step, deps, args })
6473
- );
6474
- return mapResult(workflowResult, event);
6475
- } catch (error) {
6476
- return {
6477
- success: false,
6478
- ack: false,
6479
- // Retry unexpected errors
6480
- error: {
6481
- type: "UnexpectedError",
6482
- message: error instanceof Error ? error.message : String(error),
6483
- retryable: true
6484
- }
6485
- };
6486
- }
6487
- };
6488
- }
6489
-
6490
- // src/engine/index.ts
6491
- var import_node_crypto = require("crypto");
6492
- function createEngine(options) {
6493
- const {
6494
- store,
6495
- workflows,
6496
- concurrency = 5,
6497
- onEvent,
6498
- onError
6499
- } = options;
6500
- const schedules = /* @__PURE__ */ new Map();
6501
- let pollTimer;
6502
- let running = false;
6503
- let tickInFlight = false;
6504
- function emit(event) {
6505
- try {
6506
- onEvent?.(event);
6507
- } catch {
6508
- }
6509
- }
6510
- async function enqueue(name, opts) {
6511
- const wf = workflows[name];
6512
- if (!wf) {
6513
- throw new Error(`Unknown workflow: '${name}'. Registered: ${Object.keys(workflows).join(", ")}`);
6514
- }
6515
- const id = opts?.id ?? `${name}:${(0, import_node_crypto.randomUUID)()}`;
6516
- const snapshot = {
6517
- formatVersion: 1,
6518
- workflowName: name,
6519
- steps: {},
6520
- execution: {
6521
- status: "running",
6522
- // "running" so durable.run can resume it
6523
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
6524
- },
6525
- metadata: {
6526
- engineState: "queued",
6527
- workflowName: name,
6528
- input: opts?.input ?? null,
6529
- idempotencyKey: opts?.idempotencyKey,
6530
- enqueuedAt: (/* @__PURE__ */ new Date()).toISOString(),
6531
- ...opts?.metadata
6532
- }
6533
- };
6534
- await store.save(id, snapshot);
6535
- emit({ type: "workflow_enqueued", workflowName: name, id, ts: Date.now() });
6536
- return id;
6537
- }
6538
- async function tick() {
6539
- if (tickInFlight) return 0;
6540
- tickInFlight = true;
6541
- try {
6542
- const entries = await store.list({ limit: concurrency * 2 });
6543
- let processed = 0;
6544
- const queued = [];
6545
- for (const entry of entries) {
6546
- if (queued.length >= concurrency) break;
6547
- const snapshot = await store.load(entry.id);
6548
- if (snapshot && snapshot.metadata?.engineState === "queued" && typeof snapshot.metadata?.workflowName === "string") {
6549
- queued.push({ id: entry.id, workflowName: snapshot.metadata.workflowName, snapshot });
6550
- }
6551
- }
6552
- const results = await Promise.allSettled(
6553
- queued.map(async ({ id, workflowName, snapshot }) => {
6554
- const wf = workflows[workflowName];
6555
- if (!wf) return;
6556
- const processingSnapshot = {
6557
- ...snapshot,
6558
- metadata: {
6559
- ...snapshot.metadata,
6560
- engineState: "processing"
6561
- }
6562
- };
6563
- await store.save(id, processingSnapshot);
6564
- emit({ type: "workflow_started", workflowName, id, ts: Date.now() });
6565
- const result = await durable.run(wf.deps, wf.fn, {
6566
- id,
6567
- store,
6568
- idempotencyKey: snapshot.metadata?.idempotencyKey,
6569
- input: snapshot.metadata?.input,
6570
- ...wf.durableDefaults
6571
- });
6572
- if (result.ok) {
6573
- emit({ type: "workflow_completed", workflowName, id, ts: Date.now() });
6574
- } else {
6575
- emit({ type: "workflow_failed", workflowName, id, error: result.error, ts: Date.now() });
6576
- }
6577
- processed++;
6578
- })
6579
- );
6580
- for (const r of results) {
6581
- if (r.status === "rejected") {
6582
- try {
6583
- onError?.(r.reason);
6584
- } catch {
6585
- }
6586
- }
6587
- }
6588
- emit({ type: "engine_tick", ts: Date.now(), processed });
6589
- return processed;
6590
- } finally {
6591
- tickInFlight = false;
6592
- }
6593
- }
6594
- function schedule(name, opts) {
6595
- const scheduleId = opts?.id ? `schedule:${opts.id}` : `schedule:${(0, import_node_crypto.randomUUID)()}`;
6596
- if (opts.immediate) {
6597
- void enqueue(name, opts).catch((e) => {
6598
- try {
6599
- onError?.(e);
6600
- } catch {
6601
- }
6602
- });
6603
- }
6604
- const interval = setInterval(() => {
6605
- void enqueue(name, opts).catch((e) => {
6606
- try {
6607
- onError?.(e);
6608
- } catch {
6609
- }
6610
- });
6611
- }, opts.intervalMs);
6612
- schedules.set(scheduleId, interval);
6613
- emit({ type: "schedule_created", workflowName: name, scheduleId, intervalMs: opts.intervalMs, ts: Date.now() });
6614
- return scheduleId;
6615
- }
6616
- function unschedule(scheduleId) {
6617
- const interval = schedules.get(scheduleId);
6618
- if (!interval) return false;
6619
- clearInterval(interval);
6620
- schedules.delete(scheduleId);
6621
- emit({ type: "schedule_removed", scheduleId, ts: Date.now() });
6622
- return true;
6623
- }
6624
- function start(pollIntervalMs = 1e3) {
6625
- if (running) return;
6626
- running = true;
6627
- emit({ type: "engine_start", ts: Date.now() });
6628
- void tick().catch((e) => {
6629
- try {
6630
- onError?.(e);
6631
- } catch {
6632
- }
6633
- });
6634
- pollTimer = setInterval(() => {
6635
- void tick().catch((e) => {
6636
- try {
6637
- onError?.(e);
6638
- } catch {
6639
- }
6640
- });
6641
- }, pollIntervalMs);
6642
- }
6643
- async function stop() {
6644
- if (!running) return;
6645
- running = false;
6646
- if (pollTimer) {
6647
- clearInterval(pollTimer);
6648
- pollTimer = void 0;
6649
- }
6650
- for (const [id, interval] of schedules) {
6651
- clearInterval(interval);
6652
- schedules.delete(id);
6653
- }
6654
- while (tickInFlight) {
6655
- await new Promise((resolve) => setTimeout(resolve, 50));
6656
- }
6657
- emit({ type: "engine_stop", ts: Date.now() });
6658
- }
6659
- return {
6660
- enqueue,
6661
- schedule,
6662
- unschedule,
6663
- start,
6664
- stop,
6665
- tick,
6666
- status: () => ({ running, pendingSchedules: schedules.size })
6667
- };
6668
- }
6669
-
6670
4137
  // src/resource.ts
6671
4138
  function isResourceCleanupError(error) {
6672
4139
  return typeof error === "object" && error !== null && error.type === "RESOURCE_CLEANUP_ERROR";
@@ -6959,66 +4426,26 @@ var batchPresets = {
6959
4426
  Duration,
6960
4427
  HOOK_STEP_KEY_PREFIX,
6961
4428
  STEP_TIMEOUT_MARKER,
6962
- STREAM_BACKPRESSURE_ERROR,
6963
- STREAM_CLOSE_ERROR,
6964
- STREAM_ENDED,
6965
- STREAM_READ_ERROR,
6966
- STREAM_STORE_ERROR,
6967
- STREAM_WRITE_ERROR,
6968
4429
  SnapshotDecodeError,
6969
4430
  SnapshotFormatError,
6970
4431
  SnapshotMismatchError,
6971
4432
  UnexpectedError,
6972
4433
  assertValidSnapshot,
6973
4434
  batchPresets,
6974
- chunk,
6975
4435
  clearStep,
6976
- collect,
6977
- composeMigrations,
6978
- composeValidators,
6979
- createApprovalChecker,
6980
4436
  createApprovalStateCollector,
6981
4437
  createApprovalStep,
6982
- createApprovalWebhookHandler,
6983
- createBackpressureController,
6984
- createEngine,
6985
- createEventHandler,
6986
- createExpressHandler,
6987
- createFileStreamStore,
6988
- createHITLOrchestrator,
6989
4438
  createHook,
6990
- createKeyRemoveMigration,
6991
- createKeyRenameMigration,
6992
- createMemoryApprovalStore,
6993
- createMemoryCache,
6994
- createMemoryStreamStore,
6995
- createMemoryWorkflowStateStore,
6996
4439
  createResource,
6997
4440
  createResourceScope,
6998
- createResultMapper,
6999
4441
  createResumeStateCollector,
7000
- createSagaWorkflow,
7001
- createSimpleHandler,
7002
- createTestableMemoryStreamStore,
7003
- createValueTransformMigration,
7004
- createVersionedState,
7005
- createVersionedStateLoader,
7006
- createWebhookHandler,
7007
4442
  createWorkflow,
7008
4443
  days,
7009
- defaultUnexpectedErrorMapper,
7010
- defaultValidationErrorMapper,
7011
- deserializeCauseNew,
7012
4444
  deserializeResumeState,
7013
- durable,
7014
- filter,
7015
- flatMap,
7016
- flatMapAsync,
7017
4445
  gatedStep,
7018
4446
  getPendingApprovals,
7019
4447
  getPendingHooks,
7020
4448
  getStepTimeoutMeta,
7021
- getStreamReader,
7022
4449
  hasPendingApproval,
7023
4450
  hasPendingHook,
7024
4451
  hours,
@@ -7026,75 +4453,36 @@ var batchPresets = {
7026
4453
  injectHook,
7027
4454
  isApprovalRejected,
7028
4455
  isBatchProcessingError,
7029
- isConcurrentExecution,
7030
4456
  isDuration,
7031
- isIdempotencyConflict,
7032
4457
  isInputValidationError,
7033
4458
  isInvalidBatchConfigError,
7034
- isLeaseExpired,
7035
- isMigrationError,
7036
4459
  isPendingApproval,
7037
4460
  isPendingHook,
7038
- isPersistenceError,
7039
4461
  isResourceCleanupError,
7040
4462
  isResumeState,
7041
- isSagaCompensationError,
7042
4463
  isSerializedResumeState,
7043
4464
  isStepComplete,
7044
4465
  isStepTimeoutError,
7045
- isStreamBackpressureError,
7046
- isStreamEnded,
7047
- isStreamReadError,
7048
- isStreamStoreError,
7049
- isStreamWriteError,
7050
- isValidationError,
7051
- isVersionIncompatibleError,
7052
- isVersionMismatch,
7053
4466
  isWorkflowCancelled,
7054
4467
  isWorkflowSnapshot,
7055
4468
  looksLikeWorkflowSnapshot,
7056
- map,
7057
- mapAsync,
7058
4469
  mergeSnapshots,
7059
4470
  millis,
7060
4471
  minutes,
7061
- parseVersionedState,
7062
4472
  pendingApproval,
7063
4473
  pendingHook,
7064
- pipe,
7065
4474
  processInBatches,
7066
- reduce,
7067
- requireFields,
7068
4475
  run,
7069
- runSaga,
7070
4476
  seconds,
7071
- sendWebhookResponse,
7072
- serializeError,
7073
4477
  serializeResumeState,
7074
- serializeThrown,
7075
- shouldApplyBackpressure,
7076
- skip,
7077
- skipWhile,
7078
- streamBackpressureError,
7079
- streamCloseError,
7080
- streamEnded,
7081
- streamReadError,
7082
- streamStoreError,
7083
- streamWriteError,
7084
- stringifyVersionedState,
7085
- take,
7086
- takeWhile,
7087
- toAsyncIterable,
7088
4478
  toDays,
7089
4479
  toHours,
7090
4480
  toMillis,
7091
4481
  toMinutes,
7092
4482
  toResumeState,
7093
4483
  toSeconds,
7094
- toWebhookRequest,
7095
4484
  validateInput,
7096
4485
  validateSnapshot,
7097
- validationError,
7098
4486
  withDeps,
7099
4487
  withScope
7100
4488
  });