@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.
- package/LICENSE.md +25 -0
- package/README.md +380 -0
- package/dist/Job.d.ts +190 -0
- package/dist/Job.js +689 -0
- package/dist/Job.js.map +1 -0
- package/dist/JobProcessingQueue.d.ts +47 -0
- package/dist/JobProcessingQueue.js +107 -0
- package/dist/JobProcessingQueue.js.map +1 -0
- package/dist/JobProcessor.d.ts +72 -0
- package/dist/JobProcessor.js +512 -0
- package/dist/JobProcessor.js.map +1 -0
- package/dist/backends/index.d.ts +18 -0
- package/dist/backends/index.js +20 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/cjs/Job.d.ts +190 -0
- package/dist/cjs/Job.js +696 -0
- package/dist/cjs/Job.js.map +1 -0
- package/dist/cjs/JobProcessingQueue.d.ts +47 -0
- package/dist/cjs/JobProcessingQueue.js +111 -0
- package/dist/cjs/JobProcessingQueue.js.map +1 -0
- package/dist/cjs/JobProcessor.d.ts +72 -0
- package/dist/cjs/JobProcessor.js +519 -0
- package/dist/cjs/JobProcessor.js.map +1 -0
- package/dist/cjs/backends/index.d.ts +17 -0
- package/dist/cjs/backends/index.js +20 -0
- package/dist/cjs/backends/index.js.map +1 -0
- package/dist/cjs/decorators/Define.d.ts +33 -0
- package/dist/cjs/decorators/Define.js +47 -0
- package/dist/cjs/decorators/Define.js.map +1 -0
- package/dist/cjs/decorators/Every.d.ts +43 -0
- package/dist/cjs/decorators/Every.js +58 -0
- package/dist/cjs/decorators/Every.js.map +1 -0
- package/dist/cjs/decorators/JobsController.d.ts +43 -0
- package/dist/cjs/decorators/JobsController.js +40 -0
- package/dist/cjs/decorators/JobsController.js.map +1 -0
- package/dist/cjs/decorators/index.d.ts +41 -0
- package/dist/cjs/decorators/index.js +57 -0
- package/dist/cjs/decorators/index.js.map +1 -0
- package/dist/cjs/decorators/metadata.d.ts +50 -0
- package/dist/cjs/decorators/metadata.js +41 -0
- package/dist/cjs/decorators/metadata.js.map +1 -0
- package/dist/cjs/decorators/register.d.ts +52 -0
- package/dist/cjs/decorators/register.js +145 -0
- package/dist/cjs/decorators/register.js.map +1 -0
- package/dist/cjs/index.d.ts +498 -0
- package/dist/cjs/index.js +776 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/notifications/BaseNotificationChannel.d.ts +47 -0
- package/dist/cjs/notifications/BaseNotificationChannel.js +112 -0
- package/dist/cjs/notifications/BaseNotificationChannel.js.map +1 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.d.ts +13 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.js +37 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.js.map +1 -0
- package/dist/cjs/notifications/index.d.ts +2 -0
- package/dist/cjs/notifications/index.js +8 -0
- package/dist/cjs/notifications/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/AgendaBackend.d.ts +61 -0
- package/dist/cjs/types/AgendaBackend.js +3 -0
- package/dist/cjs/types/AgendaBackend.js.map +1 -0
- package/dist/cjs/types/AgendaConfig.d.ts +10 -0
- package/dist/cjs/types/AgendaConfig.js +3 -0
- package/dist/cjs/types/AgendaConfig.js.map +1 -0
- package/dist/cjs/types/AgendaStatus.d.ts +32 -0
- package/dist/cjs/types/AgendaStatus.js +3 -0
- package/dist/cjs/types/AgendaStatus.js.map +1 -0
- package/dist/cjs/types/DbOptions.d.ts +4 -0
- package/dist/cjs/types/DbOptions.js +3 -0
- package/dist/cjs/types/DbOptions.js.map +1 -0
- package/dist/cjs/types/DrainOptions.d.ts +37 -0
- package/dist/cjs/types/DrainOptions.js +3 -0
- package/dist/cjs/types/DrainOptions.js.map +1 -0
- package/dist/cjs/types/JobDefinition.d.ts +49 -0
- package/dist/cjs/types/JobDefinition.js +3 -0
- package/dist/cjs/types/JobDefinition.js.map +1 -0
- package/dist/cjs/types/JobLogger.d.ts +111 -0
- package/dist/cjs/types/JobLogger.js +3 -0
- package/dist/cjs/types/JobLogger.js.map +1 -0
- package/dist/cjs/types/JobParameters.d.ts +106 -0
- package/dist/cjs/types/JobParameters.js +21 -0
- package/dist/cjs/types/JobParameters.js.map +1 -0
- package/dist/cjs/types/JobQuery.d.ts +79 -0
- package/dist/cjs/types/JobQuery.js +36 -0
- package/dist/cjs/types/JobQuery.js.map +1 -0
- package/dist/cjs/types/JobRepository.d.ts +101 -0
- package/dist/cjs/types/JobRepository.js +3 -0
- package/dist/cjs/types/JobRepository.js.map +1 -0
- package/dist/cjs/types/NotificationChannel.d.ts +120 -0
- package/dist/cjs/types/NotificationChannel.js +3 -0
- package/dist/cjs/types/NotificationChannel.js.map +1 -0
- package/dist/cjs/utils/backoff.d.ts +152 -0
- package/dist/cjs/utils/backoff.js +175 -0
- package/dist/cjs/utils/backoff.js.map +1 -0
- package/dist/cjs/utils/dateConstraints.d.ts +48 -0
- package/dist/cjs/utils/dateConstraints.js +127 -0
- package/dist/cjs/utils/dateConstraints.js.map +1 -0
- package/dist/cjs/utils/isValidDate.d.ts +1 -0
- package/dist/cjs/utils/isValidDate.js +8 -0
- package/dist/cjs/utils/isValidDate.js.map +1 -0
- package/dist/cjs/utils/nextRunAt.d.ts +10 -0
- package/dist/cjs/utils/nextRunAt.js +119 -0
- package/dist/cjs/utils/nextRunAt.js.map +1 -0
- package/dist/cjs/utils/priority.d.ts +13 -0
- package/dist/cjs/utils/priority.js +23 -0
- package/dist/cjs/utils/priority.js.map +1 -0
- package/dist/cjs/utils/processEvery.d.ts +1 -0
- package/dist/cjs/utils/processEvery.js +13 -0
- package/dist/cjs/utils/processEvery.js.map +1 -0
- package/dist/cjs/utils/stack.d.ts +1 -0
- package/dist/cjs/utils/stack.js +21 -0
- package/dist/cjs/utils/stack.js.map +1 -0
- package/dist/cjs/version.d.ts +9 -0
- package/dist/cjs/version.js +13 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/decorators/Define.d.ts +33 -0
- package/dist/decorators/Define.js +44 -0
- package/dist/decorators/Define.js.map +1 -0
- package/dist/decorators/Every.d.ts +43 -0
- package/dist/decorators/Every.js +55 -0
- package/dist/decorators/Every.js.map +1 -0
- package/dist/decorators/JobsController.d.ts +43 -0
- package/dist/decorators/JobsController.js +37 -0
- package/dist/decorators/JobsController.js.map +1 -0
- package/dist/decorators/index.d.ts +41 -0
- package/dist/decorators/index.js +46 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/metadata.d.ts +50 -0
- package/dist/decorators/metadata.js +35 -0
- package/dist/decorators/metadata.js.map +1 -0
- package/dist/decorators/register.d.ts +52 -0
- package/dist/decorators/register.js +139 -0
- package/dist/decorators/register.js.map +1 -0
- package/dist/index.d.ts +498 -0
- package/dist/index.js +744 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/BaseNotificationChannel.d.ts +47 -0
- package/dist/notifications/BaseNotificationChannel.js +108 -0
- package/dist/notifications/BaseNotificationChannel.js.map +1 -0
- package/dist/notifications/InMemoryNotificationChannel.d.ts +13 -0
- package/dist/notifications/InMemoryNotificationChannel.js +33 -0
- package/dist/notifications/InMemoryNotificationChannel.js.map +1 -0
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.js +3 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/types/AgendaBackend.d.ts +61 -0
- package/dist/types/AgendaBackend.js +2 -0
- package/dist/types/AgendaBackend.js.map +1 -0
- package/dist/types/AgendaConfig.d.ts +10 -0
- package/dist/types/AgendaConfig.js +2 -0
- package/dist/types/AgendaConfig.js.map +1 -0
- package/dist/types/AgendaStatus.d.ts +32 -0
- package/dist/types/AgendaStatus.js +2 -0
- package/dist/types/AgendaStatus.js.map +1 -0
- package/dist/types/DbOptions.d.ts +4 -0
- package/dist/types/DbOptions.js +2 -0
- package/dist/types/DbOptions.js.map +1 -0
- package/dist/types/DrainOptions.d.ts +37 -0
- package/dist/types/DrainOptions.js +2 -0
- package/dist/types/DrainOptions.js.map +1 -0
- package/dist/types/JobDefinition.d.ts +49 -0
- package/dist/types/JobDefinition.js +2 -0
- package/dist/types/JobDefinition.js.map +1 -0
- package/dist/types/JobLogger.d.ts +111 -0
- package/dist/types/JobLogger.js +2 -0
- package/dist/types/JobLogger.js.map +1 -0
- package/dist/types/JobParameters.d.ts +106 -0
- package/dist/types/JobParameters.js +17 -0
- package/dist/types/JobParameters.js.map +1 -0
- package/dist/types/JobQuery.d.ts +79 -0
- package/dist/types/JobQuery.js +33 -0
- package/dist/types/JobQuery.js.map +1 -0
- package/dist/types/JobRepository.d.ts +101 -0
- package/dist/types/JobRepository.js +2 -0
- package/dist/types/JobRepository.js.map +1 -0
- package/dist/types/NotificationChannel.d.ts +120 -0
- package/dist/types/NotificationChannel.js +2 -0
- package/dist/types/NotificationChannel.js.map +1 -0
- package/dist/utils/backoff.d.ts +152 -0
- package/dist/utils/backoff.js +167 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/dateConstraints.d.ts +48 -0
- package/dist/utils/dateConstraints.js +117 -0
- package/dist/utils/dateConstraints.js.map +1 -0
- package/dist/utils/isValidDate.d.ts +1 -0
- package/dist/utils/isValidDate.js +5 -0
- package/dist/utils/isValidDate.js.map +1 -0
- package/dist/utils/nextRunAt.d.ts +10 -0
- package/dist/utils/nextRunAt.js +110 -0
- package/dist/utils/nextRunAt.js.map +1 -0
- package/dist/utils/priority.d.ts +13 -0
- package/dist/utils/priority.js +20 -0
- package/dist/utils/priority.js.map +1 -0
- package/dist/utils/processEvery.d.ts +1 -0
- package/dist/utils/processEvery.js +7 -0
- package/dist/utils/processEvery.js.map +1 -0
- package/dist/utils/stack.d.ts +1 -0
- package/dist/utils/stack.js +18 -0
- package/dist/utils/stack.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +79 -0
- package/test/fixtures/forkHelper.js +40 -0
- package/test/fixtures/someJobDefinition.ts +12 -0
- package/test/shared/agenda-test-suite.ts +1876 -0
- package/test/shared/backoff-test-suite.ts +408 -0
- package/test/shared/debounce-test-suite.ts +377 -0
- package/test/shared/full-test-suite.ts +161 -0
- package/test/shared/index.ts +50 -0
- package/test/shared/joblogger-test-suite.ts +305 -0
- package/test/shared/jobprocessor-test-suite.ts +266 -0
- package/test/shared/notification-channel-test-suite.ts +471 -0
- package/test/shared/removeoncomplete-test-suite.ts +228 -0
- package/test/shared/repository-test-suite.ts +708 -0
- package/test/shared/retry-test-suite.ts +174 -0
- package/test/shared/test-utils.ts +100 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce test factory
|
|
3
|
+
*
|
|
4
|
+
* This file exports a test factory for job debounce tests.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
|
8
|
+
import type { AgendaBackend } from '../../src/index.js';
|
|
9
|
+
import { Agenda } from '../../src/index.js';
|
|
10
|
+
|
|
11
|
+
export interface DebounceTestConfig {
|
|
12
|
+
/** Name for the test suite */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Factory to create a fresh backend instance */
|
|
15
|
+
createBackend: () => Promise<AgendaBackend>;
|
|
16
|
+
/** Cleanup function called after tests */
|
|
17
|
+
cleanupBackend: (backend: AgendaBackend) => Promise<void>;
|
|
18
|
+
/** Clear all jobs between tests */
|
|
19
|
+
clearJobs: (backend: AgendaBackend) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Debounce tests
|
|
24
|
+
*/
|
|
25
|
+
export function debounceTestSuite(config: DebounceTestConfig): void {
|
|
26
|
+
describe(`${config.name} - Debounce`, () => {
|
|
27
|
+
let backend: AgendaBackend;
|
|
28
|
+
let agenda: Agenda;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
backend = await config.createBackend();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await config.cleanupBackend(backend);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
await config.clearJobs(backend);
|
|
40
|
+
agenda = new Agenda({
|
|
41
|
+
backend,
|
|
42
|
+
processEvery: 50 // Fast polling for tests
|
|
43
|
+
});
|
|
44
|
+
await agenda.ready;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
if (agenda) {
|
|
49
|
+
await agenda.stop();
|
|
50
|
+
}
|
|
51
|
+
await config.clearJobs(backend);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('trailing debounce (default)', () => {
|
|
55
|
+
it('should delay job execution by the debounce delay', async () => {
|
|
56
|
+
agenda.define('debounceTest', async () => {});
|
|
57
|
+
|
|
58
|
+
const beforeSave = Date.now();
|
|
59
|
+
const job = await agenda
|
|
60
|
+
.create('debounceTest', { key: 'test1' })
|
|
61
|
+
.unique({ 'data.key': 'test1' })
|
|
62
|
+
.debounce(1000)
|
|
63
|
+
.save();
|
|
64
|
+
|
|
65
|
+
const afterSave = Date.now();
|
|
66
|
+
|
|
67
|
+
// nextRunAt should be approximately now + 1000ms
|
|
68
|
+
expect(job.attrs.nextRunAt).toBeDefined();
|
|
69
|
+
const nextRunAt = job.attrs.nextRunAt!.getTime();
|
|
70
|
+
expect(nextRunAt).toBeGreaterThanOrEqual(beforeSave + 1000);
|
|
71
|
+
expect(nextRunAt).toBeLessThanOrEqual(afterSave + 1000 + 100); // Allow 100ms tolerance
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should reset the timer on subsequent saves', async () => {
|
|
75
|
+
agenda.define('debounceTest', async () => {});
|
|
76
|
+
|
|
77
|
+
// First save
|
|
78
|
+
const job1 = await agenda
|
|
79
|
+
.create('debounceTest', { key: 'test2' })
|
|
80
|
+
.unique({ 'data.key': 'test2' })
|
|
81
|
+
.debounce(1000)
|
|
82
|
+
.save();
|
|
83
|
+
|
|
84
|
+
const firstNextRunAt = job1.attrs.nextRunAt!.getTime();
|
|
85
|
+
|
|
86
|
+
// Wait a bit
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
88
|
+
|
|
89
|
+
// Second save should push nextRunAt forward
|
|
90
|
+
const job2 = await agenda
|
|
91
|
+
.create('debounceTest', { key: 'test2', updated: true })
|
|
92
|
+
.unique({ 'data.key': 'test2' })
|
|
93
|
+
.debounce(1000)
|
|
94
|
+
.save();
|
|
95
|
+
|
|
96
|
+
const secondNextRunAt = job2.attrs.nextRunAt!.getTime();
|
|
97
|
+
|
|
98
|
+
// Second nextRunAt should be later than first
|
|
99
|
+
expect(secondNextRunAt).toBeGreaterThan(firstNextRunAt);
|
|
100
|
+
// And should be approximately 1000ms from now (not from first save)
|
|
101
|
+
expect(secondNextRunAt).toBeGreaterThanOrEqual(Date.now() + 900);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should update job data on subsequent saves', async () => {
|
|
105
|
+
agenda.define('debounceTest', async () => {});
|
|
106
|
+
|
|
107
|
+
// First save
|
|
108
|
+
await agenda
|
|
109
|
+
.create('debounceTest', { key: 'test3', value: 'first' })
|
|
110
|
+
.unique({ 'data.key': 'test3' })
|
|
111
|
+
.debounce(1000)
|
|
112
|
+
.save();
|
|
113
|
+
|
|
114
|
+
// Second save with updated data
|
|
115
|
+
const job2 = await agenda
|
|
116
|
+
.create('debounceTest', { key: 'test3', value: 'second' })
|
|
117
|
+
.unique({ 'data.key': 'test3' })
|
|
118
|
+
.debounce(1000)
|
|
119
|
+
.save();
|
|
120
|
+
|
|
121
|
+
// Data should be updated to the latest
|
|
122
|
+
expect(job2.attrs.data).toEqual({ key: 'test3', value: 'second' });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should only create one job in the database', async () => {
|
|
126
|
+
agenda.define('debounceTest', async () => {});
|
|
127
|
+
|
|
128
|
+
// Multiple rapid saves
|
|
129
|
+
for (let i = 0; i < 5; i++) {
|
|
130
|
+
await agenda
|
|
131
|
+
.create('debounceTest', { key: 'test4', iteration: i })
|
|
132
|
+
.unique({ 'data.key': 'test4' })
|
|
133
|
+
.debounce(500)
|
|
134
|
+
.save();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Query jobs
|
|
138
|
+
const result = await agenda.queryJobs({ name: 'debounceTest' });
|
|
139
|
+
expect(result.jobs.length).toBe(1);
|
|
140
|
+
expect(result.jobs[0].data).toEqual({ key: 'test4', iteration: 4 });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('leading debounce', () => {
|
|
145
|
+
it('should execute immediately on first call', async () => {
|
|
146
|
+
agenda.define('debounceLeading', async () => {});
|
|
147
|
+
|
|
148
|
+
const beforeSave = Date.now();
|
|
149
|
+
const job = await agenda
|
|
150
|
+
.create('debounceLeading', { key: 'lead1' })
|
|
151
|
+
.unique({ 'data.key': 'lead1' })
|
|
152
|
+
.debounce(5000, { strategy: 'leading' })
|
|
153
|
+
.save();
|
|
154
|
+
|
|
155
|
+
const afterSave = Date.now();
|
|
156
|
+
|
|
157
|
+
// nextRunAt should be approximately now (immediate execution)
|
|
158
|
+
expect(job.attrs.nextRunAt).toBeDefined();
|
|
159
|
+
const nextRunAt = job.attrs.nextRunAt!.getTime();
|
|
160
|
+
expect(nextRunAt).toBeGreaterThanOrEqual(beforeSave - 100);
|
|
161
|
+
expect(nextRunAt).toBeLessThanOrEqual(afterSave + 100);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should keep original nextRunAt on subsequent saves', async () => {
|
|
165
|
+
agenda.define('debounceLeading', async () => {});
|
|
166
|
+
|
|
167
|
+
// First save
|
|
168
|
+
const job1 = await agenda
|
|
169
|
+
.create('debounceLeading', { key: 'lead2' })
|
|
170
|
+
.unique({ 'data.key': 'lead2' })
|
|
171
|
+
.debounce(5000, { strategy: 'leading' })
|
|
172
|
+
.save();
|
|
173
|
+
|
|
174
|
+
const firstNextRunAt = job1.attrs.nextRunAt!.getTime();
|
|
175
|
+
|
|
176
|
+
// Wait a bit
|
|
177
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
178
|
+
|
|
179
|
+
// Second save should NOT change nextRunAt
|
|
180
|
+
const job2 = await agenda
|
|
181
|
+
.create('debounceLeading', { key: 'lead2', updated: true })
|
|
182
|
+
.unique({ 'data.key': 'lead2' })
|
|
183
|
+
.debounce(5000, { strategy: 'leading' })
|
|
184
|
+
.save();
|
|
185
|
+
|
|
186
|
+
const secondNextRunAt = job2.attrs.nextRunAt!.getTime();
|
|
187
|
+
|
|
188
|
+
// nextRunAt should remain the same
|
|
189
|
+
expect(secondNextRunAt).toBe(firstNextRunAt);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should still update job data on subsequent saves', async () => {
|
|
193
|
+
agenda.define('debounceLeading', async () => {});
|
|
194
|
+
|
|
195
|
+
// First save
|
|
196
|
+
await agenda
|
|
197
|
+
.create('debounceLeading', { key: 'lead3', value: 'first' })
|
|
198
|
+
.unique({ 'data.key': 'lead3' })
|
|
199
|
+
.debounce(5000, { strategy: 'leading' })
|
|
200
|
+
.save();
|
|
201
|
+
|
|
202
|
+
// Second save with updated data
|
|
203
|
+
const job2 = await agenda
|
|
204
|
+
.create('debounceLeading', { key: 'lead3', value: 'second' })
|
|
205
|
+
.unique({ 'data.key': 'lead3' })
|
|
206
|
+
.debounce(5000, { strategy: 'leading' })
|
|
207
|
+
.save();
|
|
208
|
+
|
|
209
|
+
// Data should be updated even though nextRunAt isn't
|
|
210
|
+
expect(job2.attrs.data).toEqual({ key: 'lead3', value: 'second' });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('maxWait', () => {
|
|
215
|
+
it('should force execution when maxWait is exceeded', async () => {
|
|
216
|
+
agenda.define('debounceMaxWait', async () => {});
|
|
217
|
+
|
|
218
|
+
// First save with short maxWait
|
|
219
|
+
const job1 = await agenda
|
|
220
|
+
.create('debounceMaxWait', { key: 'max1' })
|
|
221
|
+
.unique({ 'data.key': 'max1' })
|
|
222
|
+
.debounce(2000, { maxWait: 500 })
|
|
223
|
+
.save();
|
|
224
|
+
|
|
225
|
+
// debounceStartedAt should be set
|
|
226
|
+
expect(job1.attrs.debounceStartedAt).toBeDefined();
|
|
227
|
+
|
|
228
|
+
// Wait for maxWait to be exceeded
|
|
229
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
230
|
+
|
|
231
|
+
// Second save should trigger immediate execution
|
|
232
|
+
const job2 = await agenda
|
|
233
|
+
.create('debounceMaxWait', { key: 'max1', updated: true })
|
|
234
|
+
.unique({ 'data.key': 'max1' })
|
|
235
|
+
.debounce(2000, { maxWait: 500 })
|
|
236
|
+
.save();
|
|
237
|
+
|
|
238
|
+
// nextRunAt should be now (not delayed by 2000ms)
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
expect(job2.attrs.nextRunAt!.getTime()).toBeLessThanOrEqual(now + 100);
|
|
241
|
+
|
|
242
|
+
// debounceStartedAt should be cleared for next cycle
|
|
243
|
+
// (may be undefined or null depending on backend)
|
|
244
|
+
expect(job2.attrs.debounceStartedAt ?? undefined).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should keep debouncing while within maxWait', async () => {
|
|
248
|
+
agenda.define('debounceMaxWait', async () => {});
|
|
249
|
+
|
|
250
|
+
// First save
|
|
251
|
+
const job1 = await agenda
|
|
252
|
+
.create('debounceMaxWait', { key: 'max2' })
|
|
253
|
+
.unique({ 'data.key': 'max2' })
|
|
254
|
+
.debounce(500, { maxWait: 5000 })
|
|
255
|
+
.save();
|
|
256
|
+
|
|
257
|
+
const firstNextRunAt = job1.attrs.nextRunAt!.getTime();
|
|
258
|
+
|
|
259
|
+
// Wait less than maxWait
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
261
|
+
|
|
262
|
+
// Second save should still debounce normally
|
|
263
|
+
const job2 = await agenda
|
|
264
|
+
.create('debounceMaxWait', { key: 'max2', updated: true })
|
|
265
|
+
.unique({ 'data.key': 'max2' })
|
|
266
|
+
.debounce(500, { maxWait: 5000 })
|
|
267
|
+
.save();
|
|
268
|
+
|
|
269
|
+
const secondNextRunAt = job2.attrs.nextRunAt!.getTime();
|
|
270
|
+
|
|
271
|
+
// Should be pushed forward (normal debounce behavior)
|
|
272
|
+
expect(secondNextRunAt).toBeGreaterThan(firstNextRunAt);
|
|
273
|
+
// Should still be delayed by ~500ms from now
|
|
274
|
+
expect(secondNextRunAt).toBeGreaterThanOrEqual(Date.now() + 400);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('debounce with different unique keys', () => {
|
|
279
|
+
it('should debounce independently per unique key', async () => {
|
|
280
|
+
agenda.define('debounceMulti', async () => {});
|
|
281
|
+
|
|
282
|
+
// Save with key 1
|
|
283
|
+
const job1 = await agenda
|
|
284
|
+
.create('debounceMulti', { entityId: 1 })
|
|
285
|
+
.unique({ 'data.entityId': 1 })
|
|
286
|
+
.debounce(1000)
|
|
287
|
+
.save();
|
|
288
|
+
|
|
289
|
+
// Save with key 2
|
|
290
|
+
const job2 = await agenda
|
|
291
|
+
.create('debounceMulti', { entityId: 2 })
|
|
292
|
+
.unique({ 'data.entityId': 2 })
|
|
293
|
+
.debounce(1000)
|
|
294
|
+
.save();
|
|
295
|
+
|
|
296
|
+
// Query jobs
|
|
297
|
+
const result = await agenda.queryJobs({ name: 'debounceMulti' });
|
|
298
|
+
expect(result.jobs.length).toBe(2);
|
|
299
|
+
|
|
300
|
+
// Each should have its own debounce timer
|
|
301
|
+
expect(job1.attrs._id).not.toBe(job2.attrs._id);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('debounce requires unique constraint', () => {
|
|
306
|
+
it('should work only when unique constraint is set', async () => {
|
|
307
|
+
agenda.define('debounceNoUnique', async () => {});
|
|
308
|
+
|
|
309
|
+
// Without unique - creates new jobs each time
|
|
310
|
+
const job1 = await agenda
|
|
311
|
+
.create('debounceNoUnique', { value: 1 })
|
|
312
|
+
.debounce(1000)
|
|
313
|
+
.save();
|
|
314
|
+
|
|
315
|
+
const job2 = await agenda
|
|
316
|
+
.create('debounceNoUnique', { value: 2 })
|
|
317
|
+
.debounce(1000)
|
|
318
|
+
.save();
|
|
319
|
+
|
|
320
|
+
// Should create separate jobs since no unique constraint
|
|
321
|
+
expect(job1.attrs._id).not.toBe(job2.attrs._id);
|
|
322
|
+
|
|
323
|
+
const result = await agenda.queryJobs({ name: 'debounceNoUnique' });
|
|
324
|
+
expect(result.jobs.length).toBe(2);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('job execution with debounce', () => {
|
|
329
|
+
it('should execute job after debounce delay', async () => {
|
|
330
|
+
let executed = false;
|
|
331
|
+
let executedData: unknown;
|
|
332
|
+
|
|
333
|
+
agenda.define('debounceExec', async (job) => {
|
|
334
|
+
executed = true;
|
|
335
|
+
executedData = job.attrs.data;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Multiple saves within debounce window
|
|
339
|
+
await agenda
|
|
340
|
+
.create('debounceExec', { key: 'exec1', value: 1 })
|
|
341
|
+
.unique({ 'data.key': 'exec1' })
|
|
342
|
+
.debounce(200)
|
|
343
|
+
.save();
|
|
344
|
+
|
|
345
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
346
|
+
|
|
347
|
+
await agenda
|
|
348
|
+
.create('debounceExec', { key: 'exec1', value: 2 })
|
|
349
|
+
.unique({ 'data.key': 'exec1' })
|
|
350
|
+
.debounce(200)
|
|
351
|
+
.save();
|
|
352
|
+
|
|
353
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
354
|
+
|
|
355
|
+
await agenda
|
|
356
|
+
.create('debounceExec', { key: 'exec1', value: 3 })
|
|
357
|
+
.unique({ 'data.key': 'exec1' })
|
|
358
|
+
.debounce(200)
|
|
359
|
+
.save();
|
|
360
|
+
|
|
361
|
+
// Start processing
|
|
362
|
+
await agenda.start();
|
|
363
|
+
|
|
364
|
+
// Wait for debounce delay + processing time
|
|
365
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
366
|
+
|
|
367
|
+
// Job should have executed once with the latest data
|
|
368
|
+
expect(executed).toBe(true);
|
|
369
|
+
expect(executedData).toEqual({ key: 'exec1', value: 3 });
|
|
370
|
+
|
|
371
|
+
// Only one job should exist
|
|
372
|
+
const result = await agenda.queryJobs({ name: 'debounceExec' });
|
|
373
|
+
expect(result.jobs.length).toBe(1);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full Agenda test suite that runs all shared test suites
|
|
3
|
+
*
|
|
4
|
+
* This is the recommended way to test a backend implementation.
|
|
5
|
+
* It runs all the individual test suites (repository, agenda, job, etc.)
|
|
6
|
+
* with a single configuration.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { fullAgendaTestSuite } from 'agenda/test/shared';
|
|
11
|
+
*
|
|
12
|
+
* fullAgendaTestSuite({
|
|
13
|
+
* name: 'MongoDB',
|
|
14
|
+
* createBackend: async () => {
|
|
15
|
+
* const backend = new MongoBackend({ mongo: db });
|
|
16
|
+
* await backend.connect();
|
|
17
|
+
* return backend;
|
|
18
|
+
* },
|
|
19
|
+
* cleanupBackend: async (backend) => {
|
|
20
|
+
* await backend.disconnect();
|
|
21
|
+
* },
|
|
22
|
+
* clearJobs: async (backend) => {
|
|
23
|
+
* await db.collection('agendaJobs').deleteMany({});
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { AgendaBackend, NotificationChannel } from '../../src/index.js';
|
|
30
|
+
import { repositoryTestSuite } from './repository-test-suite.js';
|
|
31
|
+
import { agendaTestSuite, type ForkHelperConfig } from './agenda-test-suite.js';
|
|
32
|
+
import { jobProcessorTestSuite } from './jobprocessor-test-suite.js';
|
|
33
|
+
import { retryTestSuite } from './retry-test-suite.js';
|
|
34
|
+
import { backoffTestSuite } from './backoff-test-suite.js';
|
|
35
|
+
import { debounceTestSuite } from './debounce-test-suite.js';
|
|
36
|
+
import { removeOnCompleteTestSuite } from './removeoncomplete-test-suite.js';
|
|
37
|
+
|
|
38
|
+
export interface FullAgendaTestConfig {
|
|
39
|
+
/** Name for the test suite (e.g., 'MongoDB', 'PostgreSQL') */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Factory to create a fresh backend instance */
|
|
42
|
+
createBackend: () => Promise<AgendaBackend>;
|
|
43
|
+
/** Cleanup function called after tests */
|
|
44
|
+
cleanupBackend: (backend: AgendaBackend) => Promise<void>;
|
|
45
|
+
/** Clear all jobs between tests */
|
|
46
|
+
clearJobs: (backend: AgendaBackend) => Promise<void>;
|
|
47
|
+
/** Optional notification channel factory for testing notification integration */
|
|
48
|
+
createNotificationChannel?: () => Promise<NotificationChannel>;
|
|
49
|
+
/** Cleanup notification channel */
|
|
50
|
+
cleanupNotificationChannel?: (channel: NotificationChannel) => Promise<void>;
|
|
51
|
+
/** Fork mode configuration (backend-specific fork helper) */
|
|
52
|
+
forkHelper?: ForkHelperConfig;
|
|
53
|
+
/** Skip specific test suites */
|
|
54
|
+
skip?: {
|
|
55
|
+
repository?: boolean;
|
|
56
|
+
agenda?: boolean;
|
|
57
|
+
jobProcessor?: boolean;
|
|
58
|
+
retry?: boolean;
|
|
59
|
+
backoff?: boolean;
|
|
60
|
+
debounce?: boolean;
|
|
61
|
+
removeOnComplete?: boolean;
|
|
62
|
+
forkMode?: boolean;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runs the full Agenda test suite for a backend implementation
|
|
68
|
+
*
|
|
69
|
+
* This includes:
|
|
70
|
+
* - Repository tests (IJobRepository interface compliance)
|
|
71
|
+
* - Agenda integration tests (scheduling, processing, events, Job class)
|
|
72
|
+
* - JobProcessor tests (concurrency, locking, stats)
|
|
73
|
+
* - Retry tests (job retry behavior)
|
|
74
|
+
* - Backoff tests (automatic retry with backoff strategies)
|
|
75
|
+
* - Debounce tests (job debouncing with unique constraints)
|
|
76
|
+
* - RemoveOnComplete tests (auto-removal of completed one-time jobs)
|
|
77
|
+
*/
|
|
78
|
+
export function fullAgendaTestSuite(config: FullAgendaTestConfig): void {
|
|
79
|
+
const skip = config.skip ?? {};
|
|
80
|
+
|
|
81
|
+
// Repository tests
|
|
82
|
+
if (!skip.repository) {
|
|
83
|
+
repositoryTestSuite({
|
|
84
|
+
name: `${config.name} Repository`,
|
|
85
|
+
createRepository: async () => {
|
|
86
|
+
const backend = await config.createBackend();
|
|
87
|
+
return backend.repository;
|
|
88
|
+
},
|
|
89
|
+
cleanupRepository: async () => {
|
|
90
|
+
// Backend cleanup handles this
|
|
91
|
+
}
|
|
92
|
+
// clearJobs not passed - repositoryTestSuite uses removeJobs as default
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Main Agenda integration tests
|
|
97
|
+
if (!skip.agenda) {
|
|
98
|
+
agendaTestSuite({
|
|
99
|
+
name: config.name,
|
|
100
|
+
createBackend: config.createBackend,
|
|
101
|
+
cleanupBackend: config.cleanupBackend,
|
|
102
|
+
clearJobs: config.clearJobs,
|
|
103
|
+
createNotificationChannel: config.createNotificationChannel,
|
|
104
|
+
cleanupNotificationChannel: config.cleanupNotificationChannel,
|
|
105
|
+
forkHelper: config.forkHelper,
|
|
106
|
+
skip: {
|
|
107
|
+
forkMode: skip.forkMode
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// JobProcessor tests
|
|
113
|
+
if (!skip.jobProcessor) {
|
|
114
|
+
jobProcessorTestSuite({
|
|
115
|
+
name: config.name,
|
|
116
|
+
createBackend: config.createBackend,
|
|
117
|
+
cleanupBackend: config.cleanupBackend,
|
|
118
|
+
clearJobs: config.clearJobs
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Retry tests
|
|
123
|
+
if (!skip.retry) {
|
|
124
|
+
retryTestSuite({
|
|
125
|
+
name: config.name,
|
|
126
|
+
createBackend: config.createBackend,
|
|
127
|
+
cleanupBackend: config.cleanupBackend,
|
|
128
|
+
clearJobs: config.clearJobs
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Backoff tests
|
|
133
|
+
if (!skip.backoff) {
|
|
134
|
+
backoffTestSuite({
|
|
135
|
+
name: config.name,
|
|
136
|
+
createBackend: config.createBackend,
|
|
137
|
+
cleanupBackend: config.cleanupBackend,
|
|
138
|
+
clearJobs: config.clearJobs
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Debounce tests
|
|
143
|
+
if (!skip.debounce) {
|
|
144
|
+
debounceTestSuite({
|
|
145
|
+
name: config.name,
|
|
146
|
+
createBackend: config.createBackend,
|
|
147
|
+
cleanupBackend: config.cleanupBackend,
|
|
148
|
+
clearJobs: config.clearJobs
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// RemoveOnComplete tests
|
|
153
|
+
if (!skip.removeOnComplete) {
|
|
154
|
+
removeOnCompleteTestSuite({
|
|
155
|
+
name: config.name,
|
|
156
|
+
createBackend: config.createBackend,
|
|
157
|
+
cleanupBackend: config.cleanupBackend,
|
|
158
|
+
clearJobs: config.clearJobs
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test suites for Agenda backend implementations
|
|
3
|
+
*
|
|
4
|
+
* The recommended way to test a backend is to use fullAgendaTestSuite,
|
|
5
|
+
* which runs all the individual test suites with a single configuration.
|
|
6
|
+
*
|
|
7
|
+
* Usage in a backend package:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { fullAgendaTestSuite } from 'agenda/test/shared';
|
|
10
|
+
*
|
|
11
|
+
* fullAgendaTestSuite({
|
|
12
|
+
* name: 'MongoDB',
|
|
13
|
+
* createBackend: async () => {
|
|
14
|
+
* const backend = new MongoBackend({ mongo: db });
|
|
15
|
+
* await backend.connect();
|
|
16
|
+
* return backend;
|
|
17
|
+
* },
|
|
18
|
+
* cleanupBackend: async (backend) => await backend.disconnect(),
|
|
19
|
+
* clearJobs: async (backend) => await db.collection('agendaJobs').deleteMany({})
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Individual test suites are also available if you need more control:
|
|
24
|
+
* - repositoryTestSuite - Tests IJobRepository interface compliance
|
|
25
|
+
* - agendaTestSuite - Main Agenda integration tests (includes Job class tests)
|
|
26
|
+
* - notificationChannelTestSuite - Tests INotificationChannel interface
|
|
27
|
+
* - jobProcessorTestSuite - JobProcessor concurrency and stats tests
|
|
28
|
+
* - retryTestSuite - Job retry behavior tests
|
|
29
|
+
* - debounceTestSuite - Job debouncing with unique constraints
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Test utilities
|
|
33
|
+
export { delay, waitForEvent, waitForEvents, runJobAndWait } from './test-utils.js';
|
|
34
|
+
|
|
35
|
+
// Main entry point - runs all test suites
|
|
36
|
+
export { fullAgendaTestSuite, type FullAgendaTestConfig } from './full-test-suite.js';
|
|
37
|
+
|
|
38
|
+
// Individual test suites for fine-grained control
|
|
39
|
+
export { repositoryTestSuite, type RepositoryTestConfig } from './repository-test-suite.js';
|
|
40
|
+
export {
|
|
41
|
+
notificationChannelTestSuite,
|
|
42
|
+
type NotificationChannelTestConfig
|
|
43
|
+
} from './notification-channel-test-suite.js';
|
|
44
|
+
export { agendaTestSuite, type AgendaTestConfig, type ForkHelperConfig } from './agenda-test-suite.js';
|
|
45
|
+
export { jobProcessorTestSuite, type JobProcessorTestConfig } from './jobprocessor-test-suite.js';
|
|
46
|
+
export { retryTestSuite, type RetryTestConfig } from './retry-test-suite.js';
|
|
47
|
+
export { backoffTestSuite, type BackoffTestConfig } from './backoff-test-suite.js';
|
|
48
|
+
export { debounceTestSuite, type DebounceTestConfig } from './debounce-test-suite.js';
|
|
49
|
+
export { removeOnCompleteTestSuite, type RemoveOnCompleteTestConfig } from './removeoncomplete-test-suite.js';
|
|
50
|
+
export { jobLoggerTestSuite, type JobLoggerTestConfig } from './joblogger-test-suite.js';
|