@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,75 @@
1
+ import type { WorkflowInstance, WorkflowStatus } from '../interfaces/workflow-instance.interface.js';
2
+ import type { NewWorkflowInstance, NewWorkflowSignal, WorkflowCancelRequest, WorkflowClaim, WorkflowClaimRequest, WorkflowInstanceDetails, WorkflowListQuery, WorkflowPurgeQuery, WorkflowPurgeResult, WorkflowReopen, WorkflowScheduleClaimRequest, WorkflowScheduleQuery, WorkflowScheduleRecord, WorkflowScheduleSave, WorkflowScheduleWrite, WorkflowSignalQuery, WorkflowSignalRecord, WorkflowSignalResult, WorkflowStore, WorkflowWrite } from '../interfaces/workflow-store.interface.js';
3
+ /**
4
+ * The default store, used when no source is registered: everything lives in this process's
5
+ * memory, so it is lost on restart and not shared with other instances of the application.
6
+ * Also the store for tests: `overrideProvider(YourStore).useValue(new InMemoryWorkflowStore())`
7
+ * (a plain instance doesn't register, so the default applies), or register one instance with
8
+ * `WorkflowStorage.registerSource(store, { replace: true })` in several applications of one
9
+ * test to share its "database" between them, as a restart would.
10
+ *
11
+ * Every method does its work synchronously before its first `await`, so calls never
12
+ * interleave: claims, fenced writes and signals are atomic without locks. Values are copied
13
+ * on the way in and out, as a database would.
14
+ *
15
+ * It can't join your database's transactions: `createInTransaction()` and
16
+ * `signalInTransaction()` (`start()` and `signal()` with `{ transaction }`) write at once, so
17
+ * a rollback doesn't undo them, and the first call logs a warning (once per store).
18
+ */
19
+ export declare class InMemoryWorkflowStore implements WorkflowStore {
20
+ private static readonly logger;
21
+ private readonly rows;
22
+ private readonly signalLog;
23
+ /** Rate-limit windows by `JSON.stringify([workflow])` or `JSON.stringify([workflow, key])`. */
24
+ private readonly windows;
25
+ /** The schedules: the core's in-memory store, in workflows' words. */
26
+ private readonly scheduleStore;
27
+ private readonly scheduleMethods;
28
+ private warnedAboutTransactions;
29
+ /** `create()`, at once: it can't join `transaction` (see the class). */
30
+ createInTransaction(transaction: unknown, i: NewWorkflowInstance): Promise<{
31
+ instance: WorkflowInstance;
32
+ created: boolean;
33
+ }>;
34
+ /** `signal()`, at once: it can't join `transaction` (see the class). */
35
+ signalInTransaction(transaction: unknown, s: NewWorkflowSignal): Promise<WorkflowSignalResult>;
36
+ private cannotJoin;
37
+ create(i: NewWorkflowInstance): Promise<{
38
+ instance: WorkflowInstance;
39
+ created: boolean;
40
+ }>;
41
+ get(id: string, options?: {
42
+ journal?: boolean;
43
+ }): Promise<WorkflowInstanceDetails | null>;
44
+ list(query: WorkflowListQuery): Promise<WorkflowInstance[]>;
45
+ requestCancel(id: string, { reason, now, terminate }: WorkflowCancelRequest): Promise<boolean>;
46
+ reopen(id: string, r: WorkflowReopen): Promise<boolean>;
47
+ delete(id: string, statuses: WorkflowStatus[]): Promise<boolean>;
48
+ signal(s: NewWorkflowSignal): Promise<WorkflowSignalResult>;
49
+ private record;
50
+ signals(query: WorkflowSignalQuery): Promise<WorkflowSignalRecord[]>;
51
+ purge(query: WorkflowPurgeQuery): Promise<WorkflowPurgeResult>;
52
+ saveSchedule(save: WorkflowScheduleSave): Promise<WorkflowScheduleRecord | null>;
53
+ getSchedule(id: string): Promise<WorkflowScheduleRecord | null>;
54
+ listSchedules(query: WorkflowScheduleQuery): Promise<WorkflowScheduleRecord[]>;
55
+ deleteSchedule(id: string, revision?: number): Promise<boolean>;
56
+ claimSchedules(request: WorkflowScheduleClaimRequest): Promise<WorkflowScheduleRecord[]>;
57
+ writeSchedule(id: string, token: string, write: WorkflowScheduleWrite): Promise<boolean>;
58
+ claim(request: WorkflowClaimRequest): Promise<WorkflowClaim>;
59
+ /**
60
+ * The staged pick of `WorkflowStore.claim()` under limits: candidates with no room at all are passed over first;
61
+ * then each concurrency key keeps its first candidates, as many as it has free slots; of those, each rate-limit
62
+ * key as many as its window has room for; of those, each workflow as many as both its free slots and its window
63
+ * allow; then the first `limit`.
64
+ */
65
+ private pick;
66
+ /** Records the claims of `claimed` in their rate-limit windows, starting a window where the last one ended. */
67
+ private recordWindows;
68
+ renew(id: string, token: string, leaseUntil: number): Promise<{
69
+ cancelRequested: boolean;
70
+ terminateRequested: boolean;
71
+ } | null>;
72
+ write(id: string, token: string, w: WorkflowWrite): Promise<boolean>;
73
+ private leased;
74
+ private lastSignalId;
75
+ }
@@ -0,0 +1,380 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import { rateWindowRoom, takeRateWindow } from '../core/limits/limits.js';
3
+ import { InMemoryScheduleStore } from '../core/scheduling/in-memory-schedule.store.js';
4
+ import { workflowScheduleMethods } from '../utils/schedule-store.util.js';
5
+ const RUNNABLE = new Set(['pending', 'running', 'suspended', 'compensating']);
6
+ const FINISHED = new Set(['completed', 'failed', 'cancelled', 'compensation_failed']);
7
+ const CANCELLABLE = new Set(['pending', 'running', 'suspended']);
8
+ /**
9
+ * The default store, used when no source is registered: everything lives in this process's
10
+ * memory, so it is lost on restart and not shared with other instances of the application.
11
+ * Also the store for tests: `overrideProvider(YourStore).useValue(new InMemoryWorkflowStore())`
12
+ * (a plain instance doesn't register, so the default applies), or register one instance with
13
+ * `WorkflowStorage.registerSource(store, { replace: true })` in several applications of one
14
+ * test to share its "database" between them, as a restart would.
15
+ *
16
+ * Every method does its work synchronously before its first `await`, so calls never
17
+ * interleave: claims, fenced writes and signals are atomic without locks. Values are copied
18
+ * on the way in and out, as a database would.
19
+ *
20
+ * It can't join your database's transactions: `createInTransaction()` and
21
+ * `signalInTransaction()` (`start()` and `signal()` with `{ transaction }`) write at once, so
22
+ * a rollback doesn't undo them, and the first call logs a warning (once per store).
23
+ */
24
+ export class InMemoryWorkflowStore {
25
+ constructor() {
26
+ this.rows = new Map();
27
+ this.signalLog = [];
28
+ /** Rate-limit windows by `JSON.stringify([workflow])` or `JSON.stringify([workflow, key])`. */
29
+ this.windows = new Map();
30
+ /** The schedules: the core's in-memory store, in workflows' words. */
31
+ this.scheduleStore = new InMemoryScheduleStore();
32
+ this.scheduleMethods = workflowScheduleMethods(this.scheduleStore);
33
+ this.warnedAboutTransactions = false;
34
+ }
35
+ /** `create()`, at once: it can't join `transaction` (see the class). */
36
+ async createInTransaction(transaction, i) {
37
+ this.cannotJoin('createInTransaction');
38
+ return this.create(i);
39
+ }
40
+ /** `signal()`, at once: it can't join `transaction` (see the class). */
41
+ async signalInTransaction(transaction, s) {
42
+ this.cannotJoin('signalInTransaction');
43
+ return this.signal(s);
44
+ }
45
+ cannotJoin(method) {
46
+ if (this.warnedAboutTransactions) {
47
+ return;
48
+ }
49
+ this.warnedAboutTransactions = true;
50
+ InMemoryWorkflowStore.logger.warn(`InMemoryWorkflowStore.${method}() received your transaction handle, and can't join it: the write applies at ` +
51
+ "once, so a rolled-back transaction won't undo it. Register a store for your database with " +
52
+ 'WorkflowStorage.registerSource(). (Logged once.)');
53
+ }
54
+ async create(i) {
55
+ const existing = this.rows.get(i.id);
56
+ if (existing) {
57
+ return { instance: copy(existing.instance), created: false };
58
+ }
59
+ const instance = {
60
+ id: i.id,
61
+ workflow: i.workflow,
62
+ version: i.version,
63
+ parentId: i.parentId ?? null,
64
+ parentClose: i.parentClose ?? null,
65
+ concurrencyKey: i.concurrencyKey ?? null,
66
+ rateLimitKey: i.rateLimitKey ?? null,
67
+ priority: i.priority ?? 0,
68
+ scheduleId: i.scheduleId ?? null,
69
+ scheduledAt: i.scheduledAt ?? null,
70
+ status: 'pending',
71
+ input: copy(i.input),
72
+ output: undefined,
73
+ error: null,
74
+ wakeAt: i.now,
75
+ leaseOwner: null,
76
+ leaseUntil: null,
77
+ cancelRequested: false,
78
+ terminateRequested: false,
79
+ cancelReason: null,
80
+ deadline: i.deadline,
81
+ customStatus: null,
82
+ signalCursor: this.lastSignalId(),
83
+ runs: 0,
84
+ createdAt: i.now,
85
+ updatedAt: i.now,
86
+ };
87
+ this.rows.set(i.id, { instance, leaseToken: null, journal: new Map(), waits: [] });
88
+ return { instance: copy(instance), created: true };
89
+ }
90
+ async get(id, options = {}) {
91
+ const row = this.rows.get(id);
92
+ if (!row) {
93
+ return null;
94
+ }
95
+ return {
96
+ ...copy(row.instance),
97
+ waits: copy(row.waits),
98
+ ...(options.journal ? { journal: copy([...row.journal.values()]) } : {}),
99
+ };
100
+ }
101
+ async list(query) {
102
+ return [...this.rows.values()]
103
+ .map((row) => row.instance)
104
+ .filter((i) => (query.status === undefined || query.status.includes(i.status)) &&
105
+ (query.workflow === undefined || i.workflow === query.workflow) &&
106
+ (query.version === undefined || i.version === query.version) &&
107
+ (query.parentId === undefined || i.parentId === query.parentId) &&
108
+ (query.scheduleId === undefined || i.scheduleId === query.scheduleId))
109
+ .sort((a, b) => a.createdAt - b.createdAt || compare(a.id, b.id))
110
+ .slice(query.offset, query.offset + query.limit)
111
+ .map(copy);
112
+ }
113
+ async requestCancel(id, { reason, now, terminate }) {
114
+ const instance = this.rows.get(id)?.instance;
115
+ const applies = terminate ? RUNNABLE.has(instance?.status ?? '') && !instance.terminateRequested : CANCELLABLE.has(instance?.status ?? '') && !instance.cancelRequested;
116
+ if (!instance || !applies) {
117
+ return false;
118
+ }
119
+ instance.cancelRequested = true;
120
+ instance.terminateRequested ||= terminate;
121
+ instance.cancelReason = reason;
122
+ instance.updatedAt = now;
123
+ if (instance.wakeAt === null || instance.wakeAt > now) {
124
+ instance.wakeAt = now;
125
+ }
126
+ return true;
127
+ }
128
+ async reopen(id, r) {
129
+ const row = this.rows.get(id);
130
+ if (!row || row.leaseToken !== null || row.instance.status !== r.expect.status || row.instance.runs !== r.expect.runs) {
131
+ return false;
132
+ }
133
+ for (const entry of r.entries) {
134
+ row.journal.set(entry.name, copy(entry));
135
+ }
136
+ const i = row.instance;
137
+ i.status = r.status;
138
+ i.error = copy(r.error);
139
+ if (r.deadline !== undefined) {
140
+ i.deadline = r.deadline;
141
+ }
142
+ i.wakeAt = r.now;
143
+ i.updatedAt = r.now;
144
+ return true;
145
+ }
146
+ async delete(id, statuses) {
147
+ const row = this.rows.get(id);
148
+ if (!row || !statuses.includes(row.instance.status)) {
149
+ return false;
150
+ }
151
+ this.rows.delete(id);
152
+ return true;
153
+ }
154
+ async signal(s) {
155
+ return this.record(s);
156
+ }
157
+ record(s) {
158
+ const earlier = s.dedupeId === null ? undefined : this.signalLog.find((r) => r.name === s.name && r.dedupeId === s.dedupeId);
159
+ if (earlier) {
160
+ return { id: earlier.id, woken: 0, created: false, key: earlier.key };
161
+ }
162
+ const id = this.lastSignalId() + 1;
163
+ this.signalLog.push({ id, name: s.name, key: s.key, dedupeId: s.dedupeId, payload: copy(s.payload), createdAt: s.now });
164
+ let woken = 0;
165
+ for (const { instance, waits } of this.rows.values()) {
166
+ if (instance.status !== 'suspended' || (instance.wakeAt !== null && instance.wakeAt <= s.now)) {
167
+ continue;
168
+ }
169
+ if (!waits.some((wait) => wait.signal === s.name && wait.key === s.key)) {
170
+ continue;
171
+ }
172
+ instance.wakeAt = s.now;
173
+ instance.updatedAt = s.now;
174
+ woken++;
175
+ }
176
+ return { id, woken, created: true, key: s.key };
177
+ }
178
+ async signals(query) {
179
+ return this.signalLog
180
+ .filter((s) => s.name === query.name && s.key === query.key && s.id > query.afterId && s.id <= query.upToId)
181
+ .map(({ dedupeId: _dedupeId, ...record }) => copy(record));
182
+ }
183
+ async purge(query) {
184
+ const finished = [...this.rows.values()]
185
+ .map((row) => row.instance)
186
+ .filter((i) => FINISHED.has(i.status) && query.statuses.includes(i.status) && i.updatedAt < query.before)
187
+ .sort((a, b) => a.updatedAt - b.updatedAt || compare(a.id, b.id))
188
+ .slice(0, query.limit);
189
+ for (const instance of finished) {
190
+ this.rows.delete(instance.id);
191
+ }
192
+ const newest = this.lastSignalId();
193
+ const cursors = [...this.rows.values()].filter((row) => RUNNABLE.has(row.instance.status)).map((row) => row.instance.signalCursor);
194
+ const floor = Math.min(newest, ...cursors);
195
+ const prunable = new Set(this.signalLog
196
+ .filter((s) => s.id <= floor && s.id < newest && s.createdAt < query.before)
197
+ .slice(0, query.limit)
198
+ .map((s) => s.id));
199
+ const kept = this.signalLog.filter((s) => !prunable.has(s.id));
200
+ this.signalLog.splice(0, this.signalLog.length, ...kept);
201
+ const ended = [...this.windows]
202
+ .filter(([, window]) => window.windowEnd < query.before)
203
+ .sort(([a, x], [b, y]) => x.windowEnd - y.windowEnd || compare(a, b))
204
+ .slice(0, query.limit);
205
+ for (const [key] of ended) {
206
+ this.windows.delete(key);
207
+ }
208
+ return { instances: finished.length, signals: prunable.size, rateLimits: ended.length };
209
+ }
210
+ saveSchedule(save) {
211
+ return this.scheduleMethods.saveSchedule(save);
212
+ }
213
+ getSchedule(id) {
214
+ return this.scheduleMethods.getSchedule(id);
215
+ }
216
+ listSchedules(query) {
217
+ return this.scheduleMethods.listSchedules(query);
218
+ }
219
+ deleteSchedule(id, revision) {
220
+ return this.scheduleMethods.deleteSchedule(id, revision);
221
+ }
222
+ claimSchedules(request) {
223
+ return this.scheduleMethods.claimSchedules(request);
224
+ }
225
+ writeSchedule(id, token, write) {
226
+ return this.scheduleMethods.writeSchedule(id, token, write);
227
+ }
228
+ async claim(request) {
229
+ const runs = new Set(request.workflows.map((w) => `${w.version}:${w.name}`));
230
+ const candidates = [...this.rows.values()]
231
+ .filter(({ instance: i }) => RUNNABLE.has(i.status) &&
232
+ i.wakeAt !== null &&
233
+ i.wakeAt <= request.now &&
234
+ (i.leaseUntil === null || i.leaseUntil < request.now) &&
235
+ runs.has(`${i.version}:${i.workflow}`))
236
+ .sort((a, b) => a.instance.priority - b.instance.priority ||
237
+ a.instance.wakeAt - b.instance.wakeAt ||
238
+ a.instance.createdAt - b.instance.createdAt ||
239
+ compare(a.instance.id, b.instance.id));
240
+ const due = this.pick(request, candidates);
241
+ this.recordWindows(request, due);
242
+ for (const row of due) {
243
+ const i = row.instance;
244
+ row.leaseToken = request.token;
245
+ i.leaseOwner = request.owner;
246
+ i.leaseUntil = request.leaseUntil;
247
+ i.runs++;
248
+ i.updatedAt = request.now;
249
+ if (i.status !== 'compensating') {
250
+ i.status = 'running';
251
+ }
252
+ }
253
+ return { instances: due.map((row) => copy(row.instance)), lastSignalId: this.lastSignalId() };
254
+ }
255
+ /**
256
+ * The staged pick of `WorkflowStore.claim()` under limits: candidates with no room at all are passed over first;
257
+ * then each concurrency key keeps its first candidates, as many as it has free slots; of those, each rate-limit
258
+ * key as many as its window has room for; of those, each workflow as many as both its free slots and its window
259
+ * allow; then the first `limit`.
260
+ */
261
+ pick(request, candidates) {
262
+ const limits = new Map((request.limits ?? []).map((limit) => [limit.workflow, limit]));
263
+ const rules = new Map((request.rateLimits ?? []).map((rule) => [rule.workflow, rule]));
264
+ const held = new Map();
265
+ for (const { instance: i } of this.rows.values()) {
266
+ if (limits.has(i.workflow) && i.leaseUntil !== null && i.leaseUntil >= request.now) {
267
+ for (const key of [slot(i.workflow), ...(i.concurrencyKey === null ? [] : [slot(i.workflow, i.concurrencyKey)])]) {
268
+ held.set(key, (held.get(key) ?? 0) + 1);
269
+ }
270
+ }
271
+ }
272
+ const heldBy = (key) => held.get(key) ?? 0;
273
+ const roomIn = (key, max) => rateWindowRoom(this.windows.get(key), max, request.now);
274
+ const free = (i) => {
275
+ const limit = limits.get(i.workflow);
276
+ const rule = rules.get(i.workflow);
277
+ return {
278
+ total: limit?.limit != null ? limit.limit - heldBy(slot(i.workflow)) : Infinity,
279
+ key: limit?.perKey != null && i.concurrencyKey !== null ? limit.perKey - heldBy(slot(i.workflow, i.concurrencyKey)) : Infinity,
280
+ rate: rule?.limit ? roomIn(slot(i.workflow), rule.limit.max) : Infinity,
281
+ rateKey: rule?.perKey && i.rateLimitKey !== null ? roomIn(slot(i.workflow, i.rateLimitKey), rule.perKey.max) : Infinity,
282
+ };
283
+ };
284
+ const open = candidates.filter(({ instance: i }) => Object.values(free(i)).every((room) => room > 0));
285
+ const byKey = firsts(open, (i) => (Number.isFinite(free(i).key) ? slot(i.workflow, i.concurrencyKey) : null), (i) => free(i).key);
286
+ const byRateKey = firsts(byKey, (i) => (Number.isFinite(free(i).rateKey) ? slot(i.workflow, i.rateLimitKey) : null), (i) => free(i).rateKey);
287
+ const byWorkflow = firsts(byRateKey, (i) => (limits.has(i.workflow) || rules.has(i.workflow) ? i.workflow : null), (i) => Math.min(free(i).total, free(i).rate));
288
+ return byWorkflow.slice(0, request.limit);
289
+ }
290
+ /** Records the claims of `claimed` in their rate-limit windows, starting a window where the last one ended. */
291
+ recordWindows(request, claimed) {
292
+ const rules = new Map((request.rateLimits ?? []).map((rule) => [rule.workflow, rule]));
293
+ for (const { instance: i } of claimed) {
294
+ const rule = rules.get(i.workflow);
295
+ const windows = [
296
+ ...(rule?.limit ? [{ key: slot(i.workflow), duration: rule.limit.duration }] : []),
297
+ ...(rule?.perKey && i.rateLimitKey !== null ? [{ key: slot(i.workflow, i.rateLimitKey), duration: rule.perKey.duration }] : []),
298
+ ];
299
+ for (const { key, duration } of windows) {
300
+ this.windows.set(key, takeRateWindow(this.windows.get(key), duration, request.now));
301
+ }
302
+ }
303
+ }
304
+ async renew(id, token, leaseUntil) {
305
+ const row = this.leased(id, token);
306
+ if (!row) {
307
+ return null;
308
+ }
309
+ row.instance.leaseUntil = leaseUntil;
310
+ return { cancelRequested: row.instance.cancelRequested, terminateRequested: row.instance.terminateRequested };
311
+ }
312
+ async write(id, token, w) {
313
+ const row = this.leased(id, token);
314
+ if (!row) {
315
+ return false;
316
+ }
317
+ for (const entry of w.entries) {
318
+ row.journal.set(entry.name, copy(entry));
319
+ }
320
+ const i = row.instance;
321
+ if (w.status !== undefined) {
322
+ i.status = w.status;
323
+ }
324
+ if (w.output !== undefined) {
325
+ i.output = copy(w.output);
326
+ }
327
+ if (w.error !== undefined) {
328
+ i.error = copy(w.error);
329
+ }
330
+ if (w.customStatus !== undefined) {
331
+ i.customStatus = copy(w.customStatus);
332
+ }
333
+ if (w.signal) {
334
+ this.record(w.signal);
335
+ }
336
+ if (w.release) {
337
+ const { waits, signalCursor } = w.release;
338
+ row.waits = copy(waits);
339
+ row.leaseToken = null;
340
+ i.leaseUntil = null;
341
+ const missed = this.signalLog.some((s) => s.id > signalCursor && waits.some((wait) => wait.signal === s.name && wait.key === s.key));
342
+ i.wakeAt = missed || (i.cancelRequested && w.status === 'suspended') ? w.now : w.release.wakeAt;
343
+ }
344
+ if (w.status !== undefined || w.output !== undefined || w.error !== undefined || w.customStatus !== undefined || w.release) {
345
+ i.updatedAt = w.now;
346
+ }
347
+ return true;
348
+ }
349
+ leased(id, token) {
350
+ const row = this.rows.get(id);
351
+ return row && row.leaseToken !== null && row.leaseToken === token ? row : undefined;
352
+ }
353
+ lastSignalId() {
354
+ return this.signalLog.at(-1)?.id ?? 0;
355
+ }
356
+ }
357
+ InMemoryWorkflowStore.logger = new Logger('WorkflowsModule');
358
+ function copy(value) {
359
+ return value === undefined ? value : structuredClone(value);
360
+ }
361
+ function compare(a, b) {
362
+ return a < b ? -1 : a > b ? 1 : 0;
363
+ }
364
+ /** A workflow's slot or window key, or one of its keys'. */
365
+ function slot(workflow, key) {
366
+ return JSON.stringify(key === undefined ? [workflow] : [workflow, key]);
367
+ }
368
+ /** Of `rows` (in order), each partition's first ones, as many as `room` says; rows in no partition (`null`) all stay. */
369
+ function firsts(rows, partition, room) {
370
+ const seen = new Map();
371
+ return rows.filter(({ instance }) => {
372
+ const key = partition(instance);
373
+ if (key === null) {
374
+ return true;
375
+ }
376
+ const n = (seen.get(key) ?? 0) + 1;
377
+ seen.set(key, n);
378
+ return n <= room(instance);
379
+ });
380
+ }
@@ -0,0 +1 @@
1
+ export * from './in-memory-workflow.store.js';
@@ -0,0 +1 @@
1
+ export * from './in-memory-workflow.store.js';
@@ -0,0 +1,10 @@
1
+ /** A random pause of up to 4 ms, to shuffle the calls a race starts together. */
2
+ export declare const jitter: () => Promise<unknown>;
3
+ export declare function show(value: unknown): string;
4
+ export declare function equal(actual: unknown, expected: unknown, label: string): void;
5
+ /** Every key of `pattern` matches, recursively (arrays by length and element); other keys are ignored. */
6
+ export declare function expect(actual: unknown, pattern: unknown, label: string): void;
7
+ export declare function absent(value: unknown, label: string, options?: {
8
+ orNull?: boolean;
9
+ }): void;
10
+ export declare function rejects(promise: Promise<unknown>, message: string): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ /** A random pause of up to 4 ms, to shuffle the calls a race starts together. */
3
+ export const jitter = () => new Promise((resolve) => setTimeout(resolve, Math.random() * 4));
4
+ export function show(value) {
5
+ return JSON.stringify(value, (_key, v) => (v === undefined ? '<undefined>' : v)) ?? String(value);
6
+ }
7
+ export function equal(actual, expected, label) {
8
+ if (!isDeepStrictEqual(normalizeUndefined(actual), normalizeUndefined(expected))) {
9
+ throw new Error(`${label}: expected ${show(expected)}, got ${show(actual)}`);
10
+ }
11
+ }
12
+ /** Every key of `pattern` matches, recursively (arrays by length and element); other keys are ignored. */
13
+ export function expect(actual, pattern, label) {
14
+ const mismatch = match(actual, pattern, '');
15
+ if (mismatch !== null) {
16
+ throw new Error(`${label}: ${mismatch}\n expected ${show(pattern)}\n got ${show(actual)}`);
17
+ }
18
+ }
19
+ function match(actual, pattern, path) {
20
+ if (Array.isArray(pattern)) {
21
+ if (!Array.isArray(actual) || actual.length !== pattern.length) {
22
+ return `at ${path || 'the root'}: expected an array of ${pattern.length}`;
23
+ }
24
+ for (let i = 0; i < pattern.length; i++) {
25
+ const mismatch = match(actual[i], pattern[i], `${path}[${i}]`);
26
+ if (mismatch) {
27
+ return mismatch;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ if (pattern !== null && typeof pattern === 'object') {
33
+ if (actual === null || typeof actual !== 'object') {
34
+ return `at ${path || 'the root'}: expected an object`;
35
+ }
36
+ for (const [key, value] of Object.entries(pattern)) {
37
+ const mismatch = match(actual[key], value, path ? `${path}.${key}` : key);
38
+ if (mismatch) {
39
+ return mismatch;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ return Object.is(actual, pattern) ? null : `at ${path || 'the root'}: expected ${show(pattern)}, got ${show(actual)}`;
45
+ }
46
+ export function absent(value, label, options = {}) {
47
+ if (value !== undefined && !(options.orNull && value === null)) {
48
+ throw new Error(`${label}: expected undefined, got ${show(value)}`);
49
+ }
50
+ }
51
+ export async function rejects(promise, message) {
52
+ try {
53
+ await promise;
54
+ }
55
+ catch (error) {
56
+ if (String(error?.message ?? error).includes(message)) {
57
+ return;
58
+ }
59
+ throw error;
60
+ }
61
+ throw new Error(`expected a rejection with "${message}"`);
62
+ }
63
+ /** Drops `undefined` object fields, so `{ a: undefined }` equals `{}` (a store may omit them). */
64
+ function normalizeUndefined(value) {
65
+ if (Array.isArray(value)) {
66
+ return value.map(normalizeUndefined);
67
+ }
68
+ if (value !== null && typeof value === 'object') {
69
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined).map(([k, v]) => [k, normalizeUndefined(v)]));
70
+ }
71
+ return value;
72
+ }
@@ -0,0 +1,38 @@
1
+ import type { WorkflowStore } from '../interfaces/workflow-store.interface.js';
2
+ export { scheduleStoreContract, type ScheduleStoreContractCase, type ScheduleStoreContractOptions } from './schedule-store.contract.js';
3
+ export interface WorkflowStoreContractOptions {
4
+ /**
5
+ * Also run the concurrency cases: many calls in flight at once, racing claims, signals,
6
+ * suspensions and cancels. Set it for a store on a connection pool (they are the cases a
7
+ * store without the right locks fails); a single connection serializes them, which they
8
+ * must survive too.
9
+ */
10
+ concurrent?: boolean;
11
+ /**
12
+ * Opens an application transaction and runs `work` in it, such as
13
+ * `(work) => db.transaction(work)`: the cases for `createInTransaction()` and
14
+ * `signalInTransaction()` run when it is set and the store has them.
15
+ */
16
+ transaction?: <T>(work: (transaction: unknown) => Promise<T>) => Promise<T>;
17
+ /** Called after each case with the store `createStore` returned (close it, drop its tables). */
18
+ dispose?: (store: WorkflowStore) => unknown;
19
+ }
20
+ export interface WorkflowStoreContractCase {
21
+ name: string;
22
+ /** Throws on failure. */
23
+ run(): Promise<void>;
24
+ }
25
+ /**
26
+ * The `WorkflowStore` contract as test cases, for any test runner:
27
+ *
28
+ * ```ts
29
+ * const cases = workflowStoreContract(async () => new DrizzleWorkflowStore(await freshDb(), new WorkflowStorage()), {
30
+ * concurrent: true,
31
+ * transaction: (work) => db.transaction(work),
32
+ * });
33
+ * for (const c of cases) it(c.name, c.run);
34
+ * ```
35
+ *
36
+ * `createStore` is called once per case and must return a store on empty tables.
37
+ */
38
+ export declare function workflowStoreContract(createStore: () => WorkflowStore | Promise<WorkflowStore>, options?: WorkflowStoreContractOptions): WorkflowStoreContractCase[];