@nestjs/workflows 0.0.1

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 (265) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +50 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +8 -0
  5. package/dist/core/codecs/aes-gcm-payload.codec.d.ts +42 -0
  6. package/dist/core/codecs/aes-gcm-payload.codec.js +119 -0
  7. package/dist/core/codecs/index.d.ts +2 -0
  8. package/dist/core/codecs/index.js +2 -0
  9. package/dist/core/codecs/payload-codecs.d.ts +59 -0
  10. package/dist/core/codecs/payload-codecs.js +130 -0
  11. package/dist/core/index.d.ts +16 -0
  12. package/dist/core/index.js +21 -0
  13. package/dist/core/interfaces/clock.interface.d.ts +18 -0
  14. package/dist/core/interfaces/clock.interface.js +1 -0
  15. package/dist/core/interfaces/index.d.ts +9 -0
  16. package/dist/core/interfaces/index.js +9 -0
  17. package/dist/core/interfaces/leased-worker-options.interface.d.ts +94 -0
  18. package/dist/core/interfaces/leased-worker-options.interface.js +1 -0
  19. package/dist/core/interfaces/limits.interface.d.ts +92 -0
  20. package/dist/core/interfaces/limits.interface.js +1 -0
  21. package/dist/core/interfaces/payload-codec.interface.d.ts +64 -0
  22. package/dist/core/interfaces/payload-codec.interface.js +1 -0
  23. package/dist/core/interfaces/result-waiter-options.interface.d.ts +52 -0
  24. package/dist/core/interfaces/result-waiter-options.interface.js +1 -0
  25. package/dist/core/interfaces/retry-settings.interface.d.ts +71 -0
  26. package/dist/core/interfaces/retry-settings.interface.js +1 -0
  27. package/dist/core/interfaces/schedule-store.interface.d.ts +143 -0
  28. package/dist/core/interfaces/schedule-store.interface.js +1 -0
  29. package/dist/core/interfaces/schedule.interface.d.ts +106 -0
  30. package/dist/core/interfaces/schedule.interface.js +1 -0
  31. package/dist/core/interfaces/scheduler-options.interface.d.ts +152 -0
  32. package/dist/core/interfaces/scheduler-options.interface.js +1 -0
  33. package/dist/core/limits/index.d.ts +1 -0
  34. package/dist/core/limits/index.js +1 -0
  35. package/dist/core/limits/limits.d.ts +61 -0
  36. package/dist/core/limits/limits.js +130 -0
  37. package/dist/core/results/index.d.ts +1 -0
  38. package/dist/core/results/index.js +1 -0
  39. package/dist/core/results/result-waiter.d.ts +54 -0
  40. package/dist/core/results/result-waiter.js +140 -0
  41. package/dist/core/retries/index.d.ts +1 -0
  42. package/dist/core/retries/index.js +1 -0
  43. package/dist/core/retries/retry.d.ts +60 -0
  44. package/dist/core/retries/retry.js +107 -0
  45. package/dist/core/scheduling/cron.util.d.ts +52 -0
  46. package/dist/core/scheduling/cron.util.js +262 -0
  47. package/dist/core/scheduling/encoded-schedule.store.d.ts +26 -0
  48. package/dist/core/scheduling/encoded-schedule.store.js +54 -0
  49. package/dist/core/scheduling/in-memory-schedule.store.d.ts +20 -0
  50. package/dist/core/scheduling/in-memory-schedule.store.js +90 -0
  51. package/dist/core/scheduling/index.d.ts +3 -0
  52. package/dist/core/scheduling/index.js +3 -0
  53. package/dist/core/scheduling/rrule.util.d.ts +55 -0
  54. package/dist/core/scheduling/rrule.util.js +371 -0
  55. package/dist/core/scheduling/schedule-spec.d.ts +57 -0
  56. package/dist/core/scheduling/schedule-spec.js +233 -0
  57. package/dist/core/scheduling/scheduler.d.ts +134 -0
  58. package/dist/core/scheduling/scheduler.js +450 -0
  59. package/dist/core/scheduling/time-zone.util.d.ts +18 -0
  60. package/dist/core/scheduling/time-zone.util.js +88 -0
  61. package/dist/core/time/clock.d.ts +28 -0
  62. package/dist/core/time/clock.js +35 -0
  63. package/dist/core/time/duration.d.ts +21 -0
  64. package/dist/core/time/duration.js +30 -0
  65. package/dist/core/time/index.d.ts +2 -0
  66. package/dist/core/time/index.js +2 -0
  67. package/dist/core/utils/canonical.util.d.ts +2 -0
  68. package/dist/core/utils/canonical.util.js +4 -0
  69. package/dist/core/utils/maybe.util.d.ts +8 -0
  70. package/dist/core/utils/maybe.util.js +14 -0
  71. package/dist/core/utils/payload-place.util.d.ts +10 -0
  72. package/dist/core/utils/payload-place.util.js +32 -0
  73. package/dist/core/workers/index.d.ts +1 -0
  74. package/dist/core/workers/index.js +1 -0
  75. package/dist/core/workers/leased-worker.d.ts +86 -0
  76. package/dist/core/workers/leased-worker.js +280 -0
  77. package/dist/cqrs/decorators/index.d.ts +2 -0
  78. package/dist/cqrs/decorators/index.js +2 -0
  79. package/dist/cqrs/decorators/signal-on.decorator.d.ts +18 -0
  80. package/dist/cqrs/decorators/signal-on.decorator.js +34 -0
  81. package/dist/cqrs/decorators/start-on.decorator.d.ts +18 -0
  82. package/dist/cqrs/decorators/start-on.decorator.js +49 -0
  83. package/dist/cqrs/index.d.ts +3 -0
  84. package/dist/cqrs/index.js +5 -0
  85. package/dist/cqrs/interfaces/index.d.ts +4 -0
  86. package/dist/cqrs/interfaces/index.js +4 -0
  87. package/dist/cqrs/interfaces/signal-on-options.interface.d.ts +22 -0
  88. package/dist/cqrs/interfaces/signal-on-options.interface.js +1 -0
  89. package/dist/cqrs/interfaces/start-on-options.interface.d.ts +23 -0
  90. package/dist/cqrs/interfaces/start-on-options.interface.js +1 -0
  91. package/dist/cqrs/interfaces/workflow-dispatcher-context.interface.d.ts +10 -0
  92. package/dist/cqrs/interfaces/workflow-dispatcher-context.interface.js +1 -0
  93. package/dist/cqrs/interfaces/workflow-event-route.interface.d.ts +20 -0
  94. package/dist/cqrs/interfaces/workflow-event-route.interface.js +1 -0
  95. package/dist/cqrs/utils/event-routes.util.d.ts +7 -0
  96. package/dist/cqrs/utils/event-routes.util.js +22 -0
  97. package/dist/cqrs/workflow-event.publisher.d.ts +52 -0
  98. package/dist/cqrs/workflow-event.publisher.js +257 -0
  99. package/dist/cqrs/workflow-events.explorer.d.ts +27 -0
  100. package/dist/cqrs/workflow-events.explorer.js +92 -0
  101. package/dist/cqrs/workflows-cqrs.module.d.ts +10 -0
  102. package/dist/cqrs/workflows-cqrs.module.js +27 -0
  103. package/dist/decorators/index.d.ts +1 -0
  104. package/dist/decorators/index.js +1 -0
  105. package/dist/decorators/workflow.decorator.d.ts +7 -0
  106. package/dist/decorators/workflow.decorator.js +52 -0
  107. package/dist/errors/child-workflow-failed.error.d.ts +13 -0
  108. package/dist/errors/child-workflow-failed.error.js +14 -0
  109. package/dist/errors/index.d.ts +13 -0
  110. package/dist/errors/index.js +13 -0
  111. package/dist/errors/non-retryable-step.error.d.ts +4 -0
  112. package/dist/errors/non-retryable-step.error.js +7 -0
  113. package/dist/errors/step-failed.error.d.ts +15 -0
  114. package/dist/errors/step-failed.error.js +16 -0
  115. package/dist/errors/step-timeout.error.d.ts +5 -0
  116. package/dist/errors/step-timeout.error.js +8 -0
  117. package/dist/errors/workflow-definition.error.d.ts +10 -0
  118. package/dist/errors/workflow-definition.error.js +13 -0
  119. package/dist/errors/workflow-failed.error.d.ts +20 -0
  120. package/dist/errors/workflow-failed.error.js +17 -0
  121. package/dist/errors/workflow-id-conflict.error.d.ts +6 -0
  122. package/dist/errors/workflow-id-conflict.error.js +9 -0
  123. package/dist/errors/workflow-interrupt.error.d.ts +32 -0
  124. package/dist/errors/workflow-interrupt.error.js +19 -0
  125. package/dist/errors/workflow-non-determinism.error.d.ts +4 -0
  126. package/dist/errors/workflow-non-determinism.error.js +7 -0
  127. package/dist/errors/workflow-not-found.error.d.ts +10 -0
  128. package/dist/errors/workflow-not-found.error.js +13 -0
  129. package/dist/errors/workflow-result-timeout.error.d.ts +11 -0
  130. package/dist/errors/workflow-result-timeout.error.js +13 -0
  131. package/dist/errors/workflow-state.error.d.ts +11 -0
  132. package/dist/errors/workflow-state.error.js +14 -0
  133. package/dist/errors/workflow.error.d.ts +7 -0
  134. package/dist/errors/workflow.error.js +7 -0
  135. package/dist/events/index.d.ts +3 -0
  136. package/dist/events/index.js +3 -0
  137. package/dist/events/workflow-events.interface.d.ts +145 -0
  138. package/dist/events/workflow-events.interface.js +1 -0
  139. package/dist/events/workflow-events.service.d.ts +17 -0
  140. package/dist/events/workflow-events.service.js +38 -0
  141. package/dist/events/workflows.channels.d.ts +3 -0
  142. package/dist/events/workflows.channels.js +9 -0
  143. package/dist/index.d.ts +20 -0
  144. package/dist/index.js +23 -0
  145. package/dist/interfaces/index.d.ts +13 -0
  146. package/dist/interfaces/index.js +13 -0
  147. package/dist/interfaces/serialized-workflow-error.interface.d.ts +6 -0
  148. package/dist/interfaces/serialized-workflow-error.interface.js +1 -0
  149. package/dist/interfaces/workflow-client.interface.d.ts +156 -0
  150. package/dist/interfaces/workflow-client.interface.js +1 -0
  151. package/dist/interfaces/workflow-clock.interface.d.ts +7 -0
  152. package/dist/interfaces/workflow-clock.interface.js +1 -0
  153. package/dist/interfaces/workflow-context.interface.d.ts +241 -0
  154. package/dist/interfaces/workflow-context.interface.js +1 -0
  155. package/dist/interfaces/workflow-decorator-options.interface.d.ts +101 -0
  156. package/dist/interfaces/workflow-decorator-options.interface.js +1 -0
  157. package/dist/interfaces/workflow-instance.interface.d.ts +83 -0
  158. package/dist/interfaces/workflow-instance.interface.js +1 -0
  159. package/dist/interfaces/workflow-payload-codec.interface.d.ts +32 -0
  160. package/dist/interfaces/workflow-payload-codec.interface.js +1 -0
  161. package/dist/interfaces/workflow-retry-options.interface.d.ts +9 -0
  162. package/dist/interfaces/workflow-retry-options.interface.js +1 -0
  163. package/dist/interfaces/workflow-runner.interface.d.ts +11 -0
  164. package/dist/interfaces/workflow-runner.interface.js +1 -0
  165. package/dist/interfaces/workflow-schedule.interface.d.ts +104 -0
  166. package/dist/interfaces/workflow-schedule.interface.js +1 -0
  167. package/dist/interfaces/workflow-storage.interface.d.ts +4 -0
  168. package/dist/interfaces/workflow-storage.interface.js +1 -0
  169. package/dist/interfaces/workflow-store.interface.d.ts +488 -0
  170. package/dist/interfaces/workflow-store.interface.js +1 -0
  171. package/dist/interfaces/workflows-module-options.interface.d.ts +80 -0
  172. package/dist/interfaces/workflows-module-options.interface.js +1 -0
  173. package/dist/mysql/index.d.ts +5 -0
  174. package/dist/mysql/index.js +8 -0
  175. package/dist/mysql/interfaces/index.d.ts +1 -0
  176. package/dist/mysql/interfaces/index.js +1 -0
  177. package/dist/mysql/interfaces/mysql-workflow-store-options.interface.d.ts +25 -0
  178. package/dist/mysql/interfaces/mysql-workflow-store-options.interface.js +1 -0
  179. package/dist/mysql/key-limits.d.ts +30 -0
  180. package/dist/mysql/key-limits.js +41 -0
  181. package/dist/mysql/migrations/index.d.ts +7 -0
  182. package/dist/mysql/migrations/index.js +16 -0
  183. package/dist/mysql/migrations/initial.migration.d.ts +9 -0
  184. package/dist/mysql/migrations/initial.migration.js +104 -0
  185. package/dist/mysql/mysql-workflow.store.d.ts +197 -0
  186. package/dist/mysql/mysql-workflow.store.js +828 -0
  187. package/dist/postgres/errors/index.d.ts +1 -0
  188. package/dist/postgres/errors/index.js +2 -0
  189. package/dist/postgres/index.d.ts +5 -0
  190. package/dist/postgres/index.js +8 -0
  191. package/dist/postgres/interfaces/index.d.ts +1 -0
  192. package/dist/postgres/interfaces/index.js +1 -0
  193. package/dist/postgres/interfaces/postgres-workflow-store-options.interface.d.ts +22 -0
  194. package/dist/postgres/interfaces/postgres-workflow-store-options.interface.js +1 -0
  195. package/dist/postgres/migrations/index.d.ts +6 -0
  196. package/dist/postgres/migrations/index.js +15 -0
  197. package/dist/postgres/migrations/initial.migration.d.ts +6 -0
  198. package/dist/postgres/migrations/initial.migration.js +106 -0
  199. package/dist/postgres/postgres-workflow.store.d.ts +132 -0
  200. package/dist/postgres/postgres-workflow.store.js +586 -0
  201. package/dist/services/index.d.ts +6 -0
  202. package/dist/services/index.js +6 -0
  203. package/dist/services/workflow-client.service.d.ts +146 -0
  204. package/dist/services/workflow-client.service.js +436 -0
  205. package/dist/services/workflow-execution.service.d.ts +267 -0
  206. package/dist/services/workflow-execution.service.js +1076 -0
  207. package/dist/services/workflow-registry.service.d.ts +56 -0
  208. package/dist/services/workflow-registry.service.js +172 -0
  209. package/dist/services/workflow-scheduler.service.d.ts +54 -0
  210. package/dist/services/workflow-scheduler.service.js +173 -0
  211. package/dist/services/workflow-schedules.service.d.ts +60 -0
  212. package/dist/services/workflow-schedules.service.js +193 -0
  213. package/dist/services/workflow-worker.service.d.ts +92 -0
  214. package/dist/services/workflow-worker.service.js +468 -0
  215. package/dist/signals/index.d.ts +1 -0
  216. package/dist/signals/index.js +1 -0
  217. package/dist/signals/workflow.signal.d.ts +17 -0
  218. package/dist/signals/workflow.signal.js +28 -0
  219. package/dist/sql/rate-windows.d.ts +41 -0
  220. package/dist/sql/rate-windows.js +53 -0
  221. package/dist/sql/workflow-rows.d.ts +16 -0
  222. package/dist/sql/workflow-rows.js +102 -0
  223. package/dist/sql/workflow-schema.error.d.ts +21 -0
  224. package/dist/sql/workflow-schema.error.js +15 -0
  225. package/dist/storage/encoded-workflow.store.d.ts +71 -0
  226. package/dist/storage/encoded-workflow.store.js +201 -0
  227. package/dist/storage/index.d.ts +1 -0
  228. package/dist/storage/index.js +1 -0
  229. package/dist/storage/workflow.storage.d.ts +55 -0
  230. package/dist/storage/workflow.storage.js +159 -0
  231. package/dist/stores/in-memory-workflow.store.d.ts +75 -0
  232. package/dist/stores/in-memory-workflow.store.js +380 -0
  233. package/dist/stores/index.d.ts +1 -0
  234. package/dist/stores/index.js +1 -0
  235. package/dist/testing/assertions.d.ts +10 -0
  236. package/dist/testing/assertions.js +72 -0
  237. package/dist/testing/index.d.ts +38 -0
  238. package/dist/testing/index.js +1089 -0
  239. package/dist/testing/schedule-store.contract.d.ts +33 -0
  240. package/dist/testing/schedule-store.contract.js +161 -0
  241. package/dist/utils/index.d.ts +7 -0
  242. package/dist/utils/index.js +7 -0
  243. package/dist/utils/journal-limits.util.d.ts +6 -0
  244. package/dist/utils/journal-limits.util.js +25 -0
  245. package/dist/utils/new-instance.util.d.ts +26 -0
  246. package/dist/utils/new-instance.util.js +87 -0
  247. package/dist/utils/normalize.util.d.ts +5 -0
  248. package/dist/utils/normalize.util.js +11 -0
  249. package/dist/utils/run-timeout.util.d.ts +3 -0
  250. package/dist/utils/run-timeout.util.js +15 -0
  251. package/dist/utils/schedule-spec.util.d.ts +16 -0
  252. package/dist/utils/schedule-spec.util.js +9 -0
  253. package/dist/utils/schedule-store.util.d.ts +15 -0
  254. package/dist/utils/schedule-store.util.js +44 -0
  255. package/dist/utils/serialize-error.util.d.ts +7 -0
  256. package/dist/utils/serialize-error.util.js +22 -0
  257. package/dist/utils/step-scope.util.d.ts +16 -0
  258. package/dist/utils/step-scope.util.js +38 -0
  259. package/dist/workflows.constants.d.ts +10 -0
  260. package/dist/workflows.constants.js +10 -0
  261. package/dist/workflows.module-definition.d.ts +4 -0
  262. package/dist/workflows.module-definition.js +6 -0
  263. package/dist/workflows.module.d.ts +24 -0
  264. package/dist/workflows.module.js +68 -0
  265. package/package.json +123 -0
@@ -0,0 +1,1076 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { Logger } from '@nestjs/common';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { setImmediate as nextMacrotask } from 'node:timers/promises';
5
+ import { parseDuration } from '../core/time/duration.js';
6
+ import { WorkflowNonDeterminismError } from '../errors/workflow-non-determinism.error.js';
7
+ import { WorkflowDefinitionError } from '../errors/workflow-definition.error.js';
8
+ import { StepTimeoutError } from '../errors/step-timeout.error.js';
9
+ import { WorkflowFailedError } from '../errors/workflow-failed.error.js';
10
+ import { StepFailedError } from '../errors/step-failed.error.js';
11
+ import { NonRetryableStepError } from '../errors/non-retryable-step.error.js';
12
+ import { isWorkflowInterrupt, WorkflowInterrupt, } from '../errors/workflow-interrupt.error.js';
13
+ import { serializeError } from '../utils/serialize-error.util.js';
14
+ import { runInStepScope } from '../utils/step-scope.util.js';
15
+ import { ChildWorkflowFailedError } from '../errors/child-workflow-failed.error.js';
16
+ import { WorkflowIdConflictError } from '../errors/workflow-id-conflict.error.js';
17
+ import { CHILD_ENDED_SIGNAL } from '../workflows.constants.js';
18
+ import { newInstance } from '../utils/new-instance.util.js';
19
+ import { nextRetry, resolveRetry } from '../core/retries/retry.js';
20
+ import { entryBytes } from '../utils/journal-limits.util.js';
21
+ import { canonical } from '../core/utils/canonical.util.js';
22
+ import { normalize } from '../utils/normalize.util.js';
23
+ import { signalName } from '../signals/workflow.signal.js';
24
+ /** One entry per name, as the store expects: the last version of each, in the order the names first appear. */
25
+ export function uniqueEntries(entries) {
26
+ const latest = new Map();
27
+ for (const entry of entries) {
28
+ latest.set(entry.name, entry);
29
+ }
30
+ return [...latest.values()];
31
+ }
32
+ /**
33
+ * What `ctx.signalWait()` and `ctx.timer()` make for `ctx.waitForAny()`: a description of what to
34
+ * wait for, and how the winner's journaled payload becomes its value.
35
+ */
36
+ export class Condition {
37
+ constructor(wait, match, timer, settle) {
38
+ this.wait = wait;
39
+ this.match = match;
40
+ this.timer = timer;
41
+ this.settle = settle;
42
+ }
43
+ static signal(wait, match, settle = (payload) => payload) {
44
+ return new Condition(wait, match, undefined, settle);
45
+ }
46
+ static timer(when) {
47
+ // Checked now, where the mistake is: the deadline itself is fixed when the wait is reached.
48
+ wakeTime('timer', when, 0);
49
+ return new Condition(null, undefined, when, () => null);
50
+ }
51
+ }
52
+ /** `ChildWorkflowHandle`: waits for the child's end, once per execution, as `result()` or as a `waitForAny()` condition. */
53
+ export class ChildHandle {
54
+ constructor(id, workflow, version, wait) {
55
+ this.id = id;
56
+ this.workflow = workflow;
57
+ this.version = version;
58
+ this.wait = wait;
59
+ this.condition = Condition.signal({ signal: CHILD_ENDED_SIGNAL, key: id }, undefined, (payload) => this.settle(payload));
60
+ }
61
+ result() {
62
+ if (this.outcome) {
63
+ return 'error' in this.outcome ? Promise.reject(this.outcome.error) : Promise.resolve(this.outcome.value);
64
+ }
65
+ return (this.waiting ??= this.wait(this));
66
+ }
67
+ /** The child's output, from its journaled end, or its failure, thrown. Kept, so a later `result()` returns it. */
68
+ settle(payload) {
69
+ const ended = payload;
70
+ if (ended.status === 'completed') {
71
+ this.outcome = { value: ended.output };
72
+ return ended.output;
73
+ }
74
+ const error = new ChildWorkflowFailedError(this.workflow, this.id, ended.status, ended.error ?? { name: 'Error', message: 'Unknown failure.' });
75
+ this.outcome = { error };
76
+ throw error;
77
+ }
78
+ }
79
+ /**
80
+ * One execution of a workflow instance: runs `run()` from the top against the
81
+ * journal loaded at claim time.
82
+ *
83
+ * - A journaled step returns its stored result (or re-throws its stored
84
+ * failure) without running.
85
+ * - The first step that is not journaled is the frontier. Before it runs, the
86
+ * execution lets the replay settle and checks that every journaled entry was
87
+ * reached. If one was not, the code changed under a running instance and the
88
+ * instance fails instead of re-running a renamed side effect.
89
+ * - A pending sleep, wait or retry backoff records a suspension and throws
90
+ * `WorkflowInterrupt`. After that no new step starts and nothing new is
91
+ * journaled except other sleeps and waits of the same `Promise.all`; the
92
+ * engine parks the instance once in-flight steps settle.
93
+ * - A cancel, shutdown, lost lease or store failure also throws
94
+ * `WorkflowInterrupt`, and decides the outcome whatever `run()` does with it:
95
+ * swallowing or wrapping it can't complete the instance.
96
+ * - In replay-only mode (compensating or cancelling after a restart) nothing
97
+ * new runs: the replay only rebuilds the list of compensations.
98
+ */
99
+ export class WorkflowExecution {
100
+ constructor(instance, journal,
101
+ /** Highest signal id visible to this execution's waits. */
102
+ signalCursor, deps,
103
+ /** The worker's run of this instance: its lease, renewed on a heartbeat, and a signal for a lost lease or a shutdown. */
104
+ lease, replayOnly) {
105
+ this.instance = instance;
106
+ this.signalCursor = signalCursor;
107
+ this.deps = deps;
108
+ this.lease = lease;
109
+ this.abort = new AbortController();
110
+ this.suspension = null;
111
+ this.fatal = null;
112
+ /** Why the journal limit stopped the run, once it did. */
113
+ this.journalLimitError = null;
114
+ this.leaseLost = false;
115
+ this.storeError = null;
116
+ this.shuttingDown = false;
117
+ this.cancelRequested = false;
118
+ /** A terminate was requested: stop compensating too. */
119
+ this.terminateRequested = false;
120
+ /** Attempt counters to restore when a shutdown interrupts an attempt. */
121
+ this.rollbacks = [];
122
+ this.userSettled = false;
123
+ this.buffer = [];
124
+ this.visited = new Set();
125
+ this.compensations = [];
126
+ this.inflight = new Set();
127
+ this.consumed = new Set();
128
+ /** The last journal write issued; the next one waits for it (see `write()`). */
129
+ this.writes = Promise.resolve();
130
+ this.counters = { now: 0, random: 0, uuid: 0 };
131
+ /** Children started so far in this run, per workflow name: the default child ids. */
132
+ this.childCounters = new Map();
133
+ /** The first interrupt that stops this execution (not a suspension). */
134
+ this.stoppedBy = null;
135
+ /** The step whose function is running, in that function's async context. */
136
+ this.insideStep = new AsyncLocalStorage();
137
+ /** Each journal entry's size in bytes, by name, and their sum. */
138
+ this.entrySizes = new Map();
139
+ this.journalBytes = 0;
140
+ const stop = () => (lease.leaseLost ? this.loseLease() : this.requestShutdown());
141
+ if (lease.signal.aborted) {
142
+ stop();
143
+ }
144
+ else {
145
+ lease.signal.addEventListener('abort', stop, { once: true });
146
+ }
147
+ this.mode = replayOnly ? 'replay' : 'run';
148
+ this.journal = new Map(journal.map((entry) => [entry.name, entry]));
149
+ for (const entry of journal) {
150
+ this.measure(entry);
151
+ }
152
+ // Crossed in an earlier execution, which warned then.
153
+ this.journalLarge = this.pastWarning();
154
+ this.customStatus = instance.customStatus ?? null;
155
+ this.storedStatus = canonical(this.customStatus);
156
+ for (const entry of journal) {
157
+ const signalId = entry.result?.signalId;
158
+ if ((entry.kind === 'signal' || entry.kind === 'any') && typeof signalId === 'number') {
159
+ this.consumed.add(signalId);
160
+ }
161
+ }
162
+ this.context = {
163
+ workflowId: instance.id,
164
+ workflowName: instance.workflow,
165
+ version: instance.version,
166
+ schedule: instance.scheduleId === null ? null : { id: instance.scheduleId, at: instance.scheduledAt },
167
+ step: (name, fn, options) => this.track(this.step(name, fn, options)),
168
+ sleep: (name, duration) => this.track(this.sleep(name, duration)),
169
+ waitForSignal: (name, signal, options = {}) => this.track(this.receive(name, { signal: signalName(signal), key: options.key ?? null }, options, false)),
170
+ startChild: (workflow, input, options) => this.track(this.startChild(workflow, input, options)),
171
+ executeChild: (workflow, input, options) => this.track(this.startChild(workflow, input, options).then((handle) => handle.result())),
172
+ waitForAny: (name, conditions) => this.track(this.waitForAny(name, conditions)),
173
+ signalWait: (signal, options = {}) => Condition.signal({ signal: signalName(signal), key: options.key ?? null }, options.match),
174
+ timer: (when) => Condition.timer(when),
175
+ now: () => this.helper('now', () => this.deps.clock.now()),
176
+ random: () => this.helper('random', () => Math.random()),
177
+ uuid: () => this.helper('uuid', () => randomUUID()),
178
+ commit: (name) => this.commit(name),
179
+ setStatus: (status) => this.setStatus(status),
180
+ fail: (message) => {
181
+ this.assertNotInStep('fail()', 'Throw a NonRetryableStepError from the step instead.');
182
+ throw new WorkflowFailedError(message);
183
+ },
184
+ };
185
+ }
186
+ /** Runs the user's `run()` and waits for every step it started. */
187
+ async run(fn) {
188
+ let outcome;
189
+ try {
190
+ outcome = { ok: true, value: await fn() };
191
+ }
192
+ catch (error) {
193
+ outcome = { ok: false, error };
194
+ }
195
+ this.userSettled = true;
196
+ while (this.inflight.size) {
197
+ await Promise.all(this.inflight);
198
+ }
199
+ this.mode = 'closed';
200
+ // A cancel, shutdown, lost lease or store failure reached run() as an
201
+ // interrupt. Whether run() rethrew, wrapped or swallowed it, it decides the
202
+ // outcome: a run that carried on anyway never completes the instance.
203
+ return this.stoppedBy ? { ok: false, error: this.stoppedBy } : outcome;
204
+ }
205
+ /** Journal entries that this execution never reached (compensations and retry records excluded). */
206
+ unvisited() {
207
+ return [...this.journal.values()]
208
+ .filter((entry) => entry.kind !== 'compensation' && entry.kind !== 'retry' && !this.visited.has(entry.name))
209
+ .map((entry) => entry.name);
210
+ }
211
+ /** Staged entries (sleeps, helpers) not yet written; the engine commits them. */
212
+ drainBuffer() {
213
+ const now = this.deps.clock.now();
214
+ return this.buffer.splice(0).map((entry) => ({ ...entry, updatedAt: now }));
215
+ }
216
+ requestShutdown() {
217
+ if (this.shuttingDown) {
218
+ return;
219
+ }
220
+ this.shuttingDown = true;
221
+ this.abort.abort(new WorkflowInterrupt('shutdown'));
222
+ }
223
+ /** Past the shutdown timeout, the worker stopped waiting for it: never touch the store again. */
224
+ get detached() {
225
+ return this.lease.detached;
226
+ }
227
+ /**
228
+ * Extends the lease, for `WorkflowStepContext.heartbeat()` (the worker renews it on its own heartbeat too). The
229
+ * worker hands the execution the cancel and terminate requests the renewal reads.
230
+ */
231
+ async renew() {
232
+ if (this.detached || this.leaseLost) {
233
+ return;
234
+ }
235
+ await this.lease.renew();
236
+ }
237
+ /** Runs registered compensations in reverse order, each as a journaled, retried step. */
238
+ async compensate(reason) {
239
+ this.mode = 'compensate';
240
+ this.suspension = null;
241
+ let count = 0;
242
+ for (const compensation of [...this.compensations].reverse()) {
243
+ if (!compensation.completed) {
244
+ continue;
245
+ }
246
+ // A running compensation finishes; the next one doesn't start.
247
+ if (this.terminateRequested) {
248
+ return { state: 'terminated' };
249
+ }
250
+ const name = `${COMPENSATE}${compensation.step}`;
251
+ const entry = this.journal.get(name);
252
+ if (entry?.status === 'completed') {
253
+ continue;
254
+ }
255
+ if (entry?.status === 'failed') {
256
+ return { state: 'failed', error: entry.error };
257
+ }
258
+ try {
259
+ const attempt = await this.attempt(name, 'compensation',
260
+ // Whatever the undo returns (a provider's response, say) is discarded: nothing
261
+ // reads it, so it can't fail the undo by being unserializable.
262
+ async (ctx) => {
263
+ await compensation.fn(compensation.result, { ...ctx, reason });
264
+ }, this.plan(name, { retry: compensation.retry }), entry);
265
+ count++;
266
+ this.emit({ type: 'step-compensated', step: compensation.step, attempt: attempt.attempt });
267
+ }
268
+ catch (error) {
269
+ if (error instanceof StepFailedError) {
270
+ return { state: 'failed', error: { ...error.cause, message: `${error.message}` } };
271
+ }
272
+ // A compensation that calls ctx can never run: it needs a person.
273
+ if (this.fatal) {
274
+ return { state: 'failed', error: serializeError(this.fatal) };
275
+ }
276
+ if (isWorkflowInterrupt(error)) {
277
+ return error.reason === 'suspend' ? { state: 'suspended', wakeAt: this.suspension.wakeAt } : { state: 'interrupted' };
278
+ }
279
+ // Invalid options: retrying the same code would fail the same way.
280
+ return { state: 'failed', error: serializeError(error) };
281
+ }
282
+ }
283
+ return { state: 'done', count };
284
+ }
285
+ // ---------------------------------------------------------------------------
286
+ // ctx operations
287
+ async step(name, fn, options = {}) {
288
+ this.assertNotInStep(`step("${name}")`);
289
+ this.visit(name, 'step');
290
+ const entry = this.journal.get(name);
291
+ if (entry?.status === 'completed') {
292
+ this.reserveCompensation(name, options)?.arm(entry.result);
293
+ return entry.result;
294
+ }
295
+ if (entry?.status === 'failed') {
296
+ throw new StepFailedError(name, entry.attempts, entry.error);
297
+ }
298
+ if (this.mode !== 'run') {
299
+ throw this.interrupt('halt');
300
+ }
301
+ this.assertCanStart();
302
+ const plan = this.plan(name, options);
303
+ // Reserved in call order, so parallel steps compensate in the same order
304
+ // on the first run and on a replay, whichever finished first.
305
+ const compensation = this.reserveCompensation(name, options);
306
+ await this.reachFrontier(name);
307
+ this.assertCanStart();
308
+ const { value } = await this.attempt(name, 'step', fn, plan, entry);
309
+ compensation?.arm(value);
310
+ return value;
311
+ }
312
+ async sleep(name, duration) {
313
+ this.assertNotInStep(`sleep("${name}")`);
314
+ this.visit(name, 'sleep');
315
+ const entry = unlessCancelled(this.journal.get(name));
316
+ if (entry?.status === 'completed') {
317
+ return;
318
+ }
319
+ if (this.mode !== 'run') {
320
+ throw this.interrupt('halt');
321
+ }
322
+ this.assertAlive();
323
+ const now = this.deps.clock.now();
324
+ const wakeAt = entry?.wakeAt ?? wakeTime(name, duration, now);
325
+ if (now >= wakeAt) {
326
+ this.stage({ name, kind: 'sleep', status: 'completed', attempts: 0, wakeAt });
327
+ return;
328
+ }
329
+ if (!entry) {
330
+ this.stage({ name, kind: 'sleep', status: 'pending', attempts: 0, wakeAt });
331
+ }
332
+ throw this.suspendUntil(wakeAt);
333
+ }
334
+ /** `waitForSignal()`, and (`internal`) a child handle's `result()`, whose wait is named `$result:<id>`. */
335
+ async receive(name, wait, options, internal) {
336
+ this.assertNotInStep(internal ? 'startChild() handle result()' : `waitForSignal("${name}")`);
337
+ this.visit(name, 'signal', internal);
338
+ const entry = unlessCancelled(this.journal.get(name));
339
+ if (entry?.status === 'completed') {
340
+ return entry.result.payload;
341
+ }
342
+ if (this.mode !== 'run') {
343
+ throw this.interrupt('halt');
344
+ }
345
+ this.assertAlive();
346
+ const deadline = entry
347
+ ? (entry.wakeAt ?? null)
348
+ : options.timeout !== undefined
349
+ ? this.deps.clock.now() + parseDuration(options.timeout)
350
+ : null;
351
+ if (!entry) {
352
+ this.stage({ name, kind: 'signal', status: 'pending', attempts: 0, wakeAt: deadline, data: wait });
353
+ }
354
+ const candidates = await this.signalsFor(wait);
355
+ this.assertAlive();
356
+ const candidate = this.take(candidates, deadline, options.match);
357
+ if (candidate) {
358
+ await this.write([
359
+ {
360
+ name,
361
+ kind: 'signal',
362
+ status: 'completed',
363
+ attempts: 0,
364
+ wakeAt: deadline,
365
+ data: wait,
366
+ result: { signalId: candidate.id, payload: candidate.payload },
367
+ },
368
+ ]);
369
+ if (!internal) {
370
+ this.emit({ type: 'signal-received', wait: name, signal: wait.signal, signalId: candidate.id });
371
+ }
372
+ return candidate.payload;
373
+ }
374
+ if (deadline !== null && this.deps.clock.now() >= deadline) {
375
+ await this.write([
376
+ { name, kind: 'signal', status: 'completed', attempts: 0, wakeAt: deadline, data: wait, result: { signalId: null, payload: null } },
377
+ ]);
378
+ this.emit({ type: 'signal-timed-out', wait: name, signal: wait.signal });
379
+ return null;
380
+ }
381
+ throw this.suspendUntil(deadline, [wait]);
382
+ }
383
+ async startChild(workflow, input, options = {}) {
384
+ this.assertNotInStep('startChild()', 'Start the child from run(), and pass it what the step returned.');
385
+ if (this.fatal) {
386
+ throw this.fatal;
387
+ }
388
+ const parentClose = options.parentClose ?? 'cancel';
389
+ if (!PARENT_CLOSE.includes(parentClose)) {
390
+ throw new TypeError(`Invalid parentClose ${JSON.stringify(parentClose)} for startChild(). Use 'cancel', 'terminate' or 'abandon'.`);
391
+ }
392
+ const resolved = this.deps.resolve(workflow, options.version);
393
+ const n = (this.childCounters.get(resolved.name) ?? 0) + 1;
394
+ this.childCounters.set(resolved.name, n);
395
+ const id = options.id ?? `${this.instance.id}/${resolved.name}#${n}`;
396
+ // The journal first: a replay returns the child it started, whatever the keys of its workflow's limits
397
+ // compute today (a deploy may change them); they are computed only to start it.
398
+ const name = `$child:${id}`;
399
+ this.visit(name, 'child', true);
400
+ const request = { id, workflow: resolved.name, input: createHash('sha256').update(canonical(normalize(input) ?? null)).digest('base64url') };
401
+ const entry = this.journal.get(name);
402
+ if (entry && entry.status !== 'pending') {
403
+ const recorded = entry.data;
404
+ if (recorded.workflow !== request.workflow || recorded.input !== request.input) {
405
+ throw this.setFatal(new WorkflowNonDeterminismError(`${this.describe()} does not match its journal: child "${id}" was started as "${recorded.workflow}"` +
406
+ `${recorded.workflow === request.workflow ? ' with another input' : ''}, but the code now starts "${request.workflow}". ` +
407
+ 'Children started from parallel branches need ids of their own ({ id }). ' +
408
+ ADVICE));
409
+ }
410
+ if (entry.status === 'failed') {
411
+ throw new WorkflowIdConflictError(entry.error.message);
412
+ }
413
+ return this.handle(entry.result);
414
+ }
415
+ if (this.mode !== 'run') {
416
+ throw this.interrupt('halt');
417
+ }
418
+ this.assertCanStart();
419
+ const child = newInstance(resolved, id, input, {
420
+ caller: 'startChild()',
421
+ now: this.deps.clock.now(),
422
+ timeout: options.timeout,
423
+ concurrencyKey: options.concurrencyKey,
424
+ rateLimitKey: options.rateLimitKey,
425
+ priority: options.priority ?? this.instance.priority,
426
+ parentId: this.instance.id,
427
+ parentClose,
428
+ });
429
+ await this.reachFrontier(name);
430
+ this.assertCanStart();
431
+ // Recorded before the child exists, so a parent that crashes right after creating it still
432
+ // knows it has children to close when it ends.
433
+ if (!entry) {
434
+ await this.write([{ name, kind: 'child', status: 'pending', attempts: 0, data: request }]);
435
+ }
436
+ let created;
437
+ try {
438
+ created = await this.deps.createChild(child);
439
+ }
440
+ catch (error) {
441
+ if (!(error instanceof WorkflowIdConflictError)) {
442
+ this.storeError = error;
443
+ this.abort.abort(new WorkflowInterrupt('store-error'));
444
+ throw this.interrupt('store-error');
445
+ }
446
+ await this.write([{ name, kind: 'child', status: 'failed', attempts: 1, data: request, error: serializeError(error) }]);
447
+ throw error;
448
+ }
449
+ const started = { id: child.id, workflow: child.workflow, version: created.version };
450
+ await this.write([{ name, kind: 'child', status: 'completed', attempts: 1, data: request, result: started }]);
451
+ this.emit({ type: 'child-started', child: started.id, childWorkflow: started.workflow, childVersion: started.version });
452
+ return this.handle(started);
453
+ }
454
+ handle(started) {
455
+ return new ChildHandle(started.id, started.workflow, started.version, (handle) => this.track(this.receive(`$result:${handle.id}`, handle.condition.wait, {}, true).then((payload) => handle.settle(payload))));
456
+ }
457
+ /** Whether this instance started children, whose `parentClose` applies when it ends. */
458
+ hasChildren() {
459
+ return [...this.journal.values()].some((entry) => entry.kind === 'child');
460
+ }
461
+ async waitForAny(name, conditions) {
462
+ this.assertNotInStep(`waitForAny("${name}")`);
463
+ const branches = conditionsOf(name, conditions);
464
+ this.visit(name, 'any');
465
+ const entry = unlessCancelled(this.journal.get(name));
466
+ if (entry?.status === 'completed') {
467
+ const won = entry.result;
468
+ const branch = branches.get(won.key);
469
+ const type = won.signalId === null ? 'timer' : 'signal';
470
+ if (!branch || (branch.wait === null) !== (type === 'timer')) {
471
+ throw this.setFatal(new WorkflowNonDeterminismError(`${this.describe()} does not match its journal: waitForAny("${name}") was won by its ${type} "${won.key}", which the code no ` +
472
+ `longer has. ${ADVICE}`));
473
+ }
474
+ return { key: won.key, value: branch.settle(won.payload) };
475
+ }
476
+ if (this.mode !== 'run') {
477
+ throw this.interrupt('halt');
478
+ }
479
+ this.assertAlive();
480
+ // Timers count from when the wait was first reached: a replay keeps their deadlines.
481
+ const now = this.deps.clock.now();
482
+ const reached = entry?.data;
483
+ const data = { waits: {}, timers: {} };
484
+ for (const [key, branch] of branches) {
485
+ if (branch.wait) {
486
+ data.waits[key] = branch.wait;
487
+ }
488
+ else {
489
+ data.timers[key] = reached?.timers[key] ?? wakeTime(`${name}: ${key}`, branch.timer, now);
490
+ }
491
+ }
492
+ const deadlines = Object.values(data.timers);
493
+ const deadline = deadlines.length > 0 ? Math.min(...deadlines) : null;
494
+ const pending = { name, kind: 'any', status: 'pending', attempts: 0, wakeAt: deadline, data };
495
+ if (!entry) {
496
+ this.stage(pending);
497
+ }
498
+ // Read every signal condition's candidates first, then pick without awaiting in between, so
499
+ // a parallel wait can't take the same signal.
500
+ const reads = [...branches].filter(([, branch]) => branch.wait !== null);
501
+ const candidates = await Promise.all(reads.map(([, branch]) => this.signalsFor(branch.wait)));
502
+ this.assertAlive();
503
+ let winner;
504
+ for (const [i, [key, branch]] of reads.entries()) {
505
+ const signal = this.peek(candidates[i], deadline, branch.match);
506
+ if (signal && (!winner || signal.id < winner.signal.id)) {
507
+ winner = { key, branch, signal };
508
+ }
509
+ }
510
+ if (winner) {
511
+ this.consumed.add(winner.signal.id);
512
+ const outcome = { key: winner.key, signalId: winner.signal.id, payload: winner.signal.payload };
513
+ await this.write([{ ...pending, status: 'completed', result: outcome }]);
514
+ if (!winner.branch.wait.signal.startsWith('$')) {
515
+ this.emit({ type: 'signal-received', wait: name, signal: winner.branch.wait.signal, signalId: winner.signal.id });
516
+ }
517
+ return { key: winner.key, value: winner.branch.settle(winner.signal.payload) };
518
+ }
519
+ if (deadline !== null && this.deps.clock.now() >= deadline) {
520
+ const key = Object.keys(data.timers).find((timer) => data.timers[timer] === deadline);
521
+ await this.write([{ ...pending, status: 'completed', result: { key, signalId: null, payload: null } }]);
522
+ return { key, value: null };
523
+ }
524
+ throw this.suspendUntil(deadline, Object.values(data.waits));
525
+ }
526
+ /** Signals sent since the instance started, up to this execution's cursor: one that arrived before the wait was reached still counts. */
527
+ signalsFor(wait) {
528
+ return this.deps.store.signals({ name: wait.signal, key: wait.key, afterId: this.instance.signalCursor, upToId: this.signalCursor });
529
+ }
530
+ /** The first of `candidates` a wait can take: not taken by another wait, sent by `deadline`, and matching. */
531
+ peek(candidates, deadline, match) {
532
+ return candidates.find((candidate) => !this.consumed.has(candidate.id) && (deadline === null || candidate.createdAt <= deadline) && (!match || match(candidate.payload)));
533
+ }
534
+ /** `peek()`, and marks the signal taken. Call it with no `await` between the read and the take. */
535
+ take(candidates, deadline, match) {
536
+ const candidate = this.peek(candidates, deadline, match);
537
+ if (candidate) {
538
+ this.consumed.add(candidate.id);
539
+ }
540
+ return candidate;
541
+ }
542
+ commit(name) {
543
+ this.assertNotInStep(`commit("${name}")`);
544
+ this.visit(name, 'commit');
545
+ const entry = this.journal.get(name);
546
+ if (entry?.status !== 'completed') {
547
+ // Not reached by the run being compensated: its compensations stand.
548
+ if (this.mode !== 'run') {
549
+ throw this.interrupt('halt');
550
+ }
551
+ // Not after a suspension either: code that swallowed the interrupt of a
552
+ // wait must not pass the point of no return on the wait's behalf.
553
+ this.assertCanStart();
554
+ // Written with the next journal write, which always happens before the
555
+ // next side effect, a suspension, or the switch to compensating.
556
+ this.stage({ name, kind: 'commit', status: 'completed', attempts: 0 });
557
+ }
558
+ this.compensations.splice(0);
559
+ }
560
+ setStatus(status) {
561
+ this.assertNotInStep('setStatus()', 'Set it from run(), before or after the step.');
562
+ if (this.fatal) {
563
+ throw this.fatal;
564
+ }
565
+ let json;
566
+ try {
567
+ json = JSON.stringify(status);
568
+ }
569
+ catch (error) {
570
+ throw new TypeError(`ctx.setStatus() takes a JSON-serializable value: ${error.message}`);
571
+ }
572
+ const bytes = Buffer.byteLength(json ?? 'null', 'utf8');
573
+ if (bytes > MAX_CUSTOM_STATUS_BYTES) {
574
+ throw new TypeError(`ctx.setStatus() got ${bytes} bytes of JSON, over the ${MAX_CUSTOM_STATUS_BYTES}-byte limit. Keep the status a summary; ` +
575
+ 'return large data from the workflow, or keep it in your own tables.');
576
+ }
577
+ this.customStatus = json === undefined ? null : JSON.parse(json);
578
+ }
579
+ /** `{ customStatus }` when `ctx.setStatus()` changed it since it was last written, else `{}`: spread into a write. */
580
+ statusChange() {
581
+ return canonical(this.customStatus) === this.storedStatus ? {} : { customStatus: this.customStatus };
582
+ }
583
+ /** After a write that carried `change` landed. */
584
+ statusWritten(change) {
585
+ if (!('customStatus' in change)) {
586
+ return;
587
+ }
588
+ this.storedStatus = canonical(change.customStatus);
589
+ this.emit({ type: 'custom-status', status: change.customStatus });
590
+ }
591
+ /**
592
+ * Entries still pending when the instance ends without completing: a sleep
593
+ * or wait that will never resolve, a retry that will never run.
594
+ */
595
+ abandoned() {
596
+ return [...this.journal.values()]
597
+ .filter((entry) => entry.status === 'pending' && entry.kind !== 'compensation')
598
+ .map((entry) => ({ ...entry, status: 'cancelled', wakeAt: null }));
599
+ }
600
+ helper(kind, produce) {
601
+ this.assertNotInStep(`${kind}()`, 'Inside a step, read the real value directly: the step result is journaled.');
602
+ const name = `$${kind}:${++this.counters[kind]}`;
603
+ this.visit(name, kind);
604
+ const entry = this.journal.get(name);
605
+ if (entry?.status === 'completed') {
606
+ return entry.result;
607
+ }
608
+ if (this.mode !== 'run') {
609
+ throw this.interrupt('halt');
610
+ }
611
+ this.assertCanStart();
612
+ const value = produce();
613
+ // Written with the next journal write, which always happens before the
614
+ // next side effect: a value can never influence an effect without being
615
+ // persisted first.
616
+ this.stage({ name, kind, status: 'completed', attempts: 0, result: value });
617
+ return value;
618
+ }
619
+ // ---------------------------------------------------------------------------
620
+ // attempts
621
+ /**
622
+ * Resolves a step's (or compensation's) retry and timeouts. Throws a
623
+ * `TypeError` for invalid options before an attempt is recorded, instead of
624
+ * spending the step's attempts on it.
625
+ */
626
+ plan(name, options) {
627
+ try {
628
+ return {
629
+ policy: resolveRetry(options.retry, this.deps.defaultRetry),
630
+ timeoutMs: options.timeout === undefined ? undefined : parseDuration(options.timeout),
631
+ heartbeatTimeoutMs: options.heartbeatTimeout === undefined ? undefined : parseDuration(options.heartbeatTimeout),
632
+ };
633
+ }
634
+ catch (error) {
635
+ throw new TypeError(`Invalid options for "${name}": ${error.message}`);
636
+ }
637
+ }
638
+ async attempt(name, kind, fn, plan, entry) {
639
+ const { policy } = plan;
640
+ const previous = entry?.status === 'pending' ? entry.attempts : 0;
641
+ if (entry?.status === 'pending' && previous > 0) {
642
+ if (entry.wakeAt != null && this.deps.clock.now() < entry.wakeAt) {
643
+ throw this.suspendUntil(entry.wakeAt);
644
+ }
645
+ if (entry.wakeAt == null && previous >= policy.attempts) {
646
+ throw await this.giveUp(entry, previous, {
647
+ name: 'StepInterruptedError',
648
+ message: `Attempt ${previous} did not finish: the worker stopped, lost its lease, or could not write the ` +
649
+ 'result to the store (the worker logged it).',
650
+ });
651
+ }
652
+ }
653
+ const attempt = previous + 1;
654
+ // Recorded before the side effect, so a step that crashes the process
655
+ // still uses up its attempts instead of crash-looping forever.
656
+ let current = {
657
+ name,
658
+ kind,
659
+ status: 'pending',
660
+ attempts: attempt,
661
+ wakeAt: null,
662
+ progress: entry?.progress,
663
+ error: entry?.error,
664
+ };
665
+ await this.write([current]);
666
+ const startedAt = performance.now();
667
+ let value;
668
+ try {
669
+ const raw = await this.invoke(name, fn, plan, attempt, current, (next) => (current = next));
670
+ try {
671
+ value = normalize(raw);
672
+ }
673
+ catch (error) {
674
+ throw new NonRetryableStepError(`Result of "${name}" is not JSON-serializable: ${error.message}`);
675
+ }
676
+ }
677
+ catch (error) {
678
+ if (this.abort.signal.aborted) {
679
+ // Shutdown, lost lease or store failure: not the step's fault, and
680
+ // nothing about this attempt is recorded. A shutdown gives the attempt back.
681
+ if (this.shuttingDown && !this.leaseLost && !this.storeError) {
682
+ this.rollbacks.push({ ...current, attempts: attempt - 1 });
683
+ }
684
+ throw this.abortInterrupt();
685
+ }
686
+ // A definition error raised inside the step (a nested ctx call) fails
687
+ // the instance; retrying the same code would hit it again.
688
+ if (this.fatal) {
689
+ throw this.fatal;
690
+ }
691
+ if (isWorkflowInterrupt(error)) {
692
+ throw error;
693
+ }
694
+ let serialized = serializeError(error);
695
+ let retryAt = null;
696
+ if (!(error instanceof NonRetryableStepError)) {
697
+ const next = nextRetry(policy, attempt, error);
698
+ if (next.retry) {
699
+ retryAt = this.deps.clock.now() + next.delay;
700
+ }
701
+ else if (next.reason === 'threw') {
702
+ // A throwing retryIf or backoff gives up, journaled like any failure,
703
+ // so a replay sees the same StepFailedError instead of re-running the step.
704
+ const thrown = serializeError(next.error);
705
+ serialized = {
706
+ ...thrown,
707
+ message: `The retry options of "${name}" threw ${thrown.name}: ${thrown.message} (handling ${serialized.name}: ${serialized.message})`,
708
+ };
709
+ }
710
+ }
711
+ if (retryAt === null) {
712
+ throw await this.giveUp(current, attempt, serialized);
713
+ }
714
+ await this.write([{ ...current, error: serialized, wakeAt: retryAt }]);
715
+ this.emit({ type: 'step-failed', step: name, attempt, error: serialized, retryAt });
716
+ throw this.suspendUntil(retryAt);
717
+ }
718
+ await this.write([{ ...current, status: 'completed', result: value, error: undefined, progress: undefined }]);
719
+ if (kind === 'step') {
720
+ this.emit({ type: 'step-completed', step: name, attempt, durationMs: Math.round(performance.now() - startedAt) });
721
+ }
722
+ return { value, attempt };
723
+ }
724
+ invoke(name, fn, { timeoutMs: overallMs, heartbeatTimeoutMs: idleMs }, attempt, entry, update) {
725
+ const controller = new AbortController();
726
+ const forward = () => controller.abort(this.abort.signal.reason);
727
+ if (this.abort.signal.aborted) {
728
+ forward();
729
+ }
730
+ else {
731
+ this.abort.signal.addEventListener('abort', forward, { once: true });
732
+ }
733
+ let expire;
734
+ const watchdog = new Promise((_, reject) => {
735
+ expire = (error) => {
736
+ controller.abort(error);
737
+ reject(error);
738
+ };
739
+ });
740
+ const overall = overallMs === undefined
741
+ ? undefined
742
+ : setTimeout(() => expire(new StepTimeoutError(`Step "${name}" timed out after ${overallMs}ms.`)), overallMs).unref();
743
+ let idle;
744
+ const armIdle = () => {
745
+ if (idleMs === undefined) {
746
+ return;
747
+ }
748
+ clearTimeout(idle);
749
+ idle = setTimeout(() => expire(new StepTimeoutError(`Step "${name}" sent no heartbeat for ${idleMs}ms.`)), idleMs).unref();
750
+ };
751
+ armIdle();
752
+ let current = entry;
753
+ let settled = false;
754
+ const ctx = {
755
+ idempotencyKey: `${this.instance.id}:${name}`,
756
+ attempt,
757
+ signal: controller.signal,
758
+ progress: entry.progress,
759
+ heartbeat: async (progress) => {
760
+ // After the attempt ended (a timeout, or a heartbeat left running),
761
+ // nothing may touch its journal entry any more.
762
+ if (settled) {
763
+ controller.signal.throwIfAborted();
764
+ return;
765
+ }
766
+ armIdle();
767
+ if (progress === undefined) {
768
+ await this.renew();
769
+ if (this.leaseLost) {
770
+ throw this.interrupt('lease-lost');
771
+ }
772
+ return;
773
+ }
774
+ current = { ...current, progress: normalize(progress) };
775
+ update(current);
776
+ await this.write([current]);
777
+ },
778
+ };
779
+ const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () => this.insideStep.run(name, () => fn(ctx))));
780
+ return Promise.race([run, watchdog]).finally(() => {
781
+ settled = true;
782
+ clearTimeout(overall);
783
+ clearTimeout(idle);
784
+ this.abort.signal.removeEventListener('abort', forward);
785
+ });
786
+ }
787
+ async giveUp(entry, attempts, error) {
788
+ await this.write([{ ...entry, status: 'failed', attempts, error, wakeAt: null, progress: undefined }]);
789
+ if (entry.kind === 'step') {
790
+ this.emit({ type: 'step-failed', step: entry.name, attempt: attempts, error, retryAt: null });
791
+ }
792
+ return new StepFailedError(entry.name, attempts, error);
793
+ }
794
+ // ---------------------------------------------------------------------------
795
+ // bookkeeping
796
+ track(promise) {
797
+ const settled = promise.then(() => undefined, () => undefined);
798
+ this.inflight.add(settled);
799
+ void settled.then(() => this.inflight.delete(settled));
800
+ return promise;
801
+ }
802
+ /**
803
+ * A step's function runs once; its result is all a replay sees. A `ctx` call
804
+ * made inside it would be journaled on the first run and missing on every
805
+ * replay, so it is a definition error, reported where it happens.
806
+ */
807
+ assertNotInStep(call, advice = 'Call ctx methods from run() and pass the values the step needs into it.') {
808
+ const step = this.insideStep.getStore();
809
+ if (step === undefined) {
810
+ return;
811
+ }
812
+ const where = step.startsWith(COMPENSATE) ? `the compensation of step "${step.slice(COMPENSATE.length)}"` : `step "${step}"`;
813
+ throw this.setFatal(new WorkflowDefinitionError(`${this.describe()}: ${where} called ctx.${call}. A step's function runs once and replays return its ` +
814
+ `result, so it can't use ctx. ${advice}`));
815
+ }
816
+ visit(name, kind, internal = false) {
817
+ if (this.fatal) {
818
+ throw this.fatal;
819
+ }
820
+ const helper = kind === 'now' || kind === 'random' || kind === 'uuid';
821
+ if (typeof name !== 'string' || name.length === 0 || (!helper && !internal && name.startsWith('$'))) {
822
+ throw this.setFatal(new WorkflowDefinitionError(`Invalid ${kind} name "${name}". Names cannot be empty or start with "$".`));
823
+ }
824
+ if (this.visited.has(name)) {
825
+ throw this.setFatal(new WorkflowDefinitionError(`"${name}" is used twice in one run of workflow "${this.instance.workflow}@${this.instance.version}". ` +
826
+ 'Step, sleep and wait names must be unique per run; inside a loop, add the index (`remind-${i}`).'));
827
+ }
828
+ this.visited.add(name);
829
+ const entry = this.journal.get(name);
830
+ if (!entry && this.mode === 'run') {
831
+ this.assertJournalRoom(name);
832
+ }
833
+ if (entry && entry.kind !== kind) {
834
+ throw this.setFatal(new WorkflowNonDeterminismError(`${this.describe()} does not match its journal: "${name}" was recorded as a ${entry.kind}, but the code now calls it as a ${kind}. ${ADVICE}`));
835
+ }
836
+ }
837
+ /**
838
+ * Before the first new step runs, let every replayable call happen (replayed
839
+ * results resolve in microtasks, so one macrotask is enough), then require
840
+ * that the whole journal was reached.
841
+ */
842
+ reachFrontier(step) {
843
+ this.frontier ??= (async () => {
844
+ await nextMacrotask();
845
+ const missing = this.unvisited();
846
+ if (missing.length) {
847
+ throw this.setFatal(new WorkflowNonDeterminismError(`${this.describe()} does not match its journal: ${missing.map((n) => `"${n}"`).join(', ')} ` +
848
+ `${missing.length === 1 ? 'was' : 'were'} recorded by an earlier run but not reached before the new step "${step}". ${ADVICE}`));
849
+ }
850
+ })();
851
+ return this.frontier;
852
+ }
853
+ /** Reserves the step's compensation in call order; `arm()` it with the result once the step completed. */
854
+ reserveCompensation(step, options) {
855
+ if (!options.compensate) {
856
+ return undefined;
857
+ }
858
+ const compensation = {
859
+ step,
860
+ completed: false,
861
+ fn: options.compensate,
862
+ retry: options.compensateRetry ?? options.retry,
863
+ };
864
+ this.compensations.push(compensation);
865
+ return {
866
+ arm: (result) => {
867
+ compensation.result = result;
868
+ compensation.completed = true;
869
+ },
870
+ };
871
+ }
872
+ assertAlive() {
873
+ if (this.fatal) {
874
+ throw this.fatal;
875
+ }
876
+ if (this.leaseLost) {
877
+ throw this.interrupt('lease-lost');
878
+ }
879
+ if (this.storeError) {
880
+ throw this.interrupt('store-error');
881
+ }
882
+ if (this.shuttingDown) {
883
+ throw this.interrupt('shutdown');
884
+ }
885
+ if (this.cancelRequested) {
886
+ throw this.interrupt('cancel');
887
+ }
888
+ // Only new work stops: a replay to compensate never gets here.
889
+ if (this.pastDeadline()) {
890
+ throw this.interrupt('timeout');
891
+ }
892
+ }
893
+ /** Whether the instance's run timeout has passed. */
894
+ pastDeadline() {
895
+ return this.instance.deadline !== null && this.deps.clock.now() >= this.instance.deadline;
896
+ }
897
+ /** No new step starts once the execution is suspending or `run()` has settled. */
898
+ assertCanStart() {
899
+ this.assertAlive();
900
+ if (this.suspension) {
901
+ throw this.interrupt('suspend');
902
+ }
903
+ if (this.userSettled) {
904
+ throw this.interrupt('halt');
905
+ }
906
+ }
907
+ suspendUntil(wakeAt, waits = []) {
908
+ this.suspension ??= { wakeAt: null, waits: [] };
909
+ if (wakeAt !== null) {
910
+ this.suspension.wakeAt = this.suspension.wakeAt === null ? wakeAt : Math.min(this.suspension.wakeAt, wakeAt);
911
+ }
912
+ this.suspension.waits.push(...waits);
913
+ return this.interrupt('suspend');
914
+ }
915
+ stage(entry) {
916
+ this.record(entry);
917
+ this.buffer.push(entry);
918
+ }
919
+ /** Keeps `entry` as the journal's entry of its name, and tracks the journal's size. */
920
+ record(entry) {
921
+ this.journal.set(entry.name, entry);
922
+ this.measure(entry);
923
+ if (this.journalLarge || !this.pastWarning()) {
924
+ return;
925
+ }
926
+ this.journalLarge = true;
927
+ const { warnEntries, warnBytes } = this.deps.journalLimits;
928
+ WorkflowExecution.logger.warn(`${this.describe()} has ${this.journal.size} journal entries (${this.journalBytes} bytes), past the warning line of ` +
929
+ `${warnEntries} entries or ${warnBytes} bytes: every execution loads and replays all of it. Bound the loop that grows it, ` +
930
+ 'or continue in a new instance (see https://docs.nestjs.com/reliability/workflows#keep-journals-short).');
931
+ this.emit({ type: 'journal-large', entries: this.journal.size, bytes: this.journalBytes });
932
+ }
933
+ measure(entry) {
934
+ const bytes = entryBytes(entry);
935
+ this.journalBytes += bytes - (this.entrySizes.get(entry.name) ?? 0);
936
+ this.entrySizes.set(entry.name, bytes);
937
+ }
938
+ pastWarning() {
939
+ const { warnEntries, warnBytes } = this.deps.journalLimits;
940
+ return this.journal.size >= warnEntries || this.journalBytes >= warnBytes;
941
+ }
942
+ /**
943
+ * Before a new name is journaled in run mode: a journal at its limit stops the run, which then
944
+ * compensates (compensations may still record their entries) and fails.
945
+ */
946
+ assertJournalRoom(name) {
947
+ const { maxEntries, maxBytes } = this.deps.journalLimits;
948
+ if (this.journal.size < maxEntries && this.journalBytes < maxBytes) {
949
+ return;
950
+ }
951
+ this.journalLimitError ??= {
952
+ name: 'WorkflowJournalLimitError',
953
+ message: `${this.describe()} reached its journal limit before "${name}": ${this.journal.size} entries, ${this.journalBytes} bytes ` +
954
+ `(journal.maxEntries ${maxEntries}, journal.maxBytes ${maxBytes}). Continue a long loop in a new instance instead ` +
955
+ '(see https://docs.nestjs.com/reliability/workflows#keep-journals-short).',
956
+ };
957
+ throw this.interrupt('journal-limit');
958
+ }
959
+ /** Fenced write of staged entries plus `entries`. */
960
+ async write(entries) {
961
+ if (this.detached) {
962
+ throw this.interrupt('shutdown');
963
+ }
964
+ for (const entry of entries) {
965
+ this.record(entry);
966
+ }
967
+ const now = this.deps.clock.now();
968
+ const batch = [...this.buffer.splice(0), ...entries].map((entry) => ({ ...entry, updatedAt: now }));
969
+ // Writes reach the store one at a time, in the order they were issued. On a store with
970
+ // a connection pool, two writes in flight can commit in either order, and the later one
971
+ // wins: a checkpoint the step didn't await, landing after the step's completion, would
972
+ // turn the completed step back into a pending one.
973
+ const previous = this.writes;
974
+ let done;
975
+ this.writes = new Promise((resolve) => (done = resolve));
976
+ try {
977
+ await previous;
978
+ if (this.detached) {
979
+ throw this.interrupt('shutdown');
980
+ }
981
+ let ok;
982
+ const change = this.statusChange();
983
+ try {
984
+ ok = await this.deps.store.write(this.instance.id, this.instance.leaseToken, { now, entries: uniqueEntries(batch), ...change });
985
+ }
986
+ catch (error) {
987
+ this.storeError = error;
988
+ this.abort.abort(new WorkflowInterrupt('store-error'));
989
+ throw this.interrupt('store-error');
990
+ }
991
+ if (!ok) {
992
+ this.loseLease();
993
+ throw this.interrupt('lease-lost');
994
+ }
995
+ this.statusWritten(change);
996
+ }
997
+ finally {
998
+ done();
999
+ }
1000
+ }
1001
+ loseLease() {
1002
+ if (this.leaseLost) {
1003
+ return;
1004
+ }
1005
+ this.leaseLost = true;
1006
+ this.abort.abort(new WorkflowInterrupt('lease-lost'));
1007
+ this.lease.loseLease();
1008
+ }
1009
+ abortInterrupt() {
1010
+ return this.interrupt(this.leaseLost ? 'lease-lost' : this.storeError ? 'store-error' : 'shutdown');
1011
+ }
1012
+ /** Every interrupt `ctx` throws goes through here; the first one that stops the run is kept. */
1013
+ interrupt(reason) {
1014
+ const interrupt = new WorkflowInterrupt(reason);
1015
+ if (reason !== 'suspend' && reason !== 'halt') {
1016
+ this.stoppedBy ??= interrupt;
1017
+ }
1018
+ return interrupt;
1019
+ }
1020
+ setFatal(error) {
1021
+ this.fatal ??= error;
1022
+ return this.fatal;
1023
+ }
1024
+ describe() {
1025
+ return `Instance "${this.instance.id}" of workflow "${this.instance.workflow}@${this.instance.version}"`;
1026
+ }
1027
+ emit(body) {
1028
+ this.deps.events.emit({
1029
+ id: this.instance.id,
1030
+ workflow: this.instance.workflow,
1031
+ version: this.instance.version,
1032
+ at: this.deps.clock.now(),
1033
+ ...body,
1034
+ });
1035
+ }
1036
+ }
1037
+ WorkflowExecution.logger = new Logger('Workflows');
1038
+ /**
1039
+ * A sleep or wait the instance abandoned when it ended (`cancelled`) starts over when an
1040
+ * operator retries the instance: a new deadline from now, as when it was first reached.
1041
+ */
1042
+ function unlessCancelled(entry) {
1043
+ return entry?.status === 'cancelled' ? undefined : entry;
1044
+ }
1045
+ function wakeTime(name, when, now) {
1046
+ if (typeof when !== 'object') {
1047
+ return now + parseDuration(when);
1048
+ }
1049
+ const until = when.until instanceof Date ? when.until.getTime() : when.until;
1050
+ if (typeof until !== 'number' || !Number.isFinite(until)) {
1051
+ throw new TypeError(`Invalid deadline for sleep "${name}": ${String(when.until)}. Pass a valid Date or a timestamp in milliseconds.`);
1052
+ }
1053
+ return until;
1054
+ }
1055
+ /** `waitForAny()`'s conditions, checked before anything is journaled. */
1056
+ function conditionsOf(name, conditions) {
1057
+ const entries = conditions !== null && typeof conditions === 'object' ? Object.entries(conditions) : [];
1058
+ if (entries.length === 0) {
1059
+ throw new TypeError(`waitForAny("${name}") takes an object with at least one condition, such as { delivered: ctx.signalWait(shipmentDelivered) }.`);
1060
+ }
1061
+ const branches = new Map();
1062
+ for (const [key, condition] of entries) {
1063
+ const branch = condition instanceof ChildHandle ? condition.condition : condition;
1064
+ if (!(branch instanceof Condition)) {
1065
+ throw new TypeError(`waitForAny("${name}"): "${key}" is not a condition. Make each one with ctx.signalWait() or ctx.timer(), or pass a child's handle.`);
1066
+ }
1067
+ branches.set(key, branch);
1068
+ }
1069
+ return branches;
1070
+ }
1071
+ const COMPENSATE = '$compensate:';
1072
+ const PARENT_CLOSE = ['cancel', 'terminate', 'abandon'];
1073
+ /** The most a custom status (`ctx.setStatus()`) may take as JSON. */
1074
+ const MAX_CUSTOM_STATUS_BYTES = 16_384;
1075
+ const ADVICE = 'A deployed change renamed, removed or reordered steps. Ship such changes as a new version ' +
1076
+ '(@Workflow(name, { version })) and keep the old class registered until its instances finish.';