├── 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 | project-logo 3 |

4 |

5 |

Nestjs-PULSE

6 |

7 |

8 | The modern MongoDB-powered scheduling library for NestJS 9 |

10 |

11 | license 12 | last-commit 13 | repo-top-language 14 | repo-language-count 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 | --------------------------------------------------------------------------------