@etohq/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 +39 -0
  14. package/dist/joiner/remote-joiner.d.ts.map +1 -0
  15. package/dist/joiner/remote-joiner.js +872 -0
  16. package/dist/joiner/remote-joiner.js.map +1 -0
  17. package/dist/transaction/datastore/abstract-storage.d.ts +42 -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 +11 -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 +33 -0
  24. package/dist/transaction/datastore/base-in-memory-storage.js.map +1 -0
  25. package/dist/transaction/distributed-transaction.d.ts +99 -0
  26. package/dist/transaction/distributed-transaction.d.ts.map +1 -0
  27. package/dist/transaction/distributed-transaction.js +260 -0
  28. package/dist/transaction/distributed-transaction.js.map +1 -0
  29. package/dist/transaction/errors.d.ts +27 -0
  30. package/dist/transaction/errors.d.ts.map +1 -0
  31. package/dist/transaction/errors.js +78 -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 +118 -0
  42. package/dist/transaction/transaction-orchestrator.d.ts.map +1 -0
  43. package/dist/transaction/transaction-orchestrator.js +924 -0
  44. package/dist/transaction/transaction-orchestrator.js.map +1 -0
  45. package/dist/transaction/transaction-step.d.ts +67 -0
  46. package/dist/transaction/transaction-step.d.ts.map +1 -0
  47. package/dist/transaction/transaction-step.js +146 -0
  48. package/dist/transaction/transaction-step.js.map +1 -0
  49. package/dist/transaction/types.d.ts +223 -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 +93 -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 +44 -0
  63. package/dist/workflow/local-workflow.d.ts.map +1 -0
  64. package/dist/workflow/local-workflow.js +327 -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 +36 -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 +52 -0
@@ -0,0 +1,924 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransactionOrchestrator = void 0;
4
+ const distributed_transaction_1 = require("./distributed-transaction");
5
+ const transaction_step_1 = require("./transaction-step");
6
+ const types_1 = require("./types");
7
+ const utils_1 = require("@etohq/utils");
8
+ const events_1 = require("events");
9
+ const errors_1 = require("./errors");
10
+ /**
11
+ * @class TransactionOrchestrator is responsible for managing and executing distributed transactions.
12
+ * It is based on a single transaction definition, which is used to execute all the transaction steps
13
+ */
14
+ class TransactionOrchestrator extends events_1.EventEmitter {
15
+ static getWorkflowOptions(modelId) {
16
+ return this.workflowOptions[modelId];
17
+ }
18
+ constructor({ id, definition, options, isClone, }) {
19
+ super();
20
+ this.invokeSteps = [];
21
+ this.compensateSteps = [];
22
+ this.id = id;
23
+ this.definition = definition;
24
+ this.options = options;
25
+ if (!isClone) {
26
+ this.parseFlowOptions();
27
+ }
28
+ }
29
+ static clone(orchestrator) {
30
+ return new TransactionOrchestrator({
31
+ id: orchestrator.id,
32
+ definition: orchestrator.definition,
33
+ options: orchestrator.options,
34
+ isClone: true,
35
+ });
36
+ }
37
+ static getKeyName(...params) {
38
+ return params.join(this.SEPARATOR);
39
+ }
40
+ getPreviousStep(flow, step) {
41
+ const id = step.id.split(".");
42
+ id.pop();
43
+ const parentId = id.join(".");
44
+ return flow.steps[parentId];
45
+ }
46
+ getOptions() {
47
+ return this.options ?? {};
48
+ }
49
+ getInvokeSteps(flow) {
50
+ if (this.invokeSteps.length) {
51
+ return this.invokeSteps;
52
+ }
53
+ const steps = Object.keys(flow.steps);
54
+ steps.sort((a, b) => flow.steps[a].depth - flow.steps[b].depth);
55
+ this.invokeSteps = steps;
56
+ return steps;
57
+ }
58
+ getCompensationSteps(flow) {
59
+ if (this.compensateSteps.length) {
60
+ return this.compensateSteps;
61
+ }
62
+ const steps = Object.keys(flow.steps);
63
+ steps.sort((a, b) => (flow.steps[b].depth || 0) - (flow.steps[a].depth || 0));
64
+ this.compensateSteps = steps;
65
+ return steps;
66
+ }
67
+ canMoveForward(flow, previousStep) {
68
+ const states = [
69
+ utils_1.TransactionStepState.DONE,
70
+ utils_1.TransactionStepState.FAILED,
71
+ utils_1.TransactionStepState.TIMEOUT,
72
+ utils_1.TransactionStepState.SKIPPED,
73
+ utils_1.TransactionStepState.SKIPPED_FAILURE,
74
+ ];
75
+ const siblings = this.getPreviousStep(flow, previousStep).next.map((sib) => flow.steps[sib]);
76
+ return (!!previousStep.definition.noWait ||
77
+ siblings.every((sib) => states.includes(sib.invoke.state)));
78
+ }
79
+ canMoveBackward(flow, step) {
80
+ const states = [
81
+ utils_1.TransactionStepState.DONE,
82
+ utils_1.TransactionStepState.REVERTED,
83
+ utils_1.TransactionStepState.FAILED,
84
+ utils_1.TransactionStepState.DORMANT,
85
+ utils_1.TransactionStepState.SKIPPED,
86
+ ];
87
+ const siblings = step.next.map((sib) => flow.steps[sib]);
88
+ return (siblings.length === 0 ||
89
+ siblings.every((sib) => states.includes(sib.compensate.state)));
90
+ }
91
+ canContinue(flow, step) {
92
+ if (flow.state == types_1.TransactionState.COMPENSATING) {
93
+ return this.canMoveBackward(flow, step);
94
+ }
95
+ else {
96
+ const previous = this.getPreviousStep(flow, step);
97
+ if (previous.id === TransactionOrchestrator.ROOT_STEP) {
98
+ return true;
99
+ }
100
+ return this.canMoveForward(flow, previous);
101
+ }
102
+ }
103
+ hasExpired({ transaction, step, }, dateNow) {
104
+ const hasStepTimedOut = step &&
105
+ step.hasTimeout() &&
106
+ !step.isCompensating() &&
107
+ dateNow > step.startedAt + step.getTimeout() * 1e3;
108
+ const hasTransactionTimedOut = transaction &&
109
+ transaction.hasTimeout() &&
110
+ transaction.getFlow().state !== types_1.TransactionState.COMPENSATING &&
111
+ dateNow >
112
+ transaction.getFlow().startedAt + transaction.getTimeout() * 1e3;
113
+ return !!hasStepTimedOut || !!hasTransactionTimedOut;
114
+ }
115
+ async checkTransactionTimeout(transaction, currentSteps) {
116
+ const flow = transaction.getFlow();
117
+ let hasTimedOut = false;
118
+ if (!flow.timedOutAt && this.hasExpired({ transaction }, Date.now())) {
119
+ flow.timedOutAt = Date.now();
120
+ void transaction.clearTransactionTimeout();
121
+ for (const step of currentSteps) {
122
+ await TransactionOrchestrator.setStepTimeout(transaction, step, new errors_1.TransactionTimeoutError());
123
+ }
124
+ await transaction.saveCheckpoint();
125
+ this.emit(types_1.DistributedTransactionEvent.TIMEOUT, { transaction });
126
+ hasTimedOut = true;
127
+ }
128
+ return hasTimedOut;
129
+ }
130
+ async checkStepTimeout(transaction, step) {
131
+ let hasTimedOut = false;
132
+ if (!step.timedOutAt &&
133
+ step.canCancel() &&
134
+ this.hasExpired({ step }, Date.now())) {
135
+ step.timedOutAt = Date.now();
136
+ await TransactionOrchestrator.setStepTimeout(transaction, step, new errors_1.TransactionStepTimeoutError());
137
+ hasTimedOut = true;
138
+ await transaction.saveCheckpoint();
139
+ this.emit(types_1.DistributedTransactionEvent.TIMEOUT, { transaction });
140
+ }
141
+ return hasTimedOut;
142
+ }
143
+ async checkAllSteps(transaction) {
144
+ let hasSkipped = false;
145
+ let hasSkippedOnFailure = false;
146
+ let hasIgnoredFailure = false;
147
+ let hasFailed = false;
148
+ let hasWaiting = false;
149
+ let hasReverted = false;
150
+ let completedSteps = 0;
151
+ const flow = transaction.getFlow();
152
+ const nextSteps = [];
153
+ const currentSteps = [];
154
+ const allSteps = flow.state === types_1.TransactionState.COMPENSATING
155
+ ? this.getCompensationSteps(flow)
156
+ : this.getInvokeSteps(flow);
157
+ for (const step of allSteps) {
158
+ if (step === TransactionOrchestrator.ROOT_STEP ||
159
+ !this.canContinue(flow, flow.steps[step])) {
160
+ continue;
161
+ }
162
+ const stepDef = flow.steps[step];
163
+ const curState = stepDef.getStates();
164
+ const hasTimedOut = await this.checkStepTimeout(transaction, stepDef);
165
+ if (hasTimedOut) {
166
+ continue;
167
+ }
168
+ if (curState.status === types_1.TransactionStepStatus.WAITING) {
169
+ currentSteps.push(stepDef);
170
+ hasWaiting = true;
171
+ if (stepDef.hasAwaitingRetry()) {
172
+ if (stepDef.canRetryAwaiting()) {
173
+ stepDef.retryRescheduledAt = null;
174
+ nextSteps.push(stepDef);
175
+ }
176
+ else if (!stepDef.retryRescheduledAt) {
177
+ stepDef.hasScheduledRetry = true;
178
+ stepDef.retryRescheduledAt = Date.now();
179
+ await transaction.scheduleRetry(stepDef, stepDef.definition.retryIntervalAwaiting);
180
+ }
181
+ }
182
+ continue;
183
+ }
184
+ else if (curState.status === types_1.TransactionStepStatus.TEMPORARY_FAILURE) {
185
+ currentSteps.push(stepDef);
186
+ if (!stepDef.canRetry()) {
187
+ if (stepDef.hasRetryInterval() && !stepDef.retryRescheduledAt) {
188
+ stepDef.hasScheduledRetry = true;
189
+ stepDef.retryRescheduledAt = Date.now();
190
+ await transaction.scheduleRetry(stepDef, stepDef.definition.retryInterval);
191
+ }
192
+ continue;
193
+ }
194
+ stepDef.retryRescheduledAt = null;
195
+ }
196
+ if (stepDef.canInvoke(flow.state) || stepDef.canCompensate(flow.state)) {
197
+ nextSteps.push(stepDef);
198
+ }
199
+ else {
200
+ completedSteps++;
201
+ if (curState.state === utils_1.TransactionStepState.SKIPPED_FAILURE) {
202
+ hasSkippedOnFailure = true;
203
+ }
204
+ else if (curState.state === utils_1.TransactionStepState.SKIPPED) {
205
+ hasSkipped = true;
206
+ }
207
+ else if (curState.state === utils_1.TransactionStepState.REVERTED) {
208
+ hasReverted = true;
209
+ }
210
+ else if (curState.state === utils_1.TransactionStepState.FAILED) {
211
+ if (stepDef.definition.continueOnPermanentFailure) {
212
+ hasIgnoredFailure = true;
213
+ }
214
+ else {
215
+ hasFailed = true;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ flow.hasWaitingSteps = hasWaiting;
221
+ flow.hasRevertedSteps = hasReverted;
222
+ const totalSteps = allSteps.length - 1;
223
+ if (flow.state === types_1.TransactionState.WAITING_TO_COMPENSATE &&
224
+ nextSteps.length === 0 &&
225
+ !hasWaiting) {
226
+ flow.state = types_1.TransactionState.COMPENSATING;
227
+ this.flagStepsToRevert(flow);
228
+ this.emit(types_1.DistributedTransactionEvent.COMPENSATE_BEGIN, { transaction });
229
+ return await this.checkAllSteps(transaction);
230
+ }
231
+ else if (completedSteps === totalSteps) {
232
+ if (hasSkippedOnFailure) {
233
+ flow.hasSkippedOnFailureSteps = true;
234
+ }
235
+ if (hasSkipped) {
236
+ flow.hasSkippedSteps = true;
237
+ }
238
+ if (hasIgnoredFailure) {
239
+ flow.hasFailedSteps = true;
240
+ }
241
+ if (hasFailed) {
242
+ flow.state = types_1.TransactionState.FAILED;
243
+ }
244
+ else {
245
+ flow.state = hasReverted
246
+ ? types_1.TransactionState.REVERTED
247
+ : types_1.TransactionState.DONE;
248
+ }
249
+ }
250
+ return {
251
+ current: currentSteps,
252
+ next: nextSteps,
253
+ total: totalSteps,
254
+ remaining: totalSteps - completedSteps,
255
+ completed: completedSteps,
256
+ };
257
+ }
258
+ flagStepsToRevert(flow) {
259
+ for (const step in flow.steps) {
260
+ if (step === TransactionOrchestrator.ROOT_STEP) {
261
+ continue;
262
+ }
263
+ const stepDef = flow.steps[step];
264
+ const curState = stepDef.getStates();
265
+ if ([utils_1.TransactionStepState.DONE, utils_1.TransactionStepState.TIMEOUT].includes(curState.state) ||
266
+ curState.status === types_1.TransactionStepStatus.PERMANENT_FAILURE) {
267
+ stepDef.beginCompensation();
268
+ stepDef.changeState(utils_1.TransactionStepState.NOT_STARTED);
269
+ }
270
+ }
271
+ }
272
+ static async setStepSuccess(transaction, step, response) {
273
+ const hasStepTimedOut = step.getStates().state === utils_1.TransactionStepState.TIMEOUT;
274
+ if (step.saveResponse) {
275
+ transaction.addResponse(step.definition.action, step.isCompensating()
276
+ ? types_1.TransactionHandlerType.COMPENSATE
277
+ : types_1.TransactionHandlerType.INVOKE, response);
278
+ }
279
+ const flow = transaction.getFlow();
280
+ const options = TransactionOrchestrator.getWorkflowOptions(flow.modelId);
281
+ if (!hasStepTimedOut) {
282
+ step.changeStatus(types_1.TransactionStepStatus.OK);
283
+ }
284
+ if (step.isCompensating()) {
285
+ step.changeState(utils_1.TransactionStepState.REVERTED);
286
+ }
287
+ else if (!hasStepTimedOut) {
288
+ step.changeState(utils_1.TransactionStepState.DONE);
289
+ }
290
+ if (step.definition.async || options?.storeExecution) {
291
+ await transaction.saveCheckpoint();
292
+ }
293
+ const cleaningUp = [];
294
+ if (step.hasRetryScheduled()) {
295
+ cleaningUp.push(transaction.clearRetry(step));
296
+ }
297
+ if (step.hasTimeout()) {
298
+ cleaningUp.push(transaction.clearStepTimeout(step));
299
+ }
300
+ await (0, utils_1.promiseAll)(cleaningUp);
301
+ const eventName = step.isCompensating()
302
+ ? types_1.DistributedTransactionEvent.COMPENSATE_STEP_SUCCESS
303
+ : types_1.DistributedTransactionEvent.STEP_SUCCESS;
304
+ transaction.emit(eventName, { step, transaction });
305
+ }
306
+ static async skipStep(transaction, step) {
307
+ const hasStepTimedOut = step.getStates().state === utils_1.TransactionStepState.TIMEOUT;
308
+ const flow = transaction.getFlow();
309
+ const options = TransactionOrchestrator.getWorkflowOptions(flow.modelId);
310
+ if (!hasStepTimedOut) {
311
+ step.changeStatus(types_1.TransactionStepStatus.OK);
312
+ step.changeState(utils_1.TransactionStepState.SKIPPED);
313
+ }
314
+ if (step.definition.async || options?.storeExecution) {
315
+ await transaction.saveCheckpoint();
316
+ }
317
+ const cleaningUp = [];
318
+ if (step.hasRetryScheduled()) {
319
+ cleaningUp.push(transaction.clearRetry(step));
320
+ }
321
+ if (step.hasTimeout()) {
322
+ cleaningUp.push(transaction.clearStepTimeout(step));
323
+ }
324
+ await (0, utils_1.promiseAll)(cleaningUp);
325
+ const eventName = types_1.DistributedTransactionEvent.STEP_SKIPPED;
326
+ transaction.emit(eventName, { step, transaction });
327
+ }
328
+ static async setStepTimeout(transaction, step, error) {
329
+ if ([
330
+ utils_1.TransactionStepState.TIMEOUT,
331
+ utils_1.TransactionStepState.DONE,
332
+ utils_1.TransactionStepState.REVERTED,
333
+ ].includes(step.getStates().state)) {
334
+ return;
335
+ }
336
+ step.changeState(utils_1.TransactionStepState.TIMEOUT);
337
+ if (error?.stack) {
338
+ const workflowId = transaction.modelId;
339
+ const stepAction = step.definition.action;
340
+ const sourcePath = transaction.getFlow().metadata?.sourcePath;
341
+ const sourceStack = sourcePath
342
+ ? `\n⮑ \sat ${sourcePath}: [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`
343
+ : `\n⮑ \sat [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`;
344
+ error.stack += sourceStack;
345
+ }
346
+ transaction.addError(step.definition.action, types_1.TransactionHandlerType.INVOKE, error);
347
+ await TransactionOrchestrator.setStepFailure(transaction, step, undefined, 0, true, error);
348
+ await transaction.clearStepTimeout(step);
349
+ }
350
+ static async setStepFailure(transaction, step, error, maxRetries = TransactionOrchestrator.DEFAULT_RETRIES, isTimeout = false, timeoutError) {
351
+ step.failures++;
352
+ if ((0, utils_1.isErrorLike)(error)) {
353
+ error = (0, utils_1.serializeError)(error);
354
+ }
355
+ if (!isTimeout &&
356
+ step.getStates().status !== types_1.TransactionStepStatus.PERMANENT_FAILURE) {
357
+ step.changeStatus(types_1.TransactionStepStatus.TEMPORARY_FAILURE);
358
+ }
359
+ const flow = transaction.getFlow();
360
+ const options = TransactionOrchestrator.getWorkflowOptions(flow.modelId);
361
+ const cleaningUp = [];
362
+ const hasTimedOut = step.getStates().state === utils_1.TransactionStepState.TIMEOUT;
363
+ if (step.failures > maxRetries || hasTimedOut) {
364
+ if (!hasTimedOut) {
365
+ step.changeState(utils_1.TransactionStepState.FAILED);
366
+ }
367
+ step.changeStatus(types_1.TransactionStepStatus.PERMANENT_FAILURE);
368
+ if (!isTimeout) {
369
+ const handlerType = step.isCompensating()
370
+ ? types_1.TransactionHandlerType.COMPENSATE
371
+ : types_1.TransactionHandlerType.INVOKE;
372
+ if (error?.stack) {
373
+ const workflowId = transaction.modelId;
374
+ const stepAction = step.definition.action;
375
+ const sourcePath = transaction.getFlow().metadata?.sourcePath;
376
+ const sourceStack = sourcePath
377
+ ? `\n⮑ \sat ${sourcePath}: [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`
378
+ : `\n⮑ \sat [${workflowId} -> ${stepAction} (${types_1.TransactionHandlerType.INVOKE})]`;
379
+ error.stack += sourceStack;
380
+ }
381
+ transaction.addError(step.definition.action, handlerType, error);
382
+ }
383
+ if (!step.isCompensating()) {
384
+ if (step.definition.continueOnPermanentFailure &&
385
+ !errors_1.TransactionTimeoutError.isTransactionTimeoutError(timeoutError)) {
386
+ for (const childStep of step.next) {
387
+ const child = flow.steps[childStep];
388
+ child.changeState(utils_1.TransactionStepState.SKIPPED_FAILURE);
389
+ }
390
+ }
391
+ else {
392
+ flow.state = types_1.TransactionState.WAITING_TO_COMPENSATE;
393
+ }
394
+ }
395
+ if (step.hasTimeout()) {
396
+ cleaningUp.push(transaction.clearStepTimeout(step));
397
+ }
398
+ }
399
+ if (step.definition.async || options?.storeExecution) {
400
+ await transaction.saveCheckpoint();
401
+ }
402
+ if (step.hasRetryScheduled()) {
403
+ cleaningUp.push(transaction.clearRetry(step));
404
+ }
405
+ await (0, utils_1.promiseAll)(cleaningUp);
406
+ const eventName = step.isCompensating()
407
+ ? types_1.DistributedTransactionEvent.COMPENSATE_STEP_FAILURE
408
+ : types_1.DistributedTransactionEvent.STEP_FAILURE;
409
+ transaction.emit(eventName, { step, transaction });
410
+ }
411
+ async executeNext(transaction) {
412
+ let continueExecution = true;
413
+ while (continueExecution) {
414
+ if (transaction.hasFinished()) {
415
+ return;
416
+ }
417
+ const flow = transaction.getFlow();
418
+ const options = TransactionOrchestrator.getWorkflowOptions(flow.modelId);
419
+ const nextSteps = await this.checkAllSteps(transaction);
420
+ const execution = [];
421
+ const hasTimedOut = await this.checkTransactionTimeout(transaction, nextSteps.current);
422
+ if (hasTimedOut) {
423
+ continue;
424
+ }
425
+ if (nextSteps.remaining === 0) {
426
+ if (transaction.hasTimeout()) {
427
+ void transaction.clearTransactionTimeout();
428
+ }
429
+ await transaction.saveCheckpoint();
430
+ this.emit(types_1.DistributedTransactionEvent.FINISH, { transaction });
431
+ }
432
+ let hasSyncSteps = false;
433
+ for (const step of nextSteps.next) {
434
+ const curState = step.getStates();
435
+ const type = step.isCompensating()
436
+ ? types_1.TransactionHandlerType.COMPENSATE
437
+ : types_1.TransactionHandlerType.INVOKE;
438
+ step.lastAttempt = Date.now();
439
+ step.attempts++;
440
+ if (curState.state === utils_1.TransactionStepState.NOT_STARTED) {
441
+ if (!step.startedAt) {
442
+ step.startedAt = Date.now();
443
+ }
444
+ if (step.isCompensating()) {
445
+ step.changeState(utils_1.TransactionStepState.COMPENSATING);
446
+ if (step.definition.noCompensation) {
447
+ step.changeState(utils_1.TransactionStepState.REVERTED);
448
+ continue;
449
+ }
450
+ }
451
+ else if (flow.state === types_1.TransactionState.INVOKING) {
452
+ step.changeState(utils_1.TransactionStepState.INVOKING);
453
+ }
454
+ }
455
+ step.changeStatus(types_1.TransactionStepStatus.WAITING);
456
+ const payload = new distributed_transaction_1.TransactionPayload({
457
+ model_id: flow.modelId,
458
+ idempotency_key: TransactionOrchestrator.getKeyName(flow.modelId, flow.transactionId, step.definition.action, type),
459
+ action: step.definition.action + "",
460
+ action_type: type,
461
+ attempt: step.attempts,
462
+ timestamp: Date.now(),
463
+ }, transaction.payload, transaction.getContext());
464
+ if (step.hasTimeout() && !step.timedOutAt && step.attempts === 1) {
465
+ await transaction.scheduleStepTimeout(step, step.definition.timeout);
466
+ }
467
+ transaction.emit(types_1.DistributedTransactionEvent.STEP_BEGIN, {
468
+ step,
469
+ transaction,
470
+ });
471
+ const isAsync = step.isCompensating()
472
+ ? step.definition.compensateAsync
473
+ : step.definition.async;
474
+ const setStepFailure = async (error, { endRetry, response, } = {}) => {
475
+ if ((0, utils_1.isDefined)(response) && step.saveResponse) {
476
+ transaction.addResponse(step.definition.action, step.isCompensating()
477
+ ? types_1.TransactionHandlerType.COMPENSATE
478
+ : types_1.TransactionHandlerType.INVOKE, response);
479
+ }
480
+ await TransactionOrchestrator.setStepFailure(transaction, step, error, endRetry ? 0 : step.definition.maxRetries);
481
+ if (isAsync) {
482
+ await transaction.scheduleRetry(step, step.definition.retryInterval ?? 0);
483
+ }
484
+ };
485
+ const traceData = {
486
+ action: step.definition.action + "",
487
+ type,
488
+ step_id: step.id,
489
+ step_uuid: step.uuid + "",
490
+ attempts: step.attempts,
491
+ failures: step.failures,
492
+ async: !!(type === "invoke"
493
+ ? step.definition.async
494
+ : step.definition.compensateAsync),
495
+ idempotency_key: payload.metadata.idempotency_key,
496
+ };
497
+ const handlerArgs = [
498
+ step.definition.action + "",
499
+ type,
500
+ payload,
501
+ transaction,
502
+ step,
503
+ this,
504
+ ];
505
+ if (!isAsync) {
506
+ hasSyncSteps = true;
507
+ const stepHandler = async () => {
508
+ return await transaction.handler(...handlerArgs);
509
+ };
510
+ let promise;
511
+ if (TransactionOrchestrator.traceStep) {
512
+ promise = TransactionOrchestrator.traceStep(stepHandler, traceData);
513
+ }
514
+ else {
515
+ promise = stepHandler();
516
+ }
517
+ execution.push(promise
518
+ .then(async (response) => {
519
+ if (this.hasExpired({ transaction, step }, Date.now())) {
520
+ await this.checkStepTimeout(transaction, step);
521
+ await this.checkTransactionTimeout(transaction, nextSteps.next.includes(step) ? nextSteps.next : [step]);
522
+ }
523
+ const output = response?.__type ? response.output : response;
524
+ if (errors_1.SkipStepResponse.isSkipStepResponse(output)) {
525
+ await TransactionOrchestrator.skipStep(transaction, step);
526
+ return;
527
+ }
528
+ await TransactionOrchestrator.setStepSuccess(transaction, step, response);
529
+ })
530
+ .catch(async (error) => {
531
+ const response = error?.getStepResponse?.();
532
+ if (this.hasExpired({ transaction, step }, Date.now())) {
533
+ await this.checkStepTimeout(transaction, step);
534
+ await this.checkTransactionTimeout(transaction, nextSteps.next.includes(step) ? nextSteps.next : [step]);
535
+ }
536
+ if (errors_1.PermanentStepFailureError.isPermanentStepFailureError(error)) {
537
+ await setStepFailure(error, {
538
+ endRetry: true,
539
+ response,
540
+ });
541
+ return;
542
+ }
543
+ await setStepFailure(error, { response });
544
+ }));
545
+ }
546
+ else {
547
+ const stepHandler = async () => {
548
+ return await transaction.handler(...handlerArgs);
549
+ };
550
+ execution.push(transaction.saveCheckpoint().then(() => {
551
+ let promise;
552
+ if (TransactionOrchestrator.traceStep) {
553
+ promise = TransactionOrchestrator.traceStep(stepHandler, traceData);
554
+ }
555
+ else {
556
+ promise = stepHandler();
557
+ }
558
+ promise
559
+ .then(async (response) => {
560
+ const output = response?.__type ? response.output : response;
561
+ if (errors_1.SkipStepResponse.isSkipStepResponse(output)) {
562
+ await TransactionOrchestrator.skipStep(transaction, step);
563
+ }
564
+ else {
565
+ if (!step.definition.backgroundExecution ||
566
+ step.definition.nested) {
567
+ const eventName = types_1.DistributedTransactionEvent.STEP_AWAITING;
568
+ transaction.emit(eventName, { step, transaction });
569
+ return;
570
+ }
571
+ if (this.hasExpired({ transaction, step }, Date.now())) {
572
+ await this.checkStepTimeout(transaction, step);
573
+ await this.checkTransactionTimeout(transaction, nextSteps.next.includes(step) ? nextSteps.next : [step]);
574
+ }
575
+ await TransactionOrchestrator.setStepSuccess(transaction, step, response);
576
+ }
577
+ // check nested flow
578
+ await transaction.scheduleRetry(step, step.definition.retryInterval ?? 0);
579
+ })
580
+ .catch(async (error) => {
581
+ const response = error?.getStepResponse?.();
582
+ if (errors_1.PermanentStepFailureError.isPermanentStepFailureError(error)) {
583
+ await setStepFailure(error, {
584
+ endRetry: true,
585
+ response,
586
+ });
587
+ return;
588
+ }
589
+ await setStepFailure(error, { response });
590
+ });
591
+ }));
592
+ }
593
+ }
594
+ if (hasSyncSteps && options?.storeExecution) {
595
+ await transaction.saveCheckpoint();
596
+ }
597
+ await (0, utils_1.promiseAll)(execution);
598
+ if (nextSteps.next.length === 0) {
599
+ continueExecution = false;
600
+ }
601
+ }
602
+ }
603
+ /**
604
+ * Start a new transaction or resume a transaction that has been previously started
605
+ * @param transaction - The transaction to resume
606
+ */
607
+ async resume(transaction) {
608
+ if (transaction.modelId !== this.id) {
609
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_ALLOWED, `TransactionModel "${transaction.modelId}" cannot be orchestrated by "${this.id}" model.`);
610
+ }
611
+ if (transaction.hasFinished()) {
612
+ return;
613
+ }
614
+ const executeNext = async () => {
615
+ const flow = transaction.getFlow();
616
+ if (flow.state === types_1.TransactionState.NOT_STARTED) {
617
+ flow.state = types_1.TransactionState.INVOKING;
618
+ flow.startedAt = Date.now();
619
+ if (this.getOptions().store) {
620
+ await transaction.saveCheckpoint(flow.hasAsyncSteps ? 0 : TransactionOrchestrator.DEFAULT_TTL);
621
+ }
622
+ if (transaction.hasTimeout()) {
623
+ await transaction.scheduleTransactionTimeout(transaction.getTimeout());
624
+ }
625
+ this.emit(types_1.DistributedTransactionEvent.BEGIN, { transaction });
626
+ }
627
+ else {
628
+ this.emit(types_1.DistributedTransactionEvent.RESUME, { transaction });
629
+ }
630
+ return await this.executeNext(transaction);
631
+ };
632
+ if (TransactionOrchestrator.traceTransaction &&
633
+ !transaction.getFlow().hasAsyncSteps) {
634
+ await TransactionOrchestrator.traceTransaction(executeNext, {
635
+ model_id: transaction.modelId,
636
+ transaction_id: transaction.transactionId,
637
+ flow_metadata: transaction.getFlow().metadata,
638
+ });
639
+ return;
640
+ }
641
+ await executeNext();
642
+ }
643
+ /**
644
+ * Cancel and revert a transaction compensating all its executed steps. It can be an ongoing transaction or a completed one
645
+ * @param transaction - The transaction to be reverted
646
+ */
647
+ async cancelTransaction(transaction) {
648
+ if (transaction.modelId !== this.id) {
649
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_ALLOWED, `TransactionModel "${transaction.modelId}" cannot be orchestrated by "${this.id}" model.`);
650
+ }
651
+ const flow = transaction.getFlow();
652
+ if (flow.state === types_1.TransactionState.FAILED) {
653
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_ALLOWED, `Cannot revert a permanent failed transaction.`);
654
+ }
655
+ flow.state = types_1.TransactionState.WAITING_TO_COMPENSATE;
656
+ await this.executeNext(transaction);
657
+ }
658
+ parseFlowOptions() {
659
+ const [steps, features] = TransactionOrchestrator.buildSteps(this.definition);
660
+ this.options ??= {};
661
+ const hasAsyncSteps = features.hasAsyncSteps;
662
+ const hasStepTimeouts = features.hasStepTimeouts;
663
+ const hasRetriesTimeout = features.hasRetriesTimeout;
664
+ const hasTransactionTimeout = !!this.options.timeout;
665
+ const isIdempotent = !!this.options.idempotent;
666
+ if (hasAsyncSteps) {
667
+ this.options.store = true;
668
+ }
669
+ if (hasStepTimeouts ||
670
+ hasRetriesTimeout ||
671
+ hasTransactionTimeout ||
672
+ isIdempotent) {
673
+ this.options.store = true;
674
+ this.options.storeExecution = true;
675
+ }
676
+ const parsedOptions = {
677
+ ...this.options,
678
+ hasAsyncSteps,
679
+ hasStepTimeouts,
680
+ hasRetriesTimeout,
681
+ };
682
+ TransactionOrchestrator.workflowOptions[this.id] = parsedOptions;
683
+ return [steps, features];
684
+ }
685
+ createTransactionFlow(transactionId, flowMetadata) {
686
+ const [steps, features] = TransactionOrchestrator.buildSteps(this.definition);
687
+ const flow = {
688
+ modelId: this.id,
689
+ options: this.options,
690
+ transactionId: transactionId,
691
+ metadata: flowMetadata,
692
+ hasAsyncSteps: features.hasAsyncSteps,
693
+ hasFailedSteps: false,
694
+ hasSkippedOnFailureSteps: false,
695
+ hasSkippedSteps: false,
696
+ hasWaitingSteps: false,
697
+ hasRevertedSteps: false,
698
+ timedOutAt: null,
699
+ state: types_1.TransactionState.NOT_STARTED,
700
+ definition: this.definition,
701
+ steps,
702
+ };
703
+ return flow;
704
+ }
705
+ static async loadTransactionById(modelId, transactionId) {
706
+ const transaction = await distributed_transaction_1.DistributedTransaction.loadTransaction(modelId, transactionId);
707
+ if (transaction !== null) {
708
+ const flow = transaction.flow;
709
+ const [steps] = TransactionOrchestrator.buildSteps(flow.definition, flow.steps);
710
+ transaction.flow.steps = steps;
711
+ return transaction;
712
+ }
713
+ return null;
714
+ }
715
+ static buildSteps(flow, existingSteps) {
716
+ const states = {
717
+ [TransactionOrchestrator.ROOT_STEP]: {
718
+ id: TransactionOrchestrator.ROOT_STEP,
719
+ next: [],
720
+ },
721
+ };
722
+ const actionNames = new Set();
723
+ const queue = [
724
+ { obj: flow, level: [TransactionOrchestrator.ROOT_STEP] },
725
+ ];
726
+ const features = {
727
+ hasAsyncSteps: false,
728
+ hasStepTimeouts: false,
729
+ hasRetriesTimeout: false,
730
+ hasNestedTransactions: false,
731
+ };
732
+ while (queue.length > 0) {
733
+ const { obj, level } = queue.shift();
734
+ for (const key of Object.keys(obj)) {
735
+ if (typeof obj[key] === "object" && obj[key] !== null) {
736
+ queue.push({ obj: obj[key], level: [...level] });
737
+ }
738
+ else if (key === "action") {
739
+ if (actionNames.has(obj.action)) {
740
+ throw new Error(`Step ${obj.action} is already defined in workflow.`);
741
+ }
742
+ actionNames.add(obj.action);
743
+ level.push(obj.action);
744
+ const id = level.join(".");
745
+ const parent = level.slice(0, level.length - 1).join(".");
746
+ if (!existingSteps || parent === TransactionOrchestrator.ROOT_STEP) {
747
+ states[parent].next?.push(id);
748
+ }
749
+ const definitionCopy = { ...obj };
750
+ delete definitionCopy.next;
751
+ if (definitionCopy.async) {
752
+ features.hasAsyncSteps = true;
753
+ }
754
+ if (definitionCopy.timeout) {
755
+ features.hasStepTimeouts = true;
756
+ }
757
+ if (definitionCopy.retryInterval ||
758
+ definitionCopy.retryIntervalAwaiting) {
759
+ features.hasRetriesTimeout = true;
760
+ }
761
+ if (definitionCopy.nested) {
762
+ features.hasNestedTransactions = true;
763
+ }
764
+ states[id] = Object.assign(new transaction_step_1.TransactionStep(), existingSteps?.[id] || {
765
+ id,
766
+ uuid: definitionCopy.uuid,
767
+ depth: level.length - 1,
768
+ definition: definitionCopy,
769
+ saveResponse: definitionCopy.saveResponse ?? true,
770
+ invoke: {
771
+ state: utils_1.TransactionStepState.NOT_STARTED,
772
+ status: types_1.TransactionStepStatus.IDLE,
773
+ },
774
+ compensate: {
775
+ state: utils_1.TransactionStepState.DORMANT,
776
+ status: types_1.TransactionStepStatus.IDLE,
777
+ },
778
+ attempts: 0,
779
+ failures: 0,
780
+ lastAttempt: null,
781
+ next: [],
782
+ });
783
+ }
784
+ }
785
+ }
786
+ return [states, features];
787
+ }
788
+ /** Create a new transaction
789
+ * @param transactionId - unique identifier of the transaction
790
+ * @param handler - function to handle action of the transaction
791
+ * @param payload - payload to be passed to all the transaction steps
792
+ * @param flowMetadata - flow metadata which can include event group id for example
793
+ */
794
+ async beginTransaction(transactionId, handler, payload, flowMetadata) {
795
+ const existingTransaction = await TransactionOrchestrator.loadTransactionById(this.id, transactionId);
796
+ let newTransaction = false;
797
+ let modelFlow;
798
+ if (!existingTransaction) {
799
+ modelFlow = this.createTransactionFlow(transactionId, flowMetadata);
800
+ newTransaction = true;
801
+ }
802
+ else {
803
+ modelFlow = existingTransaction.flow;
804
+ }
805
+ const transaction = new distributed_transaction_1.DistributedTransaction(modelFlow, handler, payload, existingTransaction?.errors, existingTransaction?.context);
806
+ if (newTransaction &&
807
+ this.getOptions().store &&
808
+ this.getOptions().storeExecution) {
809
+ await transaction.saveCheckpoint(modelFlow.hasAsyncSteps ? 0 : TransactionOrchestrator.DEFAULT_TTL);
810
+ }
811
+ return transaction;
812
+ }
813
+ /** Returns an existing transaction
814
+ * @param transactionId - unique identifier of the transaction
815
+ * @param handler - function to handle action of the transaction
816
+ */
817
+ async retrieveExistingTransaction(transactionId, handler) {
818
+ const existingTransaction = await TransactionOrchestrator.loadTransactionById(this.id, transactionId);
819
+ if (!existingTransaction) {
820
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Transaction ${transactionId} could not be found.`);
821
+ }
822
+ const transaction = new distributed_transaction_1.DistributedTransaction(existingTransaction.flow, handler, undefined, existingTransaction?.errors, existingTransaction?.context);
823
+ return transaction;
824
+ }
825
+ static getStepByAction(flow, action) {
826
+ for (const key in flow.steps) {
827
+ if (action === flow.steps[key]?.definition?.action) {
828
+ return flow.steps[key];
829
+ }
830
+ }
831
+ return null;
832
+ }
833
+ static async getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction) {
834
+ const [modelId, transactionId, action, actionType] = responseIdempotencyKey.split(TransactionOrchestrator.SEPARATOR);
835
+ if (!transaction && !handler) {
836
+ throw new Error("If a transaction is not provided, the handler is required");
837
+ }
838
+ if (!transaction) {
839
+ const existingTransaction = await TransactionOrchestrator.loadTransactionById(modelId, transactionId);
840
+ if (existingTransaction === null) {
841
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Transaction ${transactionId} could not be found.`);
842
+ }
843
+ transaction = new distributed_transaction_1.DistributedTransaction(existingTransaction.flow, handler, undefined, existingTransaction.errors, existingTransaction.context);
844
+ }
845
+ const step = TransactionOrchestrator.getStepByAction(transaction.getFlow(), action);
846
+ if (step === null) {
847
+ throw new Error("Action not found.");
848
+ }
849
+ else if (step.isCompensating()
850
+ ? actionType !== types_1.TransactionHandlerType.COMPENSATE
851
+ : actionType !== types_1.TransactionHandlerType.INVOKE) {
852
+ throw new Error("Incorrect action type.");
853
+ }
854
+ return [transaction, step];
855
+ }
856
+ /** Skip the execution of a specific transaction and step
857
+ * @param responseIdempotencyKey - The idempotency key for the step
858
+ * @param handler - The handler function to execute the step
859
+ * @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
860
+ */
861
+ async skipStep(responseIdempotencyKey, handler, transaction) {
862
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
863
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
864
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
865
+ transaction: curTransaction,
866
+ });
867
+ await TransactionOrchestrator.skipStep(curTransaction, step);
868
+ await this.executeNext(curTransaction);
869
+ }
870
+ else {
871
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_ALLOWED, `Cannot skip a step when status is ${step.getStates().status}`);
872
+ }
873
+ return curTransaction;
874
+ }
875
+ /** Register a step success for a specific transaction and step
876
+ * @param responseIdempotencyKey - The idempotency key for the step
877
+ * @param handler - The handler function to execute the step
878
+ * @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
879
+ * @param response - The response of the step
880
+ */
881
+ async registerStepSuccess(responseIdempotencyKey, handler, transaction, response) {
882
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
883
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
884
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
885
+ transaction: curTransaction,
886
+ });
887
+ await TransactionOrchestrator.setStepSuccess(curTransaction, step, response);
888
+ await this.executeNext(curTransaction);
889
+ }
890
+ else {
891
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_ALLOWED, `Cannot set step success when status is ${step.getStates().status}`);
892
+ }
893
+ return curTransaction;
894
+ }
895
+ /**
896
+ * Register a step failure for a specific transaction and step
897
+ * @param responseIdempotencyKey - The idempotency key for the step
898
+ * @param error - The error that caused the failure
899
+ * @param handler - The handler function to execute the step
900
+ * @param transaction - The current transaction
901
+ * @param response - The response of the step
902
+ */
903
+ async registerStepFailure(responseIdempotencyKey, error, handler, transaction) {
904
+ const [curTransaction, step] = await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(responseIdempotencyKey, handler, transaction);
905
+ if (step.getStates().status === types_1.TransactionStepStatus.WAITING) {
906
+ this.emit(types_1.DistributedTransactionEvent.RESUME, {
907
+ transaction: curTransaction,
908
+ });
909
+ await TransactionOrchestrator.setStepFailure(curTransaction, step, error, 0);
910
+ await this.executeNext(curTransaction);
911
+ }
912
+ else {
913
+ throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_ALLOWED, `Cannot set step failure when status is ${step.getStates().status}`);
914
+ }
915
+ return curTransaction;
916
+ }
917
+ }
918
+ exports.TransactionOrchestrator = TransactionOrchestrator;
919
+ TransactionOrchestrator.ROOT_STEP = "_root";
920
+ TransactionOrchestrator.DEFAULT_TTL = 30;
921
+ TransactionOrchestrator.DEFAULT_RETRIES = 0;
922
+ TransactionOrchestrator.workflowOptions = {};
923
+ TransactionOrchestrator.SEPARATOR = ":";
924
+ //# sourceMappingURL=transaction-orchestrator.js.map