@clipboard-health/mongo-jobs 0.1.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 (62) hide show
  1. package/README.md +26 -0
  2. package/package.json +41 -0
  3. package/src/index.d.ts +3 -0
  4. package/src/index.js +6 -0
  5. package/src/index.js.map +1 -0
  6. package/src/lib/backgroundJobs.d.ts +59 -0
  7. package/src/lib/backgroundJobs.js +108 -0
  8. package/src/lib/backgroundJobs.js.map +1 -0
  9. package/src/lib/handler.d.ts +6 -0
  10. package/src/lib/handler.js +3 -0
  11. package/src/lib/handler.js.map +1 -0
  12. package/src/lib/internal/cron.d.ts +29 -0
  13. package/src/lib/internal/cron.js +89 -0
  14. package/src/lib/internal/cron.js.map +1 -0
  15. package/src/lib/internal/duplicateRunningError.d.ts +2 -0
  16. package/src/lib/internal/duplicateRunningError.js +7 -0
  17. package/src/lib/internal/duplicateRunningError.js.map +1 -0
  18. package/src/lib/internal/handlerAlreadyRegisteredError.d.ts +2 -0
  19. package/src/lib/internal/handlerAlreadyRegisteredError.js +7 -0
  20. package/src/lib/internal/handlerAlreadyRegisteredError.js.map +1 -0
  21. package/src/lib/internal/jobsRepository.d.ts +51 -0
  22. package/src/lib/internal/jobsRepository.js +170 -0
  23. package/src/lib/internal/jobsRepository.js.map +1 -0
  24. package/src/lib/internal/logger.d.ts +4 -0
  25. package/src/lib/internal/logger.js +3 -0
  26. package/src/lib/internal/logger.js.map +1 -0
  27. package/src/lib/internal/metrics.d.ts +32 -0
  28. package/src/lib/internal/metrics.js +106 -0
  29. package/src/lib/internal/metrics.js.map +1 -0
  30. package/src/lib/internal/mongoDuplicate.d.ts +2 -0
  31. package/src/lib/internal/mongoDuplicate.js +9 -0
  32. package/src/lib/internal/mongoDuplicate.js.map +1 -0
  33. package/src/lib/internal/registry.d.ts +27 -0
  34. package/src/lib/internal/registry.js +78 -0
  35. package/src/lib/internal/registry.js.map +1 -0
  36. package/src/lib/internal/worker/actionableQueues.d.ts +7 -0
  37. package/src/lib/internal/worker/actionableQueues.js +32 -0
  38. package/src/lib/internal/worker/actionableQueues.js.map +1 -0
  39. package/src/lib/internal/worker/fairQueueConsumer.d.ts +24 -0
  40. package/src/lib/internal/worker/fairQueueConsumer.js +127 -0
  41. package/src/lib/internal/worker/fairQueueConsumer.js.map +1 -0
  42. package/src/lib/internal/worker/futureQueues.d.ts +5 -0
  43. package/src/lib/internal/worker/futureQueues.js +27 -0
  44. package/src/lib/internal/worker/futureQueues.js.map +1 -0
  45. package/src/lib/internal/worker/queueConsumer.d.ts +11 -0
  46. package/src/lib/internal/worker/queueConsumer.js +3 -0
  47. package/src/lib/internal/worker/queueConsumer.js.map +1 -0
  48. package/src/lib/internal/worker.d.ts +65 -0
  49. package/src/lib/internal/worker.js +342 -0
  50. package/src/lib/internal/worker.js.map +1 -0
  51. package/src/lib/job.d.ts +37 -0
  52. package/src/lib/job.js +29 -0
  53. package/src/lib/job.js.map +1 -0
  54. package/src/lib/schedule.d.ts +21 -0
  55. package/src/lib/schedule.js +16 -0
  56. package/src/lib/schedule.js.map +1 -0
  57. package/src/lib/testing.d.ts +9 -0
  58. package/src/lib/testing.js +37 -0
  59. package/src/lib/testing.js.map +1 -0
  60. package/src/lib/tracing.d.ts +8 -0
  61. package/src/lib/tracing.js +194 -0
  62. package/src/lib/tracing.js.map +1 -0
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @clipboard-health/mongo-jobs
2
+
3
+ > A robust, MongoDB-backed background job processing library for Node.js with TypeScript support
4
+
5
+ ## Features
6
+
7
+ - **Reliable job processing** - Built on MongoDB for persistent, reliable job storage
8
+ - **Automatic retries** - Exponential backoff with configurable max attempts
9
+ - **Delayed jobs** - Schedule jobs to run at specific times
10
+ - **Unique jobs** - Ensure only one instance of a job is enqueued or running
11
+ - **Cron scheduling** - Built-in support for recurring jobs with cron expressions
12
+ - **Job groups** - Organize jobs into groups and run dedicated workers per group
13
+ - **Transaction support** - Enqueue jobs atomically with MongoDB sessions
14
+ - **Type-safe** - Full TypeScript support with strongly-typed job payloads
15
+ - **Concurrency control** - Configure worker concurrency per group
16
+ - **Observability** - Built-in metrics reporting and logging support
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @clipboard-health/mongo-jobs
22
+ ```
23
+
24
+ ## License
25
+
26
+ MIT
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@clipboard-health/mongo-jobs",
3
+ "description": "MongoDB-powered background jobs.",
4
+ "version": "0.1.0",
5
+ "bugs": "https://github.com/ClipboardHealth/core-utils/issues",
6
+ "dependencies": {
7
+ "cron-parser": "5.4.0",
8
+ "hot-shots": "11.2.0",
9
+ "tslib": "2.8.1"
10
+ },
11
+ "devDependencies": {
12
+ "@clipboard-health/util-ts": "3.17.0",
13
+ "@opentelemetry/api": "1.9.0",
14
+ "mongodb": "6.18.0",
15
+ "mongoose": "8.18.0"
16
+ },
17
+ "keywords": [],
18
+ "license": "MIT",
19
+ "main": "./src/index.js",
20
+ "peerDependencies": {
21
+ "@opentelemetry/api": ">=1.0",
22
+ "mongodb": ">=6.9",
23
+ "mongoose": ">=8.7"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "directory": "packages/mongo-jobs",
30
+ "type": "git",
31
+ "url": "git+https://github.com/ClipboardHealth/core-utils.git"
32
+ },
33
+ "scripts": {
34
+ "build": "nx build mongo-jobs",
35
+ "lint": "nx lint mongo-jobs",
36
+ "test": "nx test mongo-jobs"
37
+ },
38
+ "type": "commonjs",
39
+ "typings": "./src/index.d.ts",
40
+ "types": "./src/index.d.ts"
41
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./lib/backgroundJobs";
2
+ export * from "./lib/handler";
3
+ export type { BackgroundJobType } from "./lib/job";
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/backgroundJobs"), exports);
5
+ tslib_1.__exportStar(require("./lib/handler"), exports);
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/mongo-jobs/src/index.ts"],"names":[],"mappings":";;;AAAA,+DAAqC;AACrC,wDAA8B"}
@@ -0,0 +1,59 @@
1
+ import mongoose from "mongoose";
2
+ import { type RegisterCronOptions } from "./internal/cron";
3
+ import { type EnqueueOptions, JobsRepository, type SessionOptions } from "./internal/jobsRepository";
4
+ import type { Logger } from "./internal/logger";
5
+ import { type MetricsReporter } from "./internal/metrics";
6
+ import { type AnyHandlerClassOrInstance, type InstantiableHandlerClassOrInstance, Registry } from "./internal/registry";
7
+ import { Worker, type WorkerOptions } from "./internal/worker";
8
+ import { type BackgroundJobType } from "./job";
9
+ import { type ScheduleType } from "./schedule";
10
+ export interface ConstructorOptions {
11
+ logger?: Logger;
12
+ dbConnection?: mongoose.Connection;
13
+ metricsReporter?: MetricsReporter;
14
+ allowHandlerOverride?: boolean;
15
+ }
16
+ export declare class BackgroundJobsService {
17
+ readonly jobModel: mongoose.Model<BackgroundJobType<unknown>>;
18
+ readonly scheduleModel: mongoose.Model<ScheduleType<unknown>>;
19
+ readonly jobsRepo: JobsRepository;
20
+ readonly registry: Registry;
21
+ private readonly logger;
22
+ private readonly connection;
23
+ private readonly metrics;
24
+ private readonly cron;
25
+ private worker?;
26
+ constructor(options?: ConstructorOptions);
27
+ register<T>(handlerClassOrInstance: InstantiableHandlerClassOrInstance<T>, group: string): void;
28
+ registerCron<T>(handlerClassOrInstance: InstantiableHandlerClassOrInstance<T>, options: RegisterCronOptions<T>): Promise<void>;
29
+ enqueue<T>(handlerClassOrInstance: string | AnyHandlerClassOrInstance<T>, data: T, options?: EnqueueOptions): Promise<BackgroundJobType<T> | undefined>;
30
+ removeCron(scheduleName: string): Promise<void>;
31
+ start(groups: string[], workerOptions?: WorkerOptions): Promise<void>;
32
+ buildWorker(groups: string[], workerOptions: WorkerOptions): Worker;
33
+ stop(waitTime?: number): Promise<void>;
34
+ getJobById(jobId: string): Promise<(mongoose.Document<unknown, {}, BackgroundJobType<unknown>, {}, {}> & BackgroundJobType<unknown> & Required<{
35
+ _id: mongoose.Types.ObjectId;
36
+ }> & {
37
+ __v: number;
38
+ }) | null>;
39
+ /**
40
+ * Retry a job by id
41
+ *
42
+ * This method is useful if you have a job that exhausted its attempts limit
43
+ * but you want to run it again. By calling this method you will bring the
44
+ * attempts count of the job to 0 and set the next run time to current time
45
+ * (so the job will be picked up as soon as possible).
46
+ **/
47
+ retryJobById(jobId: string): Promise<void>;
48
+ /**
49
+ * Cancel jobs
50
+ *
51
+ * This method will remove jobs with given ids from the DB. So if the job is
52
+ * scheduled and waiting to be run then you would prevent the job from being
53
+ * executed in the future. If you call this method while the job is currently
54
+ * running then you won't affect the current execution of the job but you
55
+ * will prevent the job from being executed again in the future (in case it
56
+ * failed)
57
+ **/
58
+ cancel(jobIds: string[], options?: SessionOptions): Promise<void>;
59
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BackgroundJobsService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
6
+ const cron_1 = require("./internal/cron");
7
+ const jobsRepository_1 = require("./internal/jobsRepository");
8
+ const metrics_1 = require("./internal/metrics");
9
+ const registry_1 = require("./internal/registry");
10
+ const worker_1 = require("./internal/worker");
11
+ const job_1 = require("./job");
12
+ const schedule_1 = require("./schedule");
13
+ class BackgroundJobsService {
14
+ jobModel;
15
+ scheduleModel;
16
+ jobsRepo;
17
+ registry;
18
+ logger;
19
+ connection;
20
+ metrics;
21
+ cron;
22
+ worker;
23
+ constructor(options = {}) {
24
+ this.logger = options.logger;
25
+ this.connection = options.dbConnection ?? mongoose_1.default.connection;
26
+ this.jobModel = this.connection.model(job_1.BackgroundJobSchemaName, job_1.BackgroundJobSchema);
27
+ this.scheduleModel = this.connection.model(schedule_1.ScheduleSchemaName, schedule_1.ScheduleSchema);
28
+ this.registry = new registry_1.Registry({ allowHandlerOverride: options.allowHandlerOverride });
29
+ const metricsReporter = options.metricsReporter ?? (0, metrics_1.defaultMetricsReporter)();
30
+ this.metrics = new metrics_1.Metrics(metricsReporter, this.jobModel, this.registry);
31
+ this.jobsRepo = new jobsRepository_1.JobsRepository({ jobModel: this.jobModel, registry: this.registry });
32
+ this.cron = new cron_1.Cron({
33
+ registry: this.registry,
34
+ scheduleModel: this.scheduleModel,
35
+ jobsRepo: this.jobsRepo,
36
+ });
37
+ }
38
+ register(handlerClassOrInstance, group) {
39
+ this.registry.register(handlerClassOrInstance, group);
40
+ }
41
+ async registerCron(handlerClassOrInstance, options) {
42
+ await this.cron.registerCron(handlerClassOrInstance, options);
43
+ }
44
+ async enqueue(handlerClassOrInstance, data, options = {}) {
45
+ return await this.jobsRepo.createJob({
46
+ handler: handlerClassOrInstance,
47
+ data,
48
+ ...options,
49
+ });
50
+ }
51
+ async removeCron(scheduleName) {
52
+ await this.cron.removeCron(scheduleName);
53
+ }
54
+ async start(groups, workerOptions = {}) {
55
+ if (this.worker && !this.worker.stopped) {
56
+ throw new Error("BackgroundJobs currently running");
57
+ }
58
+ this.worker = this.buildWorker(groups, workerOptions);
59
+ await this.worker.start();
60
+ await this.metrics.startReporting();
61
+ }
62
+ buildWorker(groups, workerOptions) {
63
+ return new worker_1.Worker({
64
+ groups,
65
+ cron: this.cron,
66
+ registry: this.registry,
67
+ logger: this.logger,
68
+ metrics: this.metrics,
69
+ jobsRepo: this.jobsRepo,
70
+ ...workerOptions,
71
+ });
72
+ }
73
+ async stop(waitTime) {
74
+ await this.worker?.stop(waitTime);
75
+ }
76
+ async getJobById(jobId) {
77
+ const jobObjectId = new mongoose_1.default.Types.ObjectId(jobId);
78
+ return await this.jobsRepo.getJob(jobObjectId);
79
+ }
80
+ /**
81
+ * Retry a job by id
82
+ *
83
+ * This method is useful if you have a job that exhausted its attempts limit
84
+ * but you want to run it again. By calling this method you will bring the
85
+ * attempts count of the job to 0 and set the next run time to current time
86
+ * (so the job will be picked up as soon as possible).
87
+ **/
88
+ async retryJobById(jobId) {
89
+ const jobObjectId = new mongoose_1.default.Types.ObjectId(jobId);
90
+ await this.jobsRepo.resetJob(jobObjectId);
91
+ }
92
+ /**
93
+ * Cancel jobs
94
+ *
95
+ * This method will remove jobs with given ids from the DB. So if the job is
96
+ * scheduled and waiting to be run then you would prevent the job from being
97
+ * executed in the future. If you call this method while the job is currently
98
+ * running then you won't affect the current execution of the job but you
99
+ * will prevent the job from being executed again in the future (in case it
100
+ * failed)
101
+ **/
102
+ async cancel(jobIds, options = {}) {
103
+ const jobObjectIds = jobIds.map((jobId) => new mongoose_1.default.Types.ObjectId(jobId));
104
+ await this.jobsRepo.deleteJobs(jobObjectIds, options);
105
+ }
106
+ }
107
+ exports.BackgroundJobsService = BackgroundJobsService;
108
+ //# sourceMappingURL=backgroundJobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backgroundJobs.js","sourceRoot":"","sources":["../../../../../packages/mongo-jobs/src/lib/backgroundJobs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { BackgroundJobType } from "./job";
2
+ export interface HandlerInterface<T> {
3
+ name: string;
4
+ maxAttempts?: number;
5
+ perform(data: T, job?: BackgroundJobType<T>): Promise<void>;
6
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../../../../packages/mongo-jobs/src/lib/handler.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import type mongoose from "mongoose";
2
+ import type { BackgroundJobType } from "../job";
3
+ import type { ScheduleType } from "../schedule";
4
+ import type { JobsRepository } from "./jobsRepository";
5
+ import type { InstantiableHandlerClassOrInstance, Registry } from "./registry";
6
+ interface ConstructorOptions {
7
+ scheduleModel: mongoose.Model<ScheduleType<unknown>>;
8
+ jobsRepo: JobsRepository;
9
+ registry: Registry;
10
+ }
11
+ export interface RegisterCronOptions<T> {
12
+ group: string;
13
+ cronExpression: string;
14
+ timeZone?: string;
15
+ scheduleName: string;
16
+ data: T;
17
+ }
18
+ export declare class Cron {
19
+ private readonly scheduleModel;
20
+ private readonly jobsRepo;
21
+ private readonly registry;
22
+ constructor(options: ConstructorOptions);
23
+ maybeScheduleNextJob(job: BackgroundJobType<unknown>): Promise<void>;
24
+ registerCron<T>(handlerClassOrInstance: InstantiableHandlerClassOrInstance<T>, options: RegisterCronOptions<T>): Promise<void>;
25
+ scheduleNextIteration(schedule: Omit<ScheduleType<unknown>, "_id">): Promise<void>;
26
+ removeCron(scheduleName: string): Promise<void>;
27
+ private upsertSchedule;
28
+ }
29
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Cron = void 0;
4
+ const cron_parser_1 = require("cron-parser");
5
+ const handlerAlreadyRegisteredError_1 = require("./handlerAlreadyRegisteredError");
6
+ function validateCronExpression(expression) {
7
+ try {
8
+ cron_parser_1.CronExpressionParser.parse(expression);
9
+ }
10
+ catch (error) {
11
+ throw new Error("Invalid cron expression", { cause: error });
12
+ }
13
+ }
14
+ class Cron {
15
+ scheduleModel;
16
+ jobsRepo;
17
+ registry;
18
+ constructor(options) {
19
+ this.scheduleModel = options.scheduleModel;
20
+ this.jobsRepo = options.jobsRepo;
21
+ this.registry = options.registry;
22
+ }
23
+ async maybeScheduleNextJob(job) {
24
+ if (!job.scheduleName) {
25
+ return;
26
+ }
27
+ if (job.attemptsCount > 0) {
28
+ return;
29
+ }
30
+ const schedule = await this.scheduleModel.findOne({ name: job.scheduleName });
31
+ if (schedule) {
32
+ await this.scheduleNextIteration(schedule);
33
+ }
34
+ }
35
+ async registerCron(handlerClassOrInstance, options) {
36
+ try {
37
+ this.registry.register(handlerClassOrInstance, options.group);
38
+ }
39
+ catch (error) {
40
+ if (!(error instanceof handlerAlreadyRegisteredError_1.HandlerAlreadyRegisteredError)) {
41
+ throw error;
42
+ }
43
+ }
44
+ const { handler, queue } = this.registry.getRegisteredHandler(handlerClassOrInstance);
45
+ await this.upsertSchedule(handler.name, queue, options);
46
+ }
47
+ async scheduleNextIteration(schedule) {
48
+ const nextRunAt = cron_parser_1.CronExpressionParser.parse(schedule.cronExpression, { tz: schedule.timeZone })
49
+ .next()
50
+ .toDate();
51
+ const nextRunAtInteger = nextRunAt.getTime();
52
+ const uniqueKey = `${schedule.name}-${nextRunAtInteger}`;
53
+ await this.jobsRepo.createJob({
54
+ handler: schedule.handlerName,
55
+ data: schedule.data,
56
+ startAt: nextRunAt,
57
+ unique: uniqueKey,
58
+ scheduleName: schedule.name,
59
+ queue: schedule.queue,
60
+ });
61
+ }
62
+ async removeCron(scheduleName) {
63
+ await this.scheduleModel.deleteOne({ name: scheduleName });
64
+ await this.jobsRepo.deleteUpcomingScheduleJobs(scheduleName);
65
+ }
66
+ async upsertSchedule(handlerName, queue, options) {
67
+ const { scheduleName, cronExpression, data } = options;
68
+ const timeZone = options.timeZone ?? "utc";
69
+ validateCronExpression(cronExpression);
70
+ const scheduleAttributes = {
71
+ name: scheduleName,
72
+ handlerName,
73
+ cronExpression,
74
+ timeZone,
75
+ queue,
76
+ data,
77
+ };
78
+ const upsertResult = await this.scheduleModel.updateOne({ name: scheduleName }, { $set: scheduleAttributes }, { upsert: true });
79
+ if (upsertResult.modifiedCount > 0) {
80
+ /* If the schedule is updated then we are clearing all future jobs for this schedule
81
+ * so that we can reschedule them with proper timing/handler etc
82
+ */
83
+ await this.jobsRepo.deleteUpcomingScheduleJobs(scheduleName);
84
+ }
85
+ await this.scheduleNextIteration(scheduleAttributes);
86
+ }
87
+ }
88
+ exports.Cron = Cron;
89
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.js","sourceRoot":"","sources":["../../../../../../packages/mongo-jobs/src/lib/internal/cron.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare class DuplicateRunningError extends Error {
2
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DuplicateRunningError = void 0;
4
+ class DuplicateRunningError extends Error {
5
+ }
6
+ exports.DuplicateRunningError = DuplicateRunningError;
7
+ //# sourceMappingURL=duplicateRunningError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicateRunningError.js","sourceRoot":"","sources":["../../../../../../packages/mongo-jobs/src/lib/internal/duplicateRunningError.ts"],"names":[],"mappings":";;;AAAA,MAAa,qBAAsB,SAAQ,KAAK;CAAG;AAAnD,sDAAmD"}
@@ -0,0 +1,2 @@
1
+ export declare class HandlerAlreadyRegisteredError extends Error {
2
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HandlerAlreadyRegisteredError = void 0;
4
+ class HandlerAlreadyRegisteredError extends Error {
5
+ }
6
+ exports.HandlerAlreadyRegisteredError = HandlerAlreadyRegisteredError;
7
+ //# sourceMappingURL=handlerAlreadyRegisteredError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlerAlreadyRegisteredError.js","sourceRoot":"","sources":["../../../../../../packages/mongo-jobs/src/lib/internal/handlerAlreadyRegisteredError.ts"],"names":[],"mappings":";;;AAAA,MAAa,6BAA8B,SAAQ,KAAK;CAAG;AAA3D,sEAA2D"}
@@ -0,0 +1,51 @@
1
+ import type { ChangeStream, ClientSession } from "mongodb";
2
+ import type mongoose from "mongoose";
3
+ import type { BackgroundJobType, JobUniqueOptions } from "../job";
4
+ import type { AnyHandlerClassOrInstance, Registry } from "./registry";
5
+ type EnqueueUniqueOptions = string | JobUniqueOptions;
6
+ export interface UpsertChangeStreamEvent {
7
+ fullDocument: BackgroundJobType<unknown>;
8
+ }
9
+ export interface SessionOptions {
10
+ session?: ClientSession;
11
+ }
12
+ export interface EnqueueOptions extends SessionOptions {
13
+ startAt?: Date;
14
+ unique?: EnqueueUniqueOptions;
15
+ }
16
+ interface CreateJobParameters<T> extends EnqueueOptions {
17
+ handler: string | AnyHandlerClassOrInstance<T>;
18
+ data: T;
19
+ scheduleName?: string;
20
+ queue?: string;
21
+ }
22
+ interface ConstructorOptions {
23
+ registry: Registry;
24
+ jobModel: mongoose.Model<BackgroundJobType<unknown>>;
25
+ }
26
+ export declare class JobsRepository {
27
+ private readonly registry;
28
+ private readonly jobModel;
29
+ constructor(options: ConstructorOptions);
30
+ createJob<T>(parameters: CreateJobParameters<T>): Promise<BackgroundJobType<T> | undefined>;
31
+ fetchAndLockNextJob(queues: string[]): Promise<BackgroundJobType<unknown> | undefined>;
32
+ fetchNextJob(queue: string): Promise<BackgroundJobType<unknown> | undefined>;
33
+ updateOne(id: mongoose.Types.ObjectId, update: mongoose.UpdateQuery<BackgroundJobType<unknown>>): Promise<void>;
34
+ unlockFirstExpiredJob(lockedAtThreshold: Date): Promise<(mongoose.Document<unknown, {}, BackgroundJobType<unknown>, {}, {}> & BackgroundJobType<unknown> & Required<{
35
+ _id: mongoose.Types.ObjectId;
36
+ }> & {
37
+ __v: number;
38
+ }) | undefined>;
39
+ deleteJobs(ids: mongoose.Types.ObjectId[], options?: SessionOptions): Promise<void>;
40
+ getJob(id: mongoose.Types.ObjectId): Promise<(mongoose.Document<unknown, {}, BackgroundJobType<unknown>, {}, {}> & BackgroundJobType<unknown> & Required<{
41
+ _id: mongoose.Types.ObjectId;
42
+ }> & {
43
+ __v: number;
44
+ }) | null>;
45
+ deleteUpcomingScheduleJobs(scheduleName: string): Promise<void>;
46
+ watchInserts(queues: string[]): ChangeStream<BackgroundJobType<unknown>>;
47
+ watchUpserts(queues: string[]): ChangeStream<BackgroundJobType<unknown>>;
48
+ resetJob(id: mongoose.Types.ObjectId): Promise<void>;
49
+ fetchQueuesWithJobs(pertinentQueues: string[]): Promise<string[]>;
50
+ }
51
+ export {};
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JobsRepository = void 0;
4
+ const tracing_1 = require("../tracing");
5
+ const mongoDuplicate_1 = require("./mongoDuplicate");
6
+ function normalizeUniqueOptions(options) {
7
+ if (typeof options === "string") {
8
+ return {
9
+ enqueuedKey: options,
10
+ runningKey: options,
11
+ };
12
+ }
13
+ return options;
14
+ }
15
+ class JobsRepository {
16
+ registry;
17
+ jobModel;
18
+ constructor(options) {
19
+ this.registry = options.registry;
20
+ this.jobModel = options.jobModel;
21
+ }
22
+ async createJob(parameters) {
23
+ const handlerClassOrInstance = parameters.handler;
24
+ const registeredHandler = this.registry.getRegisteredHandler(handlerClassOrInstance);
25
+ const { handler } = registeredHandler;
26
+ const queue = parameters.queue ?? registeredHandler.queue;
27
+ const { session, unique, scheduleName, data } = parameters;
28
+ const startAt = parameters.startAt ?? new Date();
29
+ const uniqueOptions = normalizeUniqueOptions(unique);
30
+ const uniqueKey = uniqueOptions?.enqueuedKey;
31
+ return await (0, tracing_1.withProducerTrace)(handler, data, async (dataWithTrace) => {
32
+ try {
33
+ const backgroundJobCreateResult = await this.jobModel.create([
34
+ {
35
+ handlerName: handler.name,
36
+ data: dataWithTrace,
37
+ nextRunAt: startAt,
38
+ queue,
39
+ ...(scheduleName && { scheduleName }),
40
+ ...(uniqueOptions && { options: { unique: uniqueOptions } }),
41
+ ...(uniqueKey && { uniqueKey }),
42
+ },
43
+ ], { session });
44
+ const createdJob = backgroundJobCreateResult[0];
45
+ return createdJob;
46
+ }
47
+ catch (error) {
48
+ if ((0, mongoDuplicate_1.isMongoDuplicateError)(error)) {
49
+ // eslint-disable-next-line consistent-return
50
+ return;
51
+ }
52
+ throw error;
53
+ }
54
+ });
55
+ }
56
+ async fetchAndLockNextJob(queues) {
57
+ return await (0, tracing_1.withInternalsTrace)("fetchAndLockNextJob", async () => {
58
+ const acquiredJob = await this.jobModel
59
+ .findOneAndUpdate({
60
+ queue: { $in: queues },
61
+ lockedAt: null,
62
+ nextRunAt: { $lte: new Date() },
63
+ }, {
64
+ lockedAt: new Date(),
65
+ }, {
66
+ sort: { nextRunAt: 1 },
67
+ returnDocument: "after",
68
+ })
69
+ .lean();
70
+ return acquiredJob ?? undefined;
71
+ });
72
+ }
73
+ async fetchNextJob(queue) {
74
+ return await (0, tracing_1.withInternalsTrace)("fetchNextJob", async () => {
75
+ const job = await this.jobModel
76
+ .findOne({
77
+ queue,
78
+ lockedAt: null,
79
+ }, {}, {
80
+ sort: { nextRunAt: 1 },
81
+ })
82
+ .lean();
83
+ return job ?? undefined;
84
+ });
85
+ }
86
+ async updateOne(id, update) {
87
+ await this.jobModel.updateOne({ _id: id }, update);
88
+ }
89
+ async unlockFirstExpiredJob(lockedAtThreshold) {
90
+ const expiredJob = await this.jobModel.findOneAndUpdate({
91
+ failedAt: null,
92
+ lockedAt: { $lt: lockedAtThreshold },
93
+ }, {
94
+ $unset: {
95
+ lockedAt: "",
96
+ },
97
+ }, {
98
+ sort: { lockedAt: 1 },
99
+ });
100
+ return expiredJob ?? undefined;
101
+ }
102
+ async deleteJobs(ids, options) {
103
+ if (ids.length === 0) {
104
+ return;
105
+ }
106
+ await this.jobModel.deleteMany({ _id: { $in: ids } }, options);
107
+ }
108
+ async getJob(id) {
109
+ return await this.jobModel.findById(id);
110
+ }
111
+ async deleteUpcomingScheduleJobs(scheduleName) {
112
+ await this.jobModel.deleteMany({
113
+ scheduleName,
114
+ attemptsCount: 0,
115
+ nextRunAt: { $gt: new Date() },
116
+ lockedAt: null,
117
+ });
118
+ }
119
+ watchInserts(queues) {
120
+ return this.jobModel.watch([
121
+ { $match: { operationType: "insert", "fullDocument.queue": { $in: queues } } },
122
+ ]);
123
+ }
124
+ watchUpserts(queues) {
125
+ return this.jobModel.watch([
126
+ {
127
+ $match: {
128
+ operationType: { $in: ["insert", "update"] },
129
+ "fullDocument.queue": { $in: queues },
130
+ },
131
+ },
132
+ ], { fullDocument: "updateLookup" });
133
+ }
134
+ async resetJob(id) {
135
+ const existingJob = await this.getJob(id);
136
+ if (!existingJob) {
137
+ return;
138
+ }
139
+ const resetQuery = {
140
+ $set: {
141
+ nextRunAt: new Date(),
142
+ attemptsCount: 0,
143
+ },
144
+ $unset: {
145
+ failedAt: "",
146
+ lockedAt: "",
147
+ },
148
+ };
149
+ const enqueuedUniqueKey = existingJob.options?.unique?.enqueuedKey;
150
+ if (enqueuedUniqueKey && resetQuery.$set) {
151
+ resetQuery.$set.uniqueKey = enqueuedUniqueKey;
152
+ }
153
+ if (!existingJob.queue && !existingJob.originalQueue) {
154
+ throw new Error("Job was taken off the queue but doesn't have value in originalQueue field");
155
+ }
156
+ if (!existingJob.queue && existingJob.originalQueue && resetQuery.$set) {
157
+ resetQuery.$set.queue = existingJob.originalQueue;
158
+ }
159
+ await this.updateOne(id, resetQuery);
160
+ }
161
+ async fetchQueuesWithJobs(pertinentQueues) {
162
+ if (pertinentQueues.length === 0) {
163
+ return [];
164
+ }
165
+ const queuesFilter = { queue: { $in: pertinentQueues } };
166
+ return await this.jobModel.distinct("queue", queuesFilter);
167
+ }
168
+ }
169
+ exports.JobsRepository = JobsRepository;
170
+ //# sourceMappingURL=jobsRepository.js.map