@quave/agenda-mongodb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE.md +25 -0
  2. package/README.md +273 -0
  3. package/dist/MongoBackend.d.ts +58 -0
  4. package/dist/MongoBackend.js +82 -0
  5. package/dist/MongoBackend.js.map +1 -0
  6. package/dist/MongoChangeStreamNotificationChannel.d.ts +96 -0
  7. package/dist/MongoChangeStreamNotificationChannel.js +248 -0
  8. package/dist/MongoChangeStreamNotificationChannel.js.map +1 -0
  9. package/dist/MongoJobLogger.d.ts +46 -0
  10. package/dist/MongoJobLogger.js +142 -0
  11. package/dist/MongoJobLogger.js.map +1 -0
  12. package/dist/MongoJobRepository.d.ts +84 -0
  13. package/dist/MongoJobRepository.js +713 -0
  14. package/dist/MongoJobRepository.js.map +1 -0
  15. package/dist/cjs/MongoBackend.d.ts +58 -0
  16. package/dist/cjs/MongoBackend.js +86 -0
  17. package/dist/cjs/MongoBackend.js.map +1 -0
  18. package/dist/cjs/MongoChangeStreamNotificationChannel.d.ts +96 -0
  19. package/dist/cjs/MongoChangeStreamNotificationChannel.js +255 -0
  20. package/dist/cjs/MongoChangeStreamNotificationChannel.js.map +1 -0
  21. package/dist/cjs/MongoJobLogger.d.ts +46 -0
  22. package/dist/cjs/MongoJobLogger.js +149 -0
  23. package/dist/cjs/MongoJobLogger.js.map +1 -0
  24. package/dist/cjs/MongoJobRepository.d.ts +84 -0
  25. package/dist/cjs/MongoJobRepository.js +720 -0
  26. package/dist/cjs/MongoJobRepository.js.map +1 -0
  27. package/dist/cjs/hasMongoProtocol.d.ts +1 -0
  28. package/dist/cjs/hasMongoProtocol.js +6 -0
  29. package/dist/cjs/hasMongoProtocol.js.map +1 -0
  30. package/dist/cjs/index.d.ts +39 -0
  31. package/dist/cjs/index.js +46 -0
  32. package/dist/cjs/index.js.map +1 -0
  33. package/dist/cjs/package.json +1 -0
  34. package/dist/cjs/testing.d.ts +25 -0
  35. package/dist/cjs/testing.js +55 -0
  36. package/dist/cjs/testing.js.map +1 -0
  37. package/dist/cjs/types.d.ts +52 -0
  38. package/dist/cjs/types.js +3 -0
  39. package/dist/cjs/types.js.map +1 -0
  40. package/dist/hasMongoProtocol.d.ts +1 -0
  41. package/dist/hasMongoProtocol.js +2 -0
  42. package/dist/hasMongoProtocol.js.map +1 -0
  43. package/dist/index.d.ts +39 -0
  44. package/dist/index.js +37 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/testing.d.ts +25 -0
  47. package/dist/testing.js +49 -0
  48. package/dist/testing.js.map +1 -0
  49. package/dist/types.d.ts +52 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +84 -0
@@ -0,0 +1,248 @@
1
+ import debug from 'debug';
2
+ import { BaseNotificationChannel, toJobId } from '@quave/agenda';
3
+ const log = debug('agenda:mongo:changestream');
4
+ /**
5
+ * MongoDB Change Stream notification channel.
6
+ *
7
+ * Uses MongoDB Change Streams to watch for job changes in real-time.
8
+ * This eliminates the need for polling and provides instant notifications
9
+ * when jobs are created or updated.
10
+ *
11
+ * **Requirements:**
12
+ * - MongoDB must be deployed as a replica set (even single-node replica sets work)
13
+ * - WiredTiger storage engine (default since MongoDB 3.2)
14
+ *
15
+ * **How it works:**
16
+ * - Watches the jobs collection for insert/update/replace operations
17
+ * - When a job with `nextRunAt` is modified, handlers are notified
18
+ * - The `publish()` method is a no-op since Change Streams automatically
19
+ * detect job changes from any source
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { Agenda } from '@quave/agenda';
24
+ * import { MongoBackend, MongoChangeStreamNotificationChannel } from '@quave/agenda-mongodb';
25
+ *
26
+ * const backend = new MongoBackend({ mongo: db });
27
+ * const channel = new MongoChangeStreamNotificationChannel({ db });
28
+ *
29
+ * const agenda = new Agenda({
30
+ * backend,
31
+ * notificationChannel: channel
32
+ * });
33
+ * ```
34
+ */
35
+ export class MongoChangeStreamNotificationChannel extends BaseNotificationChannel {
36
+ constructor(config = {}) {
37
+ super(config);
38
+ this.db = config.db;
39
+ this.collectionName = config.collection ?? 'agendaJobs';
40
+ this.resumeToken = config.resumeToken;
41
+ this.useFullDocument = config.fullDocument ?? true;
42
+ }
43
+ /**
44
+ * Set the MongoDB database instance.
45
+ * Used when the channel is created before the database connection.
46
+ */
47
+ setDb(db) {
48
+ if (this._state === 'connected') {
49
+ throw new Error('Cannot set database while channel is connected');
50
+ }
51
+ this.db = db;
52
+ }
53
+ /**
54
+ * Get the last resume token for reconnection purposes.
55
+ * Store this token to resume watching from the same point after restart.
56
+ */
57
+ getResumeToken() {
58
+ return this.resumeToken;
59
+ }
60
+ async connect() {
61
+ log('connecting change stream notification channel (current state=%s)', this._state);
62
+ this.clearReconnect();
63
+ // Idempotent: a second connect() must not leak the active change stream.
64
+ if (this._state === 'connected' && this.changeStream) {
65
+ log('already connected, ignoring connect()');
66
+ return;
67
+ }
68
+ if (!this.db) {
69
+ throw new Error('MongoChangeStreamNotificationChannel requires a MongoDB database instance. ' +
70
+ 'Provide it via config.db or call setDb() before connecting.');
71
+ }
72
+ try {
73
+ this.setState('connecting');
74
+ log('getting collection: %s', this.collectionName);
75
+ this.collection = this.db.collection(this.collectionName);
76
+ // Build the aggregation pipeline to filter relevant changes
77
+ const pipeline = [
78
+ {
79
+ $match: {
80
+ // Watch for job inserts, updates, and replacements
81
+ operationType: { $in: ['insert', 'update', 'replace'] }
82
+ }
83
+ }
84
+ ];
85
+ // Change stream options
86
+ const options = {};
87
+ // Use fullDocument lookup to get the complete document on updates
88
+ // This is required to get job data on update events
89
+ if (this.useFullDocument) {
90
+ options.fullDocument = 'updateLookup';
91
+ }
92
+ // Resume from a specific token if provided
93
+ if (this.resumeToken) {
94
+ options.resumeAfter = this.resumeToken;
95
+ log('resuming from token: %O', this.resumeToken);
96
+ }
97
+ log('creating change stream with options: %O', options);
98
+ // Create the change stream
99
+ this.changeStream = this.collection.watch(pipeline, options);
100
+ // Handle change events
101
+ this.changeStream.on('change', (change) => {
102
+ this.handleChange(change);
103
+ });
104
+ // Handle errors
105
+ this.changeStream.on('error', (error) => {
106
+ log('change stream error: %O', error);
107
+ this.emit('error', error);
108
+ if (this._state === 'connected') {
109
+ this.handleConnectionLoss();
110
+ }
111
+ });
112
+ // Handle stream close - only treat as connection loss if we didn't initiate it
113
+ this.changeStream.on('close', () => {
114
+ log('change stream closed (state=%s)', this._state);
115
+ // Only handle as connection loss if we're still in connected state
116
+ // (not if we're disconnecting or already disconnected)
117
+ if (this._state === 'connected') {
118
+ this.handleConnectionLoss();
119
+ }
120
+ });
121
+ log('change stream connected, watching collection: %s', this.collectionName);
122
+ this.setState('connected');
123
+ }
124
+ catch (error) {
125
+ log('connection failed: %O', error);
126
+ this.emit('error', error);
127
+ this.scheduleReconnect();
128
+ throw error;
129
+ }
130
+ }
131
+ handleChange(change) {
132
+ try {
133
+ log('received change event: operationType=%s', change.operationType);
134
+ // Store resume token for recovery
135
+ if (change._id) {
136
+ this.resumeToken = change._id;
137
+ }
138
+ // Get the full document based on operation type
139
+ const fullDocument = this.extractFullDocument(change);
140
+ if (!fullDocument) {
141
+ log('change event without full document (operationType=%s), skipping', change.operationType);
142
+ return;
143
+ }
144
+ log('extracted fullDocument: name=%s, nextRunAt=%s, disabled=%s', fullDocument.name, fullDocument.nextRunAt, fullDocument.disabled);
145
+ // Only notify for jobs that have a nextRunAt (scheduled to run)
146
+ if (!fullDocument.nextRunAt) {
147
+ log('job has no nextRunAt, skipping notification');
148
+ return;
149
+ }
150
+ // Skip disabled jobs
151
+ if (fullDocument.disabled) {
152
+ log('job is disabled, skipping notification');
153
+ return;
154
+ }
155
+ // Build notification from the document
156
+ const notification = this.buildNotification(fullDocument);
157
+ log('received change notification: %O', notification);
158
+ this.notifyHandlers(notification).catch((err) => {
159
+ this.emit('error', err);
160
+ });
161
+ }
162
+ catch (error) {
163
+ log('error processing change event: %O', error);
164
+ this.emit('error', error);
165
+ }
166
+ }
167
+ extractFullDocument(change) {
168
+ // Handle insert events
169
+ if (change.operationType === 'insert') {
170
+ const insertChange = change;
171
+ return insertChange.fullDocument;
172
+ }
173
+ // Handle update events (requires fullDocument: 'updateLookup' option)
174
+ if (change.operationType === 'update') {
175
+ const updateChange = change;
176
+ if (updateChange.fullDocument) {
177
+ return updateChange.fullDocument;
178
+ }
179
+ log('update event missing fullDocument - ensure fullDocument option is set');
180
+ return null;
181
+ }
182
+ // Handle replace events
183
+ if (change.operationType === 'replace') {
184
+ const replaceChange = change;
185
+ return replaceChange.fullDocument;
186
+ }
187
+ return null;
188
+ }
189
+ buildNotification(doc) {
190
+ return {
191
+ jobId: toJobId(String(doc._id)),
192
+ jobName: doc.name,
193
+ nextRunAt: doc.nextRunAt ? new Date(doc.nextRunAt) : null,
194
+ priority: doc.priority || 0,
195
+ timestamp: new Date(),
196
+ source: 'changestream'
197
+ };
198
+ }
199
+ handleConnectionLoss() {
200
+ log('handling connection loss');
201
+ if (this.changeStream) {
202
+ try {
203
+ this.changeStream.close().catch(() => {
204
+ // Ignore close errors
205
+ });
206
+ }
207
+ catch {
208
+ // Ignore close errors
209
+ }
210
+ this.changeStream = undefined;
211
+ }
212
+ this.scheduleReconnect();
213
+ }
214
+ async disconnect() {
215
+ log('disconnecting change stream notification channel');
216
+ this.clearReconnect();
217
+ // Set state first to prevent close event from triggering reconnection
218
+ this.setState('disconnected');
219
+ if (this.changeStream) {
220
+ try {
221
+ await this.changeStream.close();
222
+ }
223
+ catch {
224
+ // Ignore close errors
225
+ }
226
+ this.changeStream = undefined;
227
+ }
228
+ this.handlers.clear();
229
+ }
230
+ /**
231
+ * Publish a job notification.
232
+ *
233
+ * **Note:** This method is a no-op for Change Stream notifications.
234
+ * Change Streams automatically detect job changes in the database,
235
+ * so explicit publishing is not required. This method exists only
236
+ * to satisfy the NotificationChannel interface.
237
+ *
238
+ * All job saves to the watched collection will automatically trigger
239
+ * notifications to all subscribers.
240
+ */
241
+ async publish(_notification) {
242
+ // No-op: Change Streams automatically detect database changes.
243
+ // When a job is saved to MongoDB, the change stream will pick it up
244
+ // and notify all handlers automatically.
245
+ log('publish called (no-op for change stream - changes detected automatically)');
246
+ }
247
+ }
248
+ //# sourceMappingURL=MongoChangeStreamNotificationChannel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoChangeStreamNotificationChannel.js","sourceRoot":"","sources":["../src/MongoChangeStreamNotificationChannel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { Db } from 'mongodb';
2
+ import type { JobLogger, JobLogEntry, JobLogQuery, JobLogQueryResult } from '@quave/agenda';
3
+ /**
4
+ * MongoDB implementation of JobLogger.
5
+ * Stores job lifecycle events in a dedicated MongoDB collection.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Via backend (automatic connection sharing):
10
+ * import { MongoBackend } from '@quave/agenda-mongodb';
11
+ *
12
+ * const backend = new MongoBackend({
13
+ * mongo: db,
14
+ * logging: true // enables MongoJobLogger with 'agenda_logs' collection
15
+ * });
16
+ *
17
+ * // Standalone (e.g., log to Mongo while using Postgres for storage):
18
+ * import { MongoJobLogger } from '@quave/agenda-mongodb';
19
+ *
20
+ * const logger = new MongoJobLogger({ db: existingDb });
21
+ * const agenda = new Agenda({
22
+ * backend: new PostgresBackend({ connectionString: '...' }),
23
+ * logging: logger
24
+ * });
25
+ * ```
26
+ */
27
+ export declare class MongoJobLogger implements JobLogger {
28
+ private collection;
29
+ private readonly collectionName;
30
+ private initialized;
31
+ constructor(options?: {
32
+ db?: Db;
33
+ collectionName?: string;
34
+ } | string);
35
+ /**
36
+ * Set the database instance and initialize the collection.
37
+ * Called by MongoBackend after the repository connects,
38
+ * or automatically when a db is provided in the constructor.
39
+ */
40
+ setDb(db: Db): Promise<void>;
41
+ private ensureIndexes;
42
+ log(entry: Omit<JobLogEntry, '_id'>): Promise<void>;
43
+ getLogs(query?: JobLogQuery): Promise<JobLogQueryResult>;
44
+ clearLogs(query?: JobLogQuery): Promise<number>;
45
+ private buildFilter;
46
+ }
@@ -0,0 +1,142 @@
1
+ import debug from 'debug';
2
+ const log = debug('agenda:mongo:logger');
3
+ /**
4
+ * MongoDB implementation of JobLogger.
5
+ * Stores job lifecycle events in a dedicated MongoDB collection.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Via backend (automatic connection sharing):
10
+ * import { MongoBackend } from '@quave/agenda-mongodb';
11
+ *
12
+ * const backend = new MongoBackend({
13
+ * mongo: db,
14
+ * logging: true // enables MongoJobLogger with 'agenda_logs' collection
15
+ * });
16
+ *
17
+ * // Standalone (e.g., log to Mongo while using Postgres for storage):
18
+ * import { MongoJobLogger } from '@quave/agenda-mongodb';
19
+ *
20
+ * const logger = new MongoJobLogger({ db: existingDb });
21
+ * const agenda = new Agenda({
22
+ * backend: new PostgresBackend({ connectionString: '...' }),
23
+ * logging: logger
24
+ * });
25
+ * ```
26
+ */
27
+ export class MongoJobLogger {
28
+ constructor(options) {
29
+ this.initialized = false;
30
+ if (typeof options === 'string') {
31
+ // Legacy: constructor(collectionName)
32
+ this.collectionName = options;
33
+ }
34
+ else {
35
+ this.collectionName = options?.collectionName ?? 'agenda_logs';
36
+ if (options?.db) {
37
+ this.collection = options.db.collection(this.collectionName);
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Set the database instance and initialize the collection.
43
+ * Called by MongoBackend after the repository connects,
44
+ * or automatically when a db is provided in the constructor.
45
+ */
46
+ async setDb(db) {
47
+ this.collection = db.collection(this.collectionName);
48
+ await this.ensureIndexes();
49
+ }
50
+ async ensureIndexes() {
51
+ if (this.initialized || !this.collection)
52
+ return;
53
+ this.initialized = true;
54
+ log('creating indexes for %s collection', this.collectionName);
55
+ await this.collection.createIndex({ timestamp: -1 }, { name: 'agenda_logs_timestamp_idx' });
56
+ await this.collection.createIndex({ jobId: 1, timestamp: -1 }, { name: 'agenda_logs_job_id_idx' });
57
+ await this.collection.createIndex({ jobName: 1, timestamp: -1 }, { name: 'agenda_logs_job_name_idx' });
58
+ log('indexes created for %s collection', this.collectionName);
59
+ }
60
+ async log(entry) {
61
+ if (!this.collection) {
62
+ log('collection not initialized, skipping log entry');
63
+ return;
64
+ }
65
+ await this.ensureIndexes();
66
+ await this.collection.insertOne({ ...entry });
67
+ }
68
+ async getLogs(query) {
69
+ if (!this.collection) {
70
+ return { entries: [], total: 0 };
71
+ }
72
+ const filter = this.buildFilter(query);
73
+ const sort = { timestamp: query?.sort === 'asc' ? 1 : -1 };
74
+ const limit = query?.limit ?? 50;
75
+ const offset = query?.offset ?? 0;
76
+ const [entries, total] = await Promise.all([
77
+ this.collection
78
+ .find(filter)
79
+ .sort(sort)
80
+ .skip(offset)
81
+ .limit(limit)
82
+ .toArray()
83
+ .then(docs => docs.map(doc => ({
84
+ _id: doc._id.toHexString(),
85
+ timestamp: doc.timestamp,
86
+ level: doc.level,
87
+ event: doc.event,
88
+ jobId: doc.jobId,
89
+ jobName: doc.jobName,
90
+ message: doc.message,
91
+ duration: doc.duration,
92
+ error: doc.error,
93
+ failCount: doc.failCount,
94
+ retryDelay: doc.retryDelay,
95
+ retryAttempt: doc.retryAttempt,
96
+ agendaName: doc.agendaName,
97
+ meta: doc.meta
98
+ }))),
99
+ this.collection.countDocuments(filter)
100
+ ]);
101
+ return { entries, total };
102
+ }
103
+ async clearLogs(query) {
104
+ if (!this.collection) {
105
+ return 0;
106
+ }
107
+ const filter = this.buildFilter(query);
108
+ const result = await this.collection.deleteMany(filter);
109
+ return result.deletedCount;
110
+ }
111
+ buildFilter(query) {
112
+ if (!query)
113
+ return {};
114
+ const filter = {};
115
+ if (query.jobId) {
116
+ filter.jobId = query.jobId;
117
+ }
118
+ if (query.jobName) {
119
+ filter.jobName = query.jobName;
120
+ }
121
+ if (query.level) {
122
+ filter.level = Array.isArray(query.level)
123
+ ? { $in: query.level }
124
+ : query.level;
125
+ }
126
+ if (query.event) {
127
+ filter.event = Array.isArray(query.event)
128
+ ? { $in: query.event }
129
+ : query.event;
130
+ }
131
+ if (query.from || query.to) {
132
+ const timestampFilter = {};
133
+ if (query.from)
134
+ timestampFilter.$gte = query.from;
135
+ if (query.to)
136
+ timestampFilter.$lte = query.to;
137
+ filter.timestamp = timestampFilter;
138
+ }
139
+ return filter;
140
+ }
141
+ }
142
+ //# sourceMappingURL=MongoJobLogger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoJobLogger.js","sourceRoot":"","sources":["../src/MongoJobLogger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import { Collection, Db, ObjectId } from 'mongodb';
2
+ import type { JobParameters, JobId, JobsQueryOptions, JobsResult, JobsOverview, JobRepository, JobRepositoryOptions, RemoveJobsOptions } from '@quave/agenda';
3
+ import type { MongoJobRepositoryConfig } from './types.js';
4
+ /**
5
+ * Internal MongoDB document type with ObjectId for _id
6
+ * This is what's actually stored in MongoDB, separate from the public JobParameters interface
7
+ */
8
+ type MongoJobDocument = Omit<JobParameters, '_id'> & {
9
+ _id?: ObjectId;
10
+ };
11
+ /**
12
+ * @class
13
+ * MongoDB implementation of JobRepository
14
+ */
15
+ export declare class MongoJobRepository implements JobRepository {
16
+ private connectOptions;
17
+ collection: Collection<MongoJobDocument>;
18
+ private mongoClient?;
19
+ private _db?;
20
+ private ownsConnection;
21
+ constructor(connectOptions: MongoJobRepositoryConfig);
22
+ private createConnection;
23
+ getJobById(id: string): Promise<JobParameters | null>;
24
+ /**
25
+ * Convert a SortDirection value to MongoDB's numeric sort direction
26
+ */
27
+ private toMongoSortDirection;
28
+ /**
29
+ * findOneAndUpdate that tolerates both driver result shapes: driver 6+
30
+ * returns the document (or null) directly, while drivers 4/5 — e.g. the
31
+ * mongodb driver bundled with Meteor 2.x — return a ModifyResult wrapper
32
+ * with the document under `value`.
33
+ */
34
+ private findOneAndUpdateDoc;
35
+ /**
36
+ * Convert generic sort options to MongoDB sort
37
+ */
38
+ private toMongoSort;
39
+ /**
40
+ * Query jobs with database-agnostic options.
41
+ * Handles state computation and filtering internally.
42
+ */
43
+ queryJobs(options?: JobsQueryOptions): Promise<JobsResult>;
44
+ /**
45
+ * Get overview statistics for jobs grouped by name.
46
+ * Returns counts of jobs in each state for each job name.
47
+ */
48
+ getJobsOverview(): Promise<JobsOverview[]>;
49
+ /**
50
+ * Get all distinct job names
51
+ */
52
+ getDistinctJobNames(): Promise<string[]>;
53
+ getQueueSize(): Promise<number>;
54
+ removeJobs(options: RemoveJobsOptions): Promise<number>;
55
+ /**
56
+ * Build a MongoDB filter from RemoveJobsOptions
57
+ */
58
+ private buildFilterFromOptions;
59
+ disableJobs(options: RemoveJobsOptions): Promise<number>;
60
+ enableJobs(options: RemoveJobsOptions): Promise<number>;
61
+ purgeAllJobs(): Promise<number>;
62
+ unlockJob(job: JobParameters): Promise<void>;
63
+ /**
64
+ * Internal method to unlock jobs so that they can be re-run
65
+ */
66
+ unlockJobs(jobIds: (JobId | string)[]): Promise<void>;
67
+ lockJob(job: JobParameters, options: JobRepositoryOptions | undefined): Promise<JobParameters | undefined>;
68
+ getNextJobToRun(jobName: string, nextScanAt: Date, lockDeadline: Date, now: Date | undefined, options: JobRepositoryOptions | undefined): Promise<JobParameters | undefined>;
69
+ /**
70
+ * Get the underlying MongoDB Db instance (for use by job logger).
71
+ */
72
+ getDb(): Db;
73
+ connect(): Promise<void>;
74
+ private database;
75
+ disconnect(): Promise<void>;
76
+ saveJobState(job: JobParameters, options: JobRepositoryOptions | undefined): Promise<void>;
77
+ /**
78
+ * Save a job to the database (insert or update)
79
+ * @param job Job parameters to save
80
+ * @returns The saved job parameters with ID
81
+ */
82
+ saveJob<DATA = unknown>(job: JobParameters<DATA>, options: JobRepositoryOptions | undefined): Promise<JobParameters<DATA>>;
83
+ }
84
+ export {};