@freshbox-medusa/orchestration 1.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 (74) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +20 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/joiner/helpers.d.ts +3 -0
  6. package/dist/joiner/helpers.d.ts.map +1 -0
  7. package/dist/joiner/helpers.js +69 -0
  8. package/dist/joiner/helpers.js.map +1 -0
  9. package/dist/joiner/index.d.ts +3 -0
  10. package/dist/joiner/index.d.ts.map +1 -0
  11. package/dist/joiner/index.js +19 -0
  12. package/dist/joiner/index.js.map +1 -0
  13. package/dist/joiner/remote-joiner.d.ts +43 -0
  14. package/dist/joiner/remote-joiner.d.ts.map +1 -0
  15. package/dist/joiner/remote-joiner.js +1279 -0
  16. package/dist/joiner/remote-joiner.js.map +1 -0
  17. package/dist/transaction/datastore/abstract-storage.d.ts +44 -0
  18. package/dist/transaction/datastore/abstract-storage.d.ts.map +1 -0
  19. package/dist/transaction/datastore/abstract-storage.js +52 -0
  20. package/dist/transaction/datastore/abstract-storage.js.map +1 -0
  21. package/dist/transaction/datastore/base-in-memory-storage.d.ts +12 -0
  22. package/dist/transaction/datastore/base-in-memory-storage.d.ts.map +1 -0
  23. package/dist/transaction/datastore/base-in-memory-storage.js +35 -0
  24. package/dist/transaction/datastore/base-in-memory-storage.js.map +1 -0
  25. package/dist/transaction/distributed-transaction.d.ts +116 -0
  26. package/dist/transaction/distributed-transaction.d.ts.map +1 -0
  27. package/dist/transaction/distributed-transaction.js +488 -0
  28. package/dist/transaction/distributed-transaction.js.map +1 -0
  29. package/dist/transaction/errors.d.ts +41 -0
  30. package/dist/transaction/errors.d.ts.map +1 -0
  31. package/dist/transaction/errors.js +117 -0
  32. package/dist/transaction/errors.js.map +1 -0
  33. package/dist/transaction/index.d.ts +8 -0
  34. package/dist/transaction/index.d.ts.map +1 -0
  35. package/dist/transaction/index.js +24 -0
  36. package/dist/transaction/index.js.map +1 -0
  37. package/dist/transaction/orchestrator-builder.d.ts +36 -0
  38. package/dist/transaction/orchestrator-builder.d.ts.map +1 -0
  39. package/dist/transaction/orchestrator-builder.js +300 -0
  40. package/dist/transaction/orchestrator-builder.js.map +1 -0
  41. package/dist/transaction/transaction-orchestrator.d.ts +207 -0
  42. package/dist/transaction/transaction-orchestrator.d.ts.map +1 -0
  43. package/dist/transaction/transaction-orchestrator.js +1295 -0
  44. package/dist/transaction/transaction-orchestrator.js.map +1 -0
  45. package/dist/transaction/transaction-step.d.ts +69 -0
  46. package/dist/transaction/transaction-step.d.ts.map +1 -0
  47. package/dist/transaction/transaction-step.js +153 -0
  48. package/dist/transaction/transaction-step.js.map +1 -0
  49. package/dist/transaction/types.d.ts +264 -0
  50. package/dist/transaction/types.d.ts.map +1 -0
  51. package/dist/transaction/types.js +23 -0
  52. package/dist/transaction/types.js.map +1 -0
  53. package/dist/tsconfig.tsbuildinfo +1 -0
  54. package/dist/workflow/global-workflow.d.ts +14 -0
  55. package/dist/workflow/global-workflow.d.ts.map +1 -0
  56. package/dist/workflow/global-workflow.js +105 -0
  57. package/dist/workflow/global-workflow.js.map +1 -0
  58. package/dist/workflow/index.d.ts +5 -0
  59. package/dist/workflow/index.d.ts.map +1 -0
  60. package/dist/workflow/index.js +21 -0
  61. package/dist/workflow/index.js.map +1 -0
  62. package/dist/workflow/local-workflow.d.ts +47 -0
  63. package/dist/workflow/local-workflow.d.ts.map +1 -0
  64. package/dist/workflow/local-workflow.js +390 -0
  65. package/dist/workflow/local-workflow.js.map +1 -0
  66. package/dist/workflow/scheduler.d.ts +12 -0
  67. package/dist/workflow/scheduler.d.ts.map +1 -0
  68. package/dist/workflow/scheduler.js +35 -0
  69. package/dist/workflow/scheduler.js.map +1 -0
  70. package/dist/workflow/workflow-manager.d.ts +38 -0
  71. package/dist/workflow/workflow-manager.d.ts.map +1 -0
  72. package/dist/workflow/workflow-manager.js +124 -0
  73. package/dist/workflow/workflow-manager.js.map +1 -0
  74. package/package.json +41 -0
@@ -0,0 +1,1295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransactionOrchestrator = void 0;
4
+ const ulid_1 = require("ulid");
5
+ const distributed_transaction_1 = require("./distributed-transaction");
6
+ const transaction_step_1 = require("./transaction-step");
7
+ const types_1 = require("./types");
8
+ const utils_1 = require("@freshbox-medusa/utils");
9
+ const events_1 = require("events");
10
+ const errors_1 = require("./errors");
11
+ const canMoveForwardStates = new Set([
12
+ utils_1.TransactionStepState.DONE,
13
+ utils_1.TransactionStepState.FAILED,
14
+ utils_1.TransactionStepState.TIMEOUT,
15
+ utils_1.TransactionStepState.SKIPPED,
16
+ utils_1.TransactionStepState.SKIPPED_FAILURE,
17
+ ]);
18
+ const canMoveBackwardStates = new Set([
19
+ utils_1.TransactionStepState.DONE,
20
+ utils_1.TransactionStepState.REVERTED,
21
+ utils_1.TransactionStepState.FAILED,
22
+ utils_1.TransactionStepState.DORMANT,
23
+ utils_1.TransactionStepState.SKIPPED,
24
+ ]);
25
+ const flagStepsToRevertStates = new Set([
26
+ utils_1.TransactionStepState.DONE,
27
+ utils_1.TransactionStepState.TIMEOUT,
28
+ ]);
29
+ const setStepTimeoutSkipStates = new Set([
30
+ utils_1.TransactionStepState.TIMEOUT,
31
+ utils_1.TransactionStepState.DONE,
32
+ utils_1.TransactionStepState.REVERTED,
33
+ ]);
34
+ /**
35
+ * @class TransactionOrchestrator is responsible for managing and executing distributed transactions.
36
+ * It is based on a single transaction definition, which is used to execute all the transaction steps
37
+ */
38
+ class TransactionOrchestrator extends events_1.EventEmitter {
39
+ static getWorkflowOptions(modelId) {
40
+ return TransactionOrchestrator.workflowOptions[modelId];
41
+ }
42
+ constructor({ id, definition, options, isClone, }) {
43
+ super();
44
+ this.invokeSteps = [];
45
+ this.compensateSteps = [];
46
+ this.id = id;
47
+ this.definition = definition;
48
+ this.options = options;
49
+ if (!isClone) {
50
+ this.parseFlowOptions();
51
+ }
52
+ }
53
+ static isExpectedError(error) {
54
+ return (errors_1.SkipCancelledExecutionError.isSkipCancelledExecutionError(error) ||
55
+ errors_1.SkipExecutionError.isSkipExecutionError(error) ||
56
+ errors_1.SkipStepAlreadyFinishedError.isSkipStepAlreadyFinishedError(error));
57
+ }
58
+ static clone(orchestrator) {
59
+ return new TransactionOrchestrator({
60
+ id: orchestrator.id,
61
+ definition: orchestrator.definition,
62
+ options: orchestrator.options,
63
+ isClone: true,
64
+ });
65
+ }
66
+ static getKeyName(...params) {
67
+ return params.join(this.SEPARATOR);
68
+ }
69
+ static getPreviousStep(flow, step) {
70
+ const id = step.id.split(".");
71
+ id.pop();
72
+ const parentId = id.join(".");
73
+ return flow.steps[parentId];
74
+ }
75
+ getOptions() {
76
+ return this.options ?? {};
77
+ }
78
+ getInvokeSteps(flow) {
79
+ if (this.invokeSteps.length) {
80
+ return this.invokeSteps;
81
+ }
82
+ const steps = Object.keys(flow.steps);
83
+ steps.sort((a, b) => flow.steps[a].depth - flow.steps[b].depth);
84
+ this.invokeSteps = steps;
85
+ return steps;
86
+ }
87
+ getCompensationSteps(flow) {
88
+ if (this.compensateSteps.length) {
89
+ return this.compensateSteps;
90
+ }
91
+ const steps = Object.keys(flow.steps);
92
+ steps.sort((a, b) => (flow.steps[b].depth || 0) - (flow.steps[a].depth || 0));
93
+ this.compensateSteps = steps;
94
+ return steps;
95
+ }
96
+ static countSiblings(flow, step) {
97
+ const previous = TransactionOrchestrator.getPreviousStep(flow, step);
98
+ return previous.next.length;
99
+ }
100
+ canMoveForward(flow, previousStep) {
101
+ const siblings = TransactionOrchestrator.getPreviousStep(flow, previousStep).next.map((sib) => flow.steps[sib]);
102
+ return (!!previousStep.definition.noWait ||
103
+ siblings.every((sib) => canMoveForwardStates.has(sib.invoke.state)));
104
+ }
105
+ canMoveBackward(flow, step) {
106
+ const siblings = step.next.map((sib) => flow.steps[sib]);
107
+ return (siblings.length === 0 ||
108
+ siblings.every((sib) => canMoveBackwardStates.has(sib.compensate.state)));
109
+ }
110
+ canContinue(flow, step) {
111
+ if (flow.state == types_1.TransactionState.COMPENSATING) {
112
+ return this.canMoveBackward(flow, step);
113
+ }
114
+ else {
115
+ const previous = TransactionOrchestrator.getPreviousStep(flow, step);
116
+ if (previous.id === TransactionOrchestrator.ROOT_STEP) {
117
+ return true;
118
+ }
119
+ return this.canMoveForward(flow, previous);
120
+ }
121
+ }
122
+ hasExpired({ transaction, step, }, dateNow) {
123
+ const hasStepTimedOut = step &&
124
+ step.hasTimeout() &&
125
+ !step.isCompensating() &&
126
+ dateNow > step.startedAt + step.getTimeout() * 1e3;
127
+ const hasTransactionTimedOut = transaction &&
128
+ transaction.hasTimeout() &&
129
+ transaction.getFlow().state !== types_1.TransactionState.COMPENSATING &&
130
+ dateNow >
131
+ transaction.getFlow().startedAt + transaction.getTimeout() * 1e3;
132
+ return !!hasStepTimedOut || !!hasTransactionTimedOut;
133
+ }
134
+ async checkTransactionTimeout(transaction, currentSteps) {
135
+ const flow = transaction.getFlow();
136
+ let hasTimedOut = false;
137
+ if (!flow.timedOutAt && this.hasExpired({ transaction }, Date.now())) {
138
+ flow.timedOutAt = Date.now();
139
+ void transaction.clearTransactionTimeout();
140
+ for (const step of currentSteps) {
141
+ await TransactionOrchestrator.setStepTimeout(transaction, step, new errors_1.TransactionTimeoutError());
142
+ }
143
+ this.emit(types_1.DistributedTransactionEvent.TIMEOUT, { transaction });
144
+ hasTimedOut = true;
145
+ }
146
+ return hasTimedOut;
147
+ }
148
+ async checkStepTimeout(transaction, step) {
149
+ let hasTimedOut = false;
150
+ if (!step.timedOutAt &&
151
+ step.canCancel() &&
152
+ this.hasExpired({ step }, Date.now())) {
153
+ step.timedOutAt = Date.now();
154
+ await TransactionOrchestrator.setStepTimeout(transaction, step, new errors_1.TransactionStepTimeoutError());
155
+ hasTimedOut = true;
156
+ this.emit(types_1.DistributedTransactionEvent.TIMEOUT, { transaction });
157
+ }
158
+ return hasTimedOut;
159
+ }
160
+ async checkAllSteps(transaction) {
161
+ const flow = transaction.getFlow();
162
+ const result = await this.computeCurrentTransactionState(transaction);
163
+ // Handle state transitions and emit events
164
+ if (flow.state === types_1.TransactionState.WAITING_TO_COMPENSATE &&
165
+ result.next.length === 0 &&
166
+ !flow.hasWaitingSteps) {
167
+ flow.state = types_1.TransactionState.COMPENSATING;
168
+ this.flagStepsToRevert(flow);
169
+ this.emit(types_1.DistributedTransactionEvent.COMPENSATE_BEGIN, { transaction });
170
+ const result = await this.checkAllSteps(transaction);
171
+ return result;
172
+ }
173
+ else if (result.completed === result.total) {
174
+ if (result.hasSkippedOnFailure) {
175
+ flow.hasSkippedOnFailureSteps = true;
176
+ }
177
+ if (result.hasSkipped) {
178
+ flow.hasSkippedSteps = true;
179
+ }
180
+ if (result.hasIgnoredFailure) {
181
+ flow.hasFailedSteps = true;
182
+ }
183
+ if (result.hasFailed) {
184
+ flow.state = types_1.TransactionState.FAILED;
185
+ }
186
+ else {
187
+ flow.state = result.hasReverted
188
+ ? types_1.TransactionState.REVERTED
189
+ : types_1.TransactionState.DONE;
190
+ }
191
+ }
192
+ return {
193
+ current: result.current,
194
+ next: result.next,
195
+ total: result.total,
196
+ remaining: result.total - result.completed,
197
+ completed: result.completed,
198
+ };
199
+ }
200
+ async computeCurrentTransactionState(transaction) {
201
+ let hasSkipped = false;
202
+ let hasSkippedOnFailure = false;
203
+ let hasIgnoredFailure = false;
204
+ let hasFailed = false;
205
+ let hasWaiting = false;
206
+ let hasReverted = false;
207
+ let completedSteps = 0;
208
+ const flow = transaction.getFlow();
209
+ const nextSteps = [];
210
+ const currentSteps = [];
211
+ const allSteps = flow.state === types_1.TransactionState.COMPENSATING
212
+ ? this.getCompensationSteps(flow)
213
+ : this.getInvokeSteps(flow);
214
+ for (const step of allSteps) {
215
+ if (step === TransactionOrchestrator.ROOT_STEP ||
216
+ !this.canContinue(flow, flow.steps[step])) {
217
+ continue;
218
+ }
219
+ const stepDef = flow.steps[step];
220
+ const curState = stepDef.getStates();
221
+ const hasTimedOut = await this.checkStepTimeout(transaction, stepDef);
222
+ if (hasTimedOut) {
223
+ continue;
224
+ }
225
+ if (curState.status === types_1.TransactionStepStatus.WAITING) {
226
+ currentSteps.push(stepDef);
227
+ hasWaiting = true;
228
+ if (stepDef.hasAwaitingRetry()) {
229
+ if (stepDef.canRetryAwaiting()) {
230
+ stepDef.retryRescheduledAt = null;
231
+ nextSteps.push(stepDef);
232
+ }
233
+ else if (!stepDef.retryRescheduledAt) {
234
+ stepDef.hasScheduledRetry = true;
235
+ stepDef.retryRescheduledAt = Date.now();
236
+ await transaction.scheduleRetry(stepDef, stepDef.definition.retryIntervalAwaiting);
237
+ }
238
+ }
239
+ else if (stepDef.retryRescheduledAt) {
240
+ // The step is not configured for awaiting retry but is manually force to retry
241
+ stepDef.retryRescheduledAt = null;
242
+ nextSteps.push(stepDef);
243
+ }
244
+ continue;
245
+ }
246
+ else if (curState.status === types_1.TransactionStepStatus.TEMPORARY_FAILURE) {
247
+ if (!stepDef.temporaryFailedAt &&
248
+ stepDef.definition.autoRetry === false) {
249
+ stepDef.temporaryFailedAt = Date.now();
250
+ continue;
251
+ }
252
+ stepDef.temporaryFailedAt = null;
253
+ currentSteps.push(stepDef);
254
+ if (!stepDef.canRetry()) {
255
+ if (stepDef.hasRetryInterval() && !stepDef.retryRescheduledAt) {
256
+ stepDef.hasScheduledRetry = true;
257
+ stepDef.retryRescheduledAt = Date.now();
258
+ await transaction.scheduleRetry(stepDef, stepDef.definition.retryInterval);
259
+ }
260
+ continue;
261
+ }
262
+ stepDef.retryRescheduledAt = null;
263
+ }
264
+ if (stepDef.canInvoke(flow.state) || stepDef.canCompensate(flow.state)) {
265
+ nextSteps.push(stepDef);
266
+ }
267
+ else {
268
+ completedSteps++;
269
+ if (curState.state === utils_1.TransactionStepState.SKIPPED_FAILURE) {
270
+ hasSkippedOnFailure = true;
271
+ }
272
+ else if (curState.state === utils_1.TransactionStepState.SKIPPED) {
273
+ hasSkipped = true;
274
+ }
275
+ else if (curState.state === utils_1.TransactionStepState.REVERTED) {
276
+ hasReverted = true;
277
+ }
278
+ else if (curState.state === utils_1.TransactionStepState.FAILED ||
279
+ curState.state === utils_1.TransactionStepState.TIMEOUT) {
280
+ if (stepDef.definition.continueOnPermanentFailure ||
281
+ stepDef.definition.skipOnPermanentFailure) {
282
+ hasIgnoredFailure = true;
283
+ }
284
+ else {
285
+ hasFailed = true;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ flow.hasWaitingSteps = hasWaiting;
291
+ flow.hasRevertedSteps = hasReverted;
292
+ return {
293
+ current: currentSteps,
294
+ next: nextSteps,
295
+ total: allSteps.length - 1,
296
+ completed: completedSteps,
297
+ hasSkipped,
298
+ hasSkippedOnFailure,
299
+ hasIgnoredFailure,
300
+ hasFailed,
301
+ hasWaiting,
302
+ hasReverted,
303
+ };
304
+ }
305
+ flagStepsToRevert(flow) {
306
+ for (const step in flow.steps) {
307
+ if (step === TransactionOrchestrator.ROOT_STEP) {
308
+ continue;
309
+ }
310
+ const stepDef = flow.steps[step];
311
+ const curState = stepDef.getStates();
312
+ if (stepDef._v) {
313
+ flow._v = 0;
314
+ stepDef._v = 0;
315
+ }
316
+ if (flagStepsToRevertStates.has(curState.state) ||
317
+ curState.status === types_1.TransactionStepStatus.PERMANENT_FAILURE) {
318
+ stepDef.beginCompensation();
319
+ stepDef.changeState(utils_1.TransactionStepState.NOT_STARTED);
320
+ }
321
+ }
322
+ }
323
+ static async setStepSuccess(transaction, step, response) {
324
+ const hasStepTimedOut = step.getStates().state === utils_1.TransactionStepState.TIMEOUT;
325
+ if (step.saveResponse) {
326
+ transaction.addResponse(step.definition.action, step.isCompensating()
327
+ ? types_1.TransactionHandlerType.COMPENSATE
328
+ : types_1.TransactionHandlerType.INVOKE, response);
329
+ }
330
+ if (!hasStepTimedOut) {
331
+ step.changeStatus(types_1.TransactionStepStatus.OK);
332
+ }
333
+ if (step.isCompensating()) {
334
+ step.changeState(utils_1.TransactionStepState.REVERTED);
335
+ }
336
+ else if (!hasStepTimedOut) {
337
+ step.changeState(utils_1.TransactionStepState.DONE);
338
+ }
339
+ let shouldEmit = true;
340
+ let transactionIsCancelling = false;
341
+ try {
342
+ await transaction.saveCheckpoint({
343
+ _v: step._v,
344
+ parallelSteps: TransactionOrchestrator.countSiblings(transaction.getFlow(), step),
345
+ stepId: step.id,
346
+ });
347
+ }
348
+ catch (error) {
349
+ if (!TransactionOrchestrator.isExpectedError(error)) {
350
+ throw error;
351
+ }
352
+ transactionIsCancelling =
353
+ errors_1.SkipCancelledExecutionError.isSkipCancelledExecutionError(error);
354
+ shouldEmit = !errors_1.SkipExecutionError.isSkipExecutionError(error);
355
+ }
356
+ const cleaningUp = [];
357
+ if (step.hasRetryScheduled()) {
358
+ cleaningUp.push(transaction.clearRetry(step));
359
+ }
360
+ if (step.hasTimeout()) {
361
+ cleaningUp.push(transaction.clearStepTimeout(step));
362
+ }
363
+ if (cleaningUp.length) {
364
+ await (0, utils_1.promiseAll)(cleaningUp);
365
+ }
366
+ if (shouldEmit) {
367
+ const eventName = step.isCompensating()
368
+ ? types_1.DistributedTransactionEvent.COMPENSATE_STEP_SUCCESS
369
+ : types_1.DistributedTransactionEvent.STEP_SUCCESS;
370
+ transaction.emit(eventName, { step, transaction });
371
+ }
372
+ return {
373
+ stopExecution: !shouldEmit,
374
+ transactionIsCancelling,
375
+ };
376
+ }
377
+ static async retryStep(transaction, step) {
378
+ if (!step.retryRescheduledAt) {
379
+ step.hasScheduledRetry = true;
380
+ step.retryRescheduledAt = Date.now();
381
+ }
382
+ transaction.getFlow().hasWaitingSteps = true;
383
+ try {
384
+ await transaction.saveCheckpoint({
385
+ _v: step._v,
386
+ parallelSteps: TransactionOrchestrator.countSiblings(transaction.getFlow(), step),
387
+ stepId: step.id,
388
+ });
389
+ await transaction.scheduleRetry(step, 0);
390
+ }
391
+ catch (error) {
392
+ if (!TransactionOrchestrator.isExpectedError(error)) {
393
+ throw error;
394
+ }
395
+ }
396
+ }
397
+ static async skipStep({ transaction, step, }) {
398
+ const hasStepTimedOut = step.getStates().state === utils_1.TransactionStepState.TIMEOUT;
399
+ if (!hasStepTimedOut) {
400
+ step.changeStatus(types_1.TransactionStepStatus.OK);
401
+ step.changeState(utils_1.TransactionStepState.SKIPPED);
402
+ }
403
+ let shouldEmit = true;
404
+ let transactionIsCancelling = false;
405
+ try {
406
+ await transaction.saveCheckpoint({
407
+ _v: step._v,
408
+ parallelSteps: TransactionOrchestrator.countSiblings(transaction.getFlow(), step),
409
+ stepId: step.id,
410
+ });
411
+ }
412
+ catch (error) {
413
+ if (!TransactionOrchestrator.isExpectedError(error)) {
414
+ throw error;
415
+ }
416
+ transactionIsCancelling =
417
+ errors_1.SkipCancelledExecutionError.isSkipCancelledExecutionError(error);
418
+ if (errors_1.SkipExecutionError.isSkipExecutionError(error)) {
419
+ shouldEmit = false;
420
+ }
421
+ }
422
+ const cleaningUp = [];
423
+ if (step.hasRetryScheduled()) {
424
+ cleaningUp.push(transaction.clearRetry(step));
425
+ }
426
+ if (step.hasTimeout()) {
427
+ cleaningUp.push(transaction.clearStepTimeout(step));
428
+ }
429
+ if (cleaningUp.length) {
430
+ await (0, utils_1.promiseAll)(cleaningUp);
431
+ }
432
+ if (shouldEmit) {
433
+ const eventName = types_1.DistributedTransactionEvent.STEP_SKIPPED;
434
+ transaction.emit(eventName, { step, transaction });
435
+ }
436
+ return {
437
+ stopExecution: !shouldEmit,
438
+ transactionIsCancelling,
439
+ };
440
+ }
441
+ static async setStepTimeout(transaction, step, error) {
442
+ if (setStepTimeoutSkipStates.has(step.getStates().state)) {
443
+ return;
444
+ }
445
+ step.changeState(utils_1.TransactionStepState.TIMEOUT);
446
+ if (error?.stack) {
447
+ const workflowId = transaction.modelId;
448
+ const stepAction = step.definition.action;
449
+ const sourcePath = transaction.getFlow().metadata?.sourcePath;
450
+ const sourceStack = sourcePath
451
+ ? `\n⮑ \sat ${sourcePath}: [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`
452
+ : `\n⮑ \sat [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`;
453
+ error.stack += sourceStack;
454
+ }
455
+ transaction.addError(step.definition.action, types_1.TransactionHandlerType.INVOKE, error);
456
+ await TransactionOrchestrator.setStepFailure(transaction, step, undefined, 0, true, error);
457
+ await transaction.clearStepTimeout(step);
458
+ }
459
+ static async setStepFailure(transaction, step, error, maxRetries = TransactionOrchestrator.DEFAULT_RETRIES, isTimeout = false, timeoutError) {
460
+ const result = {
461
+ stopExecution: false,
462
+ transactionIsCancelling: false,
463
+ };
464
+ if (errors_1.SkipExecutionError.isSkipExecutionError(error)) {
465
+ return result;
466
+ }
467
+ step.failures++;
468
+ if ((0, utils_1.isErrorLike)(error)) {
469
+ error = (0, utils_1.serializeError)(error);
470
+ }
471
+ else {
472
+ try {
473
+ const serialized = JSON.stringify(error);
474
+ error = error?.message
475
+ ? JSON.parse(serialized)
476
+ : { message: serialized };
477
+ }
478
+ catch (e) {
479
+ error = {
480
+ message: "Unknown non-serializable error",
481
+ };
482
+ }
483
+ }
484
+ if (!isTimeout &&
485
+ step.getStates().status !== types_1.TransactionStepStatus.PERMANENT_FAILURE) {
486
+ step.changeStatus(types_1.TransactionStepStatus.TEMPORARY_FAILURE);
487
+ }
488
+ const flow = transaction.getFlow();
489
+ const cleaningUp = [];
490
+ const hasTimedOut = step.getStates().state === utils_1.TransactionStepState.TIMEOUT;
491
+ if (step.failures > maxRetries || hasTimedOut) {
492
+ if (!hasTimedOut) {
493
+ step.changeState(utils_1.TransactionStepState.FAILED);
494
+ }
495
+ step.changeStatus(types_1.TransactionStepStatus.PERMANENT_FAILURE);
496
+ if (!isTimeout) {
497
+ const handlerType = step.isCompensating()
498
+ ? types_1.TransactionHandlerType.COMPENSATE
499
+ : types_1.TransactionHandlerType.INVOKE;
500
+ error.stack ??= "";
501
+ const workflowId = transaction.modelId;
502
+ const stepAction = step.definition.action;
503
+ const sourcePath = transaction.getFlow().metadata?.sourcePath;
504
+ const sourceStack = sourcePath
505
+ ? `\n⮑ \sat ${sourcePath}: [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`
506
+ : `\n⮑ \sat [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`;
507
+ error.stack += sourceStack;
508
+ transaction.addError(step.definition.action, handlerType, error);
509
+ }
510
+ if (!step.isCompensating()) {
511
+ const isTransactionTimeout = errors_1.TransactionTimeoutError.isTransactionTimeoutError(timeoutError);
512
+ const canContinueOnFailure = (step.definition.continueOnPermanentFailure ||
513
+ step.definition.skipOnPermanentFailure) &&
514
+ !isTransactionTimeout;
515
+ if (canContinueOnFailure) {
516
+ if (step.definition.skipOnPermanentFailure) {
517
+ const until = (0, utils_1.isString)(step.definition.skipOnPermanentFailure)
518
+ ? step.definition.skipOnPermanentFailure
519
+ : undefined;
520
+ let stepsToSkip = [...step.next];
521
+ while (stepsToSkip.length > 0) {
522
+ const currentStep = flow.steps[stepsToSkip.shift()];
523
+ if (until && currentStep.definition.action === until) {
524
+ break;
525
+ }
526
+ currentStep.changeState(utils_1.TransactionStepState.SKIPPED_FAILURE);
527
+ if (currentStep.next?.length > 0) {
528
+ stepsToSkip = stepsToSkip.concat(currentStep.next);
529
+ }
530
+ }
531
+ }
532
+ }
533
+ else {
534
+ flow.state = types_1.TransactionState.WAITING_TO_COMPENSATE;
535
+ }
536
+ }
537
+ if (step.hasTimeout()) {
538
+ cleaningUp.push(transaction.clearStepTimeout(step));
539
+ }
540
+ }
541
+ else {
542
+ const isAsync = step.isCompensating()
543
+ ? step.definition.compensateAsync
544
+ : step.definition.async;
545
+ if (step.getStates().status === types_1.TransactionStepStatus.TEMPORARY_FAILURE &&
546
+ step.definition.autoRetry === false &&
547
+ isAsync) {
548
+ step.temporaryFailedAt = Date.now();
549
+ result.stopExecution = true;
550
+ }
551
+ }
552
+ try {
553
+ await transaction.saveCheckpoint({
554
+ _v: step._v,
555
+ parallelSteps: TransactionOrchestrator.countSiblings(transaction.getFlow(), step),
556
+ stepId: step.id,
557
+ });
558
+ }
559
+ catch (error) {
560
+ if (!TransactionOrchestrator.isExpectedError(error)) {
561
+ throw error;
562
+ }
563
+ result.transactionIsCancelling =
564
+ errors_1.SkipCancelledExecutionError.isSkipCancelledExecutionError(error);
565
+ if (errors_1.SkipExecutionError.isSkipExecutionError(error)) {
566
+ result.stopExecution = true;
567
+ }
568
+ }
569
+ if (step.hasRetryScheduled()) {
570
+ cleaningUp.push(transaction.clearRetry(step));
571
+ }
572
+ if (cleaningUp.length) {
573
+ await (0, utils_1.promiseAll)(cleaningUp);
574
+ }
575
+ if (!result.stopExecution) {
576
+ const eventName = step.isCompensating()
577
+ ? types_1.DistributedTransactionEvent.COMPENSATE_STEP_FAILURE
578
+ : types_1.DistributedTransactionEvent.STEP_FAILURE;
579
+ transaction.emit(eventName, { step, transaction });
580
+ }
581
+ return {
582
+ stopExecution: result.stopExecution,
583
+ transactionIsCancelling: result.transactionIsCancelling,
584
+ };
585
+ }
586
+ async executeNext(transaction) {
587
+ let continueExecution = true;
588
+ while (continueExecution) {
589
+ if (transaction.hasFinished()) {
590
+ return;
591
+ }
592
+ const flow = transaction.getFlow();
593
+ let nextSteps = await this.checkAllSteps(transaction);
594
+ const hasTimedOut = await this.checkTransactionTimeout(transaction, nextSteps.current);
595
+ if (hasTimedOut) {
596
+ continue;
597
+ }
598
+ if (nextSteps.remaining === 0) {
599
+ await this.finalizeTransaction(transaction);
600
+ return;
601
+ }
602
+ const stepsShouldContinueExecution = nextSteps.next.map((step) => {
603
+ const { shouldContinueExecution } = this.prepareStepForExecution(step, flow);
604
+ return shouldContinueExecution;
605
+ });
606
+ let asyncStepCount = 0;
607
+ for (const s of nextSteps.next) {
608
+ const stepIsAsync = s.isCompensating()
609
+ ? s.definition.compensateAsync
610
+ : s.definition.async;
611
+ if (stepIsAsync)
612
+ asyncStepCount++;
613
+ }
614
+ const hasMultipleAsyncSteps = asyncStepCount > 1;
615
+ const hasAsyncSteps = !!asyncStepCount;
616
+ // If there is any async step, we don't need to save the checkpoint here as it will be saved
617
+ // later down there
618
+ await transaction.saveCheckpoint().catch((error) => {
619
+ if (TransactionOrchestrator.isExpectedError(error)) {
620
+ continueExecution = false;
621
+ return;
622
+ }
623
+ throw error;
624
+ });
625
+ if (!continueExecution) {
626
+ break;
627
+ }
628
+ const execution = [];
629
+ const executionAsync = [];
630
+ let i = 0;
631
+ for (const step of nextSteps.next) {
632
+ const stepIndex = i++;
633
+ if (!stepsShouldContinueExecution[stepIndex]) {
634
+ continue;
635
+ }
636
+ if (step.hasTimeout() && !step.timedOutAt && step.attempts === 1) {
637
+ await transaction.scheduleStepTimeout(step, step.definition.timeout);
638
+ }
639
+ transaction.emit(types_1.DistributedTransactionEvent.STEP_BEGIN, {
640
+ step,
641
+ transaction,
642
+ });
643
+ const isAsync = step.isCompensating()
644
+ ? step.definition.compensateAsync
645
+ : step.definition.async;
646
+ // Compute current transaction state
647
+ await this.computeCurrentTransactionState(transaction);
648
+ const promise = this.createStepExecutionPromise(transaction, step);
649
+ const hasVersionControl = hasMultipleAsyncSteps || step.hasAwaitingRetry();
650
+ if (hasVersionControl && !step._v) {
651
+ transaction.getFlow()._v += 1;
652
+ step._v = transaction.getFlow()._v;
653
+ }
654
+ if (!isAsync) {
655
+ execution.push(this.executeSyncStep(promise, transaction, step, nextSteps));
656
+ }
657
+ else {
658
+ // Execute async step in background as part of the next event loop cycle and continue the execution of the transaction
659
+ executionAsync.push(() => this.executeAsyncStep(promise, transaction, step, nextSteps));
660
+ }
661
+ }
662
+ await (0, utils_1.promiseAll)(execution);
663
+ if (!nextSteps.next.length || (hasAsyncSteps && !execution.length)) {
664
+ continueExecution = false;
665
+ }
666
+ if (hasAsyncSteps) {
667
+ await transaction.saveCheckpoint().catch((error) => {
668
+ if (TransactionOrchestrator.isExpectedError(error)) {
669
+ continueExecution = false;
670
+ }
671
+ throw error;
672
+ });
673
+ for (const exec of executionAsync) {
674
+ void exec();
675
+ }
676
+ }
677
+ }
678
+ }
679
+ /**
680
+ * Finalize the transaction when all steps are complete
681
+ */
682
+ async finalizeTransaction(transaction) {
683
+ if (transaction.hasTimeout()) {
684
+ void transaction.clearTransactionTimeout();
685
+ }
686
+ await transaction.saveCheckpoint().catch((error) => {
687
+ if (!TransactionOrchestrator.isExpectedError(error)) {
688
+ throw error;
689
+ }
690
+ });
691
+ this.emit(types_1.DistributedTransactionEvent.FINISH, { transaction });
692
+ }
693
+ /**
694
+ * Prepare a step for execution by setting state and incrementing attempts
695
+ */
696
+ prepareStepForExecution(step, flow) {
697
+ const curState = step.getStates();
698
+ step.lastAttempt = Date.now();
699
+ step.attempts++;
700
+ if (curState.state === utils_1.TransactionStepState.NOT_STARTED) {
701
+ if (!step.startedAt) {
702
+ step.startedAt = Date.now();
703
+ }
704
+ if (step.isCompensating()) {
705
+ step.changeState(utils_1.TransactionStepState.COMPENSATING);
706
+ if (step.definition.noCompensation) {
707
+ step.changeState(utils_1.TransactionStepState.REVERTED);
708
+ return { shouldContinueExecution: false };
709
+ }
710
+ }
711
+ else if (flow.state === types_1.TransactionState.INVOKING) {
712
+ step.changeState(utils_1.TransactionStepState.INVOKING);
713
+ }
714
+ }
715
+ step.changeStatus(types_1.TransactionStepStatus.WAITING);
716
+ return { shouldContinueExecution: true };
717
+ }
718
+ /**
719
+ * Create the payload for a step execution
720
+ */
721
+ createStepPayload(transaction, step, flow, type) {
722
+ return new distributed_transaction_1.TransactionPayload({
723
+ model_id: flow.modelId,
724
+ idempotency_key: TransactionOrchestrator.getKeyName(flow.modelId, flow.transactionId, step.definition.action, type),
725
+ action: step.definition.action + "",
726
+ action_type: type,
727
+ attempt: step.attempts,
728
+ timestamp: Date.now(),
729
+ }, transaction.payload, transaction.getContext());
730
+ }
731
+ /**
732
+ * Prepare handler arguments for step execution
733
+ */
734
+ prepareHandlerArgs(transaction, step, payload, type) {
735
+ return [
736
+ step.definition.action + "",
737
+ type,
738
+ payload,
739
+ transaction,
740
+ step,
741
+ this,
742
+ ];
743
+ }
744
+ /**
745
+ * Create the step execution promise with optional tracing
746
+ */
747
+ createStepExecutionPromise(transaction, step) {
748
+ const type = step.isCompensating()
749
+ ? types_1.TransactionHandlerType.COMPENSATE
750
+ : types_1.TransactionHandlerType.INVOKE;
751
+ const flow = transaction.getFlow();
752
+ const payload = this.createStepPayload(transaction, step, flow, type);
753
+ const handlerArgs = this.prepareHandlerArgs(transaction, step, payload, type);
754
+ const traceData = {
755
+ action: step.definition.action + "",
756
+ type,
757
+ step_id: step.id,
758
+ step_uuid: step.uuid + "",
759
+ attempts: step.attempts,
760
+ failures: step.failures,
761
+ async: !!(type === "invoke"
762
+ ? step.definition.async
763
+ : step.definition.compensateAsync),
764
+ idempotency_key: handlerArgs[2].metadata.idempotency_key,
765
+ };
766
+ const stepHandler = async () => {
767
+ return await transaction.handler(...handlerArgs);
768
+ };
769
+ // Return the appropriate promise based on tracing configuration
770
+ if (TransactionOrchestrator.traceStep) {
771
+ return () => TransactionOrchestrator.traceStep(stepHandler, traceData);
772
+ }
773
+ else {
774
+ return stepHandler;
775
+ }
776
+ }
777
+ /**
778
+ * Execute a synchronous step and handle its result
779
+ */
780
+ executeSyncStep(promiseFn, transaction, step, nextSteps) {
781
+ return promiseFn()
782
+ .then(async (response) => {
783
+ await this.handleStepExpiration(transaction, step, nextSteps);
784
+ const output = response?.__type || response?.output?.__type
785
+ ? response.output
786
+ : response;
787
+ if (errors_1.SkipStepResponse.isSkipStepResponse(output)) {
788
+ await TransactionOrchestrator.skipStep({
789
+ transaction,
790
+ step,
791
+ });
792
+ return;
793
+ }
794
+ await this.handleStepSuccess(transaction, step, response);
795
+ })
796
+ .catch(async (error) => {
797
+ if (TransactionOrchestrator.isExpectedError(error)) {
798
+ return;
799
+ }
800
+ const response = error?.getStepResponse?.();
801
+ await this.handleStepExpiration(transaction, step, nextSteps);
802
+ if (errors_1.PermanentStepFailureError.isPermanentStepFailureError(error)) {
803
+ await this.handleStepFailure(transaction, step, error, true, response);
804
+ return;
805
+ }
806
+ await this.handleStepFailure(transaction, step, error, false, response);
807
+ });
808
+ }
809
+ /**
810
+ * Execute an asynchronous step and handle its result
811
+ */
812
+ executeAsyncStep(promiseFn, transaction, step, nextSteps) {
813
+ return promiseFn()
814
+ .then(async (response) => {
815
+ const output = response?.__type || response?.output?.__type
816
+ ? response.output
817
+ : response;
818
+ if (errors_1.SkipStepResponse.isSkipStepResponse(output)) {
819
+ await TransactionOrchestrator.skipStep({
820
+ transaction,
821
+ step,
822
+ });
823
+ // Schedule to continue the execution of async steps because they are not awaited on purpose and can be handled by another machine
824
+ await transaction.scheduleRetry(step, 0);
825
+ return;
826
+ }
827
+ else {
828
+ if (!step.definition.backgroundExecution || step.definition.nested) {
829
+ const eventName = types_1.DistributedTransactionEvent.STEP_AWAITING;
830
+ transaction.emit(eventName, { step, transaction });
831
+ return;
832
+ }
833
+ await this.handleStepExpiration(transaction, step, nextSteps);
834
+ await this.handleStepSuccess(transaction, step, response);
835
+ }
836
+ })
837
+ .catch(async (error) => {
838
+ if (TransactionOrchestrator.isExpectedError(error)) {
839
+ return;
840
+ }
841
+ const response = error?.getStepResponse?.();
842
+ if (errors_1.PermanentStepFailureError.isPermanentStepFailureError(error)) {
843
+ await this.handleStepFailure(transaction, step, error, true, response);
844
+ return;
845
+ }
846
+ await this.handleStepFailure(transaction, step, error, false, response);
847
+ });
848
+ }
849
+ /**
850
+ * Check if step or transaction has expired and handle timeouts
851
+ */
852
+ async handleStepExpiration(transaction, step, nextSteps) {
853
+ if (this.hasExpired({ transaction, step }, Date.now())) {
854
+ await this.checkStepTimeout(transaction, step);
855
+ await this.checkTransactionTimeout(transaction, nextSteps.next.includes(step) ? nextSteps.next : [step]);
856
+ }
857
+ }
858
+ /**
859
+ * Handle successful step completion
860
+ */
861
+ async handleStepSuccess(transaction, step, response) {
862
+ const isAsync = step.isCompensating()
863
+ ? step.definition.compensateAsync
864
+ : step.definition.async;
865
+ if ((0, utils_1.isDefined)(response) && step.saveResponse && !isAsync) {
866
+ transaction.addResponse(step.definition.action, step.isCompensating()
867
+ ? types_1.TransactionHandlerType.COMPENSATE
868
+ : types_1.TransactionHandlerType.INVOKE, response);
869
+ }
870
+ const ret = await TransactionOrchestrator.setStepSuccess(transaction, step, response);
871
+ if (ret.transactionIsCancelling) {
872
+ await this.cancelTransaction(transaction, {
873
+ preventExecuteNext: true,
874
+ });
875
+ }
876
+ if (isAsync && !ret.stopExecution) {
877
+ // Schedule to continue the execution of async steps because they are not awaited on purpose and can be handled by another machine
878
+ await transaction.scheduleRetry(step, 0);
879
+ }
880
+ }
881
+ /**
882
+ * Handle step failure
883
+ */
884
+ async handleStepFailure(transaction, step, error, isPermanent, response) {
885
+ const isAsync = step.isCompensating()
886
+ ? step.definition.compensateAsync
887
+ : step.definition.async;
888
+ if ((0, utils_1.isDefined)(response) && step.saveResponse) {
889
+ transaction.addResponse(step.definition.action, step.isCompensating()
890
+ ? types_1.TransactionHandlerType.COMPENSATE
891
+ : types_1.TransactionHandlerType.INVOKE, response);
892
+ }
893
+ const ret = await TransactionOrchestrator.setStepFailure(transaction, step, error, isPermanent ? 0 : step.definition.maxRetries);
894
+ if (ret.transactionIsCancelling) {
895
+ await this.cancelTransaction(transaction, {
896
+ preventExecuteNext: true,
897
+ });
898
+ }
899
+ if (isAsync && !ret.stopExecution) {
900
+ // Schedule to continue the execution of async steps because they are not awaited on purpose and can be handled by another machine
901
+ await transaction.scheduleRetry(step, 0);
902
+ }
903
+ }
904
+ /**
905
+ * Start a new transaction or resume a transaction that has been previously started
906
+ * @param transaction - The transaction to resume
907
+ */
908
+ async resume(transaction) {
909
+ if (transaction.modelId !== this.id) {
910
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `TransactionModel "${transaction.modelId}" cannot be orchestrated by "${this.id}" model.`);
911
+ }
912
+ if (transaction.hasFinished()) {
913
+ return;
914
+ }
915
+ const executeNext = async () => {
916
+ const flow = transaction.getFlow();
917
+ if (flow.state === types_1.TransactionState.NOT_STARTED) {
918
+ flow.state = types_1.TransactionState.INVOKING;
919
+ flow.startedAt = Date.now();
920
+ await transaction.saveCheckpoint({
921
+ ttl: flow.hasAsyncSteps ? 0 : TransactionOrchestrator.DEFAULT_TTL,
922
+ });
923
+ if (transaction.hasTimeout()) {
924
+ await transaction.scheduleTransactionTimeout(transaction.getTimeout());
925
+ }
926
+ this.emit(types_1.DistributedTransactionEvent.BEGIN, { transaction });
927
+ }
928
+ else {
929
+ this.emit(types_1.DistributedTransactionEvent.RESUME, { transaction });
930
+ }
931
+ return await this.executeNext(transaction);
932
+ };
933
+ if (TransactionOrchestrator.traceTransaction &&
934
+ !transaction.getFlow().hasAsyncSteps) {
935
+ await TransactionOrchestrator.traceTransaction(executeNext, {
936
+ model_id: transaction.modelId,
937
+ transaction_id: transaction.transactionId,
938
+ flow_metadata: transaction.getFlow().metadata,
939
+ });
940
+ return;
941
+ }
942
+ await executeNext();
943
+ }
944
+ /**
945
+ * Cancel and revert a transaction compensating all its executed steps. It can be an ongoing transaction or a completed one
946
+ * @param transaction - The transaction to be reverted
947
+ */
948
+ async cancelTransaction(transaction, options) {
949
+ if (transaction.modelId !== this.id) {
950
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `TransactionModel "${transaction.modelId}" cannot be orchestrated by "${this.id}" model.`);
951
+ }
952
+ const flow = transaction.getFlow();
953
+ if (flow.state === types_1.TransactionState.FAILED) {
954
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Cannot revert a permanent failed transaction.`);
955
+ }
956
+ if (flow.state === types_1.TransactionState.COMPENSATING ||
957
+ flow.state === types_1.TransactionState.WAITING_TO_COMPENSATE) {
958
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Cannot revert a transaction that is already compensating.`);
959
+ }
960
+ flow.state = types_1.TransactionState.WAITING_TO_COMPENSATE;
961
+ flow.cancelledAt = Date.now();
962
+ await transaction.saveCheckpoint();
963
+ if (options?.preventExecuteNext) {
964
+ return;
965
+ }
966
+ await this.executeNext(transaction);
967
+ }
968
+ parseFlowOptions() {
969
+ const [steps, features] = TransactionOrchestrator.buildSteps(this.definition);
970
+ this.options ??= {};
971
+ const hasAsyncSteps = features.hasAsyncSteps;
972
+ const hasStepTimeouts = features.hasStepTimeouts;
973
+ const hasRetriesTimeout = features.hasRetriesTimeout;
974
+ const hasTransactionTimeout = !!this.options.timeout;
975
+ const isIdempotent = !!this.options.idempotent;
976
+ if (hasStepTimeouts ||
977
+ hasRetriesTimeout ||
978
+ hasTransactionTimeout ||
979
+ isIdempotent ||
980
+ this.options.retentionTime ||
981
+ hasAsyncSteps) {
982
+ this.options.store = true;
983
+ }
984
+ const parsedOptions = {
985
+ ...this.options,
986
+ hasAsyncSteps,
987
+ hasStepTimeouts,
988
+ hasRetriesTimeout,
989
+ };
990
+ TransactionOrchestrator.workflowOptions[this.id] = parsedOptions;
991
+ return [steps, features];
992
+ }
993
+ createTransactionFlow(transactionId, flowMetadata, context) {
994
+ const [steps, features] = TransactionOrchestrator.buildSteps(this.definition);
995
+ const flow = {
996
+ modelId: this.id,
997
+ options: this.options,
998
+ transactionId: transactionId,
999
+ runId: context?.runId ?? (0, ulid_1.ulid)(),
1000
+ metadata: flowMetadata,
1001
+ hasAsyncSteps: features.hasAsyncSteps,
1002
+ hasFailedSteps: false,
1003
+ hasSkippedOnFailureSteps: false,
1004
+ hasSkippedSteps: false,
1005
+ hasWaitingSteps: false,
1006
+ hasRevertedSteps: false,
1007
+ timedOutAt: null,
1008
+ state: types_1.TransactionState.NOT_STARTED,
1009
+ definition: this.definition,
1010
+ steps,
1011
+ _v: 0, // Initialize version to 0
1012
+ };
1013
+ return flow;
1014
+ }
1015
+ static async loadTransactionById(modelId, transactionId, options) {
1016
+ const transaction = await distributed_transaction_1.DistributedTransaction.loadTransaction(modelId, transactionId, options);
1017
+ if (transaction !== null) {
1018
+ const flow = transaction.flow;
1019
+ const [steps] = TransactionOrchestrator.buildSteps(flow.definition, flow.steps);
1020
+ transaction.flow.steps = steps;
1021
+ return transaction;
1022
+ }
1023
+ return null;
1024
+ }
1025
+ static buildSteps(flow, existingSteps) {
1026
+ const states = {
1027
+ [TransactionOrchestrator.ROOT_STEP]: {
1028
+ id: TransactionOrchestrator.ROOT_STEP,
1029
+ next: [],
1030
+ },
1031
+ };
1032
+ const actionNames = new Set();
1033
+ const queue = [
1034
+ { obj: flow, level: [TransactionOrchestrator.ROOT_STEP] },
1035
+ ];
1036
+ const features = {
1037
+ hasAsyncSteps: false,
1038
+ hasStepTimeouts: false,
1039
+ hasRetriesTimeout: false,
1040
+ hasNestedTransactions: false,
1041
+ };
1042
+ while (queue.length > 0) {
1043
+ const { obj, level } = queue.shift();
1044
+ if (obj.action) {
1045
+ if (actionNames.has(obj.action)) {
1046
+ throw new Error(`Step ${obj.action} is already defined in workflow.`);
1047
+ }
1048
+ actionNames.add(obj.action);
1049
+ level.push(obj.action);
1050
+ const id = level.join(".");
1051
+ const parent = level.slice(0, level.length - 1).join(".");
1052
+ if (!existingSteps || parent === TransactionOrchestrator.ROOT_STEP) {
1053
+ states[parent].next?.push(id);
1054
+ }
1055
+ const definitionCopy = { ...obj };
1056
+ delete definitionCopy.next;
1057
+ const isAsync = !!definitionCopy.async;
1058
+ const hasRetryInterval = !!(definitionCopy.retryInterval || definitionCopy.retryIntervalAwaiting);
1059
+ const hasTimeout = !!definitionCopy.timeout;
1060
+ if (definitionCopy.async) {
1061
+ features.hasAsyncSteps = true;
1062
+ }
1063
+ if (definitionCopy.timeout) {
1064
+ features.hasStepTimeouts = true;
1065
+ }
1066
+ if (definitionCopy.retryInterval ||
1067
+ definitionCopy.retryIntervalAwaiting) {
1068
+ features.hasRetriesTimeout = true;
1069
+ }
1070
+ if (definitionCopy.nested) {
1071
+ features.hasNestedTransactions = true;
1072
+ }
1073
+ /**
1074
+ * Force the checkpoint to save even for sync step when they have specific configurations.
1075
+ */
1076
+ definitionCopy.store = !!(definitionCopy.store ||
1077
+ isAsync ||
1078
+ hasRetryInterval ||
1079
+ hasTimeout);
1080
+ if (existingSteps?.[id]) {
1081
+ existingSteps[id].definition.store = definitionCopy.store;
1082
+ }
1083
+ states[id] = Object.assign(new transaction_step_1.TransactionStep(), existingSteps?.[id] || {
1084
+ id,
1085
+ uuid: definitionCopy.uuid,
1086
+ depth: level.length - 1,
1087
+ definition: definitionCopy,
1088
+ saveResponse: definitionCopy.saveResponse ?? true,
1089
+ invoke: {
1090
+ state: utils_1.TransactionStepState.NOT_STARTED,
1091
+ status: types_1.TransactionStepStatus.IDLE,
1092
+ },
1093
+ compensate: {
1094
+ state: utils_1.TransactionStepState.DORMANT,
1095
+ status: types_1.TransactionStepStatus.IDLE,
1096
+ },
1097
+ attempts: 0,
1098
+ failures: 0,
1099
+ lastAttempt: null,
1100
+ next: [],
1101
+ _v: 0, // Initialize step version to 0
1102
+ });
1103
+ }
1104
+ if (Array.isArray(obj.next)) {
1105
+ for (const next of obj.next) {
1106
+ queue.push({ obj: next, level: [...level] });
1107
+ }
1108
+ }
1109
+ else if ((0, utils_1.isObject)(obj.next)) {
1110
+ queue.push({ obj: obj.next, level: [...level] });
1111
+ }
1112
+ }
1113
+ return [states, features];
1114
+ }
1115
+ /** Create a new transaction
1116
+ * @param transactionId - unique identifier of the transaction
1117
+ * @param handler - function to handle action of the transaction
1118
+ * @param payload - payload to be passed to all the transaction steps
1119
+ * @param flowMetadata - flow metadata which can include event group id for example
1120
+ */
1121
+ async beginTransaction({ transactionId, handler, payload, flowMetadata, context, onLoad, }) {
1122
+ const existingTransaction = await TransactionOrchestrator.loadTransactionById(this.id, transactionId);
1123
+ let newTransaction = false;
1124
+ let modelFlow;
1125
+ if (!existingTransaction) {
1126
+ modelFlow = this.createTransactionFlow(transactionId, flowMetadata, context);
1127
+ newTransaction = true;
1128
+ }
1129
+ else {
1130
+ modelFlow = existingTransaction.flow;
1131
+ }
1132
+ const transaction = new distributed_transaction_1.DistributedTransaction(modelFlow, handler, payload, existingTransaction?.errors, existingTransaction?.context);
1133
+ if (newTransaction && this.getOptions().store) {
1134
+ await transaction.saveCheckpoint({
1135
+ ttl: modelFlow.hasAsyncSteps ? 0 : TransactionOrchestrator.DEFAULT_TTL,
1136
+ });
1137
+ }
1138
+ if (onLoad) {
1139
+ await onLoad(transaction);
1140
+ }
1141
+ return transaction;
1142
+ }
1143
+ /** Returns an existing transaction
1144
+ * @param transactionId - unique identifier of the transaction
1145
+ * @param handler - function to handle action of the transaction
1146
+ */
1147
+ async retrieveExistingTransaction(transactionId, handler, options) {
1148
+ const existingTransaction = await TransactionOrchestrator.loadTransactionById(this.id, transactionId, { isCancelling: options?.isCancelling });
1149
+ if (!existingTransaction) {
1150
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `Transaction ${transactionId} could not be found.`);
1151
+ }
1152
+ const transaction = new distributed_transaction_1.DistributedTransaction(existingTransaction.flow, handler, undefined, existingTransaction?.errors, existingTransaction?.context);
1153
+ return transaction;
1154
+ }
1155
+ static getStepByAction(flow, action) {
1156
+ for (const key in flow.steps) {
1157
+ if (action === flow.steps[key]?.definition?.action) {
1158
+ return flow.steps[key];
1159
+ }
1160
+ }
1161
+ return null;
1162
+ }
1163
+ static async getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction) {
1164
+ const [modelId, transactionId, action, actionType] = responseIdempotencyKey.split(TransactionOrchestrator.SEPARATOR);
1165
+ if (!transaction && !handler) {
1166
+ throw new Error("If a transaction is not provided, the handler is required");
1167
+ }
1168
+ if (!transaction) {
1169
+ const existingTransaction = await TransactionOrchestrator.loadTransactionById(modelId, transactionId);
1170
+ if (existingTransaction === null) {
1171
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `Transaction ${transactionId} could not be found.`);
1172
+ }
1173
+ transaction = new distributed_transaction_1.DistributedTransaction(existingTransaction.flow, handler, undefined, existingTransaction.errors, existingTransaction.context);
1174
+ }
1175
+ const step = TransactionOrchestrator.getStepByAction(transaction.getFlow(), action);
1176
+ if (step === null) {
1177
+ throw new Error("Action not found.");
1178
+ }
1179
+ else if (step.isCompensating()
1180
+ ? actionType !== types_1.TransactionHandlerType.COMPENSATE
1181
+ : actionType !== types_1.TransactionHandlerType.INVOKE) {
1182
+ throw new Error("Incorrect action type.");
1183
+ }
1184
+ return [transaction, step];
1185
+ }
1186
+ /** Skip the execution of a specific transaction and step
1187
+ * @param responseIdempotencyKey - The idempotency key for the step
1188
+ * @param handler - The handler function to execute the step
1189
+ * @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
1190
+ */
1191
+ async skipStep({ responseIdempotencyKey, handler, transaction, }) {
1192
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
1193
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
1194
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
1195
+ transaction: curTransaction,
1196
+ });
1197
+ await TransactionOrchestrator.skipStep({
1198
+ transaction: curTransaction,
1199
+ step,
1200
+ });
1201
+ await this.executeNext(curTransaction);
1202
+ }
1203
+ else {
1204
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Cannot skip a step when status is ${step.getStates().status}`);
1205
+ }
1206
+ return curTransaction;
1207
+ }
1208
+ /**
1209
+ * Manually force a step to retry even if it is still in awaiting status
1210
+ * @param responseIdempotencyKey - The idempotency key for the step
1211
+ * @param handler - The handler function to execute the step
1212
+ * @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
1213
+ */
1214
+ async retryStep({ responseIdempotencyKey, handler, transaction, onLoad, }) {
1215
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
1216
+ if (onLoad) {
1217
+ await onLoad(curTransaction);
1218
+ }
1219
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
1220
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
1221
+ transaction: curTransaction,
1222
+ });
1223
+ await TransactionOrchestrator.retryStep(curTransaction, step);
1224
+ }
1225
+ else {
1226
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Cannot retry step when status is ${step.getStates().status}`);
1227
+ }
1228
+ return curTransaction;
1229
+ }
1230
+ /** Register a step success for a specific transaction and step
1231
+ * @param responseIdempotencyKey - The idempotency key for the step
1232
+ * @param handler - The handler function to execute the step
1233
+ * @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
1234
+ * @param response - The response of the step
1235
+ */
1236
+ async registerStepSuccess({ responseIdempotencyKey, handler, transaction, response, onLoad, }) {
1237
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
1238
+ if (onLoad) {
1239
+ await onLoad(curTransaction);
1240
+ }
1241
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
1242
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
1243
+ transaction: curTransaction,
1244
+ });
1245
+ const ret = await TransactionOrchestrator.setStepSuccess(curTransaction, step, response);
1246
+ if (ret.transactionIsCancelling) {
1247
+ await this.cancelTransaction(curTransaction);
1248
+ return curTransaction;
1249
+ }
1250
+ await this.executeNext(curTransaction);
1251
+ }
1252
+ else {
1253
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Cannot set step success when status is ${step.getStates().status}`);
1254
+ }
1255
+ return curTransaction;
1256
+ }
1257
+ /**
1258
+ * Register a step failure for a specific transaction and step
1259
+ * @param responseIdempotencyKey - The idempotency key for the step
1260
+ * @param error - The error that caused the failure
1261
+ * @param handler - The handler function to execute the step
1262
+ * @param transaction - The current transaction
1263
+ * @param response - The response of the step
1264
+ */
1265
+ async registerStepFailure({ responseIdempotencyKey, error, handler, transaction, onLoad, forcePermanentFailure, }) {
1266
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
1267
+ if (onLoad) {
1268
+ await onLoad(curTransaction);
1269
+ }
1270
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
1271
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
1272
+ transaction: curTransaction,
1273
+ });
1274
+ const ret = await TransactionOrchestrator.setStepFailure(curTransaction, step, error,
1275
+ // On permanent failure, the step should not consider any retries
1276
+ forcePermanentFailure ? 0 : step.definition.maxRetries);
1277
+ if (ret.transactionIsCancelling) {
1278
+ await this.cancelTransaction(curTransaction);
1279
+ return curTransaction;
1280
+ }
1281
+ await this.executeNext(curTransaction);
1282
+ }
1283
+ else {
1284
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Cannot set step failure when status is ${step.getStates().status}`);
1285
+ }
1286
+ return curTransaction;
1287
+ }
1288
+ }
1289
+ exports.TransactionOrchestrator = TransactionOrchestrator;
1290
+ TransactionOrchestrator.ROOT_STEP = "_root";
1291
+ TransactionOrchestrator.DEFAULT_TTL = 30;
1292
+ TransactionOrchestrator.DEFAULT_RETRIES = 0;
1293
+ TransactionOrchestrator.workflowOptions = {};
1294
+ TransactionOrchestrator.SEPARATOR = ":";
1295
+ //# sourceMappingURL=transaction-orchestrator.js.map