Réagir à un événement
Envoyer un texto à chaque rendez-vous, ajouter une ligne à un tableur à chaque soumission, prévenir un autre outil quand une facture est payée, sans toucher au module qui annonce l'opération.
Les modules annoncent ce qui se passe chez eux : une soumission reçue, un article publié, un rendez-vous pris, une facture payée. Ce guide montre comment y réagir depuis le projet ou depuis un module, et comment un module annonce ses propres événements. L'API complète est dans Événements.
Trouver l'événement
La page Événements de l'admin (/admin/events, permission events.admin) liste chaque événement déclaré par le core et les modules installés, avec la phrase qui dit quand il part et ses abonnés. La page de référence de chaque module donne aussi la charge de ses événements.
Une charge porte des identifiants, pas les données elles-mêmes :
// booking.appointment.created
{ appointmentId: 42, serviceId: 3, staffId: 7, status: 'confirmed' }L'abonné relit l'objet par son identifiant, avec les fonctions serveur du module. Il obtient ainsi l'état à jour, et aucune donnée personnelle ne reste stockée avec l'événement.
S'abonner depuis le projet
Les abonnements du projet vivent dans events.js, à la racine. instrumentation.js le passe à initializeFraza(), qui l'appelle au démarrage, après les modules. Le CLI ne le lit pas : un compte créé par fraza-db reset --admin ou par fraza-db seed, comme un événement qu'un seed annonce, n'arrive à aucun abonné.
// events.js
import { sendSms } from './lib/sms.server.js';
export default function events({ on }) {
on('booking.appointment.created', async (event) => {
await sendSms(event.payload.appointmentId);
}, { durable: true });
}// instrumentation.js, dans register()
const { default: events } = await import('./events.js');
await initializeFraza({ modules, theme, permissions, events });on reçu par events.js enregistre l'abonnement sous la source 'project'. Un projet créé avant les événements n'a ni events.js ni la ligne dans instrumentation.js : npx fraza-doctor --fix corrige instrumentation.js sur place et crée events.js vide.
Un abonnement ne se fait jamais depuis app/fraza.extensions.js ni depuis une page : ces fichiers sont chargés au premier rendu, pas au démarrage, et la file des livraisons tourne avant.
Choisir entre immédiat et durable
Durable (durable: true) pour tout ce qui sort du serveur : un courriel, un texto, un tableur, une API, un webhook. L'abonné est appelé une fois l'opération enregistrée, hors de la requête qui l'a faite. S'il échoue, il est relancé : 1 min plus tard, puis 2, puis 4…, jusqu'à 12 essais, environ une journée. Une livraison abandonnée reste dans la page Événements avec son erreur, et se relance d'un clic.
Immédiat (le défaut) pour écrire dans la même base, dans la transaction de l'opération : une ligne dans une file, un compteur, un état qui doit exister si et seulement si l'opération existe. L'abonné reçoit le client de la transaction :
on('forms.submission.created', async (event, { client }) => {
await client.query(
'INSERT INTO acme_summaries (submission_id) VALUES ($1) ON CONFLICT DO NOTHING',
[event.payload.submissionId],
);
});Un abonné immédiat qui échoue est isolé : ce qu'il a écrit est annulé, l'erreur est journalisée, et le formulaire est quand même enregistré. Il ne doit jamais attendre un service externe : la soumission attendrait avec lui.
required: true inverse ce choix : l'erreur de l'abonné annule toute l'opération. C'est réservé aux états qui ne peuvent pas diverger de l'opération, comme une licence qui suit le paiement de son abonnement.
Se protéger des doublons
Une livraison durable arrive au moins une fois. Un serveur qui s'arrête juste après la réussite de l'abonné, avant d'avoir noté la livraison, rappelle l'abonné plus tard. event.id est le même à chaque essai : s'en servir comme clé.
on('billing.invoice.paid', async (event) => {
await crm.createTask({ externalId: event.id, invoiceId: event.payload.invoiceId });
}, { durable: true });Un service tiers qui accepte une clé d'idempotence la reçoit ; sinon, enregistrer event.id avec ce que l'abonné a fait, et sauter un événement déjà vu.
S'abonner depuis un module
Un module s'abonne dans son register-server.js, sous son nom de paquet :
// src/register-server.js
import { on } from '@fraza/core/events';
import { SOURCE } from './source.js';
on('forms.submission.created', async (event, { client }) => {
await queueSummary(client, event.payload.submissionId);
}, { source: SOURCE });Un module qui écoute un module facultatif s'abonne même quand l'autre est absent : l'abonnement attend simplement des événements qui ne viennent pas, et le démarrage le signale par un avertissement.
Annoncer un événement depuis un module
Déclarer l'événement dans le manifeste, puis l'annoncer dans la transaction qui enregistre l'opération :
// src/index.js
export const manifest = {
name: '@acme/module-shop',
// …
events: [
{
name: 'shop.order.paid',
description: 'Une commande a été payée.',
payload: { orderId: 'integer', total: 'number' },
},
],
};// src/orders.server.js
import { transaction } from '@fraza/core/database';
import { emit } from '@fraza/core/events';
export async function markPaid(orderId, actor) {
return transaction(async (client) => {
const order = await setPaid(client, orderId);
await emit('shop.order.paid', { orderId: order.id, total: order.total }, { client, actor });
return order;
});
}Le nom commence par le nom court du module (shop pour module-shop). Hors de la transaction, un événement pourrait annoncer une opération annulée juste après. En développement, une charge qui ne correspond pas à la déclaration lève une erreur. Le détail est dans Créer un module.
Suivre les livraisons
La page Événements de l'admin montre les livraisons durables : abandonnées par défaut, en attente, livrées. Le détail d'une livraison donne l'erreur, la charge et qui a déclenché l'opération. Relancer remet une livraison abandonnée en file avec tous ses essais.
La tuile Santé du tableau de bord affiche le nombre de livraisons abandonnées. Les événements terminés sont purgés après 7 jours (FRAZA_EVENTS_RETENTION_DAYS).