@quave/agenda 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/LICENSE.md +25 -0
  2. package/README.md +380 -0
  3. package/dist/Job.d.ts +190 -0
  4. package/dist/Job.js +689 -0
  5. package/dist/Job.js.map +1 -0
  6. package/dist/JobProcessingQueue.d.ts +47 -0
  7. package/dist/JobProcessingQueue.js +107 -0
  8. package/dist/JobProcessingQueue.js.map +1 -0
  9. package/dist/JobProcessor.d.ts +72 -0
  10. package/dist/JobProcessor.js +512 -0
  11. package/dist/JobProcessor.js.map +1 -0
  12. package/dist/backends/index.d.ts +18 -0
  13. package/dist/backends/index.js +20 -0
  14. package/dist/backends/index.js.map +1 -0
  15. package/dist/cjs/Job.d.ts +190 -0
  16. package/dist/cjs/Job.js +696 -0
  17. package/dist/cjs/Job.js.map +1 -0
  18. package/dist/cjs/JobProcessingQueue.d.ts +47 -0
  19. package/dist/cjs/JobProcessingQueue.js +111 -0
  20. package/dist/cjs/JobProcessingQueue.js.map +1 -0
  21. package/dist/cjs/JobProcessor.d.ts +72 -0
  22. package/dist/cjs/JobProcessor.js +519 -0
  23. package/dist/cjs/JobProcessor.js.map +1 -0
  24. package/dist/cjs/backends/index.d.ts +17 -0
  25. package/dist/cjs/backends/index.js +20 -0
  26. package/dist/cjs/backends/index.js.map +1 -0
  27. package/dist/cjs/decorators/Define.d.ts +33 -0
  28. package/dist/cjs/decorators/Define.js +47 -0
  29. package/dist/cjs/decorators/Define.js.map +1 -0
  30. package/dist/cjs/decorators/Every.d.ts +43 -0
  31. package/dist/cjs/decorators/Every.js +58 -0
  32. package/dist/cjs/decorators/Every.js.map +1 -0
  33. package/dist/cjs/decorators/JobsController.d.ts +43 -0
  34. package/dist/cjs/decorators/JobsController.js +40 -0
  35. package/dist/cjs/decorators/JobsController.js.map +1 -0
  36. package/dist/cjs/decorators/index.d.ts +41 -0
  37. package/dist/cjs/decorators/index.js +57 -0
  38. package/dist/cjs/decorators/index.js.map +1 -0
  39. package/dist/cjs/decorators/metadata.d.ts +50 -0
  40. package/dist/cjs/decorators/metadata.js +41 -0
  41. package/dist/cjs/decorators/metadata.js.map +1 -0
  42. package/dist/cjs/decorators/register.d.ts +52 -0
  43. package/dist/cjs/decorators/register.js +145 -0
  44. package/dist/cjs/decorators/register.js.map +1 -0
  45. package/dist/cjs/index.d.ts +498 -0
  46. package/dist/cjs/index.js +776 -0
  47. package/dist/cjs/index.js.map +1 -0
  48. package/dist/cjs/notifications/BaseNotificationChannel.d.ts +47 -0
  49. package/dist/cjs/notifications/BaseNotificationChannel.js +112 -0
  50. package/dist/cjs/notifications/BaseNotificationChannel.js.map +1 -0
  51. package/dist/cjs/notifications/InMemoryNotificationChannel.d.ts +13 -0
  52. package/dist/cjs/notifications/InMemoryNotificationChannel.js +37 -0
  53. package/dist/cjs/notifications/InMemoryNotificationChannel.js.map +1 -0
  54. package/dist/cjs/notifications/index.d.ts +2 -0
  55. package/dist/cjs/notifications/index.js +8 -0
  56. package/dist/cjs/notifications/index.js.map +1 -0
  57. package/dist/cjs/package.json +1 -0
  58. package/dist/cjs/types/AgendaBackend.d.ts +61 -0
  59. package/dist/cjs/types/AgendaBackend.js +3 -0
  60. package/dist/cjs/types/AgendaBackend.js.map +1 -0
  61. package/dist/cjs/types/AgendaConfig.d.ts +10 -0
  62. package/dist/cjs/types/AgendaConfig.js +3 -0
  63. package/dist/cjs/types/AgendaConfig.js.map +1 -0
  64. package/dist/cjs/types/AgendaStatus.d.ts +32 -0
  65. package/dist/cjs/types/AgendaStatus.js +3 -0
  66. package/dist/cjs/types/AgendaStatus.js.map +1 -0
  67. package/dist/cjs/types/DbOptions.d.ts +4 -0
  68. package/dist/cjs/types/DbOptions.js +3 -0
  69. package/dist/cjs/types/DbOptions.js.map +1 -0
  70. package/dist/cjs/types/DrainOptions.d.ts +37 -0
  71. package/dist/cjs/types/DrainOptions.js +3 -0
  72. package/dist/cjs/types/DrainOptions.js.map +1 -0
  73. package/dist/cjs/types/JobDefinition.d.ts +49 -0
  74. package/dist/cjs/types/JobDefinition.js +3 -0
  75. package/dist/cjs/types/JobDefinition.js.map +1 -0
  76. package/dist/cjs/types/JobLogger.d.ts +111 -0
  77. package/dist/cjs/types/JobLogger.js +3 -0
  78. package/dist/cjs/types/JobLogger.js.map +1 -0
  79. package/dist/cjs/types/JobParameters.d.ts +106 -0
  80. package/dist/cjs/types/JobParameters.js +21 -0
  81. package/dist/cjs/types/JobParameters.js.map +1 -0
  82. package/dist/cjs/types/JobQuery.d.ts +79 -0
  83. package/dist/cjs/types/JobQuery.js +36 -0
  84. package/dist/cjs/types/JobQuery.js.map +1 -0
  85. package/dist/cjs/types/JobRepository.d.ts +101 -0
  86. package/dist/cjs/types/JobRepository.js +3 -0
  87. package/dist/cjs/types/JobRepository.js.map +1 -0
  88. package/dist/cjs/types/NotificationChannel.d.ts +120 -0
  89. package/dist/cjs/types/NotificationChannel.js +3 -0
  90. package/dist/cjs/types/NotificationChannel.js.map +1 -0
  91. package/dist/cjs/utils/backoff.d.ts +152 -0
  92. package/dist/cjs/utils/backoff.js +175 -0
  93. package/dist/cjs/utils/backoff.js.map +1 -0
  94. package/dist/cjs/utils/dateConstraints.d.ts +48 -0
  95. package/dist/cjs/utils/dateConstraints.js +127 -0
  96. package/dist/cjs/utils/dateConstraints.js.map +1 -0
  97. package/dist/cjs/utils/isValidDate.d.ts +1 -0
  98. package/dist/cjs/utils/isValidDate.js +8 -0
  99. package/dist/cjs/utils/isValidDate.js.map +1 -0
  100. package/dist/cjs/utils/nextRunAt.d.ts +10 -0
  101. package/dist/cjs/utils/nextRunAt.js +119 -0
  102. package/dist/cjs/utils/nextRunAt.js.map +1 -0
  103. package/dist/cjs/utils/priority.d.ts +13 -0
  104. package/dist/cjs/utils/priority.js +23 -0
  105. package/dist/cjs/utils/priority.js.map +1 -0
  106. package/dist/cjs/utils/processEvery.d.ts +1 -0
  107. package/dist/cjs/utils/processEvery.js +13 -0
  108. package/dist/cjs/utils/processEvery.js.map +1 -0
  109. package/dist/cjs/utils/stack.d.ts +1 -0
  110. package/dist/cjs/utils/stack.js +21 -0
  111. package/dist/cjs/utils/stack.js.map +1 -0
  112. package/dist/cjs/version.d.ts +9 -0
  113. package/dist/cjs/version.js +13 -0
  114. package/dist/cjs/version.js.map +1 -0
  115. package/dist/decorators/Define.d.ts +33 -0
  116. package/dist/decorators/Define.js +44 -0
  117. package/dist/decorators/Define.js.map +1 -0
  118. package/dist/decorators/Every.d.ts +43 -0
  119. package/dist/decorators/Every.js +55 -0
  120. package/dist/decorators/Every.js.map +1 -0
  121. package/dist/decorators/JobsController.d.ts +43 -0
  122. package/dist/decorators/JobsController.js +37 -0
  123. package/dist/decorators/JobsController.js.map +1 -0
  124. package/dist/decorators/index.d.ts +41 -0
  125. package/dist/decorators/index.js +46 -0
  126. package/dist/decorators/index.js.map +1 -0
  127. package/dist/decorators/metadata.d.ts +50 -0
  128. package/dist/decorators/metadata.js +35 -0
  129. package/dist/decorators/metadata.js.map +1 -0
  130. package/dist/decorators/register.d.ts +52 -0
  131. package/dist/decorators/register.js +139 -0
  132. package/dist/decorators/register.js.map +1 -0
  133. package/dist/index.d.ts +498 -0
  134. package/dist/index.js +744 -0
  135. package/dist/index.js.map +1 -0
  136. package/dist/notifications/BaseNotificationChannel.d.ts +47 -0
  137. package/dist/notifications/BaseNotificationChannel.js +108 -0
  138. package/dist/notifications/BaseNotificationChannel.js.map +1 -0
  139. package/dist/notifications/InMemoryNotificationChannel.d.ts +13 -0
  140. package/dist/notifications/InMemoryNotificationChannel.js +33 -0
  141. package/dist/notifications/InMemoryNotificationChannel.js.map +1 -0
  142. package/dist/notifications/index.d.ts +2 -0
  143. package/dist/notifications/index.js +3 -0
  144. package/dist/notifications/index.js.map +1 -0
  145. package/dist/types/AgendaBackend.d.ts +61 -0
  146. package/dist/types/AgendaBackend.js +2 -0
  147. package/dist/types/AgendaBackend.js.map +1 -0
  148. package/dist/types/AgendaConfig.d.ts +10 -0
  149. package/dist/types/AgendaConfig.js +2 -0
  150. package/dist/types/AgendaConfig.js.map +1 -0
  151. package/dist/types/AgendaStatus.d.ts +32 -0
  152. package/dist/types/AgendaStatus.js +2 -0
  153. package/dist/types/AgendaStatus.js.map +1 -0
  154. package/dist/types/DbOptions.d.ts +4 -0
  155. package/dist/types/DbOptions.js +2 -0
  156. package/dist/types/DbOptions.js.map +1 -0
  157. package/dist/types/DrainOptions.d.ts +37 -0
  158. package/dist/types/DrainOptions.js +2 -0
  159. package/dist/types/DrainOptions.js.map +1 -0
  160. package/dist/types/JobDefinition.d.ts +49 -0
  161. package/dist/types/JobDefinition.js +2 -0
  162. package/dist/types/JobDefinition.js.map +1 -0
  163. package/dist/types/JobLogger.d.ts +111 -0
  164. package/dist/types/JobLogger.js +2 -0
  165. package/dist/types/JobLogger.js.map +1 -0
  166. package/dist/types/JobParameters.d.ts +106 -0
  167. package/dist/types/JobParameters.js +17 -0
  168. package/dist/types/JobParameters.js.map +1 -0
  169. package/dist/types/JobQuery.d.ts +79 -0
  170. package/dist/types/JobQuery.js +33 -0
  171. package/dist/types/JobQuery.js.map +1 -0
  172. package/dist/types/JobRepository.d.ts +101 -0
  173. package/dist/types/JobRepository.js +2 -0
  174. package/dist/types/JobRepository.js.map +1 -0
  175. package/dist/types/NotificationChannel.d.ts +120 -0
  176. package/dist/types/NotificationChannel.js +2 -0
  177. package/dist/types/NotificationChannel.js.map +1 -0
  178. package/dist/utils/backoff.d.ts +152 -0
  179. package/dist/utils/backoff.js +167 -0
  180. package/dist/utils/backoff.js.map +1 -0
  181. package/dist/utils/dateConstraints.d.ts +48 -0
  182. package/dist/utils/dateConstraints.js +117 -0
  183. package/dist/utils/dateConstraints.js.map +1 -0
  184. package/dist/utils/isValidDate.d.ts +1 -0
  185. package/dist/utils/isValidDate.js +5 -0
  186. package/dist/utils/isValidDate.js.map +1 -0
  187. package/dist/utils/nextRunAt.d.ts +10 -0
  188. package/dist/utils/nextRunAt.js +110 -0
  189. package/dist/utils/nextRunAt.js.map +1 -0
  190. package/dist/utils/priority.d.ts +13 -0
  191. package/dist/utils/priority.js +20 -0
  192. package/dist/utils/priority.js.map +1 -0
  193. package/dist/utils/processEvery.d.ts +1 -0
  194. package/dist/utils/processEvery.js +7 -0
  195. package/dist/utils/processEvery.js.map +1 -0
  196. package/dist/utils/stack.d.ts +1 -0
  197. package/dist/utils/stack.js +18 -0
  198. package/dist/utils/stack.js.map +1 -0
  199. package/dist/version.d.ts +9 -0
  200. package/dist/version.js +10 -0
  201. package/dist/version.js.map +1 -0
  202. package/package.json +79 -0
  203. package/test/fixtures/forkHelper.js +40 -0
  204. package/test/fixtures/someJobDefinition.ts +12 -0
  205. package/test/shared/agenda-test-suite.ts +1876 -0
  206. package/test/shared/backoff-test-suite.ts +408 -0
  207. package/test/shared/debounce-test-suite.ts +377 -0
  208. package/test/shared/full-test-suite.ts +161 -0
  209. package/test/shared/index.ts +50 -0
  210. package/test/shared/joblogger-test-suite.ts +305 -0
  211. package/test/shared/jobprocessor-test-suite.ts +266 -0
  212. package/test/shared/notification-channel-test-suite.ts +471 -0
  213. package/test/shared/removeoncomplete-test-suite.ts +228 -0
  214. package/test/shared/repository-test-suite.ts +708 -0
  215. package/test/shared/retry-test-suite.ts +174 -0
  216. package/test/shared/test-utils.ts +100 -0
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Backoff test factory
3
+ *
4
+ * This file exports a test factory for job backoff/retry tests.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
8
+ import type { AgendaBackend, RetryDetails, JobWithId } from '../../src/index.js';
9
+ import {
10
+ Agenda,
11
+ backoffStrategies,
12
+ constant,
13
+ linear,
14
+ exponential,
15
+ combine,
16
+ when
17
+ } from '../../src/index.js';
18
+
19
+ export interface BackoffTestConfig {
20
+ /** Name for the test suite */
21
+ name: string;
22
+ /** Factory to create a fresh backend instance */
23
+ createBackend: () => Promise<AgendaBackend>;
24
+ /** Cleanup function called after tests */
25
+ cleanupBackend: (backend: AgendaBackend) => Promise<void>;
26
+ /** Clear all jobs between tests */
27
+ clearJobs: (backend: AgendaBackend) => Promise<void>;
28
+ }
29
+
30
+ /**
31
+ * Backoff/Retry tests
32
+ */
33
+ export function backoffTestSuite(config: BackoffTestConfig): void {
34
+ describe(`${config.name} - Backoff Strategies`, () => {
35
+ let backend: AgendaBackend;
36
+ let agenda: Agenda;
37
+
38
+ beforeAll(async () => {
39
+ backend = await config.createBackend();
40
+ });
41
+
42
+ afterAll(async () => {
43
+ await config.cleanupBackend(backend);
44
+ });
45
+
46
+ beforeEach(async () => {
47
+ await config.clearJobs(backend);
48
+ agenda = new Agenda({
49
+ backend,
50
+ processEvery: 50 // Fast polling for tests
51
+ });
52
+ await agenda.ready;
53
+ });
54
+
55
+ afterEach(async () => {
56
+ if (agenda) {
57
+ await agenda.stop();
58
+ }
59
+ await config.clearJobs(backend);
60
+ });
61
+
62
+ describe('constant backoff', () => {
63
+ it('should return the same delay for each attempt', () => {
64
+ const strategy = constant({ delay: 1000, maxRetries: 3 });
65
+
66
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
67
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
68
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
69
+ expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
70
+ });
71
+
72
+ it('should respect maxDelay', () => {
73
+ const strategy = constant({ delay: 5000, maxDelay: 2000, maxRetries: 3 });
74
+
75
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(2000);
76
+ });
77
+ });
78
+
79
+ describe('linear backoff', () => {
80
+ it('should increase delay linearly', () => {
81
+ const strategy = linear({ delay: 1000, increment: 1000, maxRetries: 4 });
82
+
83
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
84
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(2000);
85
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(3000);
86
+ expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(4000);
87
+ expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
88
+ });
89
+
90
+ it('should use delay as default increment', () => {
91
+ const strategy = linear({ delay: 500, maxRetries: 3 });
92
+
93
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(500);
94
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
95
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1500);
96
+ });
97
+
98
+ it('should respect maxDelay', () => {
99
+ const strategy = linear({ delay: 1000, increment: 2000, maxDelay: 3000, maxRetries: 5 });
100
+
101
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
102
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(3000); // capped
103
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(3000); // capped
104
+ });
105
+ });
106
+
107
+ describe('exponential backoff', () => {
108
+ it('should increase delay exponentially', () => {
109
+ const strategy = exponential({ delay: 100, factor: 2, maxRetries: 5 });
110
+
111
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
112
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(200);
113
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(400);
114
+ expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(800);
115
+ expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1600);
116
+ expect(strategy({ attempt: 6, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
117
+ });
118
+
119
+ it('should respect maxDelay', () => {
120
+ const strategy = exponential({ delay: 100, factor: 2, maxDelay: 500, maxRetries: 10 });
121
+
122
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
123
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(200);
124
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(400);
125
+ expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(500); // capped
126
+ expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(500); // capped
127
+ });
128
+
129
+ it('should apply jitter when configured', () => {
130
+ const strategy = exponential({ delay: 1000, factor: 2, maxRetries: 3, jitter: 0.5 });
131
+
132
+ // Run multiple times to check jitter variation
133
+ const results = new Set<number>();
134
+ for (let i = 0; i < 20; i++) {
135
+ const delay = strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} });
136
+ if (delay !== null) results.add(delay);
137
+ }
138
+
139
+ // With 50% jitter, we should see variation (500-1500 range)
140
+ expect(results.size).toBeGreaterThan(1);
141
+
142
+ // All values should be within jitter range
143
+ for (const delay of results) {
144
+ expect(delay).toBeGreaterThanOrEqual(500);
145
+ expect(delay).toBeLessThanOrEqual(1500);
146
+ }
147
+ });
148
+ });
149
+
150
+ describe('combine strategy', () => {
151
+ it('should try strategies in sequence', () => {
152
+ const strategy = combine(
153
+ // First 2 attempts: 100ms
154
+ (ctx) => ctx.attempt <= 2 ? 100 : null,
155
+ // Next 2 attempts: use exponential-like custom logic
156
+ // (since exponential counts globally, we need custom logic to handle offset)
157
+ (ctx) => {
158
+ if (ctx.attempt > 4) return null;
159
+ // Exponential from attempt 3 onwards
160
+ const adjustedAttempt = ctx.attempt - 2; // 3->1, 4->2
161
+ return 1000 * Math.pow(2, adjustedAttempt - 1);
162
+ }
163
+ );
164
+
165
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
166
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
167
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
168
+ expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(2000);
169
+ expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
170
+ });
171
+ });
172
+
173
+ describe('when (conditional) strategy', () => {
174
+ it('should only retry when condition is met', () => {
175
+ const strategy = when(
176
+ (ctx) => ctx.error.message.includes('timeout'),
177
+ constant({ delay: 1000, maxRetries: 3 })
178
+ );
179
+
180
+ const timeoutError = new Error('connection timeout');
181
+ const otherError = new Error('validation failed');
182
+
183
+ expect(strategy({ attempt: 1, error: timeoutError, jobName: 'test', jobData: {} })).toBe(1000);
184
+ expect(strategy({ attempt: 1, error: otherError, jobName: 'test', jobData: {} })).toBeNull();
185
+ });
186
+ });
187
+
188
+ describe('preset strategies', () => {
189
+ it('aggressive should have fast retries', () => {
190
+ const strategy = backoffStrategies.aggressive();
191
+
192
+ expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
193
+ expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(200);
194
+ expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(400);
195
+ expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
196
+ });
197
+
198
+ it('standard should have balanced retries', () => {
199
+ const strategy = backoffStrategies.standard();
200
+
201
+ // First attempt should be around 1000 (with some jitter)
202
+ const delay = strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} });
203
+ expect(delay).toBeGreaterThanOrEqual(900);
204
+ expect(delay).toBeLessThanOrEqual(1100);
205
+ });
206
+ });
207
+
208
+ describe('auto-retry integration', () => {
209
+ it('should automatically retry with backoff strategy', async () => {
210
+ let attempts = 0;
211
+ const retryDelays: number[] = [];
212
+
213
+ agenda.define(
214
+ 'auto-retry-job',
215
+ async () => {
216
+ attempts++;
217
+ if (attempts < 3) {
218
+ throw new Error(`Attempt ${attempts} failed`);
219
+ }
220
+ },
221
+ {
222
+ backoff: constant({ delay: 100, maxRetries: 5 })
223
+ }
224
+ );
225
+
226
+ agenda.on('retry:auto-retry-job', (_job: JobWithId, details: RetryDetails) => {
227
+ retryDelays.push(details.delay);
228
+ });
229
+
230
+ const successPromise = new Promise(resolve => {
231
+ agenda.on('success:auto-retry-job', resolve);
232
+ });
233
+
234
+ await agenda.now('auto-retry-job');
235
+ await agenda.start();
236
+ await successPromise;
237
+
238
+ expect(attempts).toBe(3);
239
+ expect(retryDelays).toHaveLength(2);
240
+ expect(retryDelays[0]).toBe(100);
241
+ expect(retryDelays[1]).toBe(100);
242
+ });
243
+
244
+ it('should emit retry exhausted when max retries reached', async () => {
245
+ let attempts = 0;
246
+
247
+ agenda.define(
248
+ 'exhausted-job',
249
+ async () => {
250
+ attempts++;
251
+ throw new Error('Always fails');
252
+ },
253
+ {
254
+ backoff: constant({ delay: 50, maxRetries: 2 })
255
+ }
256
+ );
257
+
258
+ const exhaustedPromise = new Promise<{ error: Error; job: JobWithId }>(resolve => {
259
+ agenda.on('retry exhausted:exhausted-job', (error: Error, job: JobWithId) => {
260
+ resolve({ error, job });
261
+ });
262
+ });
263
+
264
+ await agenda.now('exhausted-job');
265
+ await agenda.start();
266
+
267
+ const { error, job } = await exhaustedPromise;
268
+
269
+ expect(attempts).toBe(3); // Initial + 2 retries
270
+ expect(error.message).toBe('Always fails');
271
+ expect(job.attrs.failCount).toBe(3);
272
+ });
273
+
274
+ it('should provide retry details in event', async () => {
275
+ let retryDetails: RetryDetails | null = null;
276
+
277
+ agenda.define(
278
+ 'details-job',
279
+ async () => {
280
+ throw new Error('test error');
281
+ },
282
+ {
283
+ backoff: exponential({ delay: 100, maxRetries: 1 })
284
+ }
285
+ );
286
+
287
+ agenda.on('retry:details-job', (_job: JobWithId, details: RetryDetails) => {
288
+ retryDetails = details;
289
+ });
290
+
291
+ const exhaustedPromise = new Promise(resolve => {
292
+ agenda.on('retry exhausted:details-job', resolve);
293
+ });
294
+
295
+ await agenda.now('details-job');
296
+ await agenda.start();
297
+ await exhaustedPromise;
298
+
299
+ expect(retryDetails).not.toBeNull();
300
+ expect(retryDetails!.attempt).toBe(1);
301
+ expect(retryDetails!.delay).toBe(100);
302
+ expect(retryDetails!.error.message).toBe('test error');
303
+ expect(retryDetails!.nextRunAt).toBeInstanceOf(Date);
304
+ });
305
+
306
+ it('should not auto-retry jobs without backoff configured', { timeout: 5000 }, async () => {
307
+ let attempts = 0;
308
+ let retryCount = 0;
309
+
310
+ agenda.define('no-backoff-job', async () => {
311
+ attempts++;
312
+ throw new Error('Always fails');
313
+ });
314
+
315
+ agenda.on('retry:no-backoff-job', () => {
316
+ retryCount++;
317
+ });
318
+
319
+ const failPromise = new Promise(resolve => {
320
+ agenda.on('fail:no-backoff-job', resolve);
321
+ });
322
+
323
+ await agenda.now('no-backoff-job');
324
+ await agenda.start();
325
+ await failPromise;
326
+
327
+ // Wait a bit to ensure no retries happen
328
+ await new Promise(r => setTimeout(r, 200));
329
+
330
+ expect(attempts).toBe(1);
331
+ expect(retryCount).toBe(0);
332
+ });
333
+
334
+ it('should auto-retry repeating jobs when backoff is configured', { timeout: 5000 }, async () => {
335
+ let attempts = 0;
336
+ let retryCount = 0;
337
+
338
+ agenda.define(
339
+ 'repeating-job',
340
+ async () => {
341
+ attempts++;
342
+ if (attempts === 1) {
343
+ throw new Error('First attempt fails');
344
+ }
345
+ },
346
+ {
347
+ backoff: constant({ delay: 50, maxRetries: 3 })
348
+ }
349
+ );
350
+
351
+ agenda.on('retry:repeating-job', () => {
352
+ retryCount++;
353
+ });
354
+
355
+ // Use every() which creates a repeating job
356
+ await agenda.every('1 second', 'repeating-job');
357
+ await agenda.start();
358
+
359
+ // Wait for job to succeed (after retry)
360
+ await new Promise(resolve => {
361
+ agenda.on('success:repeating-job', resolve);
362
+ });
363
+
364
+ // Should have triggered auto-retry since backoff is configured
365
+ expect(retryCount).toBe(1);
366
+ expect(attempts).toBe(2); // First attempt failed, second succeeded
367
+ });
368
+
369
+ it('should work with custom backoff function', async () => {
370
+ let attempts = 0;
371
+ const delays: number[] = [];
372
+
373
+ agenda.define(
374
+ 'custom-backoff-job',
375
+ async () => {
376
+ attempts++;
377
+ if (attempts < 4) {
378
+ throw new Error('Failing');
379
+ }
380
+ },
381
+ {
382
+ backoff: (ctx) => {
383
+ // Custom: fibonacci-like sequence
384
+ if (ctx.attempt > 3) return null;
385
+ const fibDelays = [100, 100, 200];
386
+ return fibDelays[ctx.attempt - 1];
387
+ }
388
+ }
389
+ );
390
+
391
+ agenda.on('retry:custom-backoff-job', (_job: JobWithId, details: RetryDetails) => {
392
+ delays.push(details.delay);
393
+ });
394
+
395
+ const successPromise = new Promise(resolve => {
396
+ agenda.on('success:custom-backoff-job', resolve);
397
+ });
398
+
399
+ await agenda.now('custom-backoff-job');
400
+ await agenda.start();
401
+ await successPromise;
402
+
403
+ expect(attempts).toBe(4);
404
+ expect(delays).toEqual([100, 100, 200]);
405
+ });
406
+ });
407
+ });
408
+ }