aissia/docs/02-systems/message-communication-system.md
StillHammer ba42b6d9c7 Update CDC with hybrid architecture (WarFactory + multi-target)
- Add hybrid deployment modes: local_dev (MVP) and production_pwa (optional)
- Integrate WarFactory engine reuse with hot-reload 0.4ms
- Define multi-target compilation strategy (DLL/SO/WASM)
- Detail both deployment modes with cost analysis
- Add progressive roadmap: Phase 1 (local), Phase 2 (POC WASM), Phase 3 (cloud)
- Budget clarified: $10-20/mois (local) vs $13-25/mois (cloud)
- Document open questions for technical validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 11:49:09 +08:00

13 KiB

Message Communication System

Vue d'ensemble

Le système de communication inter-modules utilise des classes de messages typées au lieu de JSON brut pour garantir la fiabilité et la maintenabilité.

Concept fondamental

Une classe = un type de message Une instance = un message individuel

Exemple: La classe TankMovedMessage définit le type "mouvement de tank". Chaque fois qu'un tank bouge, on crée une nouvelle instance de cette classe.

Pourquoi des classes?

Problème avec JSON brut

  • Erreurs découvertes uniquement au runtime
  • Pas de validation automatique
  • Typos dans les clés non détectées
  • Difficile à maintenir et refactoriser

Solution avec classes

  • Validation au compile-time
  • Contrat clair entre modules
  • IDE autocomplétion et refactoring
  • Hot-reload friendly (petites classes isolées)
  • Évolutif (ajout de champs sans casser l'existant)

Architecture

IMessage - Interface de base

Interface pure définissant le contrat minimal pour tous les messages:

  • Type identification: Chaque message déclare son type via enum
  • Serialization: Conversion vers JSON pour transport via IIO
  • Deserialization: Reconstruction depuis JSON reçu

AMessage - Classe abstraite avec métadata

Classe abstraite obligatoire fournissant l'implémentation partielle commune à tous les messages.

Métadata immutables (enforced):

  • timestamp - Horodatage de création (const)
  • sender - Module émetteur (const)
  • messageId - ID unique pour tracking et reassemblage fragments (const)
  • partId - ID de fragment pour messages multi-parts (const)

Caractéristiques:

  • Constructeur protégé force passage par classes enfants
  • Métadata initialisées automatiquement à la construction
  • Impossible de modifier métadata après création
  • Tous messages garantis d'avoir timestamp/sender/messageId

MessageType - Enum central

Enum listant tous les types de messages du système:

  • TANK_MOVED, TANK_FIRED, TANK_DESTROYED
  • PRICE_UPDATED, TRADE_EXECUTED
  • ITEM_PRODUCED, BELT_CONGESTION
  • etc.

Pourquoi enum plutôt que strings?

  • Performance (comparaison d'entiers)
  • Type safety (typos détectées au compile)
  • Centralisation (tous les types visibles)

Messages concrets

Chaque type de message est une classe dédiée héritant de AMessage:

TankMovedMessage: Position, vitesse, ID tank PriceUpdatedMessage: Item, ancien prix, nouveau prix ItemProducedMessage: Item type, quantité, factory ID

Chaque classe:

  • Hérite obligatoirement de AMessage
  • Appelle constructeur AMessage(senderModule)
  • Stocke ses données spécifiques
  • Valide à la construction
  • Sérialise/désérialise son propre format

Flow de communication

Publication

Exemple complet:

void TankModule::process(const json& input) {
    // 1. Calculer nouvelle position
    Vector2 newPos = calculatePosition();
    float currentSpeed = getSpeed();

    // 2. Créer message (validation automatique à la construction)
    TankMovedMessage msg(newPos, currentSpeed, tankId);

    // 3. Sérialiser en JSON
    json serialized = msg.serialize();

    // 4. Publier via IIO
    io->publish("tank:" + std::to_string(tankId), serialized);
}

Étapes:

  1. Module crée instance de message (validation à la construction)
  2. Message sérialisé en JSON via serialize()
  3. Publié via IIO::publish(topic, json)
  4. IIO route vers subscribers du topic

Réception

Méthode 1: Type-safe template helper (recommandé)

// Clean syntax avec type safety automatique
auto tankMsg = io->pullMessageAs<TankMovedMessage>();

if (tankMsg) {  // nullptr si type mismatch ou queue vide
    Vector2 pos = tankMsg->getPosition();
    float speed = tankMsg->getSpeed();
}

Méthode 2: Manuelle (si besoin de flexibilité)

// Récupère message brut
Message rawMsg = io->pullMessage();

// Désérialise vers base
std::unique_ptr<IMessage> baseMsg = IMessage::deserialize(rawMsg.data);

// Cast vers type concret
if (TankMovedMessage* tankMsg = dynamic_cast<TankMovedMessage*>(baseMsg.get())) {
    // Accès type-safe
}

Template helper implementation:

class IIO {
    // Template helper inline (zero overhead)
    template<typename T>
    std::unique_ptr<T> pullMessageAs() {
        if (!hasMessages()) return nullptr;

        Message raw = pullMessage();
        std::unique_ptr<IMessage> base = IMessage::deserialize(raw.data);

        T* casted = dynamic_cast<T*>(base.get());
        if (!casted) return nullptr;  // Type mismatch

        base.release();
        return std::unique_ptr<T>(casted);
    }
};

Performance:

  • Template inlined = zero function call overhead
  • dynamic_cast = ~5-10ns (négligeable vs JSON parsing)
  • Bottleneck réel = JSON serialization, pas le cast

Désérialisation centralisée

Factory pattern avec routing par type:

// IMessage base class
std::unique_ptr<IMessage> IMessage::deserialize(const json& data) {
    // Extract type from JSON
    int typeInt = data.value("type", -1);
    MessageType type = static_cast<MessageType>(typeInt);

    // Route to concrete deserializer
    switch (type) {
        case MessageType::TANK_MOVED:
            return TankMovedMessage::deserialize(data);

        case MessageType::PRICE_UPDATED:
            return PriceUpdatedMessage::deserialize(data);

        case MessageType::ITEM_PRODUCED:
            return ItemProducedMessage::deserialize(data);

        // Add new messages here

        default:
            return nullptr;  // Unknown type
    }
}

Chaque message implémente sa propre désérialisation:

class TankMovedMessage : public AMessage {
    static std::unique_ptr<TankMovedMessage> deserialize(const json& data) {
        try {
            Vector2 pos{data["position"]["x"], data["position"]["y"]};
            float speed = data["speed"];
            int tankId = data["tankId"];

            return std::make_unique<TankMovedMessage>(pos, speed, tankId);
        } catch (const json::exception& e) {
            return nullptr;  // Malformed JSON
        }
    }
};

Gestion des erreurs:

  • JSON malformé → retourne nullptr
  • Type inconnu → retourne nullptr
  • Validation échoue → exception à la construction
  • Modules doivent vérifier if (msg != nullptr)

Organisation du code

Location des messages

modules/shared/messages/
├── IMessage.h              # Interface pure
├── AMessage.h              # Classe abstraite avec métadata
├── MessageType.h           # Enum des types
├── TankMovedMessage.h
├── PriceUpdatedMessage.h
└── ...

Rationale:

  • Single source of truth (pas de duplication)
  • Contrat partagé entre émetteur et récepteur
  • Facile à trouver et maintenir

Validation

À la construction: Invariants validés immédiatement

  • Vitesse négative → exception
  • ID invalide → exception
  • Fail fast pour détecter bugs tôt

À la désérialisation: Données réseau/externes validées

  • JSON malformé → retourne nullptr
  • Champs manquants → retourne nullptr
  • Protège contre données corrompues

Décisions de design finalisées

Versioning - Breaking changes assumées

Décision: Types versionnés avec breaking changes strictes

Changement de format = nouveau type + nouvelle classe:

  • TANK_MOVED_V1TANK_MOVED_V2 = types différents
  • Pas de backward compatibility
  • Pas de forward compatibility
  • Migration forcée de tous les modules

Rationale:

  • Zéro ambiguïté sur le format attendu
  • Pas de logique conditionnelle complexe
  • Hot-reload force mise à jour synchronisée
  • Détection immédiate des incompatibilités
  • Code simple et clair

Conséquence:

  • Format change → tous modules doivent migrer
  • Ancien code ne compile plus → migration manuelle obligatoire
  • Breaking changes explicites et visibles

Message inheritance - AMessage obligatoire

Décision: Classe abstraite AMessage avec métadata immutables enforced

Architecture:

IMessage (interface pure)
    ↓
AMessage (classe abstraite - métadata immutables)
    ↓
TankMovedMessage, PriceUpdatedMessage... (classes concrètes)

Enforcement:

  • Constructeur AMessage protégé → impossible de créer message sans passer par enfant
  • Métadata const → immutables après construction
  • Tous messages garantis d'avoir timestamp/sender/messageId
  • Format uniforme pour tous messages du système

Rationale:

  • Pas de duplication (métadata dans AMessage)
  • Impossible d'oublier métadata
  • Contrat strict et enforced au compile-time
  • Simplicité pour messages concrets (métadata automatique)

Décisions de design finalisées (suite)

Size limits - Fragmentation automatique par IO

Décision: Pas de limite au niveau message, fragmentation automatique par couche transport

Architecture:

  • Modules: Publient/reçoivent messages complets (transparence totale)
  • IIO: Gère fragmentation/défragmentation automatiquement
  • Transport adaptatif: Fragmentation si nécessaire selon type IO

Fragmentation par type:

  • IntraIO: Pas de fragmentation (copie mémoire directe)
  • LocalIO: Fragmentation si > seuil (ex: 64KB chunks)
  • NetworkIO: Fragmentation si > MTU (~1500 bytes)

Mécanisme:

  1. Module publie message → sérialisé en JSON
  2. IO détecte si taille > seuil transport
  3. Si oui: découpe en fragments avec messageId + partId
  4. Transport des fragments
  5. IO destination reassemble via messageId (collect tous partId)
  6. Module reçoit message complet via pullMessage()

Robustesse:

  • Packet loss: Timeout si fragments incomplets (ex: 30s)
  • Ordering: partId garantit ordre de reassemblage
  • Monitoring: Log warning si message > 100KB (design smell probable)

Conséquence:

  • Messages peuvent être arbitrairement gros
  • Complexité cachée dans couche IO
  • Performance optimisée par type de transport

Async handling - Délégation au module

Décision: Pas de gestion async spéciale au niveau messages

Rationale:

  • Messages = transport de données uniquement
  • ITaskScheduler existe déjà pour opérations coûteuses
  • Module décide si déléguer ou traiter directement
  • Keep messages simple et stupid

Responsabilité:

  • Message: Transport de données (aucune intelligence)
  • Module: Décide de déléguer opérations coûteuses au scheduler
  • ITaskScheduler: Gère threading et async

Exemple:

// Module reçoit message déclenchant opération coûteuse
if (PathfindingRequestMessage* req = dynamic_cast<...>) {
    // Module délègue au TaskScheduler
    scheduler->scheduleTask(PATHFINDING_TASK, req->serialize());
    // Continue traitement autres messages
}

// Plus tard, récupère résultat
if (scheduler->hasCompletedTasks() > 0) {
    TaskResult result = scheduler->getCompletedTask();
    // Publie résultat via message
}

Message ordering - Pas de garantie + Messages remplaçables

Décision: Pas de garantie d'ordre, modules gèrent via timestamp si critique

Approche:

  • Pas de FIFO enforced: Messages peuvent arriver dans ordre arbitraire
  • Messages remplaçables par défaut: SubscriptionConfig.replaceable = true pour majorité
  • Timestamp disponible: msg->getTimestamp() permet tri manuel si nécessaire
  • Utilité questionnable: Architecture modulaire rend ordre moins critique

Rationale:

  • Performance maximale (pas de garanties coûteuses)
  • Messages remplaçables optimisent bandwidth (garde juste dernier)
  • Timestamp dans AMessage permet tri si vraiment nécessaire
  • Architecture découple les dépendances temporelles

Exemples d'usage:

// Prix - remplaçable (dernière valeur suffit)
io->subscribeLowFreq("economy:*", {.replaceable = true});

// Tank position - remplaçable (position actuelle suffit)
io->subscribe("tank:*", {.replaceable = true});

// Events critiques - non remplaçable + tri si nécessaire
io->subscribe("combat:*", {.replaceable = false});

// Si ordre vraiment critique (rare)
std::vector<Message> messages = collectAllMessages();
std::sort(messages.begin(), messages.end(),
    [](const Message& a, const Message& b) {
        return a.getTimestamp() < b.getTimestamp();
    });

Conséquence:

  • Modules ne doivent pas assumer ordre de réception
  • Messages remplaçables = dernière valeur seulement
  • Tri par timestamp disponible mais rarement nécessaire

Questions ouvertes

Aucune question ouverte restante. Toutes les décisions de design ont été finalisées.

Statut d'implémentation

Phase actuelle: Design et documentation

Prochaines étapes:

  1. Créer interface IMessage.h
  2. Créer classe abstraite AMessage.h avec métadata immutables
  3. Créer enum MessageType.h
  4. Implémenter 5-10 messages d'exemple
  5. Tester avec modules existants
  6. Itérer selon usage réel

Références

  • src/core/include/warfactory/IIO.h - Couche pub/sub
  • docs/01-architecture/architecture-technique.md - Patterns de communication
  • docs/03-implementation/CLAUDE-HOT-RELOAD-GUIDE.md - Impact sur design messages