From a16b6c19d420b1d9f4864e8edb1d94282e8411d6 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Sun, 27 Sep 2026 06:51:50 +0000 Subject: [PATCH] Fix double ScheduleModule.forRoot() registration doubling every cron/interval job (v0.21.7) --- CHANGELOG.md | 5 +++++ package.json | 2 +- src/battery/battery.module.ts | 3 +-- src/schedule-singleton.spec.ts | 28 ++++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/schedule-singleton.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b478c..9129167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.21.7 — 2026-09-27 06:50 + +- Fixed double registration of scheduled jobs: `ScheduleModule.forRoot()` was called from both `WeatherModule` and `BatteryAlertModule`, so Nest created two ScheduleExplorers, each scanning all providers — every @Cron/@Interval job (hourly weather broadcast, per-minute battery check) fired twice. Removed the call from `battery.module.ts`; the single registration in `weather.module.ts` remains. +- Tests: 174 → 175 — new regression spec (`src/schedule-singleton.spec.ts`) asserts `ScheduleModule.forRoot(` appears exactly once across all `src/**/*.module.ts`. + ## v0.21.6 — 2026-09-26 10:37 - New `#help` command (owner request): replies with a short list of all available commands as ONE multi-line message (single packet, not one spaced packet per line) — `#ping`, the `#rss help/add/remove/get/sources/tags` family. Registered in the CommandService constructor next to `#ping`. diff --git a/package.json b/package.json index 2365922..014cdb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mesh-bot", - "version": "0.21.6", + "version": "0.21.7", "description": "", "author": "", "private": true, diff --git a/src/battery/battery.module.ts b/src/battery/battery.module.ts index 15e4035..4780367 100644 --- a/src/battery/battery.module.ts +++ b/src/battery/battery.module.ts @@ -1,10 +1,9 @@ import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; import { MeshModule } from '../mesh/mesh.module.js'; import { BatteryAlertService } from './battery.service.js'; @Module({ - imports: [ScheduleModule.forRoot(), MeshModule], + imports: [MeshModule], providers: [BatteryAlertService], exports: [BatteryAlertService], }) diff --git a/src/schedule-singleton.spec.ts b/src/schedule-singleton.spec.ts new file mode 100644 index 0000000..931a038 --- /dev/null +++ b/src/schedule-singleton.spec.ts @@ -0,0 +1,28 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const srcDir = fileURLToPath(new URL('.', import.meta.url)); + +function moduleFiles(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => + entry.isDirectory() + ? moduleFiles(join(dir, entry.name)) + : entry.name.endsWith('.module.ts') + ? [join(dir, entry.name)] + : [], + ); +} + +describe('ScheduleModule.forRoot single registration', () => { + // Regression guard (v0.21.7): each forRoot() call creates its own ScheduleExplorer that scans all providers, so a second call made every @Cron/@Interval job fire twice. + it('appears exactly once across all src/**/*.module.ts', () => { + const hits = moduleFiles(srcDir).flatMap((file) => + ( + readFileSync(file, 'utf8').match(/ScheduleModule\.forRoot\(/g) ?? [] + ).map(() => file.slice(srcDir.length)), + ); + expect(hits).toHaveLength(1); + }); +});