Nodefony 10.0.0-beta.5
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Cadence adaptative et contre-pression — quand un client ne suit plus

stable@nodefony/realtimemis à jour 2026-10-07

Un serveur temps réel produit souvent plus vite qu’un client ne consomme. Nodefony y répond par deux étages indépendants : le client ralentit lui-même la cadence des canaux d’état qu’il écoute (AIMD), et le transport refuse d’empiler au-delà d’un seuil, puis coupe un client qui n’absorbe plus rien (contre-pression, fermeture 1013). Aucun code ne relie les deux : c’est la mesure qui les fait se rencontrer. Cette page explique chaque étage, montre leurs courbes calculées par le vrai code, et dit où les observer en production.

📍 Documentation › @nodefony/realtime › Cadence et contre-pression

🧠 Schéma général#

Deux étages, deux endroits du code, deux signaux. Les sondes, qui les observent, viennent plus bas.

📖 Lexique#

Terme Développé / traduction Ce que c’est, en une ligne
AIMD Additive Increase / Multiplicative Decrease Règle de régulation de TCP : on accélère prudemment par petits pas, on ralentit franchement dès que ça coince.
Cadence intervalle de publication d’un canal Le temps entre deux valeurs publiées (1000 ms = une valeur par seconde).
Canal cadencé base:<ms> Un canal dont le nom porte la cadence : nodefony:orm:health:2000.
Famine starvation Le client attend une trame bien plus longtemps que la cadence promise.
Bande morte dead band / hystérésis Zone entre « sain » et « famine » où l’on ne change rien, pour éviter d’osciller.
Contre-pression backpressure Refuser d’accumuler pour un destinataire qui ne suit pas, au lieu de stocker sans limite.
File d’envoi bufferedAmount Les octets acceptés par la socket mais pas encore partis sur le réseau.
Solde de refus _nfDropStreak Compteur par connexion : +1 par trame refusée, −1 par trame envoyée.
1013 Try Again Later Code de fermeture WebSocket : « reviens plus tard ». Le client le traite comme transitoire et se reconnecte.
Canal d’état / canal d’événements latest-wins / every-message-counts État : seule la dernière valeur compte. Événements : chaque message compte.
Sonde probe Lecture instantanée des compteurs du hub, exposée par HTTP et par un canal de santé.
Abonner avant de couper make-before-break Changer de canal en s’abonnant au nouveau avant de quitter l’ancien : aucun trou dans le flux.

Qu’est-ce que le problème, concrètement ?#

L’image : un robinet qui remplit un verre. Si le robinet coule plus vite que l’on boit, le verre déborde. On peut fermer un peu le robinet (c’est le rôle de la cadence adaptative), et il faut de toute façon un trop-plein qui empêche l’inondation (c’est la contre-pression).

En temps réel, le « verre » est la file d’envoi de la socket côté serveur. Un client lent — réseau mobile, onglet en arrière-plan, machine saturée — la laisse grossir. Sans garde :

❗ Important

Ce n’est pas une question de débit réseau mesuré. Nodefony ne mesure jamais la bande passante d’un client : le client observe les intervalles d’arrivée des trames, le serveur observe sa file d’envoi. Ce sont deux mesures locales, simples et sans coût, qui suffisent.

La vision Nodefony — deux étages qui ne se parlent pas#

Nodefony empile deux mécanismes qui ne partagent aucun code et aucun message. Chacun agit sur ce qu’il voit lui-même :

Étage 1 — cadence adaptative Étage 2 — contre-pression
Où il vit dans le navigateur (AdaptiveRate, bindAdaptiveChannel) dans le serveur, au transport (decideSend)
Ce qu’il mesure l’écart entre deux trames reçues la file d’envoi bufferedAmount de la socket
Ce qu’il fait se réabonne à un canal plus lent ou plus rapide envoie, jette la trame, ou ferme en 1013
Sa portée un canal d’état chez un client toutes les trames d’une connexion
Quand il agit avant la saturation : il prévient à la saturation : il protège
Activé par défaut non : chaque écran l’active (hook ou option) oui : 4 Mio, refus, fermeture après un solde de 1000
Ce qu’il garantit une cadence soutenable pour ce client une mémoire bornée pour le serveur

Le point qui fait comprendre tout le reste : la contre-pression jette des trames, donc elle allonge les écarts que le client mesure ; le client y voit une famine et ralentit ; le serveur produit moins pour lui ; la file se vide. Personne n’a programmé ce dialogue : il découle de la mesure. Voir Comment les deux étages se rencontrent.

ℹ️ Note

Ni l’un ni l’autre n’appartient à JSON-RPC. Le protocole transporte des requêtes, des réponses et des notifications ; la notion de canal, de cadence et de file est propre à Nodefony. Le standard JSON-RPC 2.0 n’en dit rien, et Nodefony n’y ajoute aucun champ.

🚀 Démarrage rapide#

Le besoin vécu : ton tableau de bord affiche la charge du serveur, publiée chaque seconde. Sur le réseau d’un client en déplacement, l’écran prend du retard et la mémoire du serveur grossit. Tu veux que chaque écran tienne la cadence qu’il peut, et que le serveur ne retienne jamais un client mort.

Le contrôleur — fichier créé#

Le serveur publie à la cadence que le nom du canal demande, bornée pour se protéger. Le décorateur @RealtimeChannel ne reconnaît que des noms exacts : une famille cadencée (atelier:load, atelier:load:2000, …) passe donc par la surcharge de createRealtimeChannel(), la voie que le framework prévoit pour elle.

// modules/atelier/nodefony/controllers/AtelierController.ts
import { performance } from "node:perf_hooks";
import { controller, route } from "@nodefony/framework";
import { RealtimeController } from "@nodefony/realtime";
import type { RealtimePublish } from "@nodefony/realtime";
import type { ContextType } from "@nodefony/http";
import { isRateChannel, parseRate } from "nodefony";

/** Le canal de base, partagé avec le client. */
export const ATELIER_LOAD = "atelier:load";

/** Le serveur garde la main : jamais plus vite que 250 ms, jamais plus lent que 60 s. */
const BOUNDS = { default: 1000, min: 250, max: 60000 };

@controller("/atelier")
class AtelierController extends RealtimeController {
  constructor(context: ContextType) {
    super("atelier", context);
  }

  @route("atelier-realtime", {
    path: "/realtime",
    requirements: { methods: ["WEBSOCKET"] },
  })
  async realtime(message: string | Buffer | null): Promise<void> {
    this.handleRealtime(message);
  }

  /**
   * Sert `atelier:load` ET ses variantes `atelier:load:<ms>`.
   *
   * Le hub appelle cette fabrique au PREMIER abonné d'un canal et garde sa
   * fonction de nettoyage pour le DERNIER : un seul ticker par cadence, partagé
   * par tous ses abonnés. Il survit à la connexion qui l'a créé — il ne doit
   * donc rien capturer de `this.context`.
   */
  override createRealtimeChannel(
    channel: string,
    publish: RealtimePublish,
  ): (() => void) | null {
    if (!isRateChannel(channel, ATELIER_LOAD)) return null;
    const ms = parseRate(channel, ATELIER_LOAD, BOUNDS);
    // Utilisation de la boucle d'événements depuis le tick précédent, de 0 à 1 :
    // une mesure que Node tient déjà, identique sur Linux, macOS et Windows.
    let since = performance.eventLoopUtilization();
    const timer = setInterval(() => {
      const delta = performance.eventLoopUtilization(since);
      since = performance.eventLoopUtilization();
      publish(channel, { ts: Date.now(), load: delta.utilization });
    }, ms);
    timer.unref(); // le ticker ne retient pas l'arrêt du processus
    return () => clearInterval(timer);
  }
}

export default AtelierController;

Le client — fichier créé#

Le hook s’abonne à 1 s et laisse l’AIMD reculer ou remonter. Il rend la cadence effective, à afficher en badge.

// frontend/src/useAtelierLoad.ts
import { useNodefonyAdaptiveChannelData } from "nodefony/react";

export interface AtelierLoad {
  ts: number;
  load: number;
}

/** La charge de l'atelier, à la cadence que CE client peut tenir. */
export function useAtelierLoad(): { load: number | null; everyMs: number } {
  const { data, intervalMs } = useNodefonyAdaptiveChannelData<AtelierLoad>(
    "atelier:load",
    1000,
  );
  return { load: data?.load ?? null, everyMs: intervalMs };
}

La configuration — fichier modifié#

Deux réglages, et le second est celui qu’on oublie :

// nodefony/config/http.ts — le fragment du module, importé par le manifeste
import type { IHttpConfigInput } from "@nodefony/http";

export const httpConfig = () =>
  ({
    websocket: { maxBackpressure: 1048576, backpressureCloseAfterDrops: 200 },
    websocketSecure: {
      maxBackpressure: 1048576,
      backpressureCloseAfterDrops: 200,
    },
  }) satisfies IHttpConfigInput;
// nodefony.config.ts — extrait
import { defineConfig, use } from "nodefony";
import { httpConfig } from "./nodefony/config/http";

export default defineConfig(() => ({
  modules: [
    use("@nodefony/http", httpConfig()),
    "@nodefony/framework",
    use("@nodefony/security", {
      // Préfixe : garde atelier:load ET atelier:load:2000, atelier:load:4000…
      realtimeChannels: [{ pattern: "atelier:", authenticated: true }],
    }),
    use("@nodefony/realtime", { backplane: { driver: "loopback" } }),
    "@mon-app/atelier",
  ],
}));

Ce qu’on observe#

Situation Ce qui se passe
Réseau sain everyMs reste à 1000 ; le client est abonné à atelier:load (le défaut ne porte pas de suffixe).
Réseau dégradé (trames toutes les 4–5 s) everyMs passe à 2000 puis 4000 ; le client est abonné à atelier:load:4000.
Retour à la normale everyMs redescend d’un cran tous les 4 échantillons sains : 2000, puis 1000.
Client qui ne lit plus du tout file à 1 Mio, trames refusées, puis fermeture 1013 après un solde de 200 ; reconnexion automatique.
Deux écrans à la même cadence un seul ticker serveur : le canal atelier:load:2000 est partagé et compté par référence.
Un client anonyme demande atelier:load:250 refusé : la règle de préfixe exige une identité, quelle que soit la cadence demandée.
# La sonde du hub, sans WebSocket : clients lents et refus cumulés.
curl -sk https://127.0.0.1:5152/nodefony/realtime/api/health | jq '.backpressure'

🏗️ Étage 1 — la cadence adaptative (AIMD)#

La cadence vit dans le nom du canal#

Un canal d’état cadencé s’appelle base (cadence par défaut) ou base:<ms> (cadence explicite). Le client fabrique le nom avec rateChannel() (channelRate.ts#rateChannel), le serveur le relit et le borne avec parseRate() (channelRate.ts#parseRate). Une même fonction pour les deux bords : la convention ne peut pas diverger.

Conséquences voulues :

⚠️ Attention

Une politique de canal posée sur le nom exact ne garde pas ses cadences. Le registre des politiques s’indexe par nom : protéger atelier:load laisse atelier:load:2000 ouvert, et le hub le signale par un avertissement. Garder une famille cadencée, c’est poser une règle par préfixe (realtimeChannels, configuration de @nodefony/security) ou par motif (atelier:load:*).

Ce que le client mesure#

À chaque trame reçue, AdaptiveRate.noteFrame() (AdaptiveRate.ts#AdaptiveRate.noteFrame) compare l’écart depuis la trame précédente à la cadence courante. Trois zones, et une seule décision par trame :

Zone Condition (défauts) Décision
Famine écart > 1,8 × cadence un cran plus lent, tout de suite (Multiplicative Decrease)
Sain écart ≤ 1,25 × cadence compte un bon échantillon ; au 4ᵉ d’affilée, un cran plus rapide
Bande morte entre les deux ne change rien, et remet à zéro le compte des bons échantillons

Les seuils et la fenêtre sont des options (starvationFactor, healthyFactor, recoveryWindow), avec ces défauts (AdaptiveRate.ts#AdaptiveRate.constructor).

💡 Astuce

La bande morte est ce qui empêche l’oscillation : un réseau « moyen » ne fait ni monter ni descendre la cadence. Sans elle, un écart de 1,5 fois la cadence ferait basculer l’écran toutes les quelques secondes.

Ralentir vite, accélérer prudemment#

Changer de cadence sans trou#

L’image : on change de voie sur l’autoroute en s’insérant dans la nouvelle avant de quitter l’ancienne, jamais l’inverse. bindAdaptiveChannel() (AdaptiveRate.ts#bindAdaptiveChannel) fait pareil : il s’abonne au nouveau canal, puis quitte l’ancien, dans le même geste (applyDecision(), AdaptiveRate.ts#bindAdaptiveChannel).

Le gestionnaire de l’écran reçoit les trames des deux canaux sans interruption : la page ne voit qu’un flux dont la cadence change.

La courbe — le vrai code, sur un scénario#

Le scénario : cadence désirée 1 s ; de 20 s à 70 s, le client met 4,5 s à absorber chaque trame (réseau saturé), puis redevient rapide. La courbe est calculée en faisant tourner AdaptiveRate lui-même ; un test la rejoue et échoue si le code ne la produit plus (voir Tests).

Ce que la courbe raconte, décision par décision :

Instant Décision Pourquoi
24 s 1 s → 2 s premier écart mesuré de 4,5 s, au-delà de 1,8 s : famine
33 s 2 s → 4 s 4,5 s dépasse encore 3,6 s (1,8 × 2 s) : famine
55 s 4 s → 2 s à 4 s, l’écart (4,5 s) est sain (≤ 5 s) : après 4 bons échantillons, l’AIMD tente de remonter
64 s 2 s → 4 s la tentative échoue : famine, retour au cran sûr
84 s 4 s → 2 s la dégradation est finie depuis 70 s : 4 écarts sains
94 s 2 s → 1 s 4 écarts sains de plus : retour à la cadence désirée

La tentative de 55 s n’est pas un défaut : c’est ainsi qu’AIMD découvre que le réseau s’est rétabli. Il sonde prudemment, recule s’il s’est trompé, et ne le paie que de quelques secondes.

Réservé aux canaux d’état#

⚠️ Attention

N’active jamais la cadence adaptative sur un canal d’événements. Ralentir un canal d’état perd des valeurs intermédiaires sans conséquence : la température de 10 h 00 min 03 s est remplacée par celle de 10 h 00 min 04 s. Sur un flux d’événements (ORDER_CREATED, PAYMENT_RECEIVED, lignes de journal), chaque message compte : ces canaux se regroupent (le pont syslog de Studio coalesce), ils ne se déciment pas.

🏗️ Étage 2 — la contre-pression du transport#

La file d’envoi et le seuil#

Quand le serveur envoie une trame, la socket l’accepte et la place dans sa file d’envoi : bufferedAmount compte les octets qui attendent le réseau. Un client qui ne lit plus fait grossir cette file sans fin. Nodefony la plafonne à maxBackpressure, 4 Mio par défaut (@nodefony/http/nodefony/config/config.ts#websocketSchema).

La décision est prise trame par trame, par une seule fonction : decideSend() (wsBackpressure.ts#decideSend). Elle sert à la fois le send()/broadcast() d’un contexte WebSocket de @nodefony/http et chaque connexion temps réel (WsConnectionTransport.send(), WsConnectionTransport.ts#WsConnectionTransport.send). Une seule règle, deux usages.

Sous le seuil, le chemin est nominal : une lecture de bufferedAmount, aucune allocation. Le coût n’apparaît que pour un client déjà en difficulté.

Le solde de refus — pourquoi pas un second seuil, pourquoi pas une série#

Deux idées naturelles échouent, et le code dit pourquoi :

D’où le solde : +1 par refus, −1 par envoi. Un pic passager redescend tout seul ; un client qui refuse plus qu’il n’accepte monte jusqu’au seuil de fermeture. C’est _nfDropStreak, tenu sur la socket elle-même.

Les courbes — le vrai code, sur deux clients#

Le scénario : le serveur pousse 100 trames de 32 Kio par seconde ; le client vide sa file à 8 Mo/s, puis tombe à 1 Mo/s à partir de 2 s. Défauts de production : seuil 4 Mio, refus, fermeture à un solde de 1000. Les valeurs sont calculées par decideSend() lui-même.

La file d’envoi plafonne au seuil — la contre-pression tient la mémoire, quelle que soit la durée :

Le solde distingue un client mort d’un pic passager. Deux clients, même début : ils ralentissent à 2 s. Le premier ne se rétablit jamais, le second se rétablit à 9 s.

drop ou close — choisir en situation#

Le besoin vécu La politique Le comportement
Tableaux de bord, télémétrie, positions : un client lent doit rester connecté drop (défaut) + backpressureCloseAfterDrops il perd des trames pendant le pic, il est coupé seulement s’il ne récupère pas
Flux où une trame perdue rend la suite fausse (rejeu, synchronisation incrémentale) close coupé au premier dépassement ; il se reconnecte et resynchronise de zéro
Ne jamais couper (environnement maîtrisé, débogage) drop + backpressureCloseAfterDrops: 0 la file reste bornée, la connexion ne ferme jamais

⚠️ Attention

drop ne trie pas les trames. Au-delà du seuil, decideSend() jette la trame qui se présente, quelle qu’elle soit : valeur d’un canal d’état, événement métier, ou réponse à une requête RPC. Une requête dont la réponse a été jetée expire côté client. La contre-pression protège le serveur ; elle ne garantit la livraison de rien. Un flux où chaque message compte a besoin d’un accusé de réception et d’une reprise applicatifs, ou de la politique close.

Ce que voit le client#

Le client n’apprend jamais les seuils : il ne reçoit que leur effet.

🔌 Comment les deux étages se rencontrent#

Aucune ligne de code ne relie la contre-pression du serveur à la cadence du client. Ils se rencontrent par la mesure :

Une seule trame jetée sur un canal cadencé suffit à doubler l’écart mesuré, donc à faire ralentir le client. La contre-pression envoie un signal au client sans lui envoyer un seul octet.

📡 Les sondes — voir le mécanisme en production#

Les sondes observent, elles ne décident rien. Elles répondent à « mes clients suivent-ils ? » sans activer de trace.

Ce que la sonde du hub expose#

RealtimeHub.probe() (RealtimeHub.ts#RealtimeHub.probe) parcourt les connexions et rend un bloc backpressure :

Champ Ce qu’il dit Lecture
maxBufferedAmount la pire file d’envoi, en octets proche de maxBackpressure : un client est au plafond
totalBufferedAmount la somme des files du processus la mémoire d’envoi réellement retenue
slowConsumers les connexions dont la file atteint slowConsumer.bytes (1 Mio par défaut) non nul = au moins un client prend du retard
drops les trames refusées, cumulées qui augmente = la contre-pression agit en ce moment

❗ Important

slowConsumer.bytes compte, il ne freine pas. C’est le réglage de la sonde (@nodefony/realtime), pas de la contre-pression (@nodefony/http). Le baisser fait apparaître plus de clients lents dans les écrans, sans en couper aucun.

Où la lire#

Le détail champ par champ et le flux de santé : Observabilité de la socket.

⚙️ Configuration#

Réglage Paquet Défaut Effet
maxBackpressure @nodefony/http 4 Mio file d’envoi au-delà de laquelle la politique s’applique ; 0 = coupé
backpressurePolicy @nodefony/http drop drop jette la trame ; close ferme au premier dépassement
backpressureCloseAfterDrops @nodefony/http 1000 solde de refus qui déclenche la fermeture 1013 ; 0 = jamais
slowConsumer.bytes @nodefony/realtime 1 Mio seuil de comptage de la sonde ; n’agit sur rien
intervalMs (hook / client) nodefony — cadence désirée ; l’AIMD ne descend jamais en dessous
ladder, maxMs nodefony ×2, 60 s l’échelle des cadences possibles
starvationFactor, healthyFactor, recoveryWindow nodefony 1,8 · 1,25 · 4 les seuils de famine, de santé et la fenêtre de reprise
enabled nodefony true false = abonnement fixe, sans mesure ni chien de garde

❗ Important

Deux serveurs, deux sections : websocket (ws://) et websocketSecure (wss://). Régler l’un ne règle pas l’autre. Une application servie en wss qui ne règle que websocket garde les défauts, sans que rien ne le signale.

La table complète des réglages du module et leurs schémas : Configuration.

📜 Normes appliquées#

Norme Ce qu’elle dit Ce que fait Nodefony
RFC 6455 §7.4 — codes de fermeture une fermeture porte un code ; la plage 1000-2999 est réservée et enregistrée à l’IANA ferme en 1013 (backpressure)
Registre IANA des codes de fermeture WebSocket 1013 = Try Again Later : condition temporaire du serveur le client le traite comme transitoire et se reconnecte
RFC 5681 — contrôle de congestion TCP augmentation additive, diminution multiplicative inspiration de l’AIMD de cadence, appliquée à des abonnements et non à des octets
JSON-RPC 2.0 requêtes, réponses, notifications aucune notion de cadence ni de file : tout ce mécanisme est propre à Nodefony

⚠️ Pièges#

Symptôme Cause Correction
Des événements métier manquent chez un client lent la politique drop jette toute trame au-delà du seuil, événements compris accusé de réception et reprise applicatifs, ou politique close pour ce flux
Une requête RPC expire sans erreur serveur sa réponse a été jetée par la contre-pression surveiller drops dans la sonde ; serrer la cadence des canaux d’état de ce client
atelier:load est gardé, atelier:load:2000 répond à un anonyme la politique vise le nom exact ; les variantes cadencées n’en héritent pas règle par préfixe dans realtimeChannels ({ pattern: "atelier:", authenticated: true })
La cadence adaptative ne bouge jamais le serveur ne sert que le nom exact (@RealtimeChannel), pas base:<ms> surcharger createRealtimeChannel() avec isRateChannel() et parseRate()
Un client demande 100 ms et reçoit autre chose parseRate() borne la cadence demandée c’est voulu (anti-déni de service) : ajuster les bornes du contrôleur
Le réglage de contre-pression semble ignoré seule la section websocket est réglée, l’application est servie en wss régler aussi websocketSecure
slowConsumers est non nul mais personne n’est coupé slowConsumer.bytes (1 Mio) est un seuil de comptage, sous maxBackpressure (4 Mio) normal : la sonde prévient avant que la contre-pression agisse
Un client est coupé au premier ralentissement backpressurePolicy: "close" ferme dès le premier dépassement revenir à drop avec un solde de fermeture
L’écran oscille entre deux cadences réseau à la limite : l’AIMD sonde, recule, sonde normal et borné ; élargir recoveryWindow pour sonder moins souvent
La cadence adaptative est activée sur un journal un canal d’événements a été déclaré adaptatif revenir à un abonnement fixe ; regrouper les messages côté serveur

🧪 Tests#

Suite Ce qui est prouvé
AdaptiveRate.test.ts (nodefony) la machine pure : échelle ×2, première trame sans décision, famine immédiate, reprise après N bons échantillons, chien de garde
channelRate.test.ts (nodefony) le nom cadencé : canal nu pour le défaut, suffixe sinon, bornage côté serveur
RealtimeNotice.test.ts (nodefony) 1013 fait partie des fermetures transitoires qui déclenchent la reconnexion
wsBackpressure.test.ts (@nodefony/http) decideSend() : désactivé à 0, nominal sous le seuil, drop sans fermer, close en 1013
backpressureConfig.test.ts (@nodefony/realtime) le transport temps réel applique la règle de @nodefony/http : refus compté, fermeture après N, le solde décroît sur un pic passager
WsConnectionTransport.test.ts (@nodefony/realtime) le transport : envoi seulement sur socket ouverte, fermeture déléguée
cadencePressureFigures.test.ts (@nodefony/realtime) les courbes de cette page : rejoue les trois scénarios avec le vrai code et compare chaque valeur

Deux bancs complètent, hors des suites (skill nodefony-load-test) :

Ce qui manque : aucun test ne fait jouer ensemble un vrai serveur qui jette et un vrai client qui ralentit. La rencontre des deux étages décrite plus haut découle de leur conception, elle n’est pas éprouvée de bout en bout.

cd src/nodefony && npx vitest run src/tests/AdaptiveRate.test.ts src/tests/channelRate.test.ts
cd src/packages/@nodefony/realtime && npm test
cd src/packages/@nodefony/http && npx vitest run nodefony/tests/unit/wsBackpressure.test.ts

Ce qu’il faut retenir#

  1. La cadence adaptative est décidée par le client, canal d’état par canal d’état, en mesurant l’écart entre trames ; elle se traduit par un réabonnement à base:<ms>.
  2. La contre-pression est décidée par le serveur, trame par trame, sur la file d’envoi ; elle jette au-delà de 4 Mio et ferme en 1013 quand le solde de refus atteint 1000.
  3. Les deux se rencontrent par la mesure : une trame jetée allonge un écart, et le client ralentit.
  4. Les sondes comptent (clients lents, refus) sans rien décider.
  5. Rien de tout cela ne garantit la livraison : un flux où chaque message compte a besoin de sa propre reprise.

🔗 Pour aller plus loin#