Skip to content

Feat/compt 76 nestjs scheduler module and service#4

Merged
saadmoumou merged 2 commits intodevelopfrom
feat/COMPT-76-nestjs-scheduler-module-and-service
Apr 8, 2026
Merged

Feat/compt 76 nestjs scheduler module and service#4
saadmoumou merged 2 commits intodevelopfrom
feat/COMPT-76-nestjs-scheduler-module-and-service

Conversation

@saadmoumou
Copy link
Copy Markdown
Contributor

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

- 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
@saadmoumou saadmoumou requested a review from a team as a code owner April 8, 2026 13:34
Copilot AI review requested due to automatic review settings April 8, 2026 13:34
@saadmoumou saadmoumou merged commit 7ed2314 into develop Apr 8, 2026
3 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SchedulerService with interval/timeout/cron scheduling, job registry, status reporting, and overlap (concurrency) guarding.
  • Added SchedulerModule with register() / registerAsync() and decorators (@Cron, @Interval, @Timeout) to auto-discover and schedule jobs on module init.
  • Added cron utilities (CronExpression constants + cron builder) 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.

Comment on lines +58 to +65
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);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +138
} 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);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +135
const id = setTimeout(() => {
void guardedHandler();
this.registry.delete(job.name);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const id = setTimeout(() => {
void guardedHandler();
this.registry.delete(job.name);
const id = setTimeout(async () => {
try {
await guardedHandler();
} finally {
this.registry.delete(job.name);
}

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +5
// A unique symbol used as the key for storing scheduler metadata on methods.
// Using a unique constant prevents collisions with other decorator libraries.
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment thread src/cron-builder.ts
Comment on lines +35 to +50
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.`);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +127
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) {
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot generated this review using guidance from repository custom instructions.
Zaiidmo added a commit that referenced this pull request Apr 10, 2026
* 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>
Zaiidmo added a commit that referenced this pull request Apr 10, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants