mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
feat(server): add the schedule wiring
This commit is contained in:
@@ -21,6 +21,11 @@ export class RuntimeEnvService {
|
||||
return readEnv().demo.enabled;
|
||||
}
|
||||
|
||||
/** True only under exactly NODE_ENV='test' (case-sensitive, like the :memory: DB pick). */
|
||||
isTest(): boolean {
|
||||
return readEnv().app.isTest;
|
||||
}
|
||||
|
||||
nodeEnv(): string | undefined {
|
||||
return readEnv().app.nodeEnv;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import { AddonsModule } from './addons/addons.module';
|
||||
import { AuditModule } from './audit/audit.module';
|
||||
import { PermissionsModule } from './permissions/permissions.module';
|
||||
import { PluginsModule } from './plugins/plugins.module';
|
||||
import { SchedulingModule } from './scheduling/scheduling.module';
|
||||
import { McpModule } from '@trek/nest-mcp';
|
||||
import { trekMcpAccessPolicy, trekMcpValidateAccess } from '../mcp/nest-mcp-policy';
|
||||
import { TrekExceptionFilter } from './common/trek-exception.filter';
|
||||
@@ -66,7 +67,7 @@ import { IdempotencyInterceptor } from './common/idempotency.interceptor';
|
||||
* migrated.
|
||||
*/
|
||||
@Module({
|
||||
imports: [AppConfigModule, DatabaseModule, RealtimeModule, McpModule.forRoot({ accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess }), HealthModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, GeoModule, MapsModule, PlaceEnrichmentModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, DayNotesModule, AccommodationsModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, AuditModule, PermissionsModule, PluginsModule, BookingImportModule, ReservationImportModule, LlmParseModule],
|
||||
imports: [AppConfigModule, DatabaseModule, RealtimeModule, SchedulingModule, McpModule.forRoot({ accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess }), HealthModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, GeoModule, MapsModule, PlaceEnrichmentModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, DayNotesModule, AccommodationsModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, AuditModule, PermissionsModule, PluginsModule, BookingImportModule, ReservationImportModule, LlmParseModule],
|
||||
providers: [
|
||||
// Default-deny: a route is authenticated unless it carries @Public() or
|
||||
// @OptionalAuth(), or declares its own @UseGuards chain. Protection used to
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable, type OnApplicationShutdown } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { CronJob } from 'cron';
|
||||
import { readEnv } from '../../app-config';
|
||||
import { RuntimeEnvService } from '../app-config/runtime-env.service';
|
||||
|
||||
/**
|
||||
* The one way TREK code schedules a cron. Job providers register here from
|
||||
* their onApplicationBootstrap instead of touching cron/@nestjs/schedule
|
||||
* directly, because this is where two process-wide invariants live:
|
||||
*
|
||||
* - Tests never tick. buildApp() is shared with the integration/e2e harness,
|
||||
* so onApplicationBootstrap runs on every suite boot; register() refuses to
|
||||
* schedule when NODE_ENV is 'test' (readEnv().app.isTest, read at register
|
||||
* time — tests/setup.ts pins NODE_ENV='test' before any app boots).
|
||||
* Providers gate their boot sweeps and banner logs behind isEnabled() for
|
||||
* the same reason.
|
||||
* - Shutdown stops everything. @nestjs/schedule's orchestrator only closes
|
||||
* jobs declared via @Cron decorators, never registry-added ones, so this
|
||||
* service stops its own in onApplicationShutdown — fired by nestApp.close()
|
||||
* in prod and by the suites' afterAll close() alike.
|
||||
*
|
||||
* The timezone is resolved per register() call (readEnv().app.tz || 'UTC'),
|
||||
* keeping the retired scheduler's read-at-schedule-time behavior: a dynamic
|
||||
* job that re-registers picks up a changed TZ. timezone: 'none' schedules in
|
||||
* the server-local zone (the demo reset and the trek-photo sweep always ran
|
||||
* that way).
|
||||
*/
|
||||
@Injectable()
|
||||
export class CronRegistrarService implements OnApplicationShutdown {
|
||||
/** Names this service registered — the registry may hold other owners' jobs. */
|
||||
private readonly names = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private readonly registry: SchedulerRegistry,
|
||||
private readonly runtimeEnv: RuntimeEnvService,
|
||||
) {}
|
||||
|
||||
/** False under NODE_ENV=test — the single gate keeping the suites timer-free. */
|
||||
isEnabled(): boolean {
|
||||
return !this.runtimeEnv.isTest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a cron. An existing job under the same name is stopped and
|
||||
* replaced, so a dynamic job restarts by calling register() again.
|
||||
* Returns false without scheduling anything when isEnabled() is false.
|
||||
*/
|
||||
register(
|
||||
name: string,
|
||||
expression: string,
|
||||
onTick: () => void | Promise<void>,
|
||||
opts?: { timezone?: 'app' | 'none' },
|
||||
): boolean {
|
||||
this.unregister(name);
|
||||
if (!this.isEnabled()) return false;
|
||||
const timeZone = opts?.timezone === 'none' ? undefined : readEnv().app.tz || 'UTC';
|
||||
const job = CronJob.from({ cronTime: expression, onTick, start: true, timeZone });
|
||||
this.registry.addCronJob(name, job);
|
||||
this.names.add(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Stop and drop a job by name. A name that was never registered is a no-op. */
|
||||
unregister(name: string): void {
|
||||
if (!this.names.has(name)) return;
|
||||
this.registry.deleteCronJob(name); // deleteCronJob stops the job before dropping it
|
||||
this.names.delete(name);
|
||||
}
|
||||
|
||||
get jobCount(): number {
|
||||
return this.names.size;
|
||||
}
|
||||
|
||||
onApplicationShutdown(): void {
|
||||
for (const name of [...this.names]) {
|
||||
this.unregister(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { AppConfigModule } from '../app-config/app-config.module';
|
||||
import { CronRegistrarService } from './cron-registrar.service';
|
||||
|
||||
/**
|
||||
* Shared cron infrastructure — deliberately NOT @Global. The e2e suites boot
|
||||
* partial module graphs (DatabaseModule + one domain module), where a global
|
||||
* module absent from the compiled tree resolves nothing: every module that
|
||||
* owns a job imports SchedulingModule explicitly, so its TestingModule keeps
|
||||
* compiling. ScheduleModule.forRoot() lives here (not app.module.ts) for the
|
||||
* same reason, and AppConfigModule is imported so RuntimeEnvService resolves
|
||||
* in those partial graphs too.
|
||||
*
|
||||
* No @Cron decorators exist anywhere in the tree — jobs go through
|
||||
* CronRegistrarService so the test gate and shutdown semantics apply. Keep it
|
||||
* that way: a decorator-declared job would be mounted by the schedule
|
||||
* explorer on every harness boot, gate or no gate.
|
||||
*/
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot(), AppConfigModule],
|
||||
providers: [CronRegistrarService],
|
||||
exports: [CronRegistrarService],
|
||||
})
|
||||
export class SchedulingModule {}
|
||||
Reference in New Issue
Block a user