@whook/whook
Version:
Build strong and efficient REST web services.
151 lines (130 loc) • 4.31 kB
text/typescript
import cron, { type ScheduledTask, type TaskOptions } from 'node-cron';
import { type LogService } from 'common-services';
import { autoProvider, location } from 'knifecycle';
import { type WhookCronsDefinitionsService } from './CRONS_DEFINITIONS.js';
import { type WhookCronsHandlersService } from './CRONS_HANDLERS.js';
import { printStackTrace } from 'yerror';
import { type WhookMain } from '../types/base.js';
/* Architecture Note #2.14: Local cron runner
Whook allows you to run crons locally with the help
of this local cron runner. Best practice is to
export them and run it with some external runner
like cloud ones, systemd or crontab but for small
standalone projects or local development purposes
it may be sufficient.
*/
export type WhookCronRunnerService = undefined;
export type WhookCronRunnerOptions = Required<Pick<TaskOptions, 'timezone'>>;
export type WhookCronRunnerConfig = {
CRON_RUNNER_OPTIONS?: WhookCronRunnerOptions;
};
export type WhookCronRunnerDependencies = WhookCronRunnerConfig & {
APP_ENV: WhookMain['AppEnv'];
CRONS_HANDLERS: WhookCronsHandlersService;
CRONS_DEFINITIONS: WhookCronsDefinitionsService;
log: LogService;
};
export const DEFAULT_CRON_RUNNER_OPTIONS: WhookCronRunnerOptions = {
timezone: 'UTC',
};
async function initLocalCronRunner({
APP_ENV,
CRON_RUNNER_OPTIONS = DEFAULT_CRON_RUNNER_OPTIONS,
CRONS_HANDLERS,
CRONS_DEFINITIONS,
log,
}: WhookCronRunnerDependencies) {
const cronsNames = Object.keys(CRONS_DEFINITIONS);
const tasks: Record<
string,
{
task: ScheduledTask;
promises: Promise<void>[];
}
> = {};
log(
'warning',
`⌚ - Initializing the local cron runner (${cronsNames.length} crons).`,
);
for (const cronName of cronsNames) {
let index = 0;
for (const schedule of CRONS_DEFINITIONS[cronName].module.definition
.schedules) {
const taskName = `${cronName}-${index++}`;
const promises: Promise<void>[] = [];
if (
schedule.environments !== 'all' ||
!schedule.environments.includes(APP_ENV)
) {
log(
'debug',
`⏳ - Skipped "${cronName}" crons schedule "${schedule.rule}" since not enabled in environment "${APP_ENV}".`,
);
continue;
}
log(
'debug',
`⌚ - Scheduling the "${cronName}" cron with "${schedule.rule}" (task name: "${taskName}").`,
);
const task = cron.schedule(
schedule.rule,
() => {
const promise = (async () => {
log(
'debug',
`⌚ - Running the "${cronName}" cron with "${schedule.rule}" (task name: "${taskName}").`,
);
try {
// Run handler here and await
await CRONS_HANDLERS[cronName](
{
date: new Date().toISOString(),
body: schedule.body,
},
CRONS_DEFINITIONS[cronName].module.definition,
);
log(
'debug',
`✅ - Successfully ran the "${cronName}" cron with "${schedule.rule}" (task name: "${taskName}").`,
);
} catch (err) {
log('error', `❌ - The "${cronName}" cron produced an error.`);
log('error-stack', printStackTrace(err));
}
await Promise.resolve().then(() => {
promises.splice(promises.indexOf(promise), 1);
});
})();
promises.push(promise);
},
{
name: taskName,
...CRON_RUNNER_OPTIONS,
},
);
tasks[taskName] = {
task,
promises,
};
task.start();
}
}
return {
service: undefined,
dispose: async () => {
let runningCrons = 0;
for (const taskName of Object.keys(tasks)) {
tasks[taskName].task.stop();
runningCrons += tasks[taskName].promises.length;
}
log(
'warning',
`⌚ - Stopping the local cron runner (${runningCrons} crons running).`,
);
for (const taskName of Object.keys(tasks)) {
await Promise.all(tasks[taskName].promises);
}
},
};
}
export default location(autoProvider(initLocalCronRunner), import.meta.url);