Planifier des tâches
Enregistrer un job cron, le déclencher manuellement et éviter les doublons en multi-instance.
Toutes les tâches planifiées s'enregistrent dans core/cron. Les jobs vivent avec leur feature ou leur module, jamais dans le core.
Enregistrer un job
Regrouper les jobs d'un module dans un fichier dédié, appelé depuis register-server.js.
// @lrvo/module-blog — src/cron.server.js
import { schedule } from '@lrvo/core/cron';
import { publishScheduledPosts } from './db.server.js';
export function registerCronJobs() {
schedule('blog-publish-scheduled', '*/15 * * * *', async () => {
const count = await publishScheduledPosts();
if (count) done(`${count} billet(s) publié(s)`);
});
}// src/register-server.js
import { registerCronJobs } from './cron.server.js';
registerCronJobs();Dans un projet sans module, appeler schedule depuis l'initialisation de l'application.
Nommer un job
Le nom est la clé du registre : un second schedule sous le même nom remplace le premier.
Préfixer par le domaine du module pour éviter qu'un autre module écrase le job par accident : blog-publish-scheduled, pas publish.
Expressions
node-cron accepte 5 ou 6 champs. Avec six, le premier est celui des secondes.
| Expression | Fréquence |
|---|---|
*/15 * * * * | Toutes les 15 minutes |
0 9 * * * | Tous les jours à 9 h |
0 9 * * 1 | Tous les lundis à 9 h |
0 0 1 * * | Le 1er de chaque mois à minuit |
Le fuseau vient de LRVO_TIMEZONE, ou se précise par job :
schedule('daily-report', '0 9 * * *', handler, { timezone: 'Europe/Paris' });Valider une expression saisie par un utilisateur avant de l'enregistrer :
import { validate } from '@lrvo/core/cron';
if (!validate(expression)) return apiError('Bad Request', 'Expression cron invalide.');Exécuter immédiatement
schedule('sync-catalog', '0 * * * *', handler, { runOnInit: true });runOnInit déclenche le handler à l'enregistrement, donc à chaque démarrage du serveur. En développement, chaque rechargement compte. Réserver aux tâches idempotentes et rapides.
Déclencher à la main
Utile pour un bouton d'admin ou pour tester.
import { trigger } from '@lrvo/core/cron';
await trigger('blog-publish-scheduled');trigger lève si le job n'existe pas. Exposé par une route API, il doit être protégé par auth: 'admin' et une permission.
Inspecter
import { getJobs, getStatus, isRunning } from '@lrvo/core/cron';
getJobs(); // ['blog-publish-scheduled', 'daily-report']
isRunning('daily-report'); // true
getStatus();
// { 'daily-report': { schedule: '0 9 * * *', timezone: 'America/Toronto', registeredAt: '…' } }C'est la matière d'un widget de tableau de bord des tâches planifiées.
Gérer les erreurs
Une exception levée par un handler est interceptée et journalisée, jamais propagée. Le processus ne tombe pas.
✗ Cron daily-report: Connection timeoutConséquence : un job qui échoue silencieusement ne se voit que dans les journaux. Si l'échec doit être visible, l'enregistrer explicitement.
schedule('daily-report', '0 9 * * *', async () => {
try {
await sendReport();
await recordRun('daily-report', 'ok');
} catch (error) {
await recordRun('daily-report', 'failed', error.message);
throw error; // journalisé par le core
}
});Multi-instance
Chaque instance du serveur exécute ses propres jobs. Avec trois instances, un job horaire tourne trois fois par heure.
Pour une tâche qui ne doit s'exécuter qu'une fois, poser un verrou en base :
schedule('monthly-invoice', '0 2 1 * *', async () => {
const acquired = await acquireLock('monthly-invoice', { ttlMs: 10 * 60 * 1000 });
if (!acquired) return;
try {
await generateInvoices();
} finally {
await releaseLock('monthly-invoice');
}
});Le verrou vit en base, pas en mémoire : la mémoire n'est pas partagée entre les instances.
Hot-reload
Les jobs sont stockés sur globalThis, ce qui leur permet de survivre à l'invalidation du cache de modules de Next.js en développement. Un job enregistré deux fois remplace silencieusement l'ancien plutôt que de créer un doublon qui s'exécuterait en double. Voir Registres et singletons.
Arrêter
import { stop, stopAll } from '@lrvo/core/cron';
stop('daily-report'); // true si le job existait
stopAll();stopAll s'utilise en test, pas en production.