@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,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test suite for JobRepository implementations
|
|
3
|
+
*
|
|
4
|
+
* This file exports test suite factories that can be used to test any
|
|
5
|
+
* JobRepository implementation (MongoDB, PostgreSQL, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { repositoryTestSuite } from 'agenda/test/shared';
|
|
10
|
+
*
|
|
11
|
+
* repositoryTestSuite({
|
|
12
|
+
* name: 'PostgresJobRepository',
|
|
13
|
+
* createRepository: async () => {
|
|
14
|
+
* const repo = new PostgresJobRepository(config);
|
|
15
|
+
* await repo.connect();
|
|
16
|
+
* return repo;
|
|
17
|
+
* },
|
|
18
|
+
* cleanupRepository: async (repo) => {
|
|
19
|
+
* await repo.disconnect();
|
|
20
|
+
* }
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
26
|
+
import type { JobRepository, JobParameters } from '../../src/index.js';
|
|
27
|
+
|
|
28
|
+
export interface RepositoryTestConfig {
|
|
29
|
+
/** Name for the test suite */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Factory to create a fresh repository instance */
|
|
32
|
+
createRepository: () => Promise<JobRepository>;
|
|
33
|
+
/** Cleanup function called after each test */
|
|
34
|
+
cleanupRepository: (repo: JobRepository) => Promise<void>;
|
|
35
|
+
/** Optional: clear all jobs between tests (if not provided, uses removeJobs) */
|
|
36
|
+
clearJobs?: (repo: JobRepository) => Promise<void>;
|
|
37
|
+
/** Skip specific tests if not supported by the backend */
|
|
38
|
+
skip?: {
|
|
39
|
+
uniqueConstraints?: boolean;
|
|
40
|
+
jsonbQueries?: boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a test suite for JobRepository implementations
|
|
46
|
+
*/
|
|
47
|
+
export function repositoryTestSuite(config: RepositoryTestConfig): void {
|
|
48
|
+
describe(`${config.name} - JobRepository`, () => {
|
|
49
|
+
let repo: JobRepository;
|
|
50
|
+
|
|
51
|
+
const clearJobs = async () => {
|
|
52
|
+
if (config.clearJobs) {
|
|
53
|
+
await config.clearJobs(repo);
|
|
54
|
+
} else {
|
|
55
|
+
// Default: remove all jobs by querying for all names
|
|
56
|
+
const names = await repo.getDistinctJobNames();
|
|
57
|
+
if (names.length > 0) {
|
|
58
|
+
await repo.removeJobs({ names });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
repo = await config.createRepository();
|
|
65
|
+
await clearJobs();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
await clearJobs();
|
|
70
|
+
await config.cleanupRepository(repo);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('saveJob', () => {
|
|
74
|
+
it('should save a new job and assign an ID', async () => {
|
|
75
|
+
const job: JobParameters = {
|
|
76
|
+
name: 'test-job',
|
|
77
|
+
priority: 10,
|
|
78
|
+
nextRunAt: new Date(),
|
|
79
|
+
type: 'normal',
|
|
80
|
+
data: { foo: 'bar' }
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const saved = await repo.saveJob(job, undefined);
|
|
84
|
+
|
|
85
|
+
expect(saved._id).toBeDefined();
|
|
86
|
+
expect(saved.name).toBe('test-job');
|
|
87
|
+
expect(saved.priority).toBe(10);
|
|
88
|
+
expect(saved.type).toBe('normal');
|
|
89
|
+
expect(saved.data).toEqual({ foo: 'bar' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should update an existing job', async () => {
|
|
93
|
+
const job: JobParameters = {
|
|
94
|
+
name: 'update-test',
|
|
95
|
+
priority: 5,
|
|
96
|
+
nextRunAt: new Date(),
|
|
97
|
+
type: 'normal',
|
|
98
|
+
data: { version: 1 }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const saved = await repo.saveJob(job, undefined);
|
|
102
|
+
const updated = await repo.saveJob({
|
|
103
|
+
...saved,
|
|
104
|
+
priority: 20,
|
|
105
|
+
data: { version: 2 }
|
|
106
|
+
}, undefined);
|
|
107
|
+
|
|
108
|
+
expect(updated._id).toBe(saved._id);
|
|
109
|
+
expect(updated.priority).toBe(20);
|
|
110
|
+
expect(updated.data).toEqual({ version: 2 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle single type jobs (upsert by name)', async () => {
|
|
114
|
+
const job1: JobParameters = {
|
|
115
|
+
name: 'single-job',
|
|
116
|
+
priority: 5,
|
|
117
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
118
|
+
type: 'single',
|
|
119
|
+
data: { run: 1 }
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const saved1 = await repo.saveJob(job1, undefined);
|
|
123
|
+
|
|
124
|
+
const job2: JobParameters = {
|
|
125
|
+
name: 'single-job',
|
|
126
|
+
priority: 10,
|
|
127
|
+
nextRunAt: new Date(Date.now() + 120000),
|
|
128
|
+
type: 'single',
|
|
129
|
+
data: { run: 2 }
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const saved2 = await repo.saveJob(job2, undefined);
|
|
133
|
+
|
|
134
|
+
expect(saved2._id).toBe(saved1._id);
|
|
135
|
+
expect(saved2.priority).toBe(10);
|
|
136
|
+
expect(saved2.data).toEqual({ run: 2 });
|
|
137
|
+
|
|
138
|
+
const result = await repo.queryJobs({ name: 'single-job' });
|
|
139
|
+
expect(result.total).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should preserve disabled flag', async () => {
|
|
143
|
+
const job: JobParameters = {
|
|
144
|
+
name: 'disabled-job',
|
|
145
|
+
priority: 0,
|
|
146
|
+
nextRunAt: new Date(),
|
|
147
|
+
type: 'normal',
|
|
148
|
+
data: {},
|
|
149
|
+
disabled: true
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const saved = await repo.saveJob(job, undefined);
|
|
153
|
+
expect(saved.disabled).toBe(true);
|
|
154
|
+
|
|
155
|
+
const retrieved = await repo.getJobById(saved._id!.toString());
|
|
156
|
+
expect(retrieved?.disabled).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('getJobById', () => {
|
|
161
|
+
it('should return job by ID', async () => {
|
|
162
|
+
const saved = await repo.saveJob({
|
|
163
|
+
name: 'get-by-id-test',
|
|
164
|
+
priority: 5,
|
|
165
|
+
nextRunAt: new Date(),
|
|
166
|
+
type: 'normal',
|
|
167
|
+
data: { key: 'value' }
|
|
168
|
+
}, undefined);
|
|
169
|
+
|
|
170
|
+
const retrieved = await repo.getJobById(saved._id!.toString());
|
|
171
|
+
|
|
172
|
+
expect(retrieved).not.toBeNull();
|
|
173
|
+
expect(retrieved!._id).toBe(saved._id);
|
|
174
|
+
expect(retrieved!.name).toBe('get-by-id-test');
|
|
175
|
+
expect(retrieved!.data).toEqual({ key: 'value' });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should return null for non-existent ID', async () => {
|
|
179
|
+
const job = await repo.saveJob({
|
|
180
|
+
name: 'temp-job',
|
|
181
|
+
priority: 0,
|
|
182
|
+
nextRunAt: new Date(),
|
|
183
|
+
type: 'normal',
|
|
184
|
+
data: {}
|
|
185
|
+
}, undefined);
|
|
186
|
+
|
|
187
|
+
await repo.removeJobs({ id: job._id! });
|
|
188
|
+
|
|
189
|
+
const result = await repo.getJobById(job._id!.toString());
|
|
190
|
+
expect(result).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('queryJobs', () => {
|
|
195
|
+
beforeEach(async () => {
|
|
196
|
+
await repo.saveJob({
|
|
197
|
+
name: 'job-a',
|
|
198
|
+
priority: 10,
|
|
199
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
200
|
+
type: 'normal',
|
|
201
|
+
data: { type: 'alpha', count: 1 }
|
|
202
|
+
}, undefined);
|
|
203
|
+
await repo.saveJob({
|
|
204
|
+
name: 'job-b',
|
|
205
|
+
priority: 5,
|
|
206
|
+
nextRunAt: new Date(Date.now() + 120000),
|
|
207
|
+
type: 'normal',
|
|
208
|
+
data: { type: 'beta', count: 2 }
|
|
209
|
+
}, undefined);
|
|
210
|
+
await repo.saveJob({
|
|
211
|
+
name: 'job-a',
|
|
212
|
+
priority: 15,
|
|
213
|
+
nextRunAt: new Date(Date.now() + 180000),
|
|
214
|
+
type: 'normal',
|
|
215
|
+
data: { type: 'alpha', count: 3 }
|
|
216
|
+
}, undefined);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should query all jobs', async () => {
|
|
220
|
+
const result = await repo.queryJobs();
|
|
221
|
+
expect(result.total).toBe(3);
|
|
222
|
+
expect(result.jobs.length).toBe(3);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should query jobs by name', async () => {
|
|
226
|
+
const result = await repo.queryJobs({ name: 'job-a' });
|
|
227
|
+
expect(result.total).toBe(2);
|
|
228
|
+
result.jobs.forEach(job => {
|
|
229
|
+
expect(job.name).toBe('job-a');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should query jobs by multiple names', async () => {
|
|
234
|
+
const result = await repo.queryJobs({ names: ['job-a', 'job-b'] });
|
|
235
|
+
expect(result.total).toBe(3);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should query jobs by search pattern', async () => {
|
|
239
|
+
const result = await repo.queryJobs({ search: 'job' });
|
|
240
|
+
expect(result.total).toBe(3);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should support pagination with skip and limit', async () => {
|
|
244
|
+
const result = await repo.queryJobs({ skip: 1, limit: 1 });
|
|
245
|
+
expect(result.jobs.length).toBe(1);
|
|
246
|
+
expect(result.total).toBe(3);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should filter disabled jobs when includeDisabled is false', async () => {
|
|
250
|
+
await repo.saveJob({
|
|
251
|
+
name: 'disabled-job',
|
|
252
|
+
priority: 0,
|
|
253
|
+
nextRunAt: new Date(),
|
|
254
|
+
type: 'normal',
|
|
255
|
+
data: {},
|
|
256
|
+
disabled: true
|
|
257
|
+
}, undefined);
|
|
258
|
+
|
|
259
|
+
const withDisabled = await repo.queryJobs({ includeDisabled: true });
|
|
260
|
+
const withoutDisabled = await repo.queryJobs({ includeDisabled: false });
|
|
261
|
+
|
|
262
|
+
expect(withDisabled.total).toBe(4);
|
|
263
|
+
expect(withoutDisabled.total).toBe(3);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should support partial matching on job data', async () => {
|
|
267
|
+
await repo.saveJob({
|
|
268
|
+
name: 'data-partial-test',
|
|
269
|
+
priority: 0,
|
|
270
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
271
|
+
type: 'normal',
|
|
272
|
+
data: { searchField: 'searchValue', anotherField: 'anotherValue' }
|
|
273
|
+
}, undefined);
|
|
274
|
+
|
|
275
|
+
const result = await repo.queryJobs({ data: { searchField: 'searchValue' } });
|
|
276
|
+
expect(result.total).toBe(1);
|
|
277
|
+
expect(result.jobs[0].name).toBe('data-partial-test');
|
|
278
|
+
expect(result.jobs[0].data).toEqual({ searchField: 'searchValue', anotherField: 'anotherValue' });
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('removeJobs', () => {
|
|
283
|
+
it('should remove jobs by name', async () => {
|
|
284
|
+
await repo.saveJob({
|
|
285
|
+
name: 'remove-test',
|
|
286
|
+
priority: 0,
|
|
287
|
+
nextRunAt: new Date(),
|
|
288
|
+
type: 'normal',
|
|
289
|
+
data: {}
|
|
290
|
+
}, undefined);
|
|
291
|
+
await repo.saveJob({
|
|
292
|
+
name: 'remove-test',
|
|
293
|
+
priority: 0,
|
|
294
|
+
nextRunAt: new Date(),
|
|
295
|
+
type: 'normal',
|
|
296
|
+
data: {}
|
|
297
|
+
}, undefined);
|
|
298
|
+
await repo.saveJob({
|
|
299
|
+
name: 'keep-test',
|
|
300
|
+
priority: 0,
|
|
301
|
+
nextRunAt: new Date(),
|
|
302
|
+
type: 'normal',
|
|
303
|
+
data: {}
|
|
304
|
+
}, undefined);
|
|
305
|
+
|
|
306
|
+
const removed = await repo.removeJobs({ name: 'remove-test' });
|
|
307
|
+
expect(removed).toBe(2);
|
|
308
|
+
|
|
309
|
+
const remaining = await repo.queryJobs();
|
|
310
|
+
expect(remaining.total).toBe(1);
|
|
311
|
+
expect(remaining.jobs[0].name).toBe('keep-test');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should remove jobs by ID', async () => {
|
|
315
|
+
const saved = await repo.saveJob({
|
|
316
|
+
name: 'remove-by-id',
|
|
317
|
+
priority: 0,
|
|
318
|
+
nextRunAt: new Date(),
|
|
319
|
+
type: 'normal',
|
|
320
|
+
data: {}
|
|
321
|
+
}, undefined);
|
|
322
|
+
|
|
323
|
+
const removed = await repo.removeJobs({ id: saved._id! });
|
|
324
|
+
expect(removed).toBe(1);
|
|
325
|
+
|
|
326
|
+
const retrieved = await repo.getJobById(saved._id!.toString());
|
|
327
|
+
expect(retrieved).toBeNull();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should remove jobs by multiple IDs', async () => {
|
|
331
|
+
const job1 = await repo.saveJob({
|
|
332
|
+
name: 'multi-remove-1',
|
|
333
|
+
priority: 0,
|
|
334
|
+
nextRunAt: new Date(),
|
|
335
|
+
type: 'normal',
|
|
336
|
+
data: {}
|
|
337
|
+
}, undefined);
|
|
338
|
+
const job2 = await repo.saveJob({
|
|
339
|
+
name: 'multi-remove-2',
|
|
340
|
+
priority: 0,
|
|
341
|
+
nextRunAt: new Date(),
|
|
342
|
+
type: 'normal',
|
|
343
|
+
data: {}
|
|
344
|
+
}, undefined);
|
|
345
|
+
await repo.saveJob({
|
|
346
|
+
name: 'keep',
|
|
347
|
+
priority: 0,
|
|
348
|
+
nextRunAt: new Date(),
|
|
349
|
+
type: 'normal',
|
|
350
|
+
data: {}
|
|
351
|
+
}, undefined);
|
|
352
|
+
|
|
353
|
+
const removed = await repo.removeJobs({ ids: [job1._id!, job2._id!] });
|
|
354
|
+
expect(removed).toBe(2);
|
|
355
|
+
|
|
356
|
+
const remaining = await repo.queryJobs();
|
|
357
|
+
expect(remaining.total).toBe(1);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should return 0 when no jobs match', async () => {
|
|
361
|
+
const removed = await repo.removeJobs({ name: 'non-existent' });
|
|
362
|
+
expect(removed).toBe(0);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('getDistinctJobNames', () => {
|
|
367
|
+
it('should return unique job names', async () => {
|
|
368
|
+
await repo.saveJob({
|
|
369
|
+
name: 'name-a',
|
|
370
|
+
priority: 0,
|
|
371
|
+
nextRunAt: new Date(),
|
|
372
|
+
type: 'normal',
|
|
373
|
+
data: {}
|
|
374
|
+
}, undefined);
|
|
375
|
+
await repo.saveJob({
|
|
376
|
+
name: 'name-b',
|
|
377
|
+
priority: 0,
|
|
378
|
+
nextRunAt: new Date(),
|
|
379
|
+
type: 'normal',
|
|
380
|
+
data: {}
|
|
381
|
+
}, undefined);
|
|
382
|
+
await repo.saveJob({
|
|
383
|
+
name: 'name-a',
|
|
384
|
+
priority: 0,
|
|
385
|
+
nextRunAt: new Date(),
|
|
386
|
+
type: 'normal',
|
|
387
|
+
data: {}
|
|
388
|
+
}, undefined);
|
|
389
|
+
|
|
390
|
+
const names = await repo.getDistinctJobNames();
|
|
391
|
+
expect(names).toContain('name-a');
|
|
392
|
+
expect(names).toContain('name-b');
|
|
393
|
+
expect(names.length).toBe(2);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should return empty array when no jobs exist', async () => {
|
|
397
|
+
const names = await repo.getDistinctJobNames();
|
|
398
|
+
expect(names).toEqual([]);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe('getQueueSize', () => {
|
|
403
|
+
it('should count jobs ready to run', async () => {
|
|
404
|
+
// Jobs in the past (should be counted)
|
|
405
|
+
await repo.saveJob({
|
|
406
|
+
name: 'past-job-1',
|
|
407
|
+
priority: 0,
|
|
408
|
+
nextRunAt: new Date(Date.now() - 60000),
|
|
409
|
+
type: 'normal',
|
|
410
|
+
data: {}
|
|
411
|
+
}, undefined);
|
|
412
|
+
await repo.saveJob({
|
|
413
|
+
name: 'past-job-2',
|
|
414
|
+
priority: 0,
|
|
415
|
+
nextRunAt: new Date(Date.now() - 30000),
|
|
416
|
+
type: 'normal',
|
|
417
|
+
data: {}
|
|
418
|
+
}, undefined);
|
|
419
|
+
// Job in the future (should not be counted)
|
|
420
|
+
await repo.saveJob({
|
|
421
|
+
name: 'future-job',
|
|
422
|
+
priority: 0,
|
|
423
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
424
|
+
type: 'normal',
|
|
425
|
+
data: {}
|
|
426
|
+
}, undefined);
|
|
427
|
+
|
|
428
|
+
const queueSize = await repo.getQueueSize();
|
|
429
|
+
expect(queueSize).toBe(2);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should return 0 when no jobs are ready', async () => {
|
|
433
|
+
await repo.saveJob({
|
|
434
|
+
name: 'future-job',
|
|
435
|
+
priority: 0,
|
|
436
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
437
|
+
type: 'normal',
|
|
438
|
+
data: {}
|
|
439
|
+
}, undefined);
|
|
440
|
+
|
|
441
|
+
const queueSize = await repo.getQueueSize();
|
|
442
|
+
expect(queueSize).toBe(0);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe('locking', () => {
|
|
447
|
+
it('should lock a job', async () => {
|
|
448
|
+
const saved = await repo.saveJob({
|
|
449
|
+
name: 'lock-test',
|
|
450
|
+
priority: 0,
|
|
451
|
+
nextRunAt: new Date(Date.now() - 1000),
|
|
452
|
+
type: 'normal',
|
|
453
|
+
data: {}
|
|
454
|
+
}, undefined);
|
|
455
|
+
|
|
456
|
+
const locked = await repo.lockJob(saved, undefined);
|
|
457
|
+
|
|
458
|
+
expect(locked).toBeDefined();
|
|
459
|
+
expect(locked!.lockedAt).toBeDefined();
|
|
460
|
+
expect(locked!._id).toBe(saved._id);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should not lock an already locked job', async () => {
|
|
464
|
+
const saved = await repo.saveJob({
|
|
465
|
+
name: 'double-lock-test',
|
|
466
|
+
priority: 0,
|
|
467
|
+
nextRunAt: new Date(Date.now() - 1000),
|
|
468
|
+
type: 'normal',
|
|
469
|
+
data: {}
|
|
470
|
+
}, undefined);
|
|
471
|
+
|
|
472
|
+
const locked1 = await repo.lockJob(saved, undefined);
|
|
473
|
+
expect(locked1).toBeDefined();
|
|
474
|
+
|
|
475
|
+
// Try to lock again with original (unlocked) job params
|
|
476
|
+
const locked2 = await repo.lockJob(saved, undefined);
|
|
477
|
+
expect(locked2).toBeUndefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should unlock a job', async () => {
|
|
481
|
+
const saved = await repo.saveJob({
|
|
482
|
+
name: 'unlock-test',
|
|
483
|
+
priority: 0,
|
|
484
|
+
nextRunAt: new Date(Date.now() - 1000),
|
|
485
|
+
type: 'normal',
|
|
486
|
+
data: {}
|
|
487
|
+
}, undefined);
|
|
488
|
+
|
|
489
|
+
const locked = await repo.lockJob(saved, undefined);
|
|
490
|
+
expect(locked!.lockedAt).toBeDefined();
|
|
491
|
+
|
|
492
|
+
await repo.unlockJob(locked!);
|
|
493
|
+
|
|
494
|
+
const retrieved = await repo.getJobById(saved._id!.toString());
|
|
495
|
+
// Accept both null and undefined as "not locked"
|
|
496
|
+
expect(retrieved!.lockedAt == null).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should unlock multiple jobs', async () => {
|
|
500
|
+
const job1 = await repo.saveJob({
|
|
501
|
+
name: 'multi-unlock-1',
|
|
502
|
+
priority: 0,
|
|
503
|
+
nextRunAt: new Date(Date.now() - 1000),
|
|
504
|
+
type: 'normal',
|
|
505
|
+
data: {}
|
|
506
|
+
}, undefined);
|
|
507
|
+
const job2 = await repo.saveJob({
|
|
508
|
+
name: 'multi-unlock-2',
|
|
509
|
+
priority: 0,
|
|
510
|
+
nextRunAt: new Date(Date.now() - 1000),
|
|
511
|
+
type: 'normal',
|
|
512
|
+
data: {}
|
|
513
|
+
}, undefined);
|
|
514
|
+
|
|
515
|
+
await repo.lockJob(job1, undefined);
|
|
516
|
+
await repo.lockJob(job2, undefined);
|
|
517
|
+
|
|
518
|
+
await repo.unlockJobs([job1._id!, job2._id!]);
|
|
519
|
+
|
|
520
|
+
const retrieved1 = await repo.getJobById(job1._id!.toString());
|
|
521
|
+
const retrieved2 = await repo.getJobById(job2._id!.toString());
|
|
522
|
+
|
|
523
|
+
// Accept both null and undefined as "not locked"
|
|
524
|
+
expect(retrieved1!.lockedAt == null).toBe(true);
|
|
525
|
+
expect(retrieved2!.lockedAt == null).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
describe('getNextJobToRun', () => {
|
|
530
|
+
it('should get the next job to run', async () => {
|
|
531
|
+
await repo.saveJob({
|
|
532
|
+
name: 'runner-test',
|
|
533
|
+
priority: 5,
|
|
534
|
+
nextRunAt: new Date(Date.now() - 60000),
|
|
535
|
+
type: 'normal',
|
|
536
|
+
data: { order: 'first' }
|
|
537
|
+
}, undefined);
|
|
538
|
+
|
|
539
|
+
const now = new Date();
|
|
540
|
+
const nextScanAt = new Date(now.getTime() + 5000);
|
|
541
|
+
const lockDeadline = new Date(now.getTime() - 600000);
|
|
542
|
+
|
|
543
|
+
const next = await repo.getNextJobToRun('runner-test', nextScanAt, lockDeadline, now, undefined);
|
|
544
|
+
|
|
545
|
+
expect(next).toBeDefined();
|
|
546
|
+
expect(next!.name).toBe('runner-test');
|
|
547
|
+
expect(next!.lockedAt).toBeDefined();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should respect priority ordering', async () => {
|
|
551
|
+
// Use the same nextRunAt time so priority is the deciding factor
|
|
552
|
+
const sameTime = new Date(Date.now() - 60000);
|
|
553
|
+
|
|
554
|
+
await repo.saveJob({
|
|
555
|
+
name: 'priority-test',
|
|
556
|
+
priority: 5,
|
|
557
|
+
nextRunAt: sameTime,
|
|
558
|
+
type: 'normal',
|
|
559
|
+
data: { priority: 'low' }
|
|
560
|
+
}, undefined);
|
|
561
|
+
await repo.saveJob({
|
|
562
|
+
name: 'priority-test',
|
|
563
|
+
priority: 10,
|
|
564
|
+
nextRunAt: sameTime,
|
|
565
|
+
type: 'normal',
|
|
566
|
+
data: { priority: 'high' }
|
|
567
|
+
}, undefined);
|
|
568
|
+
|
|
569
|
+
const now = new Date();
|
|
570
|
+
const nextScanAt = new Date(now.getTime() + 5000);
|
|
571
|
+
const lockDeadline = new Date(now.getTime() - 600000);
|
|
572
|
+
|
|
573
|
+
const next = await repo.getNextJobToRun('priority-test', nextScanAt, lockDeadline, now, undefined);
|
|
574
|
+
|
|
575
|
+
expect(next).toBeDefined();
|
|
576
|
+
expect(next!.priority).toBe(10);
|
|
577
|
+
expect(next!.data).toEqual({ priority: 'high' });
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should not return disabled jobs', async () => {
|
|
581
|
+
await repo.saveJob({
|
|
582
|
+
name: 'disabled-test',
|
|
583
|
+
priority: 0,
|
|
584
|
+
nextRunAt: new Date(Date.now() - 60000),
|
|
585
|
+
type: 'normal',
|
|
586
|
+
data: {},
|
|
587
|
+
disabled: true
|
|
588
|
+
}, undefined);
|
|
589
|
+
|
|
590
|
+
const now = new Date();
|
|
591
|
+
const nextScanAt = new Date(now.getTime() + 5000);
|
|
592
|
+
const lockDeadline = new Date(now.getTime() - 600000);
|
|
593
|
+
|
|
594
|
+
const next = await repo.getNextJobToRun('disabled-test', nextScanAt, lockDeadline, now, undefined);
|
|
595
|
+
|
|
596
|
+
expect(next).toBeUndefined();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should return undefined when no jobs are ready', async () => {
|
|
600
|
+
await repo.saveJob({
|
|
601
|
+
name: 'future-test',
|
|
602
|
+
priority: 0,
|
|
603
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
604
|
+
type: 'normal',
|
|
605
|
+
data: {}
|
|
606
|
+
}, undefined);
|
|
607
|
+
|
|
608
|
+
const now = new Date();
|
|
609
|
+
const nextScanAt = new Date(now.getTime() + 5000);
|
|
610
|
+
const lockDeadline = new Date(now.getTime() - 600000);
|
|
611
|
+
|
|
612
|
+
const next = await repo.getNextJobToRun('future-test', nextScanAt, lockDeadline, now, undefined);
|
|
613
|
+
|
|
614
|
+
expect(next).toBeUndefined();
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe('saveJobState', () => {
|
|
619
|
+
it('should update job state fields', async () => {
|
|
620
|
+
const saved = await repo.saveJob({
|
|
621
|
+
name: 'state-test',
|
|
622
|
+
priority: 0,
|
|
623
|
+
nextRunAt: new Date(),
|
|
624
|
+
type: 'normal',
|
|
625
|
+
data: {}
|
|
626
|
+
}, undefined);
|
|
627
|
+
|
|
628
|
+
const now = new Date();
|
|
629
|
+
await repo.saveJobState({
|
|
630
|
+
...saved,
|
|
631
|
+
lastRunAt: now,
|
|
632
|
+
lastFinishedAt: now,
|
|
633
|
+
progress: 50,
|
|
634
|
+
failCount: 1,
|
|
635
|
+
failReason: 'Test error',
|
|
636
|
+
failedAt: now
|
|
637
|
+
}, undefined);
|
|
638
|
+
|
|
639
|
+
const retrieved = await repo.getJobById(saved._id!.toString());
|
|
640
|
+
|
|
641
|
+
expect(retrieved!.lastRunAt).toBeDefined();
|
|
642
|
+
expect(retrieved!.lastFinishedAt).toBeDefined();
|
|
643
|
+
expect(retrieved!.progress).toBe(50);
|
|
644
|
+
expect(retrieved!.failCount).toBe(1);
|
|
645
|
+
expect(retrieved!.failReason).toBe('Test error');
|
|
646
|
+
expect(retrieved!.failedAt).toBeDefined();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('should clear lockedAt when job completes', async () => {
|
|
650
|
+
const saved = await repo.saveJob({
|
|
651
|
+
name: 'complete-test',
|
|
652
|
+
priority: 0,
|
|
653
|
+
nextRunAt: new Date(Date.now() - 1000),
|
|
654
|
+
type: 'normal',
|
|
655
|
+
data: {}
|
|
656
|
+
}, undefined);
|
|
657
|
+
|
|
658
|
+
const locked = await repo.lockJob(saved, undefined);
|
|
659
|
+
expect(locked!.lockedAt).toBeDefined();
|
|
660
|
+
|
|
661
|
+
await repo.saveJobState({
|
|
662
|
+
...locked!,
|
|
663
|
+
lockedAt: undefined,
|
|
664
|
+
lastFinishedAt: new Date()
|
|
665
|
+
}, undefined);
|
|
666
|
+
|
|
667
|
+
const retrieved = await repo.getJobById(saved._id!.toString());
|
|
668
|
+
// Accept both null and undefined as "not locked"
|
|
669
|
+
expect(retrieved!.lockedAt == null).toBe(true);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe('getJobsOverview', () => {
|
|
674
|
+
it('should return overview of all job types', async () => {
|
|
675
|
+
await repo.saveJob({
|
|
676
|
+
name: 'overview-job',
|
|
677
|
+
priority: 0,
|
|
678
|
+
nextRunAt: new Date(Date.now() + 60000), // scheduled
|
|
679
|
+
type: 'normal',
|
|
680
|
+
data: {}
|
|
681
|
+
}, undefined);
|
|
682
|
+
await repo.saveJob({
|
|
683
|
+
name: 'overview-job',
|
|
684
|
+
priority: 0,
|
|
685
|
+
nextRunAt: null, // completed (no next run)
|
|
686
|
+
type: 'normal',
|
|
687
|
+
data: {},
|
|
688
|
+
lastFinishedAt: new Date()
|
|
689
|
+
}, undefined);
|
|
690
|
+
await repo.saveJob({
|
|
691
|
+
name: 'other-job',
|
|
692
|
+
priority: 0,
|
|
693
|
+
nextRunAt: new Date(Date.now() + 60000),
|
|
694
|
+
type: 'normal',
|
|
695
|
+
data: {}
|
|
696
|
+
}, undefined);
|
|
697
|
+
|
|
698
|
+
const overview = await repo.getJobsOverview();
|
|
699
|
+
|
|
700
|
+
expect(overview.length).toBeGreaterThanOrEqual(2);
|
|
701
|
+
|
|
702
|
+
const overviewJob = overview.find(o => o.name === 'overview-job');
|
|
703
|
+
expect(overviewJob).toBeDefined();
|
|
704
|
+
expect(overviewJob!.total).toBe(2);
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
}
|