nestjs-pg-boss 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +106 -3
- package/dist/decorators/inject-queue.decorator.d.ts +2 -0
- package/dist/decorators/inject-queue.decorator.js +4 -0
- package/dist/decorators/processor.decorators.d.ts +52 -0
- package/dist/decorators/processor.decorators.js +31 -0
- package/dist/explorer/pg-boss.explorer.d.ts +22 -0
- package/dist/explorer/pg-boss.explorer.js +104 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/interfaces/pg-boss-module-options.interface.d.ts +23 -0
- package/dist/interfaces/pg-boss-module-options.interface.js +1 -0
- package/dist/interfaces/queue-definition.interface.d.ts +7 -0
- package/dist/interfaces/queue-definition.interface.js +1 -0
- package/dist/pg-boss.constants.d.ts +5 -0
- package/dist/pg-boss.constants.js +5 -0
- package/dist/pg-boss.module-definition.d.ts +2 -0
- package/dist/pg-boss.module-definition.js +4 -0
- package/dist/pg-boss.module.d.ts +14 -0
- package/dist/pg-boss.module.js +72 -0
- package/dist/pg-boss.providers.d.ts +8 -0
- package/dist/pg-boss.providers.js +29 -0
- package/dist/pg-boss.service.d.ts +16 -0
- package/dist/pg-boss.service.js +51 -0
- package/dist/queue/job-queue.d.ts +36 -0
- package/dist/queue/job-queue.js +67 -0
- package/dist/queue/queue.registry.d.ts +16 -0
- package/dist/queue/queue.registry.js +69 -0
- package/dist/utils/get-queue-token.util.d.ts +2 -0
- package/dist/utils/get-queue-token.util.js +5 -0
- package/package.json +65 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AbderraHman El Haddad Asofi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# nestjs-pg-boss
|
|
2
|
+
|
|
3
|
+
A NestJS module for [pg-boss](https://github.com/timgit/pg-boss) (v12). Queues are declared in code, workers are decorated methods, and the instance follows the Nest lifecycle.
|
|
4
|
+
|
|
5
|
+
Works with NestJS 10+ and pg-boss 12. Requires Node 22.12 or newer.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i nestjs-pg-boss pg-boss
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package is ESM-only, because pg-boss 12 is. A CommonJS Nest app (the default for `nest new` before v12) can still `import` from it: Node 22.12+ loads ESM from `require()` without a flag. That is also why the Node floor is 22.12, which is pg-boss 12's own minimum.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
@Module({
|
|
17
|
+
imports: [
|
|
18
|
+
PgBossModule.forRootAsync({
|
|
19
|
+
inject: [ConfigService],
|
|
20
|
+
useFactory: (config: ConfigService) => ({
|
|
21
|
+
connectionString: config.getOrThrow('DATABASE_URL'),
|
|
22
|
+
schema: 'pgboss',
|
|
23
|
+
}),
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
export class AppModule {}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`forRoot` takes every option `new PgBoss()` does, plus:
|
|
31
|
+
|
|
32
|
+
| Option | Default | |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `start` | | Passed to `boss.start()`, e.g. `{ attempts: 5 }`. |
|
|
35
|
+
| `stop` | `{ graceful: true, timeout: 30000 }` | Passed to `boss.stop()` on shutdown. |
|
|
36
|
+
| `registerWorkers` | `true` | Set `false` in processes that only enqueue. |
|
|
37
|
+
|
|
38
|
+
Call `app.enableShutdownHooks()` in `main.ts` so SIGTERM lets running jobs finish.
|
|
39
|
+
|
|
40
|
+
## Declare a queue and enqueue
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
@Module({
|
|
44
|
+
imports: [PgBossModule.registerQueue({ name: 'email', retryLimit: 3, retryBackoff: true, deadLetter: 'email-dead' }, 'email-dead')],
|
|
45
|
+
providers: [Mailer, EmailProcessor],
|
|
46
|
+
})
|
|
47
|
+
export class EmailModule {}
|
|
48
|
+
|
|
49
|
+
@Injectable()
|
|
50
|
+
export class Mailer {
|
|
51
|
+
constructor(@InjectQueue('email') private readonly queue: JobQueue<SendEmail>) {}
|
|
52
|
+
|
|
53
|
+
send(payload: SendEmail) {
|
|
54
|
+
return this.queue.send(payload);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The queue is created on boot. If it already exists, the options pg-boss lets you change are updated to match the code. `policy` and `partition` are fixed at creation.
|
|
60
|
+
|
|
61
|
+
## Process jobs
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
@Processor('email')
|
|
65
|
+
@Injectable()
|
|
66
|
+
export class EmailProcessor {
|
|
67
|
+
constructor(private readonly mailer: MailService) {}
|
|
68
|
+
|
|
69
|
+
@Process()
|
|
70
|
+
async send(job: Job<SendEmail>) {
|
|
71
|
+
await this.mailer.deliver(job.data); // throwing fails the job and triggers a retry
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@ProcessBatch('email-digest', { batchSize: 50 })
|
|
75
|
+
async digest(jobs: Job<Digest>[]) {}
|
|
76
|
+
|
|
77
|
+
@Process('reports')
|
|
78
|
+
@Schedule('0 3 * * *', { tz: 'Europe/Paris', data: { kind: 'daily' } })
|
|
79
|
+
async nightly(job: Job<{ kind: string }>) {}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Processors are normal providers, so they can inject anything. They must be singleton-scoped.
|
|
84
|
+
|
|
85
|
+
## Enqueue inside your own transaction
|
|
86
|
+
|
|
87
|
+
Every write method on `JobQueue` accepts pg-boss's `db` option. Wrap your ORM's transaction with the matching adapter and the job commits or rolls back with your data:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await this.knex.transaction(async (trx) => {
|
|
91
|
+
const [order] = await trx('orders').insert(dto).returning('*');
|
|
92
|
+
await this.queue.send({ orderId: order.id }, { db: fromKnex(trx) }); // or fromKysely / fromDrizzle / fromPrisma
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Escape hatch
|
|
97
|
+
|
|
98
|
+
`PgBossService.boss` is the raw `PgBoss` instance. `PgBossService.isHealthy()` is a database ping for readiness probes.
|
|
99
|
+
|
|
100
|
+
## Behaviour worth knowing
|
|
101
|
+
|
|
102
|
+
- pg-boss starts inside an async provider, so anything that injects a queue gets a ready database regardless of module init order.
|
|
103
|
+
- The `error` and `warning` events are routed to the Nest `Logger`. An unhandled `error` event would otherwise crash the process.
|
|
104
|
+
- A processor method that throws is logged with the queue name and job id, then rethrown so pg-boss applies the retry policy.
|
|
105
|
+
- A queue named by `@Process` but not declared with `registerQueue` is created with pg-boss defaults.
|
|
106
|
+
- Dead letter queues are created before the queues that point at them.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ScheduleOptions, WorkOptions } from 'pg-boss';
|
|
2
|
+
/**
|
|
3
|
+
* `perJobResults` changes the handler contract (it must return a result per job), so it is not
|
|
4
|
+
* exposed here. Use the `PgBossService` escape hatch if you need it.
|
|
5
|
+
*/
|
|
6
|
+
export type ProcessOptions = Omit<WorkOptions, 'batchSize' | 'perJobResults'>;
|
|
7
|
+
export type ProcessBatchOptions = Omit<WorkOptions, 'perJobResults'>;
|
|
8
|
+
export type ScheduleDecoratorOptions = ScheduleOptions & {
|
|
9
|
+
data?: object;
|
|
10
|
+
};
|
|
11
|
+
export interface ProcessorMetadata {
|
|
12
|
+
queue?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ProcessMetadata {
|
|
15
|
+
queue?: string;
|
|
16
|
+
options: WorkOptions;
|
|
17
|
+
batch: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ScheduleMetadata {
|
|
20
|
+
cron: string;
|
|
21
|
+
options: ScheduleDecoratorOptions;
|
|
22
|
+
}
|
|
23
|
+
export declare const ProcessorMarker: import("@nestjs/core").DiscoverableDecorator<ProcessorMetadata>;
|
|
24
|
+
export declare const ProcessMarker: import("@nestjs/core").DiscoverableDecorator<ProcessMetadata>;
|
|
25
|
+
export declare const ScheduleMarker: import("@nestjs/core").DiscoverableDecorator<ScheduleMetadata>;
|
|
26
|
+
/**
|
|
27
|
+
* Marks a provider as a job processor so its `@Process` / `@ProcessBatch` methods are registered
|
|
28
|
+
* as pg-boss workers. `queue` is the default for methods that don't name one.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* @Processor('email')
|
|
32
|
+
* export class EmailProcessor {
|
|
33
|
+
* @Process()
|
|
34
|
+
* async send(job: Job<SendEmailPayload>) {}
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const Processor: (queue?: string) => import("@nestjs/common").CustomDecorator;
|
|
39
|
+
/**
|
|
40
|
+
* Handles one job at a time. The method receives the job (and, with `transactional: true`, the
|
|
41
|
+
* transaction as a second argument). Throwing fails the job and lets pg-boss retry it according
|
|
42
|
+
* to the queue's retry settings.
|
|
43
|
+
*/
|
|
44
|
+
export declare const Process: (queue?: string, options?: ProcessOptions) => import("@nestjs/common").CustomDecorator;
|
|
45
|
+
/** Handles up to `batchSize` jobs per call. The method receives an array of jobs. */
|
|
46
|
+
export declare const ProcessBatch: (queue?: string, options?: ProcessBatchOptions) => import("@nestjs/common").CustomDecorator;
|
|
47
|
+
/**
|
|
48
|
+
* Runs the decorated `@Process` method on a cron expression or RFC 5545 recurrence rule.
|
|
49
|
+
* The schedule is stored in Postgres, so a fleet of instances produces one job per tick.
|
|
50
|
+
* Pass `data` to set the job payload.
|
|
51
|
+
*/
|
|
52
|
+
export declare const Schedule: (cron: string, options?: ScheduleDecoratorOptions) => import("@nestjs/common").CustomDecorator;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DiscoveryService } from '@nestjs/core';
|
|
2
|
+
export const ProcessorMarker = DiscoveryService.createDecorator();
|
|
3
|
+
export const ProcessMarker = DiscoveryService.createDecorator();
|
|
4
|
+
export const ScheduleMarker = DiscoveryService.createDecorator();
|
|
5
|
+
/**
|
|
6
|
+
* Marks a provider as a job processor so its `@Process` / `@ProcessBatch` methods are registered
|
|
7
|
+
* as pg-boss workers. `queue` is the default for methods that don't name one.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* @Processor('email')
|
|
11
|
+
* export class EmailProcessor {
|
|
12
|
+
* @Process()
|
|
13
|
+
* async send(job: Job<SendEmailPayload>) {}
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const Processor = (queue) => ProcessorMarker({ queue });
|
|
18
|
+
/**
|
|
19
|
+
* Handles one job at a time. The method receives the job (and, with `transactional: true`, the
|
|
20
|
+
* transaction as a second argument). Throwing fails the job and lets pg-boss retry it according
|
|
21
|
+
* to the queue's retry settings.
|
|
22
|
+
*/
|
|
23
|
+
export const Process = (queue, options = {}) => ProcessMarker({ queue, options: { ...options, batchSize: 1 }, batch: false });
|
|
24
|
+
/** Handles up to `batchSize` jobs per call. The method receives an array of jobs. */
|
|
25
|
+
export const ProcessBatch = (queue, options = {}) => ProcessMarker({ queue, options, batch: true });
|
|
26
|
+
/**
|
|
27
|
+
* Runs the decorated `@Process` method on a cron expression or RFC 5545 recurrence rule.
|
|
28
|
+
* The schedule is stored in Postgres, so a fleet of instances produces one job per tick.
|
|
29
|
+
* Pass `data` to set the job payload.
|
|
30
|
+
*/
|
|
31
|
+
export const Schedule = (cron, options = {}) => ScheduleMarker({ cron, options });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap } from '@nestjs/common';
|
|
2
|
+
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
|
3
|
+
import type { PgBoss } from 'pg-boss';
|
|
4
|
+
import type { PgBossModuleOptions } from '../interfaces/pg-boss-module-options.interface.js';
|
|
5
|
+
import { QueueRegistry } from '../queue/queue.registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* On bootstrap, finds every `@Processor()` provider and registers its `@Process` methods as
|
|
8
|
+
* pg-boss workers and its `@Schedule` methods as schedules. Runs after all providers exist, so
|
|
9
|
+
* a processor can depend on anything in the application.
|
|
10
|
+
*/
|
|
11
|
+
export declare class PgBossExplorer implements OnApplicationBootstrap {
|
|
12
|
+
private readonly boss;
|
|
13
|
+
private readonly options;
|
|
14
|
+
private readonly discovery;
|
|
15
|
+
private readonly scanner;
|
|
16
|
+
private readonly registry;
|
|
17
|
+
private readonly logger;
|
|
18
|
+
constructor(boss: PgBoss, options: PgBossModuleOptions, discovery: DiscoveryService, scanner: MetadataScanner, registry: QueueRegistry);
|
|
19
|
+
onApplicationBootstrap(): Promise<void>;
|
|
20
|
+
private registerProcessor;
|
|
21
|
+
private createHandler;
|
|
22
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
11
|
+
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
|
12
|
+
import { MODULE_OPTIONS_TOKEN } from '../pg-boss.module-definition.js';
|
|
13
|
+
import { PGBOSS_INSTANCE, PGBOSS_LOGGER_CONTEXT } from '../pg-boss.constants.js';
|
|
14
|
+
import { QueueRegistry } from '../queue/queue.registry.js';
|
|
15
|
+
import { ProcessMarker, ProcessorMarker, ScheduleMarker } from '../decorators/processor.decorators.js';
|
|
16
|
+
/**
|
|
17
|
+
* On bootstrap, finds every `@Processor()` provider and registers its `@Process` methods as
|
|
18
|
+
* pg-boss workers and its `@Schedule` methods as schedules. Runs after all providers exist, so
|
|
19
|
+
* a processor can depend on anything in the application.
|
|
20
|
+
*/
|
|
21
|
+
let PgBossExplorer = class PgBossExplorer {
|
|
22
|
+
boss;
|
|
23
|
+
options;
|
|
24
|
+
discovery;
|
|
25
|
+
scanner;
|
|
26
|
+
registry;
|
|
27
|
+
logger = new Logger(PGBOSS_LOGGER_CONTEXT);
|
|
28
|
+
constructor(boss, options, discovery, scanner, registry) {
|
|
29
|
+
this.boss = boss;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.discovery = discovery;
|
|
32
|
+
this.scanner = scanner;
|
|
33
|
+
this.registry = registry;
|
|
34
|
+
}
|
|
35
|
+
async onApplicationBootstrap() {
|
|
36
|
+
if (this.options.registerWorkers === false) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const processors = this.discovery.getProviders({ metadataKey: ProcessorMarker.KEY });
|
|
40
|
+
for (const wrapper of processors) {
|
|
41
|
+
await this.registerProcessor(wrapper);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async registerProcessor(wrapper) {
|
|
45
|
+
const { instance } = wrapper;
|
|
46
|
+
const className = wrapper.metatype?.name ?? 'UnknownProcessor';
|
|
47
|
+
if (!instance) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!wrapper.isDependencyTreeStatic()) {
|
|
51
|
+
// A worker is a long-lived singleton, so a request- or transient-scoped class can't back one.
|
|
52
|
+
this.logger.warn(`${className} is not singleton-scoped, so its workers were not registered.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const processor = this.discovery.getMetadataByDecorator(ProcessorMarker, wrapper);
|
|
56
|
+
const prototype = Object.getPrototypeOf(instance);
|
|
57
|
+
for (const methodName of this.scanner.getAllMethodNames(prototype)) {
|
|
58
|
+
const process = this.discovery.getMetadataByDecorator(ProcessMarker, wrapper, methodName);
|
|
59
|
+
const schedule = this.discovery.getMetadataByDecorator(ScheduleMarker, wrapper, methodName);
|
|
60
|
+
const where = `${className}.${methodName}`;
|
|
61
|
+
if (!process) {
|
|
62
|
+
if (schedule) {
|
|
63
|
+
throw new Error(`${where} has @Schedule but no @Process, so there is nothing to run on the schedule.`);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const queue = process.queue ?? processor?.queue;
|
|
68
|
+
if (!queue) {
|
|
69
|
+
throw new Error(`${where} has no queue. Name it in @Process('queue') or in @Processor('queue').`);
|
|
70
|
+
}
|
|
71
|
+
await this.registry.ensure(queue);
|
|
72
|
+
const method = instance[methodName].bind(instance);
|
|
73
|
+
await this.boss.work(queue, process.options, this.createHandler(queue, method, process.batch));
|
|
74
|
+
this.logger.log(`${where} is processing "${queue}"`);
|
|
75
|
+
if (schedule) {
|
|
76
|
+
const { data, ...scheduleOptions } = schedule.options;
|
|
77
|
+
await this.boss.schedule(queue, schedule.cron, data ?? null, scheduleOptions);
|
|
78
|
+
this.logger.log(`${where} is scheduled on "${schedule.cron}"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
createHandler(queue, method, batch) {
|
|
83
|
+
return async (jobs, tx) => {
|
|
84
|
+
try {
|
|
85
|
+
return await method(batch ? jobs : jobs[0], tx);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const ids = jobs.map((job) => job.id).join(', ');
|
|
89
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
90
|
+
this.logger.error(`"${queue}" job ${ids} failed: ${reason.message}`, reason.stack);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
PgBossExplorer = __decorate([
|
|
97
|
+
Injectable(),
|
|
98
|
+
__param(0, Inject(PGBOSS_INSTANCE)),
|
|
99
|
+
__param(1, Inject(MODULE_OPTIONS_TOKEN)),
|
|
100
|
+
__param(2, Inject(DiscoveryService)),
|
|
101
|
+
__param(3, Inject(MetadataScanner)),
|
|
102
|
+
__param(4, Inject(QueueRegistry))
|
|
103
|
+
], PgBossExplorer);
|
|
104
|
+
export { PgBossExplorer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { PgBossModule } from './pg-boss.module.js';
|
|
2
|
+
export { PgBossService } from './pg-boss.service.js';
|
|
3
|
+
export { PGBOSS_INSTANCE } from './pg-boss.constants.js';
|
|
4
|
+
export { JobQueue } from './queue/job-queue.js';
|
|
5
|
+
export { QueueRegistry } from './queue/queue.registry.js';
|
|
6
|
+
export { InjectQueue } from './decorators/inject-queue.decorator.js';
|
|
7
|
+
export { Processor, Process, ProcessBatch, Schedule } from './decorators/processor.decorators.js';
|
|
8
|
+
export type { ProcessOptions, ProcessBatchOptions, ScheduleDecoratorOptions, } from './decorators/processor.decorators.js';
|
|
9
|
+
export { getQueueToken } from './utils/get-queue-token.util.js';
|
|
10
|
+
export type { PgBossModuleOptions } from './interfaces/pg-boss-module-options.interface.js';
|
|
11
|
+
export type { QueueDefinition } from './interfaces/queue-definition.interface.js';
|
|
12
|
+
export { fromKnex, fromKysely, fromDrizzle, fromPrisma } from 'pg-boss';
|
|
13
|
+
export type { Job, JobWithMetadata, SendOptions, Db } from 'pg-boss';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { PgBossModule } from './pg-boss.module.js';
|
|
2
|
+
export { PgBossService } from './pg-boss.service.js';
|
|
3
|
+
export { PGBOSS_INSTANCE } from './pg-boss.constants.js';
|
|
4
|
+
export { JobQueue } from './queue/job-queue.js';
|
|
5
|
+
export { QueueRegistry } from './queue/queue.registry.js';
|
|
6
|
+
export { InjectQueue } from './decorators/inject-queue.decorator.js';
|
|
7
|
+
export { Processor, Process, ProcessBatch, Schedule } from './decorators/processor.decorators.js';
|
|
8
|
+
export { getQueueToken } from './utils/get-queue-token.util.js';
|
|
9
|
+
// Transaction adapters, so callers don't need a second import from 'pg-boss'.
|
|
10
|
+
export { fromKnex, fromKysely, fromDrizzle, fromPrisma } from 'pg-boss';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConstructorOptions, PgBoss, StopOptions } from 'pg-boss';
|
|
2
|
+
type StartOptions = NonNullable<Parameters<PgBoss['start']>[0]>;
|
|
3
|
+
/**
|
|
4
|
+
* Everything `new PgBoss(...)` accepts (connection, schema, maintenance, scheduling, ...),
|
|
5
|
+
* plus a few options that control how the module drives its lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
export interface PgBossModuleOptions extends ConstructorOptions {
|
|
8
|
+
/** Passed to `boss.start()`, e.g. `{ attempts: 5 }` to ride out a database that is still booting. */
|
|
9
|
+
start?: StartOptions;
|
|
10
|
+
/**
|
|
11
|
+
* Passed to `boss.stop()` on application shutdown.
|
|
12
|
+
* @default { graceful: true, timeout: 30_000 }
|
|
13
|
+
*/
|
|
14
|
+
stop?: StopOptions;
|
|
15
|
+
/**
|
|
16
|
+
* Register `@Process` workers and `@Schedule` schedules when the app boots.
|
|
17
|
+
* Set to `false` in processes that only enqueue jobs (an HTTP API, for example)
|
|
18
|
+
* so that a separate worker deployment does the processing.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
registerWorkers?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Queue } from 'pg-boss';
|
|
2
|
+
/**
|
|
3
|
+
* A queue declared in code. The module creates it on boot and, when it already exists,
|
|
4
|
+
* reconciles the options pg-boss allows changing so the code stays the source of truth.
|
|
5
|
+
* `policy` and `partition` are fixed at creation time.
|
|
6
|
+
*/
|
|
7
|
+
export type QueueDefinition = Queue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Injection token for the underlying, already-started `PgBoss` instance. */
|
|
2
|
+
export declare const PGBOSS_INSTANCE: unique symbol;
|
|
3
|
+
export declare const PGBOSS_LOGGER_CONTEXT = "PgBoss";
|
|
4
|
+
/** Prefix used to build the injection token of a registered queue. */
|
|
5
|
+
export declare const QUEUE_TOKEN_PREFIX = "PgBossQueue";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Injection token for the underlying, already-started `PgBoss` instance. */
|
|
2
|
+
export const PGBOSS_INSTANCE = Symbol('PGBOSS_INSTANCE');
|
|
3
|
+
export const PGBOSS_LOGGER_CONTEXT = 'PgBoss';
|
|
4
|
+
/** Prefix used to build the injection token of a registered queue. */
|
|
5
|
+
export const QUEUE_TOKEN_PREFIX = 'PgBossQueue';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { PgBossModuleOptions } from './interfaces/pg-boss-module-options.interface.js';
|
|
2
|
+
export declare const ConfigurableModuleClass: import("@nestjs/common").ConfigurableModuleCls<PgBossModuleOptions, "forRoot", "create", {}>, MODULE_OPTIONS_TOKEN: string | symbol, OPTIONS_TYPE: PgBossModuleOptions & Partial<{}>, ASYNC_OPTIONS_TYPE: import("@nestjs/common").ConfigurableModuleAsyncOptions<PgBossModuleOptions, "create"> & Partial<{}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { ASYNC_OPTIONS_TYPE, ConfigurableModuleClass, OPTIONS_TYPE } from './pg-boss.module-definition.js';
|
|
3
|
+
import type { QueueDefinition } from './interfaces/queue-definition.interface.js';
|
|
4
|
+
export declare class PgBossModule extends ConfigurableModuleClass {
|
|
5
|
+
/** Configure pg-boss once, in the root module. */
|
|
6
|
+
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule;
|
|
7
|
+
/** Same as `forRoot`, with `useFactory` / `useClass` / `useExisting`, e.g. to read `ConfigService`. */
|
|
8
|
+
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule;
|
|
9
|
+
/**
|
|
10
|
+
* Declare queues in the module that owns them. Each is created (or reconciled) on boot and can be
|
|
11
|
+
* injected with `@InjectQueue(name)`. A bare string declares a queue with pg-boss defaults.
|
|
12
|
+
*/
|
|
13
|
+
static registerQueue(...queues: (string | QueueDefinition)[]): DynamicModule;
|
|
14
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var PgBossModule_1;
|
|
8
|
+
import { Module } from '@nestjs/common';
|
|
9
|
+
import { DiscoveryModule } from '@nestjs/core';
|
|
10
|
+
import { ASYNC_OPTIONS_TYPE, ConfigurableModuleClass, OPTIONS_TYPE } from './pg-boss.module-definition.js';
|
|
11
|
+
import { PGBOSS_INSTANCE } from './pg-boss.constants.js';
|
|
12
|
+
import { pgBossInstanceProvider } from './pg-boss.providers.js';
|
|
13
|
+
import { PgBossService } from './pg-boss.service.js';
|
|
14
|
+
import { PgBossExplorer } from './explorer/pg-boss.explorer.js';
|
|
15
|
+
import { QueueRegistry } from './queue/queue.registry.js';
|
|
16
|
+
import { JobQueue } from './queue/job-queue.js';
|
|
17
|
+
import { getQueueToken } from './utils/get-queue-token.util.js';
|
|
18
|
+
const coreProviders = [pgBossInstanceProvider, QueueRegistry, PgBossService, PgBossExplorer];
|
|
19
|
+
/** The core is global (like `TypeOrmModule`), so feature modules only need `registerQueue`. */
|
|
20
|
+
function asGlobalCore(definition) {
|
|
21
|
+
return {
|
|
22
|
+
...definition,
|
|
23
|
+
global: true,
|
|
24
|
+
imports: [...(definition.imports ?? []), DiscoveryModule],
|
|
25
|
+
providers: [...(definition.providers ?? []), ...coreProviders],
|
|
26
|
+
exports: [PGBOSS_INSTANCE, PgBossService, QueueRegistry],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let PgBossModule = PgBossModule_1 = class PgBossModule extends ConfigurableModuleClass {
|
|
30
|
+
/** Configure pg-boss once, in the root module. */
|
|
31
|
+
static forRoot(options) {
|
|
32
|
+
return asGlobalCore(super.forRoot(options));
|
|
33
|
+
}
|
|
34
|
+
/** Same as `forRoot`, with `useFactory` / `useClass` / `useExisting`, e.g. to read `ConfigService`. */
|
|
35
|
+
static forRootAsync(options) {
|
|
36
|
+
return asGlobalCore(super.forRootAsync(options));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Declare queues in the module that owns them. Each is created (or reconciled) on boot and can be
|
|
40
|
+
* injected with `@InjectQueue(name)`. A bare string declares a queue with pg-boss defaults.
|
|
41
|
+
*/
|
|
42
|
+
static registerQueue(...queues) {
|
|
43
|
+
const definitions = queues.map((queue) => (typeof queue === 'string' ? { name: queue } : queue));
|
|
44
|
+
// The definition is also a `useValue` provider so it is part of the module's metadata. Nest
|
|
45
|
+
// identifies a dynamic module by hashing that metadata (function bodies by their source text), so
|
|
46
|
+
// options held only in a closure would make two different declarations of one queue look identical.
|
|
47
|
+
const providers = definitions.flatMap((definition) => {
|
|
48
|
+
const definitionToken = `${getQueueToken(definition.name)}:definition`;
|
|
49
|
+
return [
|
|
50
|
+
{ provide: definitionToken, useValue: definition },
|
|
51
|
+
{
|
|
52
|
+
provide: getQueueToken(definition.name),
|
|
53
|
+
inject: [definitionToken, QueueRegistry, PGBOSS_INSTANCE],
|
|
54
|
+
useFactory: async (declared, registry, boss) => {
|
|
55
|
+
registry.declare(declared);
|
|
56
|
+
await registry.ensure(declared.name);
|
|
57
|
+
return new JobQueue(declared.name, boss);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
module: PgBossModule_1,
|
|
64
|
+
providers,
|
|
65
|
+
exports: definitions.map((definition) => getQueueToken(definition.name)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
PgBossModule = PgBossModule_1 = __decorate([
|
|
70
|
+
Module({})
|
|
71
|
+
], PgBossModule);
|
|
72
|
+
export { PgBossModule };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FactoryProvider } from '@nestjs/common';
|
|
2
|
+
import { PgBoss } from 'pg-boss';
|
|
3
|
+
/**
|
|
4
|
+
* Builds and starts pg-boss as an async provider. Because it only resolves once `start()` has
|
|
5
|
+
* finished, anything that injects the instance (queues, services) is guaranteed a ready database,
|
|
6
|
+
* whatever order Nest initializes modules in.
|
|
7
|
+
*/
|
|
8
|
+
export declare const pgBossInstanceProvider: FactoryProvider<PgBoss>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { PgBoss } from 'pg-boss';
|
|
3
|
+
import { MODULE_OPTIONS_TOKEN } from './pg-boss.module-definition.js';
|
|
4
|
+
import { PGBOSS_INSTANCE, PGBOSS_LOGGER_CONTEXT } from './pg-boss.constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Builds and starts pg-boss as an async provider. Because it only resolves once `start()` has
|
|
7
|
+
* finished, anything that injects the instance (queues, services) is guaranteed a ready database,
|
|
8
|
+
* whatever order Nest initializes modules in.
|
|
9
|
+
*/
|
|
10
|
+
export const pgBossInstanceProvider = {
|
|
11
|
+
provide: PGBOSS_INSTANCE,
|
|
12
|
+
inject: [MODULE_OPTIONS_TOKEN],
|
|
13
|
+
useFactory: async (options) => {
|
|
14
|
+
const { start, stop, registerWorkers, ...bossOptions } = options;
|
|
15
|
+
const logger = new Logger(PGBOSS_LOGGER_CONTEXT);
|
|
16
|
+
const boss = new PgBoss(bossOptions);
|
|
17
|
+
// pg-boss is an EventEmitter: an unhandled 'error' event would crash the process.
|
|
18
|
+
boss.on('error', (error) => logger.error(error.message, error.stack));
|
|
19
|
+
boss.on('warning', (warning) => logger.warn(warning.message));
|
|
20
|
+
try {
|
|
21
|
+
await boss.start(start);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
await boss.stop({ close: true, graceful: false }).catch(() => undefined);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return boss;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type OnApplicationShutdown } from '@nestjs/common';
|
|
2
|
+
import type { PgBoss } from 'pg-boss';
|
|
3
|
+
import type { PgBossModuleOptions } from './interfaces/pg-boss-module-options.interface.js';
|
|
4
|
+
/**
|
|
5
|
+
* Owns shutdown of the pg-boss instance and exposes it for anything the module doesn't wrap.
|
|
6
|
+
* Call `app.enableShutdownHooks()` so SIGTERM drains in-flight jobs before the process exits.
|
|
7
|
+
*/
|
|
8
|
+
export declare class PgBossService implements OnApplicationShutdown {
|
|
9
|
+
readonly boss: PgBoss;
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
constructor(boss: PgBoss, options: PgBossModuleOptions);
|
|
13
|
+
onApplicationShutdown(): Promise<void>;
|
|
14
|
+
/** True when the database answers. Suited to a readiness probe. */
|
|
15
|
+
isHealthy(): Promise<boolean>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
11
|
+
import { MODULE_OPTIONS_TOKEN } from './pg-boss.module-definition.js';
|
|
12
|
+
import { PGBOSS_INSTANCE, PGBOSS_LOGGER_CONTEXT } from './pg-boss.constants.js';
|
|
13
|
+
const DEFAULT_STOP = { graceful: true, timeout: 30_000 };
|
|
14
|
+
/**
|
|
15
|
+
* Owns shutdown of the pg-boss instance and exposes it for anything the module doesn't wrap.
|
|
16
|
+
* Call `app.enableShutdownHooks()` so SIGTERM drains in-flight jobs before the process exits.
|
|
17
|
+
*/
|
|
18
|
+
let PgBossService = class PgBossService {
|
|
19
|
+
boss;
|
|
20
|
+
options;
|
|
21
|
+
logger = new Logger(PGBOSS_LOGGER_CONTEXT);
|
|
22
|
+
constructor(boss, options) {
|
|
23
|
+
this.boss = boss;
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
async onApplicationShutdown() {
|
|
27
|
+
try {
|
|
28
|
+
await this.boss.stop({ ...DEFAULT_STOP, ...this.options.stop });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// Shutdown must not throw: other modules still need their own hooks to run.
|
|
32
|
+
this.logger.error('Failed to stop pg-boss cleanly', error instanceof Error ? error.stack : String(error));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** True when the database answers. Suited to a readiness probe. */
|
|
36
|
+
async isHealthy() {
|
|
37
|
+
try {
|
|
38
|
+
await this.boss.getDb().executeSql('SELECT 1');
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
PgBossService = __decorate([
|
|
47
|
+
Injectable(),
|
|
48
|
+
__param(0, Inject(PGBOSS_INSTANCE)),
|
|
49
|
+
__param(1, Inject(MODULE_OPTIONS_TOKEN))
|
|
50
|
+
], PgBossService);
|
|
51
|
+
export { PgBossService };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CommandResponse, ConnectionOptions, JobInsert, JobWithMetadata, PgBoss, QueueResult, Schedule, ScheduleOptions, SendOptions } from 'pg-boss';
|
|
2
|
+
/**
|
|
3
|
+
* A handle on one queue, typed by the payload it carries. Obtain it with `@InjectQueue('name')`.
|
|
4
|
+
*
|
|
5
|
+
* Every method that writes accepts pg-boss's `db` option, so a job can be enqueued in the same
|
|
6
|
+
* transaction as your own writes (see `fromKnex`, `fromKysely`, `fromDrizzle`, `fromPrisma`).
|
|
7
|
+
*/
|
|
8
|
+
export declare class JobQueue<TData extends object = object> {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
private readonly boss;
|
|
11
|
+
constructor(name: string, boss: PgBoss);
|
|
12
|
+
send(data: TData, options?: SendOptions): Promise<string | null>;
|
|
13
|
+
/** Delay the job: a `Date`, an ISO string, or a number of seconds from now. */
|
|
14
|
+
sendAfter(data: TData, after: Date | string | number, options?: SendOptions | null): Promise<string | null>;
|
|
15
|
+
/** At most one job per `seconds` window (per `key`), the first one wins. */
|
|
16
|
+
sendThrottled(data: TData, seconds: number, key?: string, options?: SendOptions | null): Promise<string | null>;
|
|
17
|
+
/** At most one job per `seconds` window (per `key`), the last one wins. */
|
|
18
|
+
sendDebounced(data: TData, seconds: number, key?: string, options?: SendOptions | null): Promise<string | null>;
|
|
19
|
+
/** Bulk insert. Cheaper than calling `send` in a loop. */
|
|
20
|
+
insert(jobs: JobInsert<TData>[], options?: ConnectionOptions): Promise<string[] | null>;
|
|
21
|
+
/** Create or update a recurring job. Idempotent per `(queue, key)`, so calling it on every boot is safe. */
|
|
22
|
+
schedule(cron: string, data?: TData | null, options?: ScheduleOptions): Promise<void>;
|
|
23
|
+
unschedule(key?: string, options?: ConnectionOptions): Promise<void>;
|
|
24
|
+
getSchedules(key?: string): Promise<Schedule[]>;
|
|
25
|
+
findJobs(options?: Parameters<PgBoss['findJobs']>[1]): Promise<JobWithMetadata<TData>[]>;
|
|
26
|
+
cancel(id: string | string[], options?: ConnectionOptions): Promise<CommandResponse>;
|
|
27
|
+
retry(id: string | string[], options?: ConnectionOptions): Promise<CommandResponse>;
|
|
28
|
+
deleteJob(id: string | string[], options?: ConnectionOptions): Promise<CommandResponse>;
|
|
29
|
+
/**
|
|
30
|
+
* Call on a dead letter queue's handle: moves its jobs back to the queues they failed in.
|
|
31
|
+
* Filter with `sourceName`, `ids`, `data` or `createdBefore`. Returns how many moved.
|
|
32
|
+
*/
|
|
33
|
+
redrive(options?: Parameters<PgBoss['redrive']>[1]): Promise<number>;
|
|
34
|
+
/** Queue definition plus live counts (queued, active, deferred, ...). */
|
|
35
|
+
getInfo(): Promise<QueueResult | null>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A handle on one queue, typed by the payload it carries. Obtain it with `@InjectQueue('name')`.
|
|
3
|
+
*
|
|
4
|
+
* Every method that writes accepts pg-boss's `db` option, so a job can be enqueued in the same
|
|
5
|
+
* transaction as your own writes (see `fromKnex`, `fromKysely`, `fromDrizzle`, `fromPrisma`).
|
|
6
|
+
*/
|
|
7
|
+
export class JobQueue {
|
|
8
|
+
name;
|
|
9
|
+
boss;
|
|
10
|
+
constructor(name, boss) {
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.boss = boss;
|
|
13
|
+
}
|
|
14
|
+
send(data, options) {
|
|
15
|
+
return this.boss.send(this.name, data, options);
|
|
16
|
+
}
|
|
17
|
+
/** Delay the job: a `Date`, an ISO string, or a number of seconds from now. */
|
|
18
|
+
sendAfter(data, after, options = null) {
|
|
19
|
+
// pg-boss overloads sendAfter per argument type, which a union can't select; the runtime accepts all three.
|
|
20
|
+
return this.boss.sendAfter(this.name, data, options, after);
|
|
21
|
+
}
|
|
22
|
+
/** At most one job per `seconds` window (per `key`), the first one wins. */
|
|
23
|
+
sendThrottled(data, seconds, key, options = null) {
|
|
24
|
+
return this.boss.sendThrottled(this.name, data, options, seconds, key);
|
|
25
|
+
}
|
|
26
|
+
/** At most one job per `seconds` window (per `key`), the last one wins. */
|
|
27
|
+
sendDebounced(data, seconds, key, options = null) {
|
|
28
|
+
return this.boss.sendDebounced(this.name, data, options, seconds, key);
|
|
29
|
+
}
|
|
30
|
+
/** Bulk insert. Cheaper than calling `send` in a loop. */
|
|
31
|
+
insert(jobs, options) {
|
|
32
|
+
return this.boss.insert(this.name, jobs, options);
|
|
33
|
+
}
|
|
34
|
+
/** Create or update a recurring job. Idempotent per `(queue, key)`, so calling it on every boot is safe. */
|
|
35
|
+
schedule(cron, data, options) {
|
|
36
|
+
return this.boss.schedule(this.name, cron, data, options);
|
|
37
|
+
}
|
|
38
|
+
unschedule(key, options) {
|
|
39
|
+
return this.boss.unschedule(this.name, key, options);
|
|
40
|
+
}
|
|
41
|
+
getSchedules(key) {
|
|
42
|
+
return this.boss.getSchedules(this.name, key);
|
|
43
|
+
}
|
|
44
|
+
findJobs(options) {
|
|
45
|
+
return this.boss.findJobs(this.name, options);
|
|
46
|
+
}
|
|
47
|
+
cancel(id, options) {
|
|
48
|
+
return this.boss.cancel(this.name, id, options);
|
|
49
|
+
}
|
|
50
|
+
retry(id, options) {
|
|
51
|
+
return this.boss.retry(this.name, id, options);
|
|
52
|
+
}
|
|
53
|
+
deleteJob(id, options) {
|
|
54
|
+
return this.boss.deleteJob(this.name, id, options);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Call on a dead letter queue's handle: moves its jobs back to the queues they failed in.
|
|
58
|
+
* Filter with `sourceName`, `ids`, `data` or `createdBefore`. Returns how many moved.
|
|
59
|
+
*/
|
|
60
|
+
redrive(options) {
|
|
61
|
+
return this.boss.redrive(this.name, options);
|
|
62
|
+
}
|
|
63
|
+
/** Queue definition plus live counts (queued, active, deferred, ...). */
|
|
64
|
+
getInfo() {
|
|
65
|
+
return this.boss.getQueue(this.name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PgBoss } from 'pg-boss';
|
|
2
|
+
import type { QueueDefinition } from '../interfaces/queue-definition.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* Knows every queue declared through `PgBossModule.registerQueue()` and makes sure each one
|
|
5
|
+
* exists in the database exactly once per process before anything uses it.
|
|
6
|
+
*/
|
|
7
|
+
export declare class QueueRegistry {
|
|
8
|
+
private readonly boss;
|
|
9
|
+
private readonly definitions;
|
|
10
|
+
private readonly ready;
|
|
11
|
+
constructor(boss: PgBoss);
|
|
12
|
+
declare(definition: QueueDefinition): void;
|
|
13
|
+
/** Resolves once the queue, and its dead letter queue if any, exist and match the declaration. */
|
|
14
|
+
ensure(name: string): Promise<void>;
|
|
15
|
+
private create;
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
11
|
+
import { PGBOSS_INSTANCE } from '../pg-boss.constants.js';
|
|
12
|
+
/**
|
|
13
|
+
* Knows every queue declared through `PgBossModule.registerQueue()` and makes sure each one
|
|
14
|
+
* exists in the database exactly once per process before anything uses it.
|
|
15
|
+
*/
|
|
16
|
+
let QueueRegistry = class QueueRegistry {
|
|
17
|
+
boss;
|
|
18
|
+
definitions = new Map();
|
|
19
|
+
ready = new Map();
|
|
20
|
+
constructor(boss) {
|
|
21
|
+
this.boss = boss;
|
|
22
|
+
}
|
|
23
|
+
declare(definition) {
|
|
24
|
+
const existing = this.definitions.get(definition.name);
|
|
25
|
+
if (existing && !isSameDefinition(existing, definition)) {
|
|
26
|
+
throw new Error(`Queue "${definition.name}" is registered twice with different options. ` +
|
|
27
|
+
'Register it once, or register it with identical options everywhere.');
|
|
28
|
+
}
|
|
29
|
+
this.definitions.set(definition.name, definition);
|
|
30
|
+
// A queue may have been created with defaults earlier (as another queue's dead letter queue)
|
|
31
|
+
// before its own declaration showed up. Forget that so the next ensure() reconciles it.
|
|
32
|
+
this.ready.delete(definition.name);
|
|
33
|
+
}
|
|
34
|
+
/** Resolves once the queue, and its dead letter queue if any, exist and match the declaration. */
|
|
35
|
+
ensure(name) {
|
|
36
|
+
let pending = this.ready.get(name);
|
|
37
|
+
if (!pending) {
|
|
38
|
+
pending = this.create(name).catch((error) => {
|
|
39
|
+
this.ready.delete(name);
|
|
40
|
+
throw error;
|
|
41
|
+
});
|
|
42
|
+
this.ready.set(name, pending);
|
|
43
|
+
}
|
|
44
|
+
return pending;
|
|
45
|
+
}
|
|
46
|
+
async create(name) {
|
|
47
|
+
const { name: _name, ...options } = this.definitions.get(name) ?? { name };
|
|
48
|
+
// pg-boss refuses to create a queue whose dead letter queue does not exist yet.
|
|
49
|
+
if (options.deadLetter) {
|
|
50
|
+
await this.ensure(options.deadLetter);
|
|
51
|
+
}
|
|
52
|
+
if (!(await this.boss.getQueue(name))) {
|
|
53
|
+
await this.boss.createQueue(name, options);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { policy, partition, retryDelayMax, heartbeatSeconds, ...updatable } = options;
|
|
57
|
+
if (Object.keys(updatable).length > 0) {
|
|
58
|
+
await this.boss.updateQueue(name, updatable);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
QueueRegistry = __decorate([
|
|
63
|
+
Injectable(),
|
|
64
|
+
__param(0, Inject(PGBOSS_INSTANCE))
|
|
65
|
+
], QueueRegistry);
|
|
66
|
+
export { QueueRegistry };
|
|
67
|
+
function isSameDefinition(a, b) {
|
|
68
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nestjs-pg-boss",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "nestjs-pg-boss",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NestJS module for pg-boss: declarative queues, decorator-based workers and schedules, and transactional enqueueing.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nestjs",
|
|
7
|
+
"pg-boss",
|
|
8
|
+
"postgres",
|
|
9
|
+
"queue",
|
|
10
|
+
"jobs",
|
|
11
|
+
"background-jobs"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "AbderraHman El Haddad Asofi",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/4reha/nestjs-pg-boss.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/4reha/nestjs-pg-boss#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/4reha/nestjs-pg-boss/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"prepublishOnly": "npm run clean && npm run typecheck && npm test && npm run build"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
48
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
49
|
+
"pg-boss": "^12.0.0",
|
|
50
|
+
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
|
51
|
+
"rxjs": "^7.1.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@electric-sql/pglite": "^0.5.12",
|
|
55
|
+
"@nestjs/common": "^12.3.2",
|
|
56
|
+
"@nestjs/core": "^12.3.2",
|
|
57
|
+
"@nestjs/testing": "^12.3.2",
|
|
58
|
+
"@types/node": "^26.6.5",
|
|
59
|
+
"pg-boss": "^12.38.0",
|
|
60
|
+
"reflect-metadata": "^0.2.2",
|
|
61
|
+
"rxjs": "^7.8.2",
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"vitest": "^5.0.5"
|
|
64
|
+
}
|
|
65
|
+
}
|