Feat/compt 76 nestjs scheduler module and service#4
Conversation
- SchedulerModule.register() and registerAsync() dynamic module factories - SchedulerService: schedule(), reschedule(), unschedule(), list(), status(), listStatus() - @Cron, @interval, @timeout decorators with optional job name - MetadataScanner auto-discovers decorated provider methods on onModuleInit - DuplicateJobError guard against double registration - IScheduler interface and ScheduledJobStatus type - CronExpression constants and cron fluent builder - Full unit test coverage across module, service and decorators
There was a problem hiding this comment.
Pull request overview
This PR introduces a new @ciscode/scheduler-kit library that provides a lightweight in-process scheduler for NestJS, including a SchedulerService, a SchedulerModule that auto-registers decorated methods, and cron helper utilities. It also updates build/lint configuration to support the new module packaging and tooling.
Changes:
- Added
SchedulerServicewith interval/timeout/cron scheduling, job registry, status reporting, and overlap (concurrency) guarding. - Added
SchedulerModulewithregister()/registerAsync()and decorators (@Cron,@Interval,@Timeout) to auto-discover and schedule jobs on module init. - Added cron utilities (
CronExpressionconstants +cronbuilder) and updated build/lint configs and lockfile.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.eslint.json | Extends ESLint TS project include patterns (adds *.mjs). |
| tsconfig.build.json | Adjusts build output settings and narrows the build include list to scheduler-kit sources. |
| src/services/scheduler.service.ts | Implements the core scheduler (cron/interval/timeout), registry, and status APIs. |
| src/services/scheduler.service.spec.ts | Adds unit tests for interval/timeout scheduling, rescheduling, concurrency guard, and error handling. |
| src/scheduler.module.ts | Adds NestJS dynamic module to wire options and auto-schedule decorated provider methods. |
| src/scheduler.module.spec.ts | Adds unit tests validating decorator discovery and dynamic-module wiring. |
| src/interfaces/scheduler.interface.ts | Defines public scheduler types/contracts (job union timing types + status + interface). |
| src/index.ts | Exposes the scheduler-kit public API (module/service/decorators/types/errors/helpers). |
| src/errors/duplicate-job.error.ts | Adds a dedicated error for duplicate job registration. |
| src/errors/duplicate-job.error.spec.ts | Adds unit tests for the custom error behavior. |
| src/decorators/scheduler.decorators.ts | Adds decorators and metadata key used for discovery-based scheduling. |
| src/decorators/scheduler.decorators.spec.ts | Adds unit tests for metadata produced by decorators. |
| src/cron-expression.ts | Adds common cron-expression constants for readability. |
| src/cron-expression.spec.ts | Adds unit tests for cron-expression constants. |
| src/cron-builder.ts | Adds a fluent builder for cron expressions based on human-friendly inputs. |
| src/cron-builder.spec.ts | Adds unit tests for the cron builder output formats. |
| eslint.config.mjs | Updates ESLint ignores and TypeScript project configuration. |
| eslint.config.js | Removes the previous ESLint config file. |
| lint-staged.config.js | Switches lint-staged config to CommonJS export style. |
| package-lock.json | Updates locked dependencies and dependency metadata for the new scheduler-kit setup. |
| reschedule(name: string, newTiming: ScheduleTiming): void { | ||
| const entry = this.registry.get(name); | ||
| if (!entry) return; | ||
| const newJob: ScheduledJob = { ...entry.job, ...newTiming } as ScheduledJob; | ||
| entry.stop(); | ||
| this.registry.delete(name); | ||
| this.schedule(newJob); | ||
| } |
There was a problem hiding this comment.
In reschedule(), spreading entry.job into newJob retains the previous timing property (e.g. interval) when switching to a different timing (e.g. cron), producing an object with multiple timing keys. That breaks the discriminated-union contract and can lead to confusing runtime behavior (cron branch wins, but stale interval/timeout fields remain). Build newJob from { name, handler } + newTiming (or explicitly omit the old timing keys) instead of spreading the full previous job.
| } else if ("interval" in job && job.interval !== undefined) { | ||
| const id = setInterval(() => { | ||
| void guardedHandler(); | ||
| }, job.interval); | ||
| entry.stop = () => clearInterval(id); | ||
| } else if ("timeout" in job && job.timeout !== undefined) { | ||
| const id = setTimeout(() => { | ||
| void guardedHandler(); | ||
| this.registry.delete(job.name); | ||
| }, job.timeout); | ||
| entry.stop = () => clearTimeout(id); | ||
| } |
There was a problem hiding this comment.
Interval/timeout values are used directly in setInterval/setTimeout with no validation. Passing 0, a negative number, or NaN can cause extremely tight loops or immediate execution, which is an operational risk. Consider validating job.interval/job.timeout to be finite, positive integers and throwing a clear error if invalid.
| const id = setTimeout(() => { | ||
| void guardedHandler(); | ||
| this.registry.delete(job.name); |
There was a problem hiding this comment.
For timeout-based jobs, the registry entry is deleted immediately after invoking guardedHandler() (before the handler promise resolves). This means status()/list() can no longer report isRunning while the handler is still executing, and callers can't unschedule() to stop tracking/cancel the job once it has started. Consider deleting the registry entry only after guardedHandler() completes (e.g., await it inside an async timeout callback) so runtime state stays consistent.
| const id = setTimeout(() => { | |
| void guardedHandler(); | |
| this.registry.delete(job.name); | |
| const id = setTimeout(async () => { | |
| try { | |
| await guardedHandler(); | |
| } finally { | |
| this.registry.delete(job.name); | |
| } |
| // A unique symbol used as the key for storing scheduler metadata on methods. | ||
| // Using a unique constant prevents collisions with other decorator libraries. |
There was a problem hiding this comment.
The comment says the metadata key is a "unique symbol", but CISCODE_SCHEDULER_METADATA is currently a string. Either update the comment to match reality, or switch the key to a Symbol (or Symbol.for(...)) to reduce collision risk with other metadata keys.
| // A unique symbol used as the key for storing scheduler metadata on methods. | |
| // Using a unique constant prevents collisions with other decorator libraries. | |
| // A string constant used as the key for storing scheduler metadata on methods. | |
| // Centralizing the key in one constant helps reduce accidental collisions. |
| function parseTime(time: string): { hour: number; minute: number } { | ||
| const ampm = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/i.exec(time.trim()); | ||
| if (ampm) { | ||
| let hour = parseInt(ampm[1] as string, 10); | ||
| const minute = ampm[2] ? parseInt(ampm[2], 10) : 0; | ||
| const period = (ampm[3] as string).toLowerCase(); | ||
| if (period === "pm" && hour !== 12) hour += 12; | ||
| if (period === "am" && hour === 12) hour = 0; | ||
| return { hour, minute }; | ||
| } | ||
| const h24 = /^(\d{1,2}):(\d{2})$/.exec(time.trim()); | ||
| if (h24) { | ||
| return { hour: parseInt(h24[1] as string, 10), minute: parseInt(h24[2] as string, 10) }; | ||
| } | ||
| throw new Error(`Cannot parse time: "${time}". ` + `Use "9am", "9:30pm", "14:30", etc.`); | ||
| } |
There was a problem hiding this comment.
parseTime() accepts any 1–2 digit hour and any 2 digit minute without range checks, so inputs like "99:99" will generate invalid cron expressions. Similarly, cron.every(n) and monthlyOn(dayOfMonth, ...) don't validate n/dayOfMonth ranges. Since this is a public helper API, consider validating hour (0–23), minute (0–59), dayOfMonth (1–31), and n (>0), and throwing a clear error when out of range.
| if ("cron" in job && job.cron) { | ||
| const cronJob = new CronJob( | ||
| job.cron, | ||
| () => { | ||
| void guardedHandler(); | ||
| }, | ||
| null, | ||
| true, | ||
| ); | ||
| entry.cronJob = cronJob; | ||
| entry.stop = () => { | ||
| void cronJob.stop(); | ||
| }; | ||
| } else if ("interval" in job && job.interval !== undefined) { |
There was a problem hiding this comment.
The cron-based scheduling branch (new CronJob(...) / entry.cronJob) is not covered by the current SchedulerService tests (the suite only exercises interval/timeout paths). Adding a test that schedules a cron job and asserts status().nextRun/handler invocation would help prevent regressions in the cron integration.
* ops: updated sonar variable * Feat/compt 75 typed job definitions ischeduler interface (#3) * feat(COMPT-75): define typed scheduled job contracts and IScheduler port - Add ScheduledJob type: name, handler, and one of cron | interval | timeout - Add IScheduler interface: schedule, unschedule, reschedule, list - Add ScheduledJobStatus type: name, cron, lastRun, nextRun, isRunning - Add DuplicateJobError thrown when registering a duplicate job name - Enforce cron/interval/timeout mutual exclusivity via discriminated union - Add CronExpression named constants for human-readable schedules - Add cron fluent builder: dailyAt, weeklyOn, monthlyOn, every().minutes() - Update tsconfig.build.json: CommonJS output, Task 1 files only - Update package.json: name @ciscode/scheduler-kit, version 0.0.0 * test(COMPT-75): add unit tests for CronExpression and cron builder * feat(COMPT-75): add ScheduledJobStatus type and status/listStatus methods - Add ScheduledJobStatus type: name, cron, lastRun, nextRun, isRunning - Add status(name) and listStatus() to IScheduler interface and SchedulerService - Track lastRun timestamp after each execution - Store cronJob reference for nextRun via cronJob.nextDate() - Export ScheduledJobStatus from public API - Fix lint: no-misused-promises, no-floating-promises, prettier formatting * style(COMPT-75): format all files with prettier --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * Feat/compt 76 nestjs scheduler module and service (#4) * feat(COMPT-76): NestJS SchedulerModule and SchedulerService integration - SchedulerModule.register() and registerAsync() dynamic module factories - SchedulerService: schedule(), reschedule(), unschedule(), list(), status(), listStatus() - @Cron, @interval, @timeout decorators with optional job name - MetadataScanner auto-discovers decorated provider methods on onModuleInit - DuplicateJobError guard against double registration - IScheduler interface and ScheduledJobStatus type - CronExpression constants and cron fluent builder - Full unit test coverage across module, service and decorators * test(COMPT-76): add unit tests for CronExpression and cron builder --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * Feat/compt 77 dynamic scheduling error handling (#5) * chore(COMPT-77): scope index.ts and tsconfig.build.json to COMPT-77 files only * test(COMPT-77): add tests for status, listStatus, cron jobs and default onJobError * chore(COMPT-77): restore index.ts and tsconfig.build.json to develop baseline --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * test(COMPT-78): full coverage with Jest fake timers for all schedulin… (#7) * test(COMPT-78): full coverage with Jest fake timers for all scheduling behaviors - jest.useFakeTimers() for all time-based tests — no real waiting - @interval: fires every N ms, stops after unschedule, does not fire before interval - @timeout: fires exactly once, removes itself from registry, does not fire early - schedule(): job added and fires on tick - unschedule(): timer cleared, handler not called after removal - reschedule(): new schedule applied atomically, old timer cancelled - Job error: caught via onJobError, scheduler continues — other jobs unaffected - Concurrent guard: second execution skipped when first still running (isRunning lock) - status() / listStatus(): reflect isRunning and lastRun correctly - @Cron: registered and unscheduled without throwing - Default onJobError: delegates to Logger.error - Coverage: 98.63% statements, 98.11% branches, 95.34% functions (threshold 85%) * fix(COMPT-78): resolve SonarCloud S1186 and S4524 issues - cron-builder.ts: parseInt -> Number.parseInt (Sonar S4524 x3) - scheduler.decorators.spec.ts: add comments to empty stub methods (Sonar S1186 x6) - scheduler.module.spec.ts: add comments to empty stub methods and class (Sonar S1186 x6) --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * release(COMPT-79): bump version to 0.1.0, rewrite README, add changeset (#8) - package.json: 0.0.0 -> 0.1.0 - README: full documentation (decorators, dynamic API, concurrency guard, error handling, async config, cron helpers, API reference) - .changeset/scheduler-kit-v0-1-0.md: minor bump for initial feature release Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * Feat/compt 79 readme changeset publish v0.1.0 (#10) * release(COMPT-79): bump version to 0.1.0, rewrite README, add changeset - package.json: 0.0.0 -> 0.1.0 - README: full documentation (decorators, dynamic API, concurrency guard, error handling, async config, cron helpers, API reference) - .changeset/scheduler-kit-v0-1-0.md: minor bump for initial feature release * fix(COMPT-79): add sonar-project.properties to fix SonarCloud quality gate - sonar.exclusions: exclude spec files from source analysis - sonar.tests + sonar.test.inclusions: declare spec files as test code - fixes 31.9% coverage on new code and 4.8% duplication failures --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> --------- Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com> Co-authored-by: saad moumou <saad.moumou.coder@gmail.com>
* ops: updated sonar variable * Feat/compt 75 typed job definitions ischeduler interface (#3) * feat(COMPT-75): define typed scheduled job contracts and IScheduler port - Add ScheduledJob type: name, handler, and one of cron | interval | timeout - Add IScheduler interface: schedule, unschedule, reschedule, list - Add ScheduledJobStatus type: name, cron, lastRun, nextRun, isRunning - Add DuplicateJobError thrown when registering a duplicate job name - Enforce cron/interval/timeout mutual exclusivity via discriminated union - Add CronExpression named constants for human-readable schedules - Add cron fluent builder: dailyAt, weeklyOn, monthlyOn, every().minutes() - Update tsconfig.build.json: CommonJS output, Task 1 files only - Update package.json: name @ciscode/scheduler-kit, version 0.0.0 * test(COMPT-75): add unit tests for CronExpression and cron builder * feat(COMPT-75): add ScheduledJobStatus type and status/listStatus methods - Add ScheduledJobStatus type: name, cron, lastRun, nextRun, isRunning - Add status(name) and listStatus() to IScheduler interface and SchedulerService - Track lastRun timestamp after each execution - Store cronJob reference for nextRun via cronJob.nextDate() - Export ScheduledJobStatus from public API - Fix lint: no-misused-promises, no-floating-promises, prettier formatting * style(COMPT-75): format all files with prettier --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * Feat/compt 76 nestjs scheduler module and service (#4) * feat(COMPT-76): NestJS SchedulerModule and SchedulerService integration - SchedulerModule.register() and registerAsync() dynamic module factories - SchedulerService: schedule(), reschedule(), unschedule(), list(), status(), listStatus() - @Cron, @interval, @timeout decorators with optional job name - MetadataScanner auto-discovers decorated provider methods on onModuleInit - DuplicateJobError guard against double registration - IScheduler interface and ScheduledJobStatus type - CronExpression constants and cron fluent builder - Full unit test coverage across module, service and decorators * test(COMPT-76): add unit tests for CronExpression and cron builder --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * Feat/compt 77 dynamic scheduling error handling (#5) * chore(COMPT-77): scope index.ts and tsconfig.build.json to COMPT-77 files only * test(COMPT-77): add tests for status, listStatus, cron jobs and default onJobError * chore(COMPT-77): restore index.ts and tsconfig.build.json to develop baseline --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * test(COMPT-78): full coverage with Jest fake timers for all schedulin… (#7) * test(COMPT-78): full coverage with Jest fake timers for all scheduling behaviors - jest.useFakeTimers() for all time-based tests — no real waiting - @interval: fires every N ms, stops after unschedule, does not fire before interval - @timeout: fires exactly once, removes itself from registry, does not fire early - schedule(): job added and fires on tick - unschedule(): timer cleared, handler not called after removal - reschedule(): new schedule applied atomically, old timer cancelled - Job error: caught via onJobError, scheduler continues — other jobs unaffected - Concurrent guard: second execution skipped when first still running (isRunning lock) - status() / listStatus(): reflect isRunning and lastRun correctly - @Cron: registered and unscheduled without throwing - Default onJobError: delegates to Logger.error - Coverage: 98.63% statements, 98.11% branches, 95.34% functions (threshold 85%) * fix(COMPT-78): resolve SonarCloud S1186 and S4524 issues - cron-builder.ts: parseInt -> Number.parseInt (Sonar S4524 x3) - scheduler.decorators.spec.ts: add comments to empty stub methods (Sonar S1186 x6) - scheduler.module.spec.ts: add comments to empty stub methods and class (Sonar S1186 x6) --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * release(COMPT-79): bump version to 0.1.0, rewrite README, add changeset (#8) - package.json: 0.0.0 -> 0.1.0 - README: full documentation (decorators, dynamic API, concurrency guard, error handling, async config, cron helpers, API reference) - .changeset/scheduler-kit-v0-1-0.md: minor bump for initial feature release Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * Feat/compt 79 readme changeset publish v0.1.0 (#10) * release(COMPT-79): bump version to 0.1.0, rewrite README, add changeset - package.json: 0.0.0 -> 0.1.0 - README: full documentation (decorators, dynamic API, concurrency guard, error handling, async config, cron helpers, API reference) - .changeset/scheduler-kit-v0-1-0.md: minor bump for initial feature release * fix(COMPT-79): add sonar-project.properties to fix SonarCloud quality gate - sonar.exclusions: exclude spec files from source analysis - sonar.tests + sonar.test.inclusions: declare spec files as test code - fixes 31.9% coverage on new code and 4.8% duplication failures --------- Co-authored-by: saad moumou <saad.moumou.coder@gmail.com> * patch lock file deps * chore(tsconfig): silence TS6 deprecation warnings --------- Co-authored-by: saadmoumou <s.moumou@ciscod.com> Co-authored-by: saad moumou <saad.moumou.coder@gmail.com>
Summary
Why
Checklist
npm run lintpassesnpm run typecheckpassesnpm testpassesnpm run buildpassesnpx changeset) if this affects consumersNotes