├── tsconfig.build.json
├── src
├── factories
│ ├── index.ts
│ └── pulse.factory.ts
├── index.ts
├── providers
│ ├── index.ts
│ ├── database.service.ts
│ ├── pulse-metadata.accessor.ts
│ ├── pulse.explorer.ts
│ └── pulse.orchestrator.ts
├── enums
│ ├── index.ts
│ ├── job-processor-type.enum.ts
│ └── pulse-queue-event.enum.ts
├── interfaces
│ ├── index.ts
│ ├── job-options.interface.ts
│ └── pulse-config.interface.ts
├── utils.ts
├── decorators
│ ├── index.ts
│ ├── inject-queue.decorator.ts
│ ├── queue.decorator.ts
│ ├── define.decorator.ts
│ ├── queue-hooks.decorator.ts
│ └── schedulers.decorator.ts
├── pulse.messages.ts
├── constants.ts
└── pulse.module.ts
├── pulse.png
├── examples
└── test
│ └── src
│ ├── main.ts
│ ├── notification
│ ├── notification.processor.ts
│ └── notification.module.ts
│ └── app.module.ts
├── .prettierrc
├── .npmignore
├── .github
├── dependabot.yml
└── workflows
│ └── release.yml
├── LICENSE
├── package.json
├── .gitignore
├── CHANGELOG.md
├── .releaserc
├── tsconfig.json
└── README.md
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/factories/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./pulse.factory";
2 |
--------------------------------------------------------------------------------
/pulse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pulsecron/nestjs-pulse/HEAD/pulse.png
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './decorators';
2 | export * from './pulse.module';
3 |
--------------------------------------------------------------------------------
/src/providers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./pulse-metadata.accessor";
2 | export * from "./pulse.explorer";
3 |
--------------------------------------------------------------------------------
/src/enums/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./job-processor-type.enum";
2 | export * from "./pulse-queue-event.enum";
3 |
--------------------------------------------------------------------------------
/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./pulse-config.interface";
2 | export * from "./job-options.interface";
3 |
--------------------------------------------------------------------------------
/src/enums/job-processor-type.enum.ts:
--------------------------------------------------------------------------------
1 | export enum JobProcessorType {
2 | DEFINE = 'define',
3 | EVERY = 'every',
4 | SCHEDULE = 'schedule',
5 | NOW = 'now',
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const getQueueToken = (name?: string) => (name ? `${name}` : `pulse-queue`);
2 |
3 | export const getQueueConfigToken = (name: string): string => `PulseQueueOptions_${name}`;
4 |
--------------------------------------------------------------------------------
/src/enums/pulse-queue-event.enum.ts:
--------------------------------------------------------------------------------
1 | export enum PulseQueueEvent {
2 | READY = "ready",
3 | ERROR = "error",
4 | START = "start",
5 | COMPLETE = "complete",
6 | SUCCESS = "success",
7 | FAIL = "fail",
8 | }
9 |
--------------------------------------------------------------------------------
/src/decorators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './define.decorator';
2 | export * from './inject-queue.decorator';
3 | export * from './queue-hooks.decorator';
4 | export * from './queue.decorator';
5 | export * from './schedulers.decorator';
6 |
--------------------------------------------------------------------------------
/src/decorators/inject-queue.decorator.ts:
--------------------------------------------------------------------------------
1 | import { Inject } from '@nestjs/common';
2 | import { getQueueToken } from '../utils';
3 |
4 | export const InjectQueue = (name?: string): ParameterDecorator =>
5 | Inject(getQueueToken(name));
6 |
--------------------------------------------------------------------------------
/src/pulse.messages.ts:
--------------------------------------------------------------------------------
1 | export const NO_QUEUE_FOUND = (name?: string) =>
2 | name
3 | ? `No Pulse queue was found with the given name (${name}). Check your configuration.`
4 | : "No Pulse queue was found. Check your configuration.";
5 |
--------------------------------------------------------------------------------
/src/interfaces/job-options.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DefineOptions as PulseDefineOptions,
3 | JobOptions as PulseJobOptions,
4 | } from "@pulsecron/pulse";
5 |
6 | export type JobOptions = PulseDefineOptions &
7 | PulseJobOptions & { name?: string };
8 |
--------------------------------------------------------------------------------
/examples/test/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from '../src/app.module';
3 |
4 | async function bootstrap() {
5 | const app = await NestFactory.create(AppModule);
6 | await app.listen(3000);
7 | }
8 | bootstrap();
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "es5",
8 | "bracketSpacing": true,
9 | "arrowParens": "always",
10 | "htmlWhitespaceSensitivity": "ignore",
11 | "endOfLine": "auto"
12 | }
13 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .idea
3 | .jsdoc.json
4 | .travis.yml
5 | docs
6 | .nyc_output
7 | coverage
8 | History.md
9 | Makefile
10 | renovate.json
11 | test
12 | .vscode/*
13 | src
14 | example
15 | .github
16 |
17 |
18 | # OS
19 | .DS_Store
20 |
21 | # compiled output
22 | /dist
23 | /node_modules
24 |
25 |
26 | src/**/*
27 | pulse.svg
28 | pulse.png
29 |
--------------------------------------------------------------------------------
/src/factories/pulse.factory.ts:
--------------------------------------------------------------------------------
1 | import Pulse, { PulseConfig } from "@pulsecron/pulse";
2 |
3 | export function pulseFactory(
4 | queueConfig: PulseConfig,
5 | rootConfig: PulseConfig
6 | ) {
7 | const pulseConfig = {
8 | ...rootConfig,
9 | ...queueConfig,
10 | };
11 |
12 | delete pulseConfig.db;
13 | delete pulseConfig.mongo;
14 |
15 | return new Pulse(pulseConfig);
16 | }
17 |
--------------------------------------------------------------------------------
/examples/test/src/notification/notification.processor.ts:
--------------------------------------------------------------------------------
1 | import { Job } from '@pulsecron/pulse';
2 | import { Every, Queue } from '../../../../src';
3 |
4 | // with custom collection name
5 | @Queue('notifications')
6 | export class NotificationsQueue {
7 | @Every({ name: 'send notifications', interval: '15 minutes' })
8 | async sendNotifications(job: Job) {
9 | console.log('Sending notifications');
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/test/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PulseModule } from '../../../src';
3 | import { NotificationsModule } from './notification/notification.module';
4 |
5 | @Module({
6 | imports: [
7 | PulseModule.forRoot({
8 | db: {
9 | address: 'mongodb://localhost:27017/pulse_test',
10 | },
11 | }),
12 | NotificationsModule,
13 | ],
14 | providers: [],
15 | })
16 | export class AppModule {}
17 |
--------------------------------------------------------------------------------
/examples/test/src/notification/notification.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PulseModule } from '../../../../src/pulse.module';
3 | import { NotificationsQueue } from './notification.processor';
4 |
5 | @Module({
6 | imports: [
7 | PulseModule.registerQueue('notifications', {
8 | processEvery: '5 minutes',
9 | autoStart: false, // default: true
10 | }),
11 | ],
12 | providers: [NotificationsQueue],
13 | exports: [],
14 | })
15 | export class NotificationsModule {}
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const JOB_NAME = Symbol('PULSE_MODULE:JOB_NAME');
2 | export const PULSE_JOB_OPTIONS = Symbol('PULSE_JOB_OPTIONS');
3 | export const JOB_PROCESSOR_TYPE = Symbol('PULSE_MODULE:JOB_PROCESSOR_TYPE');
4 | export const PULSE_MODULE_CONFIG = Symbol('PULSE_MODULE_CONFIG');
5 | export const PULSE_QUEUE_EVENT = Symbol('PULSE_QUEUE_EVENT');
6 | export const PULSE_MODULE_QUEUE = Symbol('PULSE_MODULE_QUEUE');
7 | export const ON_QUEUE_EVENT = Symbol('PULSE_MODULE:ON_QUEUE_EVENT');
8 | export const QUEUE_CONFIG = Symbol('PULSE_MODULE:QUEUE_CONFIG');
9 | export const DATABASE_CONNECTION = Symbol('PULSE_MODULE:DATABASE_CONNECTION');
10 |
--------------------------------------------------------------------------------
/src/decorators/queue.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata, Type } from '@nestjs/common';
2 | import { PulseQueueConfig } from '../interfaces';
3 | import { PULSE_MODULE_QUEUE } from '../constants';
4 |
5 | export function Queue(): ClassDecorator;
6 | export function Queue(name: string): ClassDecorator;
7 | export function Queue(config: PulseQueueConfig): ClassDecorator;
8 | export function Queue(nameOrConfig?: string | PulseQueueConfig): ClassDecorator {
9 | const pulseConfig = nameOrConfig
10 | ? typeof nameOrConfig === 'string'
11 | ? { queueName: nameOrConfig }
12 | : nameOrConfig
13 | : {};
14 |
15 | return (target: Type | Function) => {
16 | SetMetadata(PULSE_MODULE_QUEUE, pulseConfig)(target);
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/interfaces/pulse-config.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ModuleMetadata,
3 | Type,
4 | FactoryProvider,
5 | Provider,
6 | } from "@nestjs/common";
7 | import { PulseConfig } from "@pulsecron/pulse";
8 |
9 | export type PulseModuleConfig = PulseConfig;
10 |
11 | export type PulseQueueConfig = Omit & {
12 | autoStart?: boolean;
13 | collection?: string;
14 | };
15 |
16 | export interface PulseConfigFactory {
17 | createPulseConfig(): Promise | T;
18 | }
19 |
20 | export interface PulseModuleAsyncConfig
21 | extends Pick {
22 | useExisting?: Type>;
23 | useClass?: Type>;
24 | useFactory?: (...args: any[]) => Promise | T;
25 | inject?: FactoryProvider["inject"];
26 | extraProviders?: Provider[];
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Semantic-release
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 |
7 | permissions:
8 | contents: read # for checkout
9 |
10 | jobs:
11 | release:
12 | name: release
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: write
16 | issues: write
17 | pull-requests: write
18 | id-token: write
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/setup-node@v4
22 | with:
23 | cache: npm
24 | node-version: "lts/*"
25 | registry-url: "https://npm.pkg.github.com/"
26 | scope: '@github-id'
27 | - run: npm ci
28 | - run: npm run build
29 | - run: npx semantic-release
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
33 |
--------------------------------------------------------------------------------
/src/decorators/define.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators, SetMetadata } from '@nestjs/common';
2 | import { DefineOptions } from '@pulsecron/pulse';
3 | import { JOB_PROCESSOR_TYPE, PULSE_JOB_OPTIONS } from '../constants';
4 | import { JobProcessorType } from '../enums';
5 |
6 | type NameAndDefineOptions = DefineOptions & Record<'name', string>;
7 |
8 | export function Define(name?: string): MethodDecorator;
9 | export function Define(options?: NameAndDefineOptions): MethodDecorator;
10 | export function Define(nameOrOptions?: string | NameAndDefineOptions): MethodDecorator {
11 | let options = {};
12 |
13 | if (nameOrOptions) {
14 | options = typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions;
15 | }
16 |
17 | return applyDecorators(
18 | SetMetadata(PULSE_JOB_OPTIONS, options),
19 | SetMetadata(JOB_PROCESSOR_TYPE, JobProcessorType.DEFINE)
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/decorators/queue-hooks.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators, SetMetadata } from "@nestjs/common";
2 | import { ON_QUEUE_EVENT, JOB_NAME } from "../constants";
3 | import { PulseQueueEvent } from "../enums";
4 |
5 | export const OnQueueEvent = (
6 | type: PulseQueueEvent,
7 | jobName?: string
8 | ): MethodDecorator =>
9 | applyDecorators(
10 | SetMetadata(ON_QUEUE_EVENT, type),
11 | SetMetadata(JOB_NAME, jobName)
12 | );
13 |
14 | export const OnQueueReady = () => OnQueueEvent(PulseQueueEvent.READY);
15 |
16 | export const OnQueueError = () => OnQueueEvent(PulseQueueEvent.ERROR);
17 |
18 | export const OnJobStart = (jobName?: string) =>
19 | OnQueueEvent(PulseQueueEvent.START, jobName);
20 |
21 | export const OnJobComplete = (jobName?: string) =>
22 | OnQueueEvent(PulseQueueEvent.COMPLETE, jobName);
23 |
24 | export const OnJobSuccess = (jobName?: string) =>
25 | OnQueueEvent(PulseQueueEvent.SUCCESS, jobName);
26 |
27 | export const OnJobFail = (jobName?: string) =>
28 | OnQueueEvent(PulseQueueEvent.FAIL, jobName);
29 |
--------------------------------------------------------------------------------
/src/providers/database.service.ts:
--------------------------------------------------------------------------------
1 | import { Db, MongoClient } from 'mongodb';
2 | import { Inject, Injectable } from '@nestjs/common';
3 | import { PulseConfig } from '@pulsecron/pulse';
4 |
5 | import { PULSE_MODULE_CONFIG } from '../constants';
6 |
7 | @Injectable()
8 | export class DatabaseService {
9 | private connection!: Db;
10 | private client?: MongoClient;
11 |
12 | constructor(@Inject(PULSE_MODULE_CONFIG) private readonly config: PulseConfig) {
13 | if (config.mongo) {
14 | this.connection = config.mongo;
15 | } else {
16 | this.client = new MongoClient(config.db?.address as string, config.db?.options);
17 | }
18 | }
19 |
20 | async connect() {
21 | if (!this.connection) {
22 | this.client = new MongoClient(this.config.db?.address as string, this.config.db?.options);
23 |
24 | await this.client.connect();
25 |
26 | this.connection = this.client.db();
27 | }
28 | }
29 |
30 | getConnection() {
31 | return this.connection;
32 | }
33 |
34 | async disconnect() {
35 | if (this.client) {
36 | await this.client.close();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Pulsecron
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.
22 |
--------------------------------------------------------------------------------
/src/providers/pulse-metadata.accessor.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Inject, Type } from '@nestjs/common';
2 | import { Reflector } from '@nestjs/core';
3 | import { JOB_PROCESSOR_TYPE, JOB_NAME, PULSE_JOB_OPTIONS, PULSE_MODULE_QUEUE, ON_QUEUE_EVENT } from '../constants';
4 | import { PulseModuleJobOptions } from '../decorators';
5 | import { JobProcessorType } from '../enums';
6 |
7 | @Injectable()
8 | export class PulseMetadataAccessor {
9 | constructor(@Inject(Reflector.name) private readonly reflector: Reflector) {}
10 |
11 | isQueue(target: Type | Function): boolean {
12 | return !!this.reflector.get(PULSE_MODULE_QUEUE, target);
13 | }
14 |
15 | isEventListener(target: Type | Function): boolean {
16 | return !!this.getListenerMetadata(target);
17 | }
18 |
19 | isJobProcessor(target: Type | Function): boolean {
20 | return !!this.getJobProcessorMetadata(target);
21 | }
22 |
23 | getListenerMetadata(target: Type | Function): any {
24 | return this.reflector.get(ON_QUEUE_EVENT, target);
25 | }
26 |
27 | getQueueMetadata(target: Type | Function): any {
28 | return this.reflector.get(PULSE_MODULE_QUEUE, target);
29 | }
30 |
31 | getJobProcessorType(target: Function): JobProcessorType {
32 | return this.reflector.get(JOB_PROCESSOR_TYPE, target);
33 | }
34 |
35 | getJobName(target: Function): string | undefined {
36 | return this.reflector.get(JOB_NAME, target);
37 | }
38 |
39 | getJobProcessorMetadata(target: Type | Function): PulseModuleJobOptions {
40 | return this.reflector.get(PULSE_JOB_OPTIONS, target);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/decorators/schedulers.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators, SetMetadata } from '@nestjs/common';
2 | import { JOB_PROCESSOR_TYPE, PULSE_JOB_OPTIONS } from '../constants';
3 | import { JobProcessorType } from '../enums';
4 | import { JobOptions } from '../interfaces/job-options.interface';
5 |
6 | export type RepeatableJobOptions = JobOptions & Record<'interval', string>;
7 |
8 | export type NonRepeatableJobOptions = JobOptions & Record<'when', string | Date>;
9 |
10 | export type PulseModuleJobOptions = RepeatableJobOptions | NonRepeatableJobOptions;
11 |
12 | export function Every(interval: string): MethodDecorator;
13 | export function Every(options: RepeatableJobOptions): MethodDecorator;
14 | export function Every(intervalOrOptions: string | RepeatableJobOptions): MethodDecorator {
15 | const options = typeof intervalOrOptions === 'string' ? { interval: intervalOrOptions } : intervalOrOptions;
16 |
17 | return applyDecorators(
18 | SetMetadata(PULSE_JOB_OPTIONS, options),
19 | SetMetadata(JOB_PROCESSOR_TYPE, JobProcessorType.EVERY)
20 | );
21 | }
22 |
23 | export function Schedule(when: string): MethodDecorator;
24 | export function Schedule(options: NonRepeatableJobOptions): MethodDecorator;
25 | export function Schedule(whenOrOptions: string | NonRepeatableJobOptions) {
26 | const options = typeof whenOrOptions === 'string' ? { when: whenOrOptions } : whenOrOptions;
27 |
28 | return applyDecorators(
29 | SetMetadata(PULSE_JOB_OPTIONS, options),
30 | SetMetadata(JOB_PROCESSOR_TYPE, JobProcessorType.SCHEDULE)
31 | );
32 | }
33 |
34 | export function Now(name?: string): MethodDecorator {
35 | const options = { name };
36 |
37 | return applyDecorators(
38 | SetMetadata(PULSE_JOB_OPTIONS, options),
39 | SetMetadata(JOB_PROCESSOR_TYPE, JobProcessorType.NOW)
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pulsecron/nestjs-pulse",
3 | "version": "1.0.8",
4 | "description": "The modern MongoDB-powered scheduling library pulse for NestJS",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "publishConfig": {
8 | "access": "public"
9 | },
10 | "files": [
11 | "dist"
12 | ],
13 | "scripts": {
14 | "prepublishOnly": "npm run build",
15 | "build": "tsc",
16 | "release": "npm run build && semantic-release",
17 | "semantic-release": "semantic-release",
18 | "dry-run": "npm publish --dry-run",
19 | "lint": "eslint 'src/**/*.ts'",
20 | "test:e2e": "ts-node examples/test/src/main.ts",
21 | "ncu": "npx npm-check-updates -u"
22 | },
23 | "author": "code-xhyun ",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/pulsecron/nestjs-pulse/issues"
27 | },
28 | "homepage": "https://pulsecron.com",
29 | "repository": {
30 | "type": "git",
31 | "url": "https://github.com/pulsecron/nestjs-pulse.git"
32 | },
33 | "keywords": [
34 | "job",
35 | "jobs",
36 | "cron",
37 | "cronjob",
38 | "mongodb",
39 | "scheduling",
40 | "mongodb",
41 | "mongodb-scheduler",
42 | "queue",
43 | "delayed",
44 | "scheduler",
45 | "pulse",
46 | "bull",
47 | "bee",
48 | "kue",
49 | "nest",
50 | "nestjs"
51 | ],
52 | "devDependencies": {
53 | "@semantic-release/changelog": "^6.0.3",
54 | "@semantic-release/commit-analyzer": "^13.0.1",
55 | "@semantic-release/git": "^10.0.1",
56 | "@semantic-release/github": "^11.0.1",
57 | "@semantic-release/npm": "^12.0.1",
58 | "@semantic-release/release-notes-generator": "^14.0.3",
59 | "conventional-changelog-conventionalcommits": "^8.0.0",
60 | "@nestjs/cli": "^11.0.2",
61 | "@nestjs/common": "^11.0.8",
62 | "@nestjs/core": "^11.0.8",
63 | "@nestjs/platform-express": "^11.0.8",
64 | "@nestjs/testing": "^11.0.8",
65 | "@types/node": "^22.13.1",
66 | "@pulsecron/pulse": "^1.6.7",
67 | "reflect-metadata": "^0.1.13",
68 | "rimraf": "^6.0.1",
69 | "rxjs": "^7.8.1",
70 | "ts-node": "^10.9.2",
71 | "ts-node-dev": "^2.0.0",
72 | "typescript": "^5.7.3",
73 | "semantic-release": "^24.2.2",
74 | "mongodb-memory-server": "^10.1.3"
75 | },
76 | "peerDependencies": {
77 | "@nestjs/common": "^9.x||^10.x||^11.x",
78 | "@nestjs/core": "^9.x||^10.x||^11.x",
79 | "@pulsecron/pulse": "^1.x",
80 | "reflect-metadata": "^0.1.13",
81 | "mongodb-memory-server": "^9.1.8"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage.html
3 | .idea
4 | .DS_Store
5 | docs/pulse/*
6 | dist
7 | .ultra.cache.json
8 | .vscode
9 |
10 | # Logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | lerna-debug.log*
17 |
18 | # Diagnostic reports (https://nodejs.org/api/report.html)
19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
20 |
21 | # Runtime data
22 | pids
23 | *.pid
24 | *.seed
25 | *.pid.lock
26 |
27 | # Directory for instrumented libs generated by jscoverage/JSCover
28 | lib-cov
29 |
30 | # Coverage directory used by tools like istanbul
31 | coverage
32 | *.lcov
33 |
34 | # nyc test coverage
35 | .nyc_output
36 |
37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38 | .grunt
39 |
40 | # Bower dependency directory (https://bower.io/)
41 | bower_components
42 |
43 | # node-waf configuration
44 | .lock-wscript
45 |
46 | # Compiled binary addons (https://nodejs.org/api/addons.html)
47 | build/Release
48 |
49 | # Dependency directories
50 | node_modules/
51 | jspm_packages/
52 |
53 | # Snowpack dependency directory (https://snowpack.dev/)
54 | web_modules/
55 |
56 | # TypeScript cache
57 | *.tsbuildinfo
58 |
59 | # Optional npm cache directory
60 | .npm
61 |
62 | # Optional eslint cache
63 | .eslintcache
64 |
65 | # Microbundle cache
66 | .rpt2_cache/
67 | .rts2_cache_cjs/
68 | .rts2_cache_es/
69 | .rts2_cache_umd/
70 |
71 | # Optional REPL history
72 | .node_repl_history
73 |
74 | # Output of 'npm pack'
75 | *.tgz
76 |
77 | # Yarn Integrity file
78 | .yarn-integrity
79 |
80 | # dotenv environment variables file
81 | .env
82 | .env.test
83 |
84 | # parcel-bundler cache (https://parceljs.org/)
85 | .cache
86 | .parcel-cache
87 |
88 | # Next.js build output
89 | .next
90 | out
91 |
92 | # Nuxt.js build / generate output
93 | .nuxt
94 | dist
95 |
96 | # Gatsby files
97 | .cache/
98 | # Comment in the public line in if your project uses Gatsby and not Next.js
99 | # https://nextjs.org/blog/next-9-1#public-directory-support
100 | # public
101 |
102 | # vuepress build output
103 | .vuepress/dist
104 |
105 | # Serverless directories
106 | .serverless/
107 |
108 | # FuseBox cache
109 | .fusebox/
110 |
111 | # DynamoDB Local files
112 | .dynamodb/
113 |
114 | # TernJS port file
115 | .tern-port
116 |
117 | # Stores VSCode versions used for testing VSCode extensions
118 | .vscode-test
119 |
120 | # yarn v2
121 | .yarn/cache
122 | .yarn/unplugged
123 | .yarn/build-state.yml
124 | .yarn/install-state.gz
125 | .pnp.*
126 | !docs/pulse/4.x/
127 | !docs/pulse/6.x/
128 | dist
129 | .nyc_output
130 | coverage
131 |
132 |
--------------------------------------------------------------------------------
/src/providers/pulse.explorer.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
2 | import { DiscoveryService, MetadataScanner } from '@nestjs/core';
3 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
4 | import { JobAttributes, JobAttributesData, Processor } from '@pulsecron/pulse';
5 | import { getQueueConfigToken, getQueueToken } from '../utils';
6 | import { PulseMetadataAccessor } from './pulse-metadata.accessor';
7 | import { PulseOrchestrator } from './pulse.orchestrator';
8 |
9 | @Injectable()
10 | export class PulseExplorer implements OnModuleInit {
11 | private readonly logger = new Logger('Pulse');
12 |
13 | constructor(
14 | private readonly discoveryService: DiscoveryService,
15 | private readonly metadataAccessor: PulseMetadataAccessor,
16 | private readonly metadataScanner: MetadataScanner,
17 | private readonly orchestrator: PulseOrchestrator
18 | ) {}
19 |
20 | onModuleInit() {
21 | this.explore();
22 | }
23 |
24 | private explore() {
25 | this.discoveryService
26 | .getProviders()
27 | .filter((wrapper: InstanceWrapper) => {
28 | return this.metadataAccessor.isQueue(
29 | !wrapper.metatype || wrapper.inject ? wrapper?.constructor : wrapper.metatype
30 | );
31 | })
32 | .forEach((wrapper: InstanceWrapper) => {
33 | const { instance, metatype } = wrapper;
34 |
35 | const { queueName } = this.metadataAccessor.getQueueMetadata(instance.constructor || metatype);
36 |
37 | const queueToken = getQueueToken(queueName);
38 |
39 | const queueConfigToken = getQueueConfigToken(queueName);
40 |
41 | this.orchestrator.addQueue(queueName, queueToken, queueConfigToken);
42 |
43 | this.metadataScanner.scanFromPrototype(instance, Object.getPrototypeOf(instance), (key: string) => {
44 | const methodRef = instance[key];
45 |
46 | if (this.metadataAccessor.isJobProcessor(methodRef)) {
47 | const jobProcessorType = this.metadataAccessor.getJobProcessorType(methodRef);
48 |
49 | const jobOptions = this.metadataAccessor.getJobProcessorMetadata(methodRef);
50 |
51 | const jobProcessor: Processor & Record<'_name', string> =
52 | this.wrapFunctionInTryCatchBlocks(methodRef, instance);
53 |
54 | this.orchestrator.addJobProcessor(
55 | queueToken,
56 | jobProcessor,
57 | jobOptions,
58 | jobProcessorType,
59 | methodRef.length === 2
60 | );
61 | } else if (this.metadataAccessor.isEventListener(methodRef)) {
62 | const listener = this.wrapFunctionInTryCatchBlocks(methodRef, instance);
63 |
64 | const eventName = this.metadataAccessor.getListenerMetadata(methodRef);
65 |
66 | const jobName = this.metadataAccessor.getJobName(methodRef);
67 |
68 | return this.orchestrator.addEventListener(queueToken, listener, eventName, jobName);
69 | }
70 | });
71 | });
72 | }
73 |
74 | private wrapFunctionInTryCatchBlocks(methodRef: Function, instance: object) {
75 | const handler = (...args: unknown[]) => {
76 | try {
77 | return methodRef.call(instance, ...args);
78 | } catch (error) {
79 | this.logger.error(error);
80 | throw error;
81 | }
82 | };
83 |
84 | handler._name = methodRef.name;
85 |
86 | return handler;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/pulse.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, InjectionToken, Module, Provider, Type, forwardRef } from '@nestjs/common';
2 | import { DiscoveryModule, ModuleRef } from '@nestjs/core';
3 | import { PULSE_MODULE_CONFIG } from './constants';
4 | import { pulseFactory } from './factories';
5 | import { PulseConfigFactory, PulseModuleAsyncConfig, PulseModuleConfig, PulseQueueConfig } from './interfaces';
6 | import { PulseExplorer, PulseMetadataAccessor } from './providers';
7 | import { PulseOrchestrator } from './providers/pulse.orchestrator';
8 | import { DatabaseService } from './providers/database.service';
9 | import { getQueueConfigToken, getQueueToken } from './utils';
10 |
11 | @Module({
12 | imports: [DiscoveryModule],
13 | providers: [PulseOrchestrator, DatabaseService],
14 | exports: [PulseOrchestrator],
15 | })
16 | export class PulseModule {
17 | static forRoot(config: PulseModuleConfig): DynamicModule {
18 | const configProviders: Provider[] = [
19 | {
20 | provide: PULSE_MODULE_CONFIG,
21 | useValue: config,
22 | },
23 | DatabaseService,
24 | PulseMetadataAccessor,
25 | PulseExplorer,
26 | PulseOrchestrator,
27 | ];
28 |
29 | return {
30 | global: true,
31 | module: PulseModule,
32 | providers: configProviders,
33 | exports: configProviders,
34 | };
35 | }
36 |
37 | static forRootAsync(config: PulseModuleAsyncConfig): DynamicModule {
38 | const providers = this.createAsyncProviders(PULSE_MODULE_CONFIG, config);
39 |
40 | return {
41 | global: true,
42 | module: PulseModule,
43 | imports: config.imports || [],
44 | providers: [
45 | ...providers,
46 | DatabaseService,
47 | PulseMetadataAccessor,
48 | PulseExplorer,
49 | PulseOrchestrator,
50 | ...(config.extraProviders || []),
51 | ],
52 | exports: providers,
53 | };
54 | }
55 |
56 | static registerQueue(name: string, config: PulseQueueConfig = {}): DynamicModule {
57 | const queueConfigToken = getQueueConfigToken(name);
58 |
59 | const providers = [
60 | {
61 | provide: queueConfigToken,
62 | useValue: { autoStart: true, ...config },
63 | },
64 | {
65 | provide: getQueueToken(name),
66 | useFactory: pulseFactory,
67 | inject: [queueConfigToken, PULSE_MODULE_CONFIG],
68 | },
69 | ];
70 |
71 | return {
72 | module: PulseModule,
73 | providers,
74 | exports: providers,
75 | };
76 | }
77 |
78 | static registerQueueAsync(name: string, config: PulseModuleAsyncConfig): DynamicModule {
79 | const queueConfigToken = getQueueConfigToken(name);
80 |
81 | const providers = [
82 | {
83 | provide: getQueueToken(name),
84 | useFactory: pulseFactory,
85 | inject: [queueConfigToken, PULSE_MODULE_CONFIG],
86 | },
87 | ...this.createAsyncProviders(queueConfigToken, config),
88 | ];
89 |
90 | return {
91 | module: PulseModule,
92 | imports: config.imports || [],
93 | providers: [...providers, ...(config.extraProviders || [])],
94 | exports: providers,
95 | };
96 | }
97 |
98 | private static createAsyncProviders(token: InjectionToken, config: PulseModuleAsyncConfig): Provider[] {
99 | if (config.useExisting || config.useFactory) {
100 | return [this.createAsyncOptionsProvider(token, config)];
101 | }
102 |
103 | const useClass = config.useClass as Type>;
104 |
105 | return [
106 | this.createAsyncOptionsProvider(token, config),
107 | {
108 | provide: useClass,
109 | useClass,
110 | },
111 | ];
112 | }
113 |
114 | private static createAsyncOptionsProvider(token: InjectionToken, config: PulseModuleAsyncConfig): Provider {
115 | if (config.useFactory) {
116 | return {
117 | provide: token,
118 | useFactory: config.useFactory,
119 | inject: config.inject || [],
120 | };
121 | }
122 |
123 | const inject = [(config.useClass || config.useExisting) as Type>];
124 |
125 | return {
126 | provide: token,
127 | useFactory: async (optionsFactory: PulseConfigFactory) => optionsFactory.createPulseConfig(),
128 | inject,
129 | };
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## [1.0.8](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.7...v1.0.8) (2025-03-07)
4 |
5 | ### 🚀 FEATURES
6 |
7 | * support nestjs version 11 ([#9](https://github.com/pulsecron/nestjs-pulse/issues/9)) ([36563c9](https://github.com/pulsecron/nestjs-pulse/commit/36563c9230a5ed005ef324a04f531db19f94477c))
8 |
9 | ### 🐛 BUG FIXES
10 |
11 | * updating readme after package updates ([cc09b46](https://github.com/pulsecron/nestjs-pulse/commit/cc09b4636bfa64f9e0d055f97e26ca500b47d53e))
12 | * updating readme after package updates ([742ba01](https://github.com/pulsecron/nestjs-pulse/commit/742ba01bb9c20bd3de8e668b97ef0ca5320d818a))
13 |
14 | ## [1.0.7](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.6...v1.0.7) (2024-05-07)
15 |
16 |
17 | ### 🐛 BUG FIXES
18 |
19 | * update @pulsecron/pulse dependency to version 1.4.1 ([4ec9f78](https://github.com/pulsecron/nestjs-pulse/commit/4ec9f78d7f789aafed2909b9e47b25a21feb3a32))
20 |
21 | ## [1.0.6](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.5...v1.0.6) (2024-05-02)
22 |
23 |
24 | ### 🐛 BUG FIXES
25 |
26 | * update @pulsecron/pulse dependency to version 1.1.9 in package.json ([#3](https://github.com/pulsecron/nestjs-pulse/issues/3)) ([8bc1ca9](https://github.com/pulsecron/nestjs-pulse/commit/8bc1ca977bf8a3dff945d32a32fa9c07c3f08574))
27 | * update @pulsecron/pulse dependency to version 1.1.9 in package.json ([#4](https://github.com/pulsecron/nestjs-pulse/issues/4)) ([a50ca76](https://github.com/pulsecron/nestjs-pulse/commit/a50ca769f1434f33d9c924ab2dfca83c0966e9ff))
28 |
29 | ## [1.0.5](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.4...v1.0.5) (2024-04-17)
30 |
31 |
32 | ### 🐛 BUG FIXES
33 |
34 | * add DatabaseService provider to PulseModule ([#1](https://github.com/pulsecron/nestjs-pulse/issues/1)) ([14cd0e3](https://github.com/pulsecron/nestjs-pulse/commit/14cd0e307d5bc159393b0976f4f4d6f22126c79b))
35 | * add DatabaseService provider to PulseModule ([#2](https://github.com/pulsecron/nestjs-pulse/issues/2)) ([494bd60](https://github.com/pulsecron/nestjs-pulse/commit/494bd600565ff2b0abf7054cbd98919c5d1a2316))
36 | * update @pulsecron/pulse dependency to version 1.1.6 ([e546f64](https://github.com/pulsecron/nestjs-pulse/commit/e546f64d3d8a541c5d770cb0da8ab36b32453358))
37 |
38 | ## [1.0.4](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.3...v1.0.4) (2024-04-16)
39 |
40 |
41 | ### 📝 DOCS
42 |
43 | * add new notification service in README.md ([8a0f06e](https://github.com/pulsecron/nestjs-pulse/commit/8a0f06ef5a69bcb34ac9bccfaca06921749de786))
44 |
45 |
46 | ### 🐛 BUG FIXES
47 |
48 | * delete default suffix queue name ([6f38c8e](https://github.com/pulsecron/nestjs-pulse/commit/6f38c8e88dcf6a54199f443e613cd2140e7e5970))
49 |
50 | ## [1.0.3](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.2...v1.0.3) (2024-04-15)
51 |
52 |
53 | ### 🐛 BUG FIXES
54 |
55 | * update @pulsecron/pulse dependency to version 1.1.4 ([a0959d2](https://github.com/pulsecron/nestjs-pulse/commit/a0959d227f866859513b4102d050478507ee8c3c))
56 | * update README.md and update @pulsecron/pulse dependency to version 1.1.4 in package.json ([f0073b1](https://github.com/pulsecron/nestjs-pulse/commit/f0073b1a806130741fe59361425721486209210c))
57 |
58 | ## [1.0.2](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.1...v1.0.2) (2024-04-15)
59 |
60 |
61 | ### 🐛 BUG FIXES
62 |
63 | * update @nestjs/cli and @nestjs/common dependencies in package.json ([a701a83](https://github.com/pulsecron/nestjs-pulse/commit/a701a8317f3a990dfaef571f9386362c361d6b71))
64 | * update @pulsecron/pulse dependency to version 1.1.3 in package.json ([3bdbc0f](https://github.com/pulsecron/nestjs-pulse/commit/3bdbc0ff7ab6337d14fb38b0c327a8d42f4abc79))
65 | * update dependencies in package.json ([a62264a](https://github.com/pulsecron/nestjs-pulse/commit/a62264a9a7b9b1f4186eb0b4948f115d944254fe))
66 |
67 | ## [1.0.1](https://github.com/pulsecron/nestjs-pulse/compare/v1.0.0...v1.0.1) (2024-04-14)
68 |
69 |
70 | ### 🐛 BUG FIXES
71 |
72 | * package.json ([d812f1d](https://github.com/pulsecron/nestjs-pulse/commit/d812f1dd26ad1878e550d96dbf10df3ffbe61588))
73 |
74 | ## 1.0.0 (2024-04-14)
75 |
76 |
77 | ### 📝 DOCS
78 |
79 | * readme ([1eaea4f](https://github.com/pulsecron/nestjs-pulse/commit/1eaea4f7205763820ac5739be6850f5774d44dcf))
80 |
81 |
82 | ### 🚀 FEATURES
83 |
84 | * init ([df16769](https://github.com/pulsecron/nestjs-pulse/commit/df16769330b3dc55b7b16a0b278f7d9e7474690d))
85 |
86 |
87 | ### 🐛 BUG FIXES
88 |
89 | * init ([1ddabd8](https://github.com/pulsecron/nestjs-pulse/commit/1ddabd8e4ad9a2a9af98f690d72a4613db9df93f))
90 | * package.json ([207ddd8](https://github.com/pulsecron/nestjs-pulse/commit/207ddd89610de002267f7a5941305fe66aa02e93))
91 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "main"
4 | ],
5 | "plugins": [
6 | [
7 | "@semantic-release/commit-analyzer",
8 | {
9 | "preset": "conventionalcommits",
10 | "releaseRules": [
11 | {
12 | "scope": "breaking",
13 | "release": "major"
14 | },
15 | {
16 | "scope": "no-release",
17 | "release": false
18 | },
19 | {
20 | "type": "build",
21 | "release": false
22 | },
23 | {
24 | "type": "chore",
25 | "release": false
26 | },
27 | {
28 | "type": "ci",
29 | "release": false
30 | },
31 | {
32 | "type": "docs",
33 | "release": "patch"
34 | },
35 | {
36 | "type": "feat",
37 | "release": "minor"
38 | },
39 | {
40 | "type": "fix",
41 | "release": "patch"
42 | },
43 | {
44 | "type": "perf",
45 | "release": "patch"
46 | },
47 | {
48 | "type": "refactor",
49 | "release": "patch"
50 | },
51 | {
52 | "type": "revert",
53 | "release": "patch"
54 | },
55 | {
56 | "type": "style",
57 | "release": "patch"
58 | },
59 | {
60 | "type": "test",
61 | "release": false
62 | },
63 | {
64 | "tag": "released",
65 | "release": "patch"
66 | }
67 | ],
68 | "parserOpts": {
69 | "noteKeywords": [
70 | "BREAKING CHANGE",
71 | "BREAKING CHANGES"
72 | ]
73 | }
74 | }
75 | ],
76 | [
77 | "@semantic-release/release-notes-generator",
78 | {
79 | "preset": "conventionalcommits",
80 | "presetConfig": {
81 | "types": [
82 | {
83 | "type": "build",
84 | "section": "⚙️ SYSTEM BUILD & EXTERNAL PACKAGES",
85 | "hidden": true
86 | },
87 | {
88 | "type": "chore",
89 | "section": "📦 CHORES",
90 | "hidden": true
91 | },
92 | {
93 | "type": "ci",
94 | "section": "🪜 CI/CD",
95 | "hidden": true
96 | },
97 | {
98 | "type": "docs",
99 | "section": "📝 DOCS",
100 | "hidden": false
101 | },
102 | {
103 | "type": "feat",
104 | "section": "🚀 FEATURES",
105 | "hidden": false
106 | },
107 | {
108 | "type": "fix",
109 | "section": "🐛 BUG FIXES",
110 | "hidden": false
111 | },
112 | {
113 | "type": "perf",
114 | "section": "♻️ PERFORMANCE",
115 | "hidden": false
116 | },
117 | {
118 | "type": "refactor",
119 | "section": "♻️ REFACTOR",
120 | "hidden": false
121 | },
122 | {
123 | "type": "revert",
124 | "section": "↩️ REVERTS",
125 | "hidden": false
126 | },
127 | {
128 | "type": "style",
129 | "section": "👩🎤 STYLES",
130 | "hidden": false
131 | },
132 | {
133 | "type": "test",
134 | "section": "✅ TESTS",
135 | "hidden": true
136 | }
137 | ]
138 | },
139 | "parserOpts": {
140 | "noteKeywords": [
141 | "BREAKING CHANGE",
142 | "BREAKING CHANGES"
143 | ]
144 | },
145 | "writerOpts": {
146 | "commitsSort": [
147 | "subject",
148 | "scope"
149 | ]
150 | }
151 | }
152 | ],
153 | [
154 | "@semantic-release/changelog",
155 | {
156 | "changelogFile": "CHANGELOG.md",
157 | "changelogTitle": "# CHANGELOG"
158 | }
159 | ],
160 | [
161 | "@semantic-release/npm",
162 | {
163 | "pkgRoot": ".",
164 | "tarball": "dist"
165 | }
166 | ],
167 | "@semantic-release/github",
168 | [
169 | "@semantic-release/git",
170 | {
171 | "assets": [
172 | "package.json",
173 | "package-lock.json",
174 | "CHANGELOG.md"
175 | ],
176 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
177 | }
178 | ]
179 | ]
180 | }
181 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6 | // "lib": ["es2019"] /* Specify library files to be included in the compilation. */,
7 | "allowJs": true /* Allow javascript files to be compiled. */,
8 | // "checkJs": true, /* Report errors in .js files. */
9 | // "jsx": "react-native" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
10 | "declaration": true /* Generates corresponding '.d.ts' file. */,
11 | "sourceMap": true /* Generates corresponding '.map' file. */,
12 | // "outFile": "./", /* Concatenate and emit output to single file. */
13 | "outDir": "./dist" /* Redirect output structure to the directory. */,
14 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
15 | "removeComments": true /* Do not emit comments to output. */,
16 | // "noEmit": true /* Do not emit outputs. */,
17 | "incremental": true /* Enable incremental compilation */,
18 | "importHelpers": true, /* Import emit helpers from 'tslib'. */
19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
20 | // "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */,
21 |
22 | /* Strict Type-Checking Options */
23 | "strict": true /* Enable all strict type-checking options. */,
24 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
25 | // "strictNullChecks": true, /* Enable strict null checks. */
26 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
27 | "strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
28 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
29 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
30 |
31 | /* Additional Checks */
32 | // "noUnusedLocals": true, /* Report errors on unused locals. */
33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
36 |
37 | /* Module Resolution Options */
38 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
39 | "baseUrl": "." /* Base directory to resolve non-absolute module names. */,
40 | "paths": {
41 | "@src/*": ["src/*"]
42 | } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
44 | // "typeRoots": [], /* List of folders to include type definitions from. */
45 | "types": [ "node"] /* Type declaration files to be included in compilation. */,
46 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
49 |
50 | /* Advanced Options */
51 | "skipLibCheck": true /* Skip type checking of declaration files. */,
52 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
53 |
54 | /* Source Map Options */
55 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
56 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
59 | /* Experimental Options */
60 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
61 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
62 |
63 | },
64 | "exclude": ["node_modules", "**/*.spec.ts", "tests","_src"],
65 | "include": ["src/**/*.ts", "src/**/*.d.ts"]
66 | }
67 |
--------------------------------------------------------------------------------
/src/providers/pulse.orchestrator.ts:
--------------------------------------------------------------------------------
1 | import { BeforeApplicationShutdown, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
2 | import { ModuleRef } from '@nestjs/core';
3 | import Pulse, {
4 | PulseConfig,
5 | Job,
6 | Processor,
7 | JobAttributes,
8 | PulseOnEventType,
9 | JobAttributesData,
10 | } from '@pulsecron/pulse';
11 | import { NO_QUEUE_FOUND } from '../pulse.messages';
12 | import { PulseModuleJobOptions, NonRepeatableJobOptions, RepeatableJobOptions } from '../decorators';
13 | import { JobProcessorType } from '../enums';
14 | import { PulseQueueConfig } from '../interfaces';
15 | import { DatabaseService } from './database.service';
16 |
17 | type JobProcessorConfig = {
18 | handler: Processor;
19 | type: JobProcessorType;
20 | options: RepeatableJobOptions | NonRepeatableJobOptions;
21 | useCallback: boolean;
22 | };
23 |
24 | export type EventListener = (...args: any[]) => void;
25 |
26 | type QueueRegistry = {
27 | config: PulseQueueConfig;
28 | processors: Map;
29 | listeners: Map;
30 | queue: Pulse;
31 | };
32 |
33 | @Injectable()
34 | export class PulseOrchestrator implements OnApplicationBootstrap, BeforeApplicationShutdown {
35 | private readonly logger = new Logger('Pulse');
36 |
37 | private readonly queues: Map = new Map();
38 |
39 | constructor(private readonly moduleRef: ModuleRef, private readonly database: DatabaseService) {}
40 |
41 | async onApplicationBootstrap() {
42 | await this.database.connect();
43 |
44 | for await (const queue_ of this.queues) {
45 | const [queueToken, registry] = queue_;
46 |
47 | const { config, queue } = registry;
48 |
49 | this.attachEventListeners(queue, registry);
50 |
51 | queue.mongo(this.database.getConnection(), config.collection || queueToken);
52 |
53 | if (config.autoStart) {
54 | await queue.start();
55 | }
56 |
57 | this.defineJobProcessors(queue, registry);
58 |
59 | await this.scheduleJobs(queue, registry);
60 | }
61 | }
62 |
63 | async beforeApplicationShutdown() {
64 | for await (const queue of this.queues) {
65 | const [, config] = queue;
66 |
67 | await config.queue.stop();
68 | }
69 |
70 | await this.database.disconnect();
71 | }
72 |
73 | addQueue(queueName: string, queueToken: string, queueConfigToken: string) {
74 | const queue = this.getQueue(queueName, queueToken);
75 | const config = this.getQueueConfig(queueConfigToken);
76 |
77 | this.queues.set(queueToken, {
78 | queue,
79 | config,
80 | processors: new Map(),
81 | listeners: new Map(),
82 | });
83 | }
84 |
85 | addJobProcessor(
86 | queueToken: string,
87 | processor: Processor & Record<'_name', string>,
88 | options: PulseModuleJobOptions,
89 | type: JobProcessorType,
90 | useCallback: boolean
91 | ) {
92 | const jobName = options.name || processor._name;
93 |
94 | this.queues.get(queueToken)?.processors.set(jobName, {
95 | handler: processor,
96 | useCallback,
97 | type,
98 | options,
99 | });
100 | }
101 |
102 | addEventListener(queueToken: string, listener: EventListener, eventName: PulseOnEventType, jobName?: string) {
103 | const key = jobName ? `${eventName}:${jobName}` : eventName;
104 |
105 | this.queues.get(queueToken)?.listeners.set(key, listener);
106 | }
107 |
108 | private attachEventListeners(pulse: Pulse, registry: QueueRegistry) {
109 | registry.listeners.forEach((listener: EventListener, eventName: string) => {
110 | pulse.on(eventName as PulseOnEventType, listener);
111 | });
112 | }
113 |
114 | private defineJobProcessors(pulse: Pulse, registry: QueueRegistry) {
115 | registry.processors.forEach((jobConfig: JobProcessorConfig, jobName: string) => {
116 | const { options, handler, useCallback } = jobConfig;
117 |
118 | if (useCallback) {
119 | pulse.define(jobName, (job: Job, done: () => void = () => {}) => handler(job, done), options);
120 | } else {
121 | pulse.define(jobName, handler, options);
122 | }
123 | });
124 | }
125 |
126 | private async scheduleJobs(pulse: Pulse, registry: QueueRegistry) {
127 | for await (const processor of registry.processors) {
128 | const [jobName, jobConfig] = processor;
129 |
130 | const { type, options } = jobConfig;
131 |
132 | if (type === JobProcessorType.EVERY) {
133 | await pulse.every((options as RepeatableJobOptions).interval, jobName, {}, options);
134 | } else if (type === JobProcessorType.SCHEDULE) {
135 | await pulse.schedule((options as NonRepeatableJobOptions).when, jobName, {});
136 | } else if (type === JobProcessorType.NOW) {
137 | await pulse.now(jobName, {});
138 | }
139 | }
140 | }
141 |
142 | private getQueue(queueName: string, queueToken: string): Pulse {
143 | try {
144 | return this.moduleRef.get(queueToken, { strict: false });
145 | } catch (error) {
146 | this.logger.error(NO_QUEUE_FOUND(queueName));
147 | throw error;
148 | }
149 | }
150 |
151 | private getQueueConfig(queueConfigToken: string): PulseConfig {
152 | return this.moduleRef.get(queueConfigToken, {
153 | strict: false,
154 | });
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Nestjs-PULSE
6 |
7 |
8 | The modern MongoDB-powered scheduling library for NestJS
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Table of Contents
24 |
25 | - [Overview](#overview)
26 | - [Getting Started](#getting-started)
27 | - [Installation](#installation)
28 | - [Example](#example)
29 | - [app.module.ts](#appmodulets)
30 | - [notification.module.ts](#notificationmodulets)
31 | - [notification.processor.ts](#notificationprocessorts)
32 | - [notification.service.ts](#notificationservicets)
33 | - [Contributing](#contributing)
34 | - [License](#license)
35 |
36 |
37 |
38 |
39 | ## Overview
40 |
41 | [Pulse](https://github.com/pulsecron/pulse) module for [NestJS](https://nestjs.com/), working with nestjs v9.x, v10.x and v11.x.
42 |
43 | ---
44 |
45 |
46 |
47 | ## Getting Started
48 |
49 | ### Installation
50 |
51 | ```console
52 | npm install --save @pulsecron/nestjs-pulse @pulsecron/pulse
53 | ```
54 |
55 | #### Example
56 |
57 | ##### app.module.ts
58 |
59 | ```typescript
60 | // src/app.module.ts
61 |
62 | import { Module } from '@nestjs/common';
63 | import { PulseModule } from '@pulsecron/nestjs-pulse';
64 | import { NotificationsModule } from './notification/notification.module';
65 |
66 | @Module({
67 | imports: [
68 | PulseModule.forRoot({
69 | db: {
70 | address: 'mongodb://localhost:27017/pulse',
71 | },
72 | }),
73 | NotificationsModule,
74 | ],
75 | providers: [],
76 | })
77 | export class AppModule {}
78 |
79 | ```
80 |
81 | ##### notification.module.ts
82 |
83 | ```typescript
84 | // src/notification/notification.module.ts
85 |
86 | import { Module } from '@nestjs/common';
87 | import { PulseModule } from '@pulsecron/nestjs-pulse';
88 | import { NotificationsQueue } from './notification.processor';
89 |
90 | @Module({
91 | imports: [
92 | PulseModule.registerQueue('notifications', {
93 | processEvery: '1 minutes',
94 | autoStart: false, // default: true
95 | }),
96 | ],
97 | providers: [NotificationsQueue],
98 | exports: [],
99 | })
100 | export class NotificationsModule {}
101 |
102 | ```
103 |
104 | ##### notification.processor.ts
105 |
106 | ```typescript
107 | // src/notification/notification.processor.ts
108 |
109 | import { Job } from '@pulsecron/pulse';
110 | import { Every, Queue, Define, Schedule } from '@pulsecron/nestjs-pulse';
111 |
112 | @Queue('notifications')
113 | export class NotificationsQueue {
114 | @Every({ name: 'send notifications', interval: '1 minutes' })
115 | async sendNotifications(job: Job) {
116 | console.log('Sending notifications[1]');
117 | }
118 |
119 | @Schedule({ name: 'send notifications', when: 'tomorrow at noon' })
120 | async sendNotifications(job: Job) {
121 | console.log('Sending notifications[2]');
122 | }
123 |
124 | @Now()
125 | async sendNotifications(job: Job) {
126 | console.log('Sending notifications[3]');
127 | }
128 |
129 | @Define({ name: 'emailJob' })
130 | async test(job: Job) {
131 | console.log('Sending email to:', job.data.to);
132 | }
133 | }
134 |
135 |
136 |
137 | ```
138 |
139 | ##### notification.service.ts
140 |
141 | ```typescript
142 |
143 | import { Inject, Injectable } from '@nestjs/common';
144 | import { Pulse } from '@pulsecron/pulse';
145 |
146 | @Injectable()
147 | export class NotificationService {
148 | constructor(@Inject('notifications') private pulse: Pulse) {}
149 |
150 | async scheduleEmail(email: string) {
151 | const emailJob = this.pulse.create('emailJob', { to: email });
152 | emailJob.unique(
153 | {
154 | 'data.to': email,
155 | },
156 | {
157 | insertOnly: true,
158 | }
159 | );
160 | emailJob.schedule('5 seconds').save();
161 | }
162 | }
163 |
164 |
165 | ```
166 |
167 | ---
168 |
169 |
170 |
171 | ## Contributing
172 |
173 | Contributions are welcome! Here are several ways you can contribute:
174 |
175 | - **[Report Issues](https://github.com/pulsecron/nestjs-pulse/issues)**: Submit bugs found or log feature requests for the `pulse` project.
176 | - **[Submit Pull Requests](https://github.com/pulsecron/nestjs-pulse/pulls)**: Review open PRs, and submit your own PRs.
177 | - **[Join the Discussions](https://github.com/pulsecron/nestjs-pulse/discussions)**: Share your insights, provide feedback, or ask questions.
178 |
179 |
180 | Contributing Guidelines
181 |
182 | 1. **Fork the Repository**: Start by forking the project repository to your github account.
183 | 2. **Clone Locally**: Clone the forked repository to your local machine using a git client.
184 |
185 | ```sh
186 | git clone https://github.com/pulsecron/nestjs-pulse
187 | ```
188 |
189 | 3. **Create a New Branch**: Always work on a new branch, giving it a descriptive name.
190 |
191 | ```sh
192 | git checkout -b new-feature-x
193 | ```
194 |
195 | 4. **Make Your Changes**: Develop and test your changes locally.
196 | 5. **Commit Your Changes**: Commit with a clear message describing your updates.
197 |
198 | ```sh
199 | git commit -m 'Implemented new feature x.'
200 | ```
201 |
202 | 6. **Push to github**: Push the changes to your forked repository.
203 |
204 | ```sh
205 | git push origin new-feature-x
206 | ```
207 |
208 | 7. **Submit a Pull Request**: Create a PR against the original project repository. Clearly describe the changes and their motivations.
209 | 8. **Review**: Once your PR is reviewed and approved, it will be merged into the main branch. Congratulations on your contribution!
210 |
211 |
212 |
213 |
214 | Contributor Graph
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | ---
224 |
225 |
226 |
227 | ## License
228 |
229 | This project is protected under the [MIT](https://github.com/pulsecron/nestjs-pulse?tab=MIT-1-ov-file#readme) License. For more details, refer to the [LICENSE](https://github.com/pulsecron/nestjs-pulse?tab=MIT-1-ov-file#readme) file.
230 |
231 | ---
232 |
233 |
234 |
--------------------------------------------------------------------------------