@monque/core 1.1.0 → 1.1.2
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 +15 -0
- package/dist/CHANGELOG.md +89 -0
- package/dist/LICENSE +15 -0
- package/dist/README.md +150 -0
- package/dist/index.cjs +6 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -14
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -7
- package/src/events/index.ts +1 -0
- package/src/events/types.ts +113 -0
- package/src/index.ts +51 -0
- package/src/jobs/guards.ts +220 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/types.ts +335 -0
- package/src/scheduler/helpers.ts +107 -0
- package/src/scheduler/index.ts +5 -0
- package/src/scheduler/monque.ts +1309 -0
- package/src/scheduler/services/change-stream-handler.ts +239 -0
- package/src/scheduler/services/index.ts +8 -0
- package/src/scheduler/services/job-manager.ts +455 -0
- package/src/scheduler/services/job-processor.ts +301 -0
- package/src/scheduler/services/job-query.ts +411 -0
- package/src/scheduler/services/job-scheduler.ts +267 -0
- package/src/scheduler/services/types.ts +48 -0
- package/src/scheduler/types.ts +123 -0
- package/src/shared/errors.ts +225 -0
- package/src/shared/index.ts +18 -0
- package/src/shared/utils/backoff.ts +77 -0
- package/src/shared/utils/cron.ts +67 -0
- package/src/shared/utils/index.ts +7 -0
- package/src/workers/index.ts +1 -0
- package/src/workers/types.ts +39 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { Document } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EnqueueOptions,
|
|
5
|
+
type Job,
|
|
6
|
+
JobStatus,
|
|
7
|
+
type PersistedJob,
|
|
8
|
+
type ScheduleOptions,
|
|
9
|
+
} from '@/jobs';
|
|
10
|
+
import { ConnectionError, getNextCronDate, MonqueError } from '@/shared';
|
|
11
|
+
|
|
12
|
+
import type { SchedulerContext } from './types.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal service for job scheduling operations.
|
|
16
|
+
*
|
|
17
|
+
* Handles enqueueing new jobs, immediate dispatch, and cron scheduling.
|
|
18
|
+
* All operations are atomic and support deduplication via uniqueKey.
|
|
19
|
+
*
|
|
20
|
+
* @internal Not part of public API - use Monque class methods instead.
|
|
21
|
+
*/
|
|
22
|
+
export class JobScheduler {
|
|
23
|
+
constructor(private readonly ctx: SchedulerContext) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enqueue a job for processing.
|
|
27
|
+
*
|
|
28
|
+
* Jobs are stored in MongoDB and processed by registered workers. Supports
|
|
29
|
+
* delayed execution via `runAt` and deduplication via `uniqueKey`.
|
|
30
|
+
*
|
|
31
|
+
* When a `uniqueKey` is provided, only one pending or processing job with that key
|
|
32
|
+
* can exist. Completed or failed jobs don't block new jobs with the same key.
|
|
33
|
+
*
|
|
34
|
+
* Failed jobs are automatically retried with exponential backoff up to `maxRetries`
|
|
35
|
+
* (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.
|
|
36
|
+
*
|
|
37
|
+
* @template T - The job data payload type (must be JSON-serializable)
|
|
38
|
+
* @param name - Job type identifier, must match a registered worker
|
|
39
|
+
* @param data - Job payload, will be passed to the worker handler
|
|
40
|
+
* @param options - Scheduling and deduplication options
|
|
41
|
+
* @returns Promise resolving to the created or existing job document
|
|
42
|
+
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
43
|
+
*
|
|
44
|
+
* @example Basic job enqueueing
|
|
45
|
+
* ```typescript
|
|
46
|
+
* await monque.enqueue('send-email', {
|
|
47
|
+
* to: 'user@example.com',
|
|
48
|
+
* subject: 'Welcome!',
|
|
49
|
+
* body: 'Thanks for signing up.'
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example Delayed execution
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const oneHourLater = new Date(Date.now() + 3600000);
|
|
56
|
+
* await monque.enqueue('reminder', { message: 'Check in!' }, {
|
|
57
|
+
* runAt: oneHourLater
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example Prevent duplicates with unique key
|
|
62
|
+
* ```typescript
|
|
63
|
+
* await monque.enqueue('sync-user', { userId: '123' }, {
|
|
64
|
+
* uniqueKey: 'sync-user-123'
|
|
65
|
+
* });
|
|
66
|
+
* // Subsequent enqueues with same uniqueKey return existing pending/processing job
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
async enqueue<T>(name: string, data: T, options: EnqueueOptions = {}): Promise<PersistedJob<T>> {
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const job: Omit<Job<T>, '_id'> = {
|
|
72
|
+
name,
|
|
73
|
+
data,
|
|
74
|
+
status: JobStatus.PENDING,
|
|
75
|
+
nextRunAt: options.runAt ?? now,
|
|
76
|
+
failCount: 0,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (options.uniqueKey) {
|
|
82
|
+
job.uniqueKey = options.uniqueKey;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (options.uniqueKey) {
|
|
87
|
+
// Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)
|
|
88
|
+
const result = await this.ctx.collection.findOneAndUpdate(
|
|
89
|
+
{
|
|
90
|
+
name,
|
|
91
|
+
uniqueKey: options.uniqueKey,
|
|
92
|
+
status: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
$setOnInsert: job,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
upsert: true,
|
|
99
|
+
returnDocument: 'after',
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!result) {
|
|
104
|
+
throw new ConnectionError('Failed to enqueue job: findOneAndUpdate returned no document');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return this.ctx.documentToPersistedJob<T>(result);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const result = await this.ctx.collection.insertOne(job as Document);
|
|
111
|
+
|
|
112
|
+
return { ...job, _id: result.insertedId } as PersistedJob<T>;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof ConnectionError) {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const message = error instanceof Error ? error.message : 'Unknown error during enqueue';
|
|
118
|
+
throw new ConnectionError(
|
|
119
|
+
`Failed to enqueue job: ${message}`,
|
|
120
|
+
error instanceof Error ? { cause: error } : undefined,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Enqueue a job for immediate processing.
|
|
127
|
+
*
|
|
128
|
+
* Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.
|
|
129
|
+
* Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).
|
|
130
|
+
*
|
|
131
|
+
* @template T - The job data payload type (must be JSON-serializable)
|
|
132
|
+
* @param name - Job type identifier, must match a registered worker
|
|
133
|
+
* @param data - Job payload, will be passed to the worker handler
|
|
134
|
+
* @returns Promise resolving to the created job document
|
|
135
|
+
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
136
|
+
*
|
|
137
|
+
* @example Send email immediately
|
|
138
|
+
* ```typescript
|
|
139
|
+
* await monque.now('send-email', {
|
|
140
|
+
* to: 'admin@example.com',
|
|
141
|
+
* subject: 'Alert',
|
|
142
|
+
* body: 'Immediate attention required'
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* @example Process order in background
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const order = await createOrder(data);
|
|
149
|
+
* await monque.now('process-order', { orderId: order.id });
|
|
150
|
+
* return order; // Return immediately, processing happens async
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
async now<T>(name: string, data: T): Promise<PersistedJob<T>> {
|
|
154
|
+
return this.enqueue(name, data, { runAt: new Date() });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Schedule a recurring job with a cron expression.
|
|
159
|
+
*
|
|
160
|
+
* Creates a job that automatically re-schedules itself based on the cron pattern.
|
|
161
|
+
* Uses standard 5-field cron format: minute, hour, day of month, month, day of week.
|
|
162
|
+
* Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.
|
|
163
|
+
* After successful completion, the job is reset to `pending` status and scheduled
|
|
164
|
+
* for its next run based on the cron expression.
|
|
165
|
+
*
|
|
166
|
+
* When a `uniqueKey` is provided, only one pending or processing job with that key
|
|
167
|
+
* can exist. This prevents duplicate scheduled jobs on application restart.
|
|
168
|
+
*
|
|
169
|
+
* @template T - The job data payload type (must be JSON-serializable)
|
|
170
|
+
* @param cron - Cron expression (5 fields or predefined expression)
|
|
171
|
+
* @param name - Job type identifier, must match a registered worker
|
|
172
|
+
* @param data - Job payload, will be passed to the worker handler on each run
|
|
173
|
+
* @param options - Scheduling options (uniqueKey for deduplication)
|
|
174
|
+
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
175
|
+
* @throws {InvalidCronError} If cron expression is invalid
|
|
176
|
+
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
177
|
+
*
|
|
178
|
+
* @example Hourly cleanup job
|
|
179
|
+
* ```typescript
|
|
180
|
+
* await monque.schedule('0 * * * *', 'cleanup-temp-files', {
|
|
181
|
+
* directory: '/tmp/uploads'
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @example Prevent duplicate scheduled jobs with unique key
|
|
186
|
+
* ```typescript
|
|
187
|
+
* await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {
|
|
188
|
+
* uniqueKey: 'hourly-report-sales'
|
|
189
|
+
* });
|
|
190
|
+
* // Subsequent calls with same uniqueKey return existing pending/processing job
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* @example Daily report at midnight (using predefined expression)
|
|
194
|
+
* ```typescript
|
|
195
|
+
* await monque.schedule('@daily', 'daily-report', {
|
|
196
|
+
* reportType: 'sales',
|
|
197
|
+
* recipients: ['analytics@example.com']
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
async schedule<T>(
|
|
202
|
+
cron: string,
|
|
203
|
+
name: string,
|
|
204
|
+
data: T,
|
|
205
|
+
options: ScheduleOptions = {},
|
|
206
|
+
): Promise<PersistedJob<T>> {
|
|
207
|
+
// Validate cron and get next run date (throws InvalidCronError if invalid)
|
|
208
|
+
const nextRunAt = getNextCronDate(cron);
|
|
209
|
+
|
|
210
|
+
const now = new Date();
|
|
211
|
+
const job: Omit<Job<T>, '_id'> = {
|
|
212
|
+
name,
|
|
213
|
+
data,
|
|
214
|
+
status: JobStatus.PENDING,
|
|
215
|
+
nextRunAt,
|
|
216
|
+
repeatInterval: cron,
|
|
217
|
+
failCount: 0,
|
|
218
|
+
createdAt: now,
|
|
219
|
+
updatedAt: now,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (options.uniqueKey) {
|
|
223
|
+
job.uniqueKey = options.uniqueKey;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (options.uniqueKey) {
|
|
228
|
+
// Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)
|
|
229
|
+
const result = await this.ctx.collection.findOneAndUpdate(
|
|
230
|
+
{
|
|
231
|
+
name,
|
|
232
|
+
uniqueKey: options.uniqueKey,
|
|
233
|
+
status: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
$setOnInsert: job,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
upsert: true,
|
|
240
|
+
returnDocument: 'after',
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (!result) {
|
|
245
|
+
throw new ConnectionError(
|
|
246
|
+
'Failed to schedule job: findOneAndUpdate returned no document',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return this.ctx.documentToPersistedJob<T>(result);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result = await this.ctx.collection.insertOne(job as Document);
|
|
254
|
+
|
|
255
|
+
return { ...job, _id: result.insertedId } as PersistedJob<T>;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof MonqueError) {
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
const message = error instanceof Error ? error.message : 'Unknown error during schedule';
|
|
261
|
+
throw new ConnectionError(
|
|
262
|
+
`Failed to schedule job: ${message}`,
|
|
263
|
+
error instanceof Error ? { cause: error } : undefined,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Collection, Document, WithId } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
import type { MonqueEventMap } from '@/events';
|
|
4
|
+
import type { PersistedJob } from '@/jobs';
|
|
5
|
+
import type { WorkerRegistration } from '@/workers';
|
|
6
|
+
|
|
7
|
+
import type { MonqueOptions } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolved Monque options with all defaults applied.
|
|
11
|
+
*
|
|
12
|
+
* Required options have their defaults filled in, while truly optional
|
|
13
|
+
* options (`maxBackoffDelay`, `jobRetention`) remain optional.
|
|
14
|
+
*/
|
|
15
|
+
export interface ResolvedMonqueOptions
|
|
16
|
+
extends Required<Omit<MonqueOptions, 'maxBackoffDelay' | 'jobRetention'>>,
|
|
17
|
+
Pick<MonqueOptions, 'maxBackoffDelay' | 'jobRetention'> {}
|
|
18
|
+
/**
|
|
19
|
+
* Shared context provided to all internal Monque services.
|
|
20
|
+
*
|
|
21
|
+
* Contains references to shared state, configuration, and utilities
|
|
22
|
+
* needed by service methods. Passed to service constructors to enable
|
|
23
|
+
* access to the collection, options, and event emission.
|
|
24
|
+
*
|
|
25
|
+
* @internal Not part of public API.
|
|
26
|
+
*/
|
|
27
|
+
export interface SchedulerContext {
|
|
28
|
+
/** MongoDB collection for jobs */
|
|
29
|
+
collection: Collection<Document>;
|
|
30
|
+
|
|
31
|
+
/** Resolved scheduler options with defaults applied */
|
|
32
|
+
options: ResolvedMonqueOptions;
|
|
33
|
+
|
|
34
|
+
/** Unique identifier for this scheduler instance (for claiming jobs) */
|
|
35
|
+
instanceId: string;
|
|
36
|
+
|
|
37
|
+
/** Registered workers by job name */
|
|
38
|
+
workers: Map<string, WorkerRegistration>;
|
|
39
|
+
|
|
40
|
+
/** Whether the scheduler is currently running */
|
|
41
|
+
isRunning: () => boolean;
|
|
42
|
+
|
|
43
|
+
/** Type-safe event emitter */
|
|
44
|
+
emit: <K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]) => boolean;
|
|
45
|
+
|
|
46
|
+
/** Convert MongoDB document to typed PersistedJob */
|
|
47
|
+
documentToPersistedJob: <T>(doc: WithId<Document>) => PersistedJob<T>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the Monque scheduler.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* const monque = new Monque(db, {
|
|
7
|
+
* collectionName: 'jobs',
|
|
8
|
+
* pollInterval: 1000,
|
|
9
|
+
* maxRetries: 10,
|
|
10
|
+
* baseRetryInterval: 1000,
|
|
11
|
+
* shutdownTimeout: 30000,
|
|
12
|
+
* defaultConcurrency: 5,
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export interface MonqueOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Name of the MongoDB collection for storing jobs.
|
|
19
|
+
* @default 'monque_jobs'
|
|
20
|
+
*/
|
|
21
|
+
collectionName?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Interval in milliseconds between polling for new jobs.
|
|
25
|
+
* @default 1000
|
|
26
|
+
*/
|
|
27
|
+
pollInterval?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maximum number of retry attempts before marking a job as permanently failed.
|
|
31
|
+
* @default 10
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Base interval in milliseconds for exponential backoff calculation.
|
|
37
|
+
* Actual delay = 2^failCount * baseRetryInterval
|
|
38
|
+
* @default 1000
|
|
39
|
+
*/
|
|
40
|
+
baseRetryInterval?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maximum delay in milliseconds for exponential backoff.
|
|
44
|
+
* If calculated delay exceeds this value, it will be capped.
|
|
45
|
+
*
|
|
46
|
+
* Defaults to 24 hours to prevent unbounded delays.
|
|
47
|
+
* @default 86400000 (24 hours)
|
|
48
|
+
*/
|
|
49
|
+
maxBackoffDelay?: number | undefined;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Timeout in milliseconds for graceful shutdown.
|
|
53
|
+
* @default 30000
|
|
54
|
+
*/
|
|
55
|
+
shutdownTimeout?: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default number of concurrent jobs per worker.
|
|
59
|
+
* @default 5
|
|
60
|
+
*/
|
|
61
|
+
defaultConcurrency?: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Maximum time in milliseconds a job can be in 'processing' status before
|
|
65
|
+
* being considered stale and eligible for recovery.
|
|
66
|
+
*
|
|
67
|
+
* Stale recovery uses `lockedAt` as the source of truth; this is an absolute
|
|
68
|
+
* “time locked” limit, not a heartbeat timeout.
|
|
69
|
+
* @default 1800000 (30 minutes)
|
|
70
|
+
*/
|
|
71
|
+
lockTimeout?: number;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Unique identifier for this scheduler instance.
|
|
75
|
+
* Used for atomic job claiming - each instance uses this ID to claim jobs.
|
|
76
|
+
* Defaults to a randomly generated UUID v4.
|
|
77
|
+
* @default crypto.randomUUID()
|
|
78
|
+
*/
|
|
79
|
+
schedulerInstanceId?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Interval in milliseconds for heartbeat updates during job processing.
|
|
83
|
+
* The scheduler periodically updates `lastHeartbeat` for all jobs it is processing
|
|
84
|
+
* to indicate liveness for monitoring/debugging.
|
|
85
|
+
*
|
|
86
|
+
* Note: stale recovery is based on `lockedAt` + `lockTimeout`, not `lastHeartbeat`.
|
|
87
|
+
* @default 30000 (30 seconds)
|
|
88
|
+
*/
|
|
89
|
+
heartbeatInterval?: number;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether to recover stale processing jobs on scheduler startup.
|
|
93
|
+
* When true, jobs with lockedAt older than lockTimeout will be reset to pending.
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
96
|
+
recoverStaleJobs?: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Configuration for automatic cleanup of completed and failed jobs.
|
|
100
|
+
* If undefined, no cleanup is performed.
|
|
101
|
+
*/
|
|
102
|
+
jobRetention?:
|
|
103
|
+
| {
|
|
104
|
+
/**
|
|
105
|
+
* Age in milliseconds after which completed jobs are deleted.
|
|
106
|
+
* Cleaned up based on 'updatedAt' timestamp.
|
|
107
|
+
*/
|
|
108
|
+
completed?: number;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Age in milliseconds after which failed jobs are deleted.
|
|
112
|
+
* Cleaned up based on 'updatedAt' timestamp.
|
|
113
|
+
*/
|
|
114
|
+
failed?: number;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Interval in milliseconds for running the cleanup job.
|
|
118
|
+
* @default 3600000 (1 hour)
|
|
119
|
+
*/
|
|
120
|
+
interval?: number;
|
|
121
|
+
}
|
|
122
|
+
| undefined;
|
|
123
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Job } from '@/jobs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base error class for all Monque-related errors.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* try {
|
|
9
|
+
* await monque.enqueue('job', data);
|
|
10
|
+
* } catch (error) {
|
|
11
|
+
* if (error instanceof MonqueError) {
|
|
12
|
+
* console.error('Monque error:', error.message);
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class MonqueError extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'MonqueError';
|
|
21
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
22
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
23
|
+
if (Error.captureStackTrace) {
|
|
24
|
+
Error.captureStackTrace(this, MonqueError);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Error thrown when an invalid cron expression is provided.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* try {
|
|
35
|
+
* await monque.schedule('invalid cron', 'job', data);
|
|
36
|
+
* } catch (error) {
|
|
37
|
+
* if (error instanceof InvalidCronError) {
|
|
38
|
+
* console.error('Invalid expression:', error.expression);
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class InvalidCronError extends MonqueError {
|
|
44
|
+
constructor(
|
|
45
|
+
public readonly expression: string,
|
|
46
|
+
message: string,
|
|
47
|
+
) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'InvalidCronError';
|
|
50
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
51
|
+
if (Error.captureStackTrace) {
|
|
52
|
+
Error.captureStackTrace(this, InvalidCronError);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error thrown when there's a database connection issue.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* try {
|
|
63
|
+
* await monque.enqueue('job', data);
|
|
64
|
+
* } catch (error) {
|
|
65
|
+
* if (error instanceof ConnectionError) {
|
|
66
|
+
* console.error('Database connection lost');
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class ConnectionError extends MonqueError {
|
|
72
|
+
constructor(message: string, options?: { cause?: Error }) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = 'ConnectionError';
|
|
75
|
+
if (options?.cause) {
|
|
76
|
+
this.cause = options.cause;
|
|
77
|
+
}
|
|
78
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
79
|
+
if (Error.captureStackTrace) {
|
|
80
|
+
Error.captureStackTrace(this, ConnectionError);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Error thrown when graceful shutdown times out.
|
|
87
|
+
* Includes information about jobs that were still in progress.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* try {
|
|
92
|
+
* await monque.stop();
|
|
93
|
+
* } catch (error) {
|
|
94
|
+
* if (error instanceof ShutdownTimeoutError) {
|
|
95
|
+
* console.error('Incomplete jobs:', error.incompleteJobs.length);
|
|
96
|
+
* }
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export class ShutdownTimeoutError extends MonqueError {
|
|
101
|
+
constructor(
|
|
102
|
+
message: string,
|
|
103
|
+
public readonly incompleteJobs: Job[],
|
|
104
|
+
) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = 'ShutdownTimeoutError';
|
|
107
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
108
|
+
if (Error.captureStackTrace) {
|
|
109
|
+
Error.captureStackTrace(this, ShutdownTimeoutError);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Error thrown when attempting to register a worker for a job name
|
|
116
|
+
* that already has a registered worker, without explicitly allowing replacement.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* try {
|
|
121
|
+
* monque.register('send-email', handler1);
|
|
122
|
+
* monque.register('send-email', handler2); // throws
|
|
123
|
+
* } catch (error) {
|
|
124
|
+
* if (error instanceof WorkerRegistrationError) {
|
|
125
|
+
* console.error('Worker already registered for:', error.jobName);
|
|
126
|
+
* }
|
|
127
|
+
* }
|
|
128
|
+
*
|
|
129
|
+
* // To intentionally replace a worker:
|
|
130
|
+
* monque.register('send-email', handler2, { replace: true });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export class WorkerRegistrationError extends MonqueError {
|
|
134
|
+
constructor(
|
|
135
|
+
message: string,
|
|
136
|
+
public readonly jobName: string,
|
|
137
|
+
) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.name = 'WorkerRegistrationError';
|
|
140
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
141
|
+
if (Error.captureStackTrace) {
|
|
142
|
+
Error.captureStackTrace(this, WorkerRegistrationError);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Error thrown when a state transition is invalid.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* try {
|
|
153
|
+
* await monque.cancelJob(jobId);
|
|
154
|
+
* } catch (error) {
|
|
155
|
+
* if (error instanceof JobStateError) {
|
|
156
|
+
* console.error(`Cannot cancel job in state: ${error.currentStatus}`);
|
|
157
|
+
* }
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export class JobStateError extends MonqueError {
|
|
162
|
+
constructor(
|
|
163
|
+
message: string,
|
|
164
|
+
public readonly jobId: string,
|
|
165
|
+
public readonly currentStatus: string,
|
|
166
|
+
public readonly attemptedAction: 'cancel' | 'retry' | 'reschedule',
|
|
167
|
+
) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = 'JobStateError';
|
|
170
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
171
|
+
if (Error.captureStackTrace) {
|
|
172
|
+
Error.captureStackTrace(this, JobStateError);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Error thrown when a pagination cursor is invalid or malformed.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* try {
|
|
183
|
+
* await monque.listJobs({ cursor: 'invalid-cursor' });
|
|
184
|
+
* } catch (error) {
|
|
185
|
+
* if (error instanceof InvalidCursorError) {
|
|
186
|
+
* console.error('Invalid cursor provided');
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export class InvalidCursorError extends MonqueError {
|
|
192
|
+
constructor(message: string) {
|
|
193
|
+
super(message);
|
|
194
|
+
this.name = 'InvalidCursorError';
|
|
195
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
196
|
+
if (Error.captureStackTrace) {
|
|
197
|
+
Error.captureStackTrace(this, InvalidCursorError);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Error thrown when a statistics aggregation times out.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* try {
|
|
208
|
+
* const stats = await monque.getQueueStats();
|
|
209
|
+
* } catch (error) {
|
|
210
|
+
* if (error instanceof AggregationTimeoutError) {
|
|
211
|
+
* console.error('Stats took too long to calculate');
|
|
212
|
+
* }
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export class AggregationTimeoutError extends MonqueError {
|
|
217
|
+
constructor(message: string = 'Statistics aggregation exceeded 30 second timeout') {
|
|
218
|
+
super(message);
|
|
219
|
+
this.name = 'AggregationTimeoutError';
|
|
220
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
221
|
+
if (Error.captureStackTrace) {
|
|
222
|
+
Error.captureStackTrace(this, AggregationTimeoutError);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AggregationTimeoutError,
|
|
3
|
+
ConnectionError,
|
|
4
|
+
InvalidCronError,
|
|
5
|
+
InvalidCursorError,
|
|
6
|
+
JobStateError,
|
|
7
|
+
MonqueError,
|
|
8
|
+
ShutdownTimeoutError,
|
|
9
|
+
WorkerRegistrationError,
|
|
10
|
+
} from './errors.js';
|
|
11
|
+
export {
|
|
12
|
+
calculateBackoff,
|
|
13
|
+
calculateBackoffDelay,
|
|
14
|
+
DEFAULT_BASE_INTERVAL,
|
|
15
|
+
DEFAULT_MAX_BACKOFF_DELAY,
|
|
16
|
+
getNextCronDate,
|
|
17
|
+
validateCronExpression,
|
|
18
|
+
} from './utils/index.js';
|