fix(cronmanager): improve cron scheduling and lifecycle handling; add wake/wakeCycle to promptly recalculate scheduling when jobs are added/removed/started/stopped, fix timeout handling, and update tests and deps

This commit is contained in:
2026-02-15 22:57:28 +00:00
parent 51943fad1c
commit 40d72de979
12 changed files with 4724 additions and 4810 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smarttime',
version: '4.2.0',
version: '4.2.1',
description: 'Provides utilities for advanced time handling including cron jobs, timestamps, intervals, and more.'
}

View File

@@ -1,8 +1,6 @@
import * as plugins from './smarttime.plugins.js';
import { CronManager } from './smarttime.classes.cronmanager.js';
import { CronParser } from './smarttime.classes.cronparser.js';
export type TJobFunction =
| ((triggerTimeArg?: number) => void)
| ((triggerTimeArg?: number) => Promise<any>);
@@ -24,6 +22,9 @@ export class CronJob {
* checks wether the cronjob needs to be executed
*/
public checkExecution(): number {
if (this.status === 'stopped') {
return this.nextExecutionUnix;
}
if (this.nextExecutionUnix === 0) {
this.getNextExecutionTime();
}

View File

@@ -1,6 +1,5 @@
import * as plugins from './smarttime.plugins.js';
import { CronJob, type TJobFunction } from './smarttime.classes.cronjob.js';
import { getMilliSecondsAsHumanReadableString } from './smarttime.units.js';
export class CronManager {
public executionTimeout: plugins.smartdelay.Timeout<void>;
@@ -8,13 +7,26 @@ export class CronManager {
public status: 'started' | 'stopped' = 'stopped';
public cronjobs = new plugins.lik.ObjectMap<CronJob>();
private cycleWakeDeferred: plugins.smartpromise.Deferred<void> | null = null;
constructor() {}
/**
* Resolves the current wake deferred, causing the sleeping cycle
* to immediately recalculate instead of waiting for its timeout.
*/
private wakeCycle() {
if (this.cycleWakeDeferred && this.cycleWakeDeferred.status === 'pending') {
this.cycleWakeDeferred.resolve();
}
}
public addCronjob(cronIdentifierArg: string, cronFunctionArg: TJobFunction) {
const newCronJob = new CronJob(this, cronIdentifierArg, cronFunctionArg);
this.cronjobs.add(newCronJob);
if (this.status === 'started') {
newCronJob.start();
this.wakeCycle();
}
return newCronJob;
@@ -23,6 +35,9 @@ export class CronManager {
public removeCronjob(cronjobArg: CronJob) {
cronjobArg.stop();
this.cronjobs.remove(cronjobArg);
if (this.status === 'started') {
this.wakeCycle();
}
}
/**
@@ -39,35 +54,39 @@ export class CronManager {
}
private async runCronCycle() {
this.executionTimeout = new plugins.smartdelay.Timeout(0);
do {
let nextRunningCronjob: CronJob;
while (this.status === 'started') {
// Create a fresh wake signal for this iteration
this.cycleWakeDeferred = new plugins.smartpromise.Deferred<void>();
// Check all cronjobs and find the soonest next execution
let soonestMs = Infinity;
for (const cronJob of this.cronjobs.getArray()) {
cronJob.checkExecution();
if (
!nextRunningCronjob ||
cronJob.getTimeToNextExecution() < nextRunningCronjob.getTimeToNextExecution()
) {
nextRunningCronjob = cronJob;
const msToNext = cronJob.getTimeToNextExecution();
if (msToNext < soonestMs) {
soonestMs = msToNext;
}
}
if (nextRunningCronjob) {
this.executionTimeout = new plugins.smartdelay.Timeout(
nextRunningCronjob.getTimeToNextExecution()
);
console.log(
`Next CronJob scheduled in ${getMilliSecondsAsHumanReadableString(
this.executionTimeout.getTimeLeft()
)}`
);
} else {
this.executionTimeout = new plugins.smartdelay.Timeout(1000);
console.log('no cronjobs specified! Checking again in 1 second');
}
await this.executionTimeout.promise;
} while (this.status === 'started');
};
// Sleep until the next job is due or until woken by a lifecycle event
if (soonestMs < Infinity && soonestMs > 0) {
this.executionTimeout = new plugins.smartdelay.Timeout(soonestMs);
await Promise.race([
this.executionTimeout.promise,
this.cycleWakeDeferred.promise,
]);
// Cancel the timeout to avoid lingering timers
this.executionTimeout.cancel();
} else if (soonestMs <= 0) {
// Job is overdue, loop immediately to execute it
continue;
} else {
// No jobs — wait indefinitely until woken by addCronjob or stop
await this.cycleWakeDeferred.promise;
}
}
this.cycleWakeDeferred = null;
}
/**
* stops all cronjobs
@@ -75,9 +94,10 @@ export class CronManager {
public stop() {
if (this.status === 'started') {
this.status = 'stopped';
this.executionTimeout.cancel();
} else {
console.log(`You tried to stop a CronManager that was not actually started.`);
if (this.executionTimeout) {
this.executionTimeout.cancel();
}
this.wakeCycle();
}
for (const cron of this.cronjobs.getArray()) {
cron.stop();