mongo-job-scheduler 0.1.7 → 0.1.9
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/README.md +158 -103
- package/dist/core/scheduler.d.ts +5 -0
- package/dist/core/scheduler.js +9 -0
- package/dist/store/in-memory-job-store.d.ts +2 -0
- package/dist/store/in-memory-job-store.js +26 -0
- package/dist/store/job-store.d.ts +5 -0
- package/dist/store/mongo/mongo-job-store.d.ts +6 -0
- package/dist/store/mongo/mongo-job-store.js +42 -0
- package/dist/types/query.d.ts +11 -0
- package/dist/types/query.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,52 +1,40 @@
|
|
|
1
1
|
# Mongo Job Scheduler
|
|
2
2
|
|
|
3
|
-
A production-grade MongoDB-backed job scheduler for Node.js.
|
|
3
|
+
A production-grade MongoDB-backed job scheduler for Node.js with distributed locking, retries, cron scheduling, and crash recovery.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- reliable background jobs
|
|
8
|
-
- retries with backoff
|
|
9
|
-
- cron & interval scheduling
|
|
10
|
-
- crash recovery
|
|
11
|
-
- MongoDB sharding safety
|
|
5
|
+
[](https://www.npmjs.com/package/mongo-job-scheduler)
|
|
12
6
|
|
|
13
7
|
---
|
|
14
8
|
|
|
15
9
|
## Features
|
|
16
10
|
|
|
17
|
-
- **Distributed locking**
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **Cron
|
|
21
|
-
- **Interval jobs**
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
|
|
27
|
-
## Distributed Systems
|
|
28
|
-
|
|
29
|
-
This library is designed for distributed environments. You can run **multiple scheduler instances** (on different servers, pods, or processes) connected to the same MongoDB.
|
|
30
|
-
|
|
31
|
-
- **Atomic Locking**: Uses `findOneAndUpdate` to safe-guard against race conditions.
|
|
32
|
-
- **Concurrency**: Only one worker will execute a given job instance.
|
|
33
|
-
- **Scalable**: Horizontal scaling is supported via MongoDB sharding.
|
|
11
|
+
- ✅ **Distributed locking** — safe for multiple instances
|
|
12
|
+
- ✅ **Atomic job execution** — no double processing
|
|
13
|
+
- ✅ **Automatic retries** — with configurable backoff
|
|
14
|
+
- ✅ **Cron scheduling** — timezone-aware, non-drifting
|
|
15
|
+
- ✅ **Interval jobs** — repeated execution
|
|
16
|
+
- ✅ **Crash recovery** — resume on restart
|
|
17
|
+
- ✅ **Heartbeats** — automatic lock renewal for long jobs
|
|
18
|
+
- ✅ **Query API** — filter, sort, paginate jobs
|
|
19
|
+
- ✅ **Auto-indexing** — performance optimized out of the box
|
|
20
|
+
- ✅ **Sharding-safe** — designed for MongoDB sharding
|
|
34
21
|
|
|
35
22
|
---
|
|
36
23
|
|
|
37
|
-
##
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Installation
|
|
38
27
|
|
|
39
28
|
```bash
|
|
40
29
|
npm install mongo-job-scheduler
|
|
41
30
|
```
|
|
42
31
|
|
|
43
|
-
|
|
32
|
+
### Basic Usage
|
|
44
33
|
|
|
45
34
|
```typescript
|
|
46
35
|
import { Scheduler, MongoJobStore } from "mongo-job-scheduler";
|
|
47
36
|
import { MongoClient } from "mongodb";
|
|
48
37
|
|
|
49
|
-
// ... connect to mongo ...
|
|
50
38
|
const client = new MongoClient("mongodb://localhost:27017");
|
|
51
39
|
await client.connect();
|
|
52
40
|
const db = client.db("my-app");
|
|
@@ -62,155 +50,222 @@ const scheduler = new Scheduler({
|
|
|
62
50
|
await scheduler.start();
|
|
63
51
|
```
|
|
64
52
|
|
|
65
|
-
|
|
53
|
+
---
|
|
66
54
|
|
|
67
|
-
|
|
55
|
+
## Scheduling Jobs
|
|
68
56
|
|
|
69
|
-
|
|
70
|
-
// Shorthand: 3 max attempts, 0 delay
|
|
71
|
-
await scheduler.schedule({
|
|
72
|
-
name: "email",
|
|
73
|
-
retry: 3,
|
|
74
|
-
});
|
|
57
|
+
### One-Time Job
|
|
75
58
|
|
|
76
|
-
|
|
59
|
+
```typescript
|
|
77
60
|
await scheduler.schedule({
|
|
78
|
-
name: "
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
delay: 1000, // 1 second fixed delay
|
|
82
|
-
// delay: (attempt) => attempt * 1000 // or dynamic backoff
|
|
83
|
-
},
|
|
61
|
+
name: "send-email",
|
|
62
|
+
data: { userId: 123 },
|
|
63
|
+
runAt: new Date(Date.now() + 60000), // run in 1 minute
|
|
84
64
|
});
|
|
85
65
|
```
|
|
86
66
|
|
|
87
|
-
|
|
67
|
+
### Cron Jobs (Timezone-Aware)
|
|
88
68
|
|
|
89
69
|
```typescript
|
|
90
70
|
await scheduler.schedule({
|
|
91
71
|
name: "daily-report",
|
|
92
|
-
// data: { type: "report" }, // optional payload
|
|
93
72
|
repeat: {
|
|
94
|
-
cron: "0 9 * * *",
|
|
73
|
+
cron: "0 9 * * *", // every day at 9 AM
|
|
95
74
|
timezone: "Asia/Kolkata", // default is UTC
|
|
96
75
|
},
|
|
97
76
|
});
|
|
98
77
|
```
|
|
99
78
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Run a job repeatedly with a fixed delay between executions (e.g., every 5 minutes).
|
|
79
|
+
### Interval Jobs
|
|
103
80
|
|
|
104
81
|
```typescript
|
|
82
|
+
// Using milliseconds directly
|
|
105
83
|
await scheduler.schedule({
|
|
106
84
|
name: "cleanup-logs",
|
|
107
85
|
data: {},
|
|
108
86
|
repeat: {
|
|
109
|
-
every: 5 * 60 * 1000, // 5 minutes
|
|
87
|
+
every: 5 * 60 * 1000, // every 5 minutes
|
|
110
88
|
},
|
|
111
89
|
});
|
|
112
|
-
```
|
|
113
90
|
|
|
114
|
-
|
|
91
|
+
// Helper pattern for human-readable intervals
|
|
92
|
+
const minutes = (n: number) => n * 60 * 1000;
|
|
93
|
+
const hours = (n: number) => n * 60 * 60 * 1000;
|
|
94
|
+
const days = (n: number) => n * 24 * 60 * 60 * 1000;
|
|
115
95
|
|
|
116
|
-
|
|
96
|
+
await scheduler.schedule({
|
|
97
|
+
name: "daily-backup",
|
|
98
|
+
repeat: { every: days(1) }, // 1 day
|
|
99
|
+
});
|
|
117
100
|
|
|
118
|
-
```typescript
|
|
119
101
|
await scheduler.schedule({
|
|
120
|
-
name: "
|
|
121
|
-
|
|
122
|
-
dedupeKey: "email:user:123",
|
|
102
|
+
name: "hourly-sync",
|
|
103
|
+
repeat: { every: hours(2) }, // 2 hours
|
|
123
104
|
});
|
|
124
105
|
```
|
|
125
106
|
|
|
126
|
-
|
|
107
|
+
> **📌 Repeat Job Status**: Repeating jobs cycle through `pending` → `running` → `pending` (rescheduled). The same job document is reused with an updated `nextRunAt`. Jobs stay in the database until cancelled.
|
|
108
|
+
|
|
109
|
+
### Bulk Scheduling
|
|
110
|
+
|
|
111
|
+
For high-performance ingestion:
|
|
127
112
|
|
|
128
113
|
```typescript
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
const jobs = await scheduler.scheduleBulk([
|
|
115
|
+
{ name: "email", data: { userId: 1 } },
|
|
116
|
+
{ name: "email", data: { userId: 2 } },
|
|
117
|
+
{ name: "email", data: { userId: 3 } },
|
|
118
|
+
]);
|
|
131
119
|
```
|
|
132
120
|
|
|
133
|
-
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Job Management
|
|
124
|
+
|
|
125
|
+
### Get Job by ID
|
|
134
126
|
|
|
135
127
|
```typescript
|
|
136
128
|
const job = await scheduler.getJob(jobId);
|
|
137
129
|
```
|
|
138
130
|
|
|
139
|
-
|
|
131
|
+
### Query Jobs
|
|
140
132
|
|
|
141
|
-
|
|
133
|
+
List jobs with filtering, sorting, and pagination:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const jobs = await scheduler.getJobs({
|
|
137
|
+
name: "daily-report",
|
|
138
|
+
status: "failed", // or ["failed", "pending"]
|
|
139
|
+
sort: { field: "updatedAt", order: "desc" },
|
|
140
|
+
limit: 10,
|
|
141
|
+
skip: 0,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Update Job
|
|
146
|
+
|
|
147
|
+
Update job data, reschedule, or modify configuration:
|
|
142
148
|
|
|
143
149
|
```typescript
|
|
144
150
|
await scheduler.updateJob(jobId, {
|
|
145
151
|
data: { page: 2 },
|
|
146
152
|
nextRunAt: new Date(Date.now() + 60000), // delay by 1 min
|
|
147
|
-
repeat: { every: 60000 }, //
|
|
153
|
+
repeat: { every: 60000 }, // change to run every minute
|
|
148
154
|
});
|
|
149
155
|
```
|
|
150
156
|
|
|
151
|
-
|
|
157
|
+
### Cancel Job
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
await scheduler.cancel(jobId);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Advanced Features
|
|
152
166
|
|
|
153
|
-
|
|
167
|
+
### Retries with Backoff
|
|
154
168
|
|
|
155
169
|
```typescript
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
// Simple: 3 attempts with instant retry
|
|
171
|
+
await scheduler.schedule({
|
|
172
|
+
name: "webhook",
|
|
173
|
+
retry: 3,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Advanced: custom delay and backoff
|
|
177
|
+
await scheduler.schedule({
|
|
178
|
+
name: "api-call",
|
|
179
|
+
retry: {
|
|
180
|
+
maxAttempts: 5,
|
|
181
|
+
delay: 1000, // 1 second fixed delay
|
|
182
|
+
// or: delay: (attempt) => attempt * 1000 // dynamic backoff
|
|
183
|
+
},
|
|
184
|
+
});
|
|
160
185
|
```
|
|
161
186
|
|
|
162
|
-
|
|
187
|
+
### Job Deduplication
|
|
163
188
|
|
|
164
|
-
|
|
189
|
+
Prevent duplicate jobs using idempotency keys:
|
|
165
190
|
|
|
166
191
|
```typescript
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
192
|
+
await scheduler.schedule({
|
|
193
|
+
name: "email",
|
|
194
|
+
data: { userId: 123 },
|
|
195
|
+
dedupeKey: "email:user:123", // only one job with this key
|
|
196
|
+
});
|
|
197
|
+
```
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
scheduler.on("worker:start", (workerId) =>
|
|
176
|
-
console.log("Worker started:", workerId)
|
|
177
|
-
);
|
|
178
|
-
scheduler.on("worker:stop", (workerId) =>
|
|
179
|
-
console.log("Worker stopped:", workerId)
|
|
180
|
-
);
|
|
199
|
+
### Event Monitoring
|
|
181
200
|
|
|
182
|
-
|
|
183
|
-
scheduler.on("job:created", (job) => console.log("Job created:", job._id));
|
|
184
|
-
scheduler.on("job:start", (job) => console.log("Job processing:", job._id));
|
|
201
|
+
```typescript
|
|
185
202
|
scheduler.on("job:success", (job) => console.log("Job done:", job._id));
|
|
186
203
|
scheduler.on("job:fail", ({ job, error }) =>
|
|
187
204
|
console.error("Job failed:", job._id, error)
|
|
188
205
|
);
|
|
189
206
|
scheduler.on("job:retry", (job) =>
|
|
190
|
-
console.warn("
|
|
207
|
+
console.warn("Retrying:", job._id, "attempt", job.attempts)
|
|
191
208
|
);
|
|
192
|
-
|
|
209
|
+
|
|
210
|
+
// More events: scheduler:start, scheduler:stop, worker:start,
|
|
211
|
+
// worker:stop, job:created, job:start, job:cancel
|
|
193
212
|
```
|
|
194
213
|
|
|
195
|
-
|
|
214
|
+
### Graceful Shutdown
|
|
196
215
|
|
|
197
|
-
|
|
216
|
+
Wait for in-flight jobs to complete:
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
```typescript
|
|
219
|
+
await scheduler.stop({
|
|
220
|
+
graceful: true,
|
|
221
|
+
timeoutMs: 30000,
|
|
222
|
+
});
|
|
223
|
+
```
|
|
204
224
|
|
|
205
|
-
|
|
225
|
+
---
|
|
206
226
|
|
|
207
|
-
|
|
227
|
+
## Performance & Scaling
|
|
208
228
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
### Automatic Indexing
|
|
230
|
+
|
|
231
|
+
**MongoDB indexes are created automatically** when you initialize `MongoJobStore`. No manual setup required.
|
|
232
|
+
|
|
233
|
+
The library creates three indexes in background mode:
|
|
234
|
+
|
|
235
|
+
- `{ status: 1, nextRunAt: 1 }` — for job polling (critical)
|
|
236
|
+
- `{ dedupeKey: 1 }` — for deduplication (unique)
|
|
237
|
+
- `{ lockedAt: 1 }` — for stale lock recovery
|
|
238
|
+
|
|
239
|
+
These indexes prevent query time from degrading from O(log n) to O(n) at scale.
|
|
240
|
+
|
|
241
|
+
### Distributed Systems
|
|
242
|
+
|
|
243
|
+
Run **multiple scheduler instances** (different servers, pods, or processes) connected to the same MongoDB:
|
|
244
|
+
|
|
245
|
+
- **Atomic Locking** — uses `findOneAndUpdate` to prevent race conditions
|
|
246
|
+
- **Concurrency Control** — only one worker executes a job instance
|
|
247
|
+
- **Horizontally Scalable** — supports MongoDB sharding
|
|
248
|
+
|
|
249
|
+
See `architecture.md` for sharding strategy and production guidelines.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Documentation
|
|
254
|
+
|
|
255
|
+
- **`architecture.md`** — Internal design, MongoDB schema, sharding strategy, production checklist
|
|
256
|
+
- **Job lifecycle** — pending → running → completed/failed
|
|
257
|
+
- **Retry & repeat semantics** — at-most-once guarantees
|
|
258
|
+
- **Correctness guarantees** — what we ensure and what we don't
|
|
259
|
+
|
|
260
|
+
---
|
|
212
261
|
|
|
213
262
|
## Status
|
|
214
263
|
|
|
215
|
-
**Early-stage but production-tested.**
|
|
264
|
+
**Early-stage but production-tested.**
|
|
216
265
|
API may evolve before 1.0.0.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
package/dist/core/scheduler.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { SchedulerEventMap } from "../types/events";
|
|
|
2
2
|
import { JobStore, JobUpdates } from "../store";
|
|
3
3
|
import { Job } from "../types/job";
|
|
4
4
|
import { ScheduleOptions } from "../types/schedule";
|
|
5
|
+
import { JobQuery } from "../types/query";
|
|
5
6
|
export interface SchedulerOptions {
|
|
6
7
|
id?: string;
|
|
7
8
|
store?: JobStore;
|
|
@@ -33,6 +34,10 @@ export declare class Scheduler {
|
|
|
33
34
|
* Get a job by ID
|
|
34
35
|
*/
|
|
35
36
|
getJob(jobId: unknown): Promise<Job | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Query jobs
|
|
39
|
+
*/
|
|
40
|
+
getJobs(query: JobQuery): Promise<Job[]>;
|
|
36
41
|
/**
|
|
37
42
|
* Update job data or schedule
|
|
38
43
|
*/
|
package/dist/core/scheduler.js
CHANGED
|
@@ -94,6 +94,15 @@ class Scheduler {
|
|
|
94
94
|
}
|
|
95
95
|
return this.store.findById(jobId);
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Query jobs
|
|
99
|
+
*/
|
|
100
|
+
async getJobs(query) {
|
|
101
|
+
if (!this.store) {
|
|
102
|
+
throw new Error("Scheduler has no JobStore configured");
|
|
103
|
+
}
|
|
104
|
+
return this.store.findAll(query);
|
|
105
|
+
}
|
|
97
106
|
/**
|
|
98
107
|
* Update job data or schedule
|
|
99
108
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Job } from "../types/job";
|
|
2
2
|
import { JobStore, JobUpdates } from "./job-store";
|
|
3
|
+
import { JobQuery } from "../types/query";
|
|
3
4
|
export declare class InMemoryJobStore implements JobStore {
|
|
4
5
|
private jobs;
|
|
5
6
|
private mutex;
|
|
@@ -24,4 +25,5 @@ export declare class InMemoryJobStore implements JobStore {
|
|
|
24
25
|
findById(jobId: unknown): Promise<Job | null>;
|
|
25
26
|
renewLock(jobId: unknown, workerId: string): Promise<void>;
|
|
26
27
|
update(jobId: unknown, updates: JobUpdates): Promise<void>;
|
|
28
|
+
findAll(query: JobQuery): Promise<Job[]>;
|
|
27
29
|
}
|
|
@@ -149,5 +149,31 @@ class InMemoryJobStore {
|
|
|
149
149
|
}
|
|
150
150
|
job.updatedAt = new Date();
|
|
151
151
|
}
|
|
152
|
+
async findAll(query) {
|
|
153
|
+
let jobs = Array.from(this.jobs.values());
|
|
154
|
+
// Filter
|
|
155
|
+
if (query.name) {
|
|
156
|
+
jobs = jobs.filter((j) => j.name === query.name);
|
|
157
|
+
}
|
|
158
|
+
if (query.status) {
|
|
159
|
+
const statuses = Array.isArray(query.status)
|
|
160
|
+
? query.status
|
|
161
|
+
: [query.status];
|
|
162
|
+
jobs = jobs.filter((j) => statuses.includes(j.status));
|
|
163
|
+
}
|
|
164
|
+
// Sort
|
|
165
|
+
if (query.sort) {
|
|
166
|
+
const { field, order } = query.sort;
|
|
167
|
+
jobs.sort((a, b) => {
|
|
168
|
+
const valA = a[field].getTime();
|
|
169
|
+
const valB = b[field].getTime();
|
|
170
|
+
return order === "asc" ? valA - valB : valB - valA;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// Skip/Limit
|
|
174
|
+
const start = query.skip ?? 0;
|
|
175
|
+
const end = query.limit ? start + query.limit : undefined;
|
|
176
|
+
return jobs.slice(start, end);
|
|
177
|
+
}
|
|
152
178
|
}
|
|
153
179
|
exports.InMemoryJobStore = InMemoryJobStore;
|
|
@@ -55,9 +55,14 @@ export interface JobStore {
|
|
|
55
55
|
* Update job properties (data persistence)
|
|
56
56
|
*/
|
|
57
57
|
update(jobId: unknown, updates: JobUpdates): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Find all jobs matching query
|
|
60
|
+
*/
|
|
61
|
+
findAll(query: JobQuery): Promise<Job[]>;
|
|
58
62
|
}
|
|
59
63
|
import { RetryOptions } from "../types/retry";
|
|
60
64
|
import { RepeatOptions } from "../types/repeat";
|
|
65
|
+
import { JobQuery } from "../types/query";
|
|
61
66
|
export interface JobUpdates {
|
|
62
67
|
data?: unknown;
|
|
63
68
|
nextRunAt?: Date;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Db, ObjectId } from "mongodb";
|
|
2
2
|
import { JobStore, JobUpdates } from "../job-store";
|
|
3
3
|
import { Job } from "../../types/job";
|
|
4
|
+
import { JobQuery } from "../../types/query";
|
|
4
5
|
export interface MongoJobStoreOptions {
|
|
5
6
|
collectionName?: string;
|
|
6
7
|
lockTimeoutMs?: number;
|
|
@@ -9,6 +10,10 @@ export declare class MongoJobStore implements JobStore {
|
|
|
9
10
|
private readonly collection;
|
|
10
11
|
private readonly defaultLockTimeoutMs;
|
|
11
12
|
constructor(db: Db, options?: MongoJobStoreOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Create necessary indexes for optimal query performance
|
|
15
|
+
*/
|
|
16
|
+
private ensureIndexes;
|
|
12
17
|
create(job: Job): Promise<Job>;
|
|
13
18
|
createBulk(jobs: Job[]): Promise<Job[]>;
|
|
14
19
|
findAndLockNext(options: {
|
|
@@ -30,4 +35,5 @@ export declare class MongoJobStore implements JobStore {
|
|
|
30
35
|
}): Promise<number>;
|
|
31
36
|
renewLock(id: ObjectId, workerId: string): Promise<void>;
|
|
32
37
|
update(id: ObjectId, updates: JobUpdates): Promise<void>;
|
|
38
|
+
findAll(query: JobQuery): Promise<Job[]>;
|
|
33
39
|
}
|
|
@@ -5,6 +5,23 @@ class MongoJobStore {
|
|
|
5
5
|
constructor(db, options = {}) {
|
|
6
6
|
this.collection = db.collection(options.collectionName ?? "scheduler_jobs");
|
|
7
7
|
this.defaultLockTimeoutMs = options.lockTimeoutMs ?? 30000;
|
|
8
|
+
// Auto-create indexes for performance
|
|
9
|
+
this.ensureIndexes().catch((err) => {
|
|
10
|
+
console.error("Failed to create indexes:", err);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create necessary indexes for optimal query performance
|
|
15
|
+
*/
|
|
16
|
+
async ensureIndexes() {
|
|
17
|
+
await Promise.all([
|
|
18
|
+
// Primary index for job polling (findAndLockNext)
|
|
19
|
+
this.collection.createIndex({ status: 1, nextRunAt: 1 }, { background: true }),
|
|
20
|
+
// Index for deduplication
|
|
21
|
+
this.collection.createIndex({ dedupeKey: 1 }, { unique: true, sparse: true, background: true }),
|
|
22
|
+
// Index for stale lock recovery
|
|
23
|
+
this.collection.createIndex({ lockedAt: 1 }, { sparse: true, background: true }),
|
|
24
|
+
]);
|
|
8
25
|
}
|
|
9
26
|
// --------------------------------------------------
|
|
10
27
|
// CREATE
|
|
@@ -195,5 +212,30 @@ class MongoJobStore {
|
|
|
195
212
|
$set.repeat = updates.repeat;
|
|
196
213
|
await this.collection.updateOne({ _id: id }, { $set });
|
|
197
214
|
}
|
|
215
|
+
async findAll(query) {
|
|
216
|
+
const filter = {};
|
|
217
|
+
if (query.name) {
|
|
218
|
+
filter.name = query.name;
|
|
219
|
+
}
|
|
220
|
+
if (query.status) {
|
|
221
|
+
filter.status = Array.isArray(query.status)
|
|
222
|
+
? { $in: query.status }
|
|
223
|
+
: query.status;
|
|
224
|
+
}
|
|
225
|
+
let cursor = this.collection.find(filter);
|
|
226
|
+
if (query.sort) {
|
|
227
|
+
cursor = cursor.sort({
|
|
228
|
+
[query.sort.field]: query.sort.order === "asc" ? 1 : -1,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (query.skip) {
|
|
232
|
+
cursor = cursor.skip(query.skip);
|
|
233
|
+
}
|
|
234
|
+
if (query.limit) {
|
|
235
|
+
cursor = cursor.limit(query.limit);
|
|
236
|
+
}
|
|
237
|
+
const docs = await cursor.toArray();
|
|
238
|
+
return docs;
|
|
239
|
+
}
|
|
198
240
|
}
|
|
199
241
|
exports.MongoJobStore = MongoJobStore;
|
package/package.json
CHANGED