@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,152 @@
1
+ import type { LoggerService } from '@nestjs/common';
2
+ import type { PayloadCodecs } from '../codecs/payload-codecs.js';
3
+ import type { Clock } from './clock.interface.js';
4
+ import type { PayloadContext } from './payload-codec.interface.js';
5
+ import type { ScheduleOccurrence, ScheduleSpec } from './schedule.interface.js';
6
+ import type { ScheduleRecord, ScheduleStore } from './schedule-store.interface.js';
7
+ /**
8
+ * A schedule the code declares (a decorator's, a queue definition's), which the `Scheduler` keeps in step with the
9
+ * store at startup and every minute, also through a rolling deploy.
10
+ *
11
+ * ```ts
12
+ * const declared: DeclaredSchedule = {
13
+ * id: 'weekly-digest',
14
+ * target: 'emails',
15
+ * version: 1,
16
+ * spec: parseSchedule({ cron: '0 0 8 * * MON', tz: 'Europe/Warsaw' }),
17
+ * payload: ({ at }: ScheduleOccurrence) => ({ week: isoWeek(new Date(at)) }),
18
+ * };
19
+ * ```
20
+ */
21
+ export interface DeclaredSchedule<S extends ScheduleSpec = ScheduleSpec> {
22
+ /** Unique across the schedules of the store. */
23
+ id: string;
24
+ /** What it starts runs of. */
25
+ target: string;
26
+ /**
27
+ * The version of the code that declares it, such as the workflow's highest registered version (a package without
28
+ * versions passes 1): a process of older code leaves a newer version's declaration as it is while a process of that
29
+ * code confirms it.
30
+ */
31
+ version: number;
32
+ spec: S;
33
+ /** What each run gets: a JSON value, stored, or a function of the occurrence, which `fire` calls (never stored). */
34
+ payload: unknown | ((occurrence: ScheduleOccurrence) => unknown);
35
+ }
36
+ /**
37
+ * An occurrence the `Scheduler` asks its `fire` to start: the schedule as stored (its payload decoded), its spec, the
38
+ * code's declaration of it (for a declared schedule whose payload is a function), and the run's id, which is
39
+ * `occurrenceId(schedule.id, at)`: starting a second run with the same id must do nothing.
40
+ *
41
+ * ```ts
42
+ * fire: async ({ schedule, declared, id, at }) => {
43
+ * const data = typeof declared?.payload === 'function' ? declared.payload({ id: schedule.id, at }) : schedule.payload;
44
+ * const { created } = await jobs.add(schedule.target, data, { id });
45
+ * return { created };
46
+ * },
47
+ * ```
48
+ */
49
+ export interface DueOccurrence<S extends ScheduleSpec = ScheduleSpec> {
50
+ schedule: ScheduleRecord;
51
+ spec: S;
52
+ declared?: DeclaredSchedule<S>;
53
+ /** The run's id: `occurrenceId(schedule.id, at)`. */
54
+ id: string;
55
+ /** When the occurrence was due. */
56
+ at: number;
57
+ /** The production's time. */
58
+ now: number;
59
+ }
60
+ /**
61
+ * Occurrences a production passed over: missed while no worker was up (`'missed'`, see `missed`), or due while a run
62
+ * the schedule started was unfinished (`'overlap'`).
63
+ *
64
+ * ```ts
65
+ * skipped: (schedule, { reason, from, to }) => events.emit({ type: 'schedule-skipped', id: schedule.id, reason, from, to }),
66
+ * ```
67
+ */
68
+ export interface ScheduleSkip {
69
+ reason: 'missed' | 'overlap';
70
+ /** The first occurrence it passed over (epoch milliseconds). */
71
+ from: number;
72
+ /** The last one. */
73
+ to: number;
74
+ }
75
+ /**
76
+ * What one `Scheduler.produce()` did: how many runs it started, and the runs it cancelled (`overlap:
77
+ * 'cancel-previous'`), which a worker running them in this process should stop at once.
78
+ *
79
+ * ```ts
80
+ * const { started, cancelled } = await scheduler.produce(worker.owner, worker.leaseMs);
81
+ * ```
82
+ */
83
+ export interface ScheduleProduction {
84
+ started: number;
85
+ cancelled: string[];
86
+ }
87
+ /**
88
+ * What the `Scheduler` takes: its store, and the package's side of schedules (what a run is, how to start, list and
89
+ * cancel one), which it plugs in as functions.
90
+ *
91
+ * ```ts
92
+ * const scheduler = new Scheduler({
93
+ * store: new InMemoryScheduleStore(),
94
+ * targets: () => ['emails'],
95
+ * declared: () => new Map([[digest.id, digest]]),
96
+ * fire: ({ schedule, id }) => jobs.add(schedule.target, schedule.payload, { id }),
97
+ * running: (schedule) => jobs.unfinished({ scheduleId: schedule.id }),
98
+ * cancel: (id, reason) => jobs.cancel(id, reason),
99
+ * });
100
+ * ```
101
+ */
102
+ export interface SchedulerOptions<S extends ScheduleSpec = ScheduleSpec> {
103
+ /** Where the schedules live, their payloads as the store keeps them (the scheduler encodes them with `codecs`). */
104
+ store: ScheduleStore;
105
+ /** The targets this process runs: it produces only their schedules. None: it neither produces nor reconciles. */
106
+ targets(): string[];
107
+ /** The schedules this process's code declares, by id. Default: none. */
108
+ declared?(): ReadonlyMap<string, DeclaredSchedule<S>>;
109
+ /**
110
+ * Starts the run of an occurrence with the id it is given, or finds the one that has it: `{ created }`. `null` skips
111
+ * the occurrence for good (its payload couldn't be computed: log why); throwing (the store refused it) retries it,
112
+ * backing off from a second to a minute, ten times in all.
113
+ */
114
+ fire(occurrence: DueOccurrence<S>): Promise<{
115
+ created: boolean;
116
+ } | null>;
117
+ /** The ids of the unfinished runs the schedule started, for its `overlap`. */
118
+ running(schedule: ScheduleRecord): Promise<string[]>;
119
+ /** Cancels a run (`overlap: 'cancel-previous'`): whether it was accepted. */
120
+ cancel(run: string, reason: string, now: number): Promise<boolean>;
121
+ /** Called after a production that passed over occurrences. */
122
+ skipped?(schedule: ScheduleRecord, skip: ScheduleSkip, now: number): void;
123
+ /** Where times come from. Default `systemClock`. */
124
+ clock?: Clock;
125
+ /** Encodes the schedules' payloads (`PayloadCodecs` of the package's codecs). Default: stored as they are. */
126
+ codecs?: PayloadCodecs;
127
+ /** The context a schedule's payload is encoded with. Default `{ field: 'payload', schedule: id }`. */
128
+ payloadContext?(schedule: string): PayloadContext;
129
+ /** The error a save that keeps losing races rejects with. Default: an `Error`. */
130
+ conflictError?(message: string): Error;
131
+ /** How its logs name things, such as `{ target: (name) => \`workflow "${name}"\`, targets: 'workflows' }`. */
132
+ labels?: SchedulerLabels;
133
+ /** Where it logs what it did (a deletion, a takeover) and what failed. Default: a Nest `Logger`. */
134
+ logger?: Pick<LoggerService, 'log' | 'warn' | 'error'>;
135
+ }
136
+ /**
137
+ * How the `Scheduler`'s logs name a package's things.
138
+ *
139
+ * ```ts
140
+ * const labels: SchedulerLabels = { target: (name) => `queue "${name}"`, targets: 'queues', payload: 'data', upsert: 'schedules.upsert()' };
141
+ * ```
142
+ */
143
+ export interface SchedulerLabels {
144
+ /** A target, such as `workflow "digest"`. Default `target "digest"`. */
145
+ target?(name: string): string;
146
+ /** The targets, as what declares schedules, such as `'workflows'`. Default `'targets'`. */
147
+ targets?: string;
148
+ /** A schedule's payload, such as `'input'`. Default `'payload'`. */
149
+ payload?: string;
150
+ /** The call that saves a schedule at runtime, such as `'WorkflowSchedules.upsert()'`. Default `'upsert()'`. */
151
+ upsert?: string;
152
+ }
@@ -0,0 +1 @@
1
+ export * from './limits.js';
@@ -0,0 +1 @@
1
+ export * from './limits.js';
@@ -0,0 +1,61 @@
1
+ import type { ConcurrencyLimit, RateLimit, RateWindowState, ResolvedConcurrency, ResolvedRateLimit } from '../interfaces/limits.interface.js';
2
+ /**
3
+ * The highest priority (the last to run): BullMQ's, which the family shares. Priorities are integers from 1 (first)
4
+ * to `MAX_PRIORITY`; lower runs first, and `0` (none given) runs before every other.
5
+ *
6
+ * ```ts
7
+ * const priority = options.priority ?? 0; // 0..MAX_PRIORITY
8
+ * ```
9
+ */
10
+ export declare const MAX_PRIORITY = 2097151;
11
+ /**
12
+ * Throws a `TypeError` that names `owner` unless `priority` is a priority: an integer from 0 (none) to
13
+ * `MAX_PRIORITY`.
14
+ *
15
+ * ```ts
16
+ * assertPriority(options.priority, 'add()'); // add(): invalid priority 1.5. Use an integer from 1 (first) to 2097151.
17
+ * ```
18
+ */
19
+ export declare function assertPriority(priority: unknown, owner: string): asserts priority is number;
20
+ /** Whether `value` is an integer from 0 (none) to `MAX_PRIORITY`. */
21
+ export declare function isPriority(value: unknown): value is number;
22
+ /**
23
+ * Checks concurrency limits (one, or one without a key and one with) and resolves them. Throws a `TypeError` that
24
+ * names `owner`, such as `queue "emails"`, for anything else.
25
+ *
26
+ * ```ts
27
+ * resolveConcurrency('queue "emails"', [{ limit: 20 }, { limit: 1, key: (email) => email.to }]);
28
+ * // { limit: 20, perKey: 1, key: (email) => email.to }
29
+ * ```
30
+ */
31
+ export declare function resolveConcurrency<I = any>(owner: string, concurrency: ConcurrencyLimit<I> | ConcurrencyLimit<I>[]): ResolvedConcurrency<I>;
32
+ /**
33
+ * Checks rate limits (one, or one without a key and one with) and resolves them, durations in milliseconds. Throws a
34
+ * `TypeError` that names `owner` for anything else.
35
+ *
36
+ * ```ts
37
+ * resolveRateLimit('queue "emails"', { max: 100, duration: '1s' });
38
+ * // { limit: { max: 100, duration: 1_000 }, perKey: null }
39
+ * ```
40
+ */
41
+ export declare function resolveRateLimit<I = any>(owner: string, rateLimit: RateLimit<I> | RateLimit<I>[]): ResolvedRateLimit<I>;
42
+ /**
43
+ * How many more starts a window of `max` takes at `now`: all of `max` when it ended (`windowEnd <= now`) or never
44
+ * opened.
45
+ *
46
+ * ```ts
47
+ * rateWindowRoom({ windowEnd: 60_000, count: 7 }, 10, 30_000); // 3
48
+ * rateWindowRoom({ windowEnd: 60_000, count: 7 }, 10, 60_000); // 10: it ended
49
+ * ```
50
+ */
51
+ export declare function rateWindowRoom(window: RateWindowState | null | undefined, max: number, now: number): number;
52
+ /**
53
+ * The window after `count` more starts at `now`: the open one, counted up, or, when it ended or never opened, a new
54
+ * one from `now` for `duration` milliseconds. Take only what `rateWindowRoom()` said fits, under the store's lock.
55
+ *
56
+ * ```ts
57
+ * takeRateWindow(undefined, 60_000, 1_000); // { windowEnd: 61_000, count: 1 }
58
+ * takeRateWindow({ windowEnd: 61_000, count: 1 }, 60_000, 2_000, 2); // { windowEnd: 61_000, count: 3 }
59
+ * ```
60
+ */
61
+ export declare function takeRateWindow(window: RateWindowState | null | undefined, duration: number, now: number, count?: number): RateWindowState;
@@ -0,0 +1,130 @@
1
+ import { parseDuration } from '../time/duration.js';
2
+ /**
3
+ * The highest priority (the last to run): BullMQ's, which the family shares. Priorities are integers from 1 (first)
4
+ * to `MAX_PRIORITY`; lower runs first, and `0` (none given) runs before every other.
5
+ *
6
+ * ```ts
7
+ * const priority = options.priority ?? 0; // 0..MAX_PRIORITY
8
+ * ```
9
+ */
10
+ export const MAX_PRIORITY = 2_097_151;
11
+ /**
12
+ * Throws a `TypeError` that names `owner` unless `priority` is a priority: an integer from 0 (none) to
13
+ * `MAX_PRIORITY`.
14
+ *
15
+ * ```ts
16
+ * assertPriority(options.priority, 'add()'); // add(): invalid priority 1.5. Use an integer from 1 (first) to 2097151.
17
+ * ```
18
+ */
19
+ export function assertPriority(priority, owner) {
20
+ if (!isPriority(priority)) {
21
+ throw new TypeError(`${owner}: invalid priority ${JSON.stringify(priority)}. Use an integer from 1 (first) to ${MAX_PRIORITY}.`);
22
+ }
23
+ }
24
+ /** Whether `value` is an integer from 0 (none) to `MAX_PRIORITY`. */
25
+ export function isPriority(value) {
26
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= MAX_PRIORITY;
27
+ }
28
+ /**
29
+ * Checks concurrency limits (one, or one without a key and one with) and resolves them. Throws a `TypeError` that
30
+ * names `owner`, such as `queue "emails"`, for anything else.
31
+ *
32
+ * ```ts
33
+ * resolveConcurrency('queue "emails"', [{ limit: 20 }, { limit: 1, key: (email) => email.to }]);
34
+ * // { limit: 20, perKey: 1, key: (email) => email.to }
35
+ * ```
36
+ */
37
+ export function resolveConcurrency(owner, concurrency) {
38
+ const limits = Array.isArray(concurrency) ? concurrency : [concurrency];
39
+ if (limits.length === 0 || limits.length > 2) {
40
+ throw new TypeError(`${capitalized(owner)} has ${limits.length} concurrency limits. Give it one, or two: one without a key and one with.`);
41
+ }
42
+ const resolved = { limit: null, perKey: null };
43
+ for (const { limit, key } of limits) {
44
+ if (!Number.isSafeInteger(limit) || limit < 1) {
45
+ throw new TypeError(`Invalid concurrency limit ${JSON.stringify(limit)} for ${owner}. Use a positive integer.`);
46
+ }
47
+ if (key !== undefined && typeof key !== 'function') {
48
+ throw new TypeError(`Invalid concurrency key for ${owner}. Use a function of the input, such as (order) => order.customerId.`);
49
+ }
50
+ const slot = key === undefined ? 'limit' : 'perKey';
51
+ if (resolved[slot] !== null) {
52
+ throw new TypeError(`${capitalized(owner)} has two concurrency limits ${key === undefined ? 'without' : 'with'} a key. Give it at most one of each.`);
53
+ }
54
+ resolved[slot] = limit;
55
+ if (key) {
56
+ resolved.key = key;
57
+ }
58
+ }
59
+ return resolved;
60
+ }
61
+ /**
62
+ * Checks rate limits (one, or one without a key and one with) and resolves them, durations in milliseconds. Throws a
63
+ * `TypeError` that names `owner` for anything else.
64
+ *
65
+ * ```ts
66
+ * resolveRateLimit('queue "emails"', { max: 100, duration: '1s' });
67
+ * // { limit: { max: 100, duration: 1_000 }, perKey: null }
68
+ * ```
69
+ */
70
+ export function resolveRateLimit(owner, rateLimit) {
71
+ const limits = Array.isArray(rateLimit) ? rateLimit : [rateLimit];
72
+ if (limits.length === 0 || limits.length > 2) {
73
+ throw new TypeError(`${capitalized(owner)} has ${limits.length} rate limits. Give it one, or two: one without a key and one with.`);
74
+ }
75
+ const resolved = { limit: null, perKey: null };
76
+ for (const { max, duration, key } of limits) {
77
+ if (!Number.isSafeInteger(max) || max < 1) {
78
+ throw new TypeError(`Invalid rate limit max ${JSON.stringify(max)} for ${owner}. Use a positive integer.`);
79
+ }
80
+ let ms;
81
+ try {
82
+ ms = parseDuration(duration);
83
+ }
84
+ catch {
85
+ ms = 0;
86
+ }
87
+ if (!(ms > 0)) {
88
+ throw new TypeError(`Invalid rate limit duration ${JSON.stringify(duration)} for ${owner}. Use a positive duration, such as "1m".`);
89
+ }
90
+ if (key !== undefined && typeof key !== 'function') {
91
+ throw new TypeError(`Invalid rate limit key for ${owner}. Use a function of the input, such as (order) => order.customerId.`);
92
+ }
93
+ const slot = key === undefined ? 'limit' : 'perKey';
94
+ if (resolved[slot] !== null) {
95
+ throw new TypeError(`${capitalized(owner)} has two rate limits ${key === undefined ? 'without' : 'with'} a key. Give it at most one of each.`);
96
+ }
97
+ resolved[slot] = { max, duration: ms };
98
+ if (key) {
99
+ resolved.key = key;
100
+ }
101
+ }
102
+ return resolved;
103
+ }
104
+ /**
105
+ * How many more starts a window of `max` takes at `now`: all of `max` when it ended (`windowEnd <= now`) or never
106
+ * opened.
107
+ *
108
+ * ```ts
109
+ * rateWindowRoom({ windowEnd: 60_000, count: 7 }, 10, 30_000); // 3
110
+ * rateWindowRoom({ windowEnd: 60_000, count: 7 }, 10, 60_000); // 10: it ended
111
+ * ```
112
+ */
113
+ export function rateWindowRoom(window, max, now) {
114
+ return window && window.windowEnd > now ? max - window.count : max;
115
+ }
116
+ /**
117
+ * The window after `count` more starts at `now`: the open one, counted up, or, when it ended or never opened, a new
118
+ * one from `now` for `duration` milliseconds. Take only what `rateWindowRoom()` said fits, under the store's lock.
119
+ *
120
+ * ```ts
121
+ * takeRateWindow(undefined, 60_000, 1_000); // { windowEnd: 61_000, count: 1 }
122
+ * takeRateWindow({ windowEnd: 61_000, count: 1 }, 60_000, 2_000, 2); // { windowEnd: 61_000, count: 3 }
123
+ * ```
124
+ */
125
+ export function takeRateWindow(window, duration, now, count = 1) {
126
+ return window && window.windowEnd > now ? { windowEnd: window.windowEnd, count: window.count + count } : { windowEnd: now + duration, count };
127
+ }
128
+ function capitalized(text) {
129
+ return text.charAt(0).toUpperCase() + text.slice(1);
130
+ }
@@ -0,0 +1 @@
1
+ export * from './result-waiter.js';
@@ -0,0 +1 @@
1
+ export * from './result-waiter.js';
@@ -0,0 +1,54 @@
1
+ import type { ResultOutcome, ResultWaiterOptions, ResultWaitOptions } from '../interfaces/result-waiter-options.interface.js';
2
+ /**
3
+ * Waits for the outcome of something another part of the application runs (a workflow instance, a job): at once
4
+ * when this process ends it and says so (`settle()`), else by reading the store with a backoff (every 25 ms at
5
+ * first, doubling up to every second), which sees what other processes ended. A wait rejects past its `timeout`
6
+ * (the work goes on), when its `signal` aborts, and as soon as the waiter closes, without reading the store again:
7
+ * close it in `beforeApplicationShutdown`, which Nest runs after `onModuleDestroy` (where workers drain, so work that
8
+ * ends during the drain still answers) and before the HTTP server closes.
9
+ *
10
+ * ```ts
11
+ * @Injectable()
12
+ * export class JobResults implements BeforeApplicationShutdown {
13
+ * private readonly results = new ResultWaiter<unknown>({ read: (id) => this.readOutcome(id) });
14
+ *
15
+ * constructor(events: JobEvents) {
16
+ * events.completed$.subscribe((job) => this.results.settle(job.id, { value: job.result }));
17
+ * }
18
+ *
19
+ * result(id: string, options?: ResultWaitOptions) {
20
+ * return this.results.wait(id, options);
21
+ * }
22
+ *
23
+ * beforeApplicationShutdown() {
24
+ * return this.results.close();
25
+ * }
26
+ * }
27
+ * ```
28
+ */
29
+ export declare class ResultWaiter<T = unknown> {
30
+ private readonly options;
31
+ private readonly waits;
32
+ private readonly closing;
33
+ constructor(options: ResultWaiterOptions<T>);
34
+ /** Whether `close()` was called: every wait since rejects at once. */
35
+ get closed(): boolean;
36
+ /**
37
+ * Resolves with the value `id` ended with, or rejects with its outcome's error; past `timeout` with the
38
+ * `timeoutError`, when `signal` aborts with its reason, and once the waiter closed with the `closedError`.
39
+ */
40
+ wait(id: string, options?: ResultWaitOptions): Promise<T>;
41
+ /**
42
+ * Settles the waits for `id` with its outcome at once, without a read, and a read in flight gives way to it: for
43
+ * what this process saw end. A function is only called when something waits for `id`. The first outcome wins.
44
+ */
45
+ settle(id: string, outcome: ResultOutcome<T> | (() => ResultOutcome<T>)): void;
46
+ /**
47
+ * Ends every wait (they reject with the `closedError`), and makes later ones reject at once. Resolves after a
48
+ * macrotask, so the callers that stopped waiting have answered by then: Node's `server.close()` closes only the
49
+ * connections idle at that moment, and one that answers later stays open until its keep-alive timeout.
50
+ */
51
+ close(): Promise<void>;
52
+ private add;
53
+ private remove;
54
+ }
@@ -0,0 +1,140 @@
1
+ import { setImmediate as nextMacrotask } from 'node:timers/promises';
2
+ import { parseDuration } from '../time/duration.js';
3
+ /** What a read in flight gives way to: an outcome settled locally, or the waiter closing. */
4
+ const SETTLED = Symbol('settled');
5
+ /**
6
+ * Waits for the outcome of something another part of the application runs (a workflow instance, a job): at once
7
+ * when this process ends it and says so (`settle()`), else by reading the store with a backoff (every 25 ms at
8
+ * first, doubling up to every second), which sees what other processes ended. A wait rejects past its `timeout`
9
+ * (the work goes on), when its `signal` aborts, and as soon as the waiter closes, without reading the store again:
10
+ * close it in `beforeApplicationShutdown`, which Nest runs after `onModuleDestroy` (where workers drain, so work that
11
+ * ends during the drain still answers) and before the HTTP server closes.
12
+ *
13
+ * ```ts
14
+ * @Injectable()
15
+ * export class JobResults implements BeforeApplicationShutdown {
16
+ * private readonly results = new ResultWaiter<unknown>({ read: (id) => this.readOutcome(id) });
17
+ *
18
+ * constructor(events: JobEvents) {
19
+ * events.completed$.subscribe((job) => this.results.settle(job.id, { value: job.result }));
20
+ * }
21
+ *
22
+ * result(id: string, options?: ResultWaitOptions) {
23
+ * return this.results.wait(id, options);
24
+ * }
25
+ *
26
+ * beforeApplicationShutdown() {
27
+ * return this.results.close();
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export class ResultWaiter {
33
+ constructor(options) {
34
+ this.options = options;
35
+ this.waits = new Map();
36
+ this.closing = new AbortController();
37
+ }
38
+ /** Whether `close()` was called: every wait since rejects at once. */
39
+ get closed() {
40
+ return this.closing.signal.aborted;
41
+ }
42
+ /**
43
+ * Resolves with the value `id` ended with, or rejects with its outcome's error; past `timeout` with the
44
+ * `timeoutError`, when `signal` aborts with its reason, and once the waiter closed with the `closedError`.
45
+ */
46
+ async wait(id, options = {}) {
47
+ const timeoutMs = options.timeout === undefined ? Infinity : parseDuration(options.timeout);
48
+ const deadline = performance.now() + timeoutMs;
49
+ options.signal?.throwIfAborted();
50
+ let settle;
51
+ const settled = new Promise((resolve) => (settle = () => resolve(SETTLED)));
52
+ const wait = { closed: this.closed, settle };
53
+ const nudge = () => wait.wake?.();
54
+ const close = () => {
55
+ wait.closed = true;
56
+ settle();
57
+ nudge();
58
+ };
59
+ this.add(id, wait);
60
+ this.closing.signal.addEventListener('abort', close);
61
+ options.signal?.addEventListener('abort', nudge);
62
+ try {
63
+ for (let delay = 25;; delay = Math.min(delay * 2, 1_000)) {
64
+ const read = wait.outcome || wait.closed ? SETTLED : await Promise.race([this.options.read(id), settled]);
65
+ if (wait.outcome) {
66
+ return unwrap(wait.outcome);
67
+ }
68
+ if (read === SETTLED) {
69
+ throw this.options.closedError?.(id) ?? new Error(`The waiter closed while waiting for the result of "${id}".`);
70
+ }
71
+ if (read) {
72
+ return unwrap(read);
73
+ }
74
+ options.signal?.throwIfAborted();
75
+ const remaining = deadline - performance.now();
76
+ if (remaining <= 0) {
77
+ throw this.options.timeoutError?.(id, timeoutMs) ?? new Error(`"${id}" didn't end within ${timeoutMs}ms.`);
78
+ }
79
+ await new Promise((resolve) => {
80
+ const timer = setTimeout(resolve, Math.min(delay, remaining));
81
+ wait.wake = () => {
82
+ clearTimeout(timer);
83
+ resolve();
84
+ };
85
+ });
86
+ wait.wake = undefined;
87
+ }
88
+ }
89
+ finally {
90
+ this.remove(id, wait);
91
+ this.closing.signal.removeEventListener('abort', close);
92
+ options.signal?.removeEventListener('abort', nudge);
93
+ }
94
+ }
95
+ /**
96
+ * Settles the waits for `id` with its outcome at once, without a read, and a read in flight gives way to it: for
97
+ * what this process saw end. A function is only called when something waits for `id`. The first outcome wins.
98
+ */
99
+ settle(id, outcome) {
100
+ const waits = this.waits.get(id);
101
+ if (!waits) {
102
+ return;
103
+ }
104
+ const settled = typeof outcome === 'function' ? outcome() : outcome;
105
+ for (const wait of waits) {
106
+ wait.outcome ??= settled;
107
+ wait.settle();
108
+ wait.wake?.();
109
+ }
110
+ }
111
+ /**
112
+ * Ends every wait (they reject with the `closedError`), and makes later ones reject at once. Resolves after a
113
+ * macrotask, so the callers that stopped waiting have answered by then: Node's `server.close()` closes only the
114
+ * connections idle at that moment, and one that answers later stays open until its keep-alive timeout.
115
+ */
116
+ async close() {
117
+ this.closing.abort();
118
+ await nextMacrotask();
119
+ }
120
+ add(id, wait) {
121
+ let waits = this.waits.get(id);
122
+ if (!waits) {
123
+ this.waits.set(id, (waits = new Set()));
124
+ }
125
+ waits.add(wait);
126
+ }
127
+ remove(id, wait) {
128
+ const waits = this.waits.get(id);
129
+ waits?.delete(wait);
130
+ if (waits?.size === 0) {
131
+ this.waits.delete(id);
132
+ }
133
+ }
134
+ }
135
+ function unwrap(outcome) {
136
+ if ('error' in outcome) {
137
+ throw outcome.error;
138
+ }
139
+ return outcome.value;
140
+ }
@@ -0,0 +1 @@
1
+ export * from './retry.js';
@@ -0,0 +1 @@
1
+ export * from './retry.js';
@@ -0,0 +1,60 @@
1
+ import type { ResolvedBackoff, ResolvedRetry, RetrySettings } from '../interfaces/retry-settings.interface.js';
2
+ /**
3
+ * What `nextRetry()` decided after a failed attempt: retry after `delay` milliseconds, or give up because the
4
+ * attempts are spent (`'exhausted'`), `retryIf` returned false (`'refused'`), or `retryIf` or a backoff function threw
5
+ * (`'threw'`, with what it threw: record it as the failure, since retrying on would hide the bug).
6
+ *
7
+ * ```ts
8
+ * const next = nextRetry(policy, attempt, error);
9
+ * if (next.retry) {
10
+ * await job.retryAt(clock.now() + next.delay);
11
+ * } else {
12
+ * await job.fail(next.reason === 'threw' ? next.error : error);
13
+ * }
14
+ * ```
15
+ */
16
+ export type RetryDecision = {
17
+ retry: true;
18
+ delay: number;
19
+ } | {
20
+ retry: false;
21
+ reason: 'exhausted' | 'refused';
22
+ } | {
23
+ retry: false;
24
+ reason: 'threw';
25
+ error: unknown;
26
+ };
27
+ /**
28
+ * Checks `retry` (a count, `false` or `RetrySettings`) and resolves it over `base`, field by field: `undefined` is
29
+ * `base`, `false` one attempt, a number the attempts. `base` defaults to the family's default: 3 attempts, 1s doubling
30
+ * up to 5m, no jitter. Throws a `TypeError` for invalid settings, so they fail where they are given rather than at the
31
+ * first retry.
32
+ *
33
+ * ```ts
34
+ * const defaults = resolveRetry(moduleOptions.retry);
35
+ * const policy = resolveRetry(jobOptions.retry, defaults);
36
+ * ```
37
+ */
38
+ export declare function resolveRetry(retry: number | false | RetrySettings | undefined, base?: ResolvedRetry): ResolvedRetry;
39
+ /**
40
+ * The wait (ms) after the failed `attempt` (1-based): `delay * factor^(attempt - 1)`, capped at `maxDelay`, then
41
+ * randomized by `jitter` (`'full'`: from 0 to the wait, `'equal'`: from half of it to all of it). `random` returns a
42
+ * number in `[0, 1)`, as `Math.random()` (the default) does.
43
+ *
44
+ * ```ts
45
+ * backoffDelay({ delay: 1_000, factor: 2, maxDelay: 60_000, jitter: 'none' }, 4); // 8_000
46
+ * ```
47
+ */
48
+ export declare function backoffDelay(backoff: ResolvedBackoff, attempt: number, random?: () => number): number;
49
+ /**
50
+ * Whether, and when, to retry after `attempt` (1-based) failed with `error`: not once the policy's attempts are
51
+ * spent, nor when `retryIf` returns false; otherwise after the backoff's wait. A `retryIf` or backoff function that
52
+ * throws gives up, with what it threw. Errors that must never be retried (a `NonRetryableStepError`) are the caller's
53
+ * to check first.
54
+ *
55
+ * ```ts
56
+ * const next = nextRetry(resolveRetry({ attempts: 3 }), 1, new Error('ECONNRESET'));
57
+ * // { retry: true, delay: 1_000 }
58
+ * ```
59
+ */
60
+ export declare function nextRetry(policy: ResolvedRetry, attempt: number, error: unknown): RetryDecision;