Seiten
Inhalt
Event – Message Structure Concept
Status: CONCEPT · Erstellt März 2026 Übergeordnet:
COMP_event_spec.md,GLOBAL_adr-010-event-driven.md,GLOBAL_concept-ecosystem.mdVerwandt:COMP_logger_concept-message-structure.md
Warum ein eigenes Konzept?
Events sind die primäre Kommunikationsebene zwischen allen @xpulse/* Packages.
Was der Logger für Ausgaben ist, sind Events für die interne Kommunikation –
und genau wie beim Logger braucht es klare Regeln damit das Ökosystem konsistent
und nachvollziehbar bleibt.
Dieses Konzept legt fest wie ein Event-Name aufgebaut ist, wie ein Payload strukturiert wird, welche Regeln gelten – und warum.
Anatomie eines Events
Ein Event besteht aus zwei Teilen:
| namespace:operation ← Event-Name |
| { key, key, key } ← Payload |
Beispiel:
| event.emit('controller:navigation:ready', { |
| items: [...], |
| count: 5, |
| }); |
Event-Name
Schema
| {namespace}:{operation} |
| {namespace}:{area}:{operation} |
Maximal drei Segmente, getrennt durch :.
| Segment | Bedeutung |
|---|---|
namespace |
Woher kommt das Event – Package oder Feature |
area |
Optional – Unterbereich innerhalb des Namespace |
operation |
Was ist passiert |
Namespace
Identisch mit dem Package-Namen ohne @xpulse/ Prefix:
| @xpulse/controller → controller |
| @xpulse/http → http |
| @xpulse/router → router |
| @xpulse/template → template |
| @xpulse/logger → logger |
| @xpulse/app → app |
App-eigene Events (nicht aus einem Package) nutzen einen frei wählbaren Namespace – z.B. den Feature-Namen:
| auth:login |
| chat:message:received |
| peer:connected |
Operation
Immer Vergangenheitsform oder Partizip – ein Event beschreibt etwas das bereits passiert ist, nie etwas das gerade passiert oder passieren soll:
| ✓ controller:ready ← Discovery abgeschlossen |
| ✓ http:request ← Request eingegangen |
| ✓ router:matched ← Route gematcht |
| ✓ navigation:collected ← navItems gesammelt |
| ✓ template:render:after ← Rendering abgeschlossen |
| ✗ controller:discovering ← Verlaufsform – nicht verwenden |
| ✗ http:will-receive-request ← Zukunft – nicht verwenden |
| ✗ router:match ← Imperativ – nicht verwenden |
Ausnahme: before-Events die explizit vor einer Operation feuern –
hier ist die Verlaufsform erlaubt weil es bewusst ein "Pre-Hook" ist:
| ✓ template:render:before ← expliziter Pre-Hook, data noch mutierbar |
| ✓ http:response:before ← expliziter Pre-Hook, body noch mutierbar |
Lifecycle-Pattern
Jedes Package das initialisiert wird, feuert immer beide:
| {namespace}:init ← Initialisierung beginnt |
| {namespace}:ready ← Initialisierung abgeschlossen |
Das ist verpflichtend – nicht optional. Andere Packages können so zuverlässig auf den Start warten ohne Timing-Probleme.
Naming-Beispiele
| app:init |
| app:ready |
| app:stopping |
| app:stopped |
| http:init |
| http:ready |
| http:started |
| http:stopped |
| http:request |
| http:response:before |
| http:response |
| controller:init |
| controller:ready |
| controller:discovered |
| controller:called |
| controller:navigation:collected |
| controller:navigation:ready |
| router:matched |
| router:not-found |
| template:render:before |
| template:render:after |
| logger:init |
| logger:ready |
| logger:write |
| dotenv:loaded |
| config:loaded |
Payload
Grundregeln
Immer ein Objekt – auch wenn nur ein Wert übertragen wird:
| // ✓ Immer Objekt |
| event.emit('controller:ready', { count: 5, routes: 8 }); |
| // ✗ Kein primitiver Wert |
| event.emit('controller:ready', 5); |
Flach – keine tief verschachtelten Strukturen. Wer komplexe Daten übertragen muss, serialisiert selbst oder denkt die Payload-Struktur neu.
| // ✓ Flach |
| event.emit('http:request', { |
| traceId: 'abc123', |
| method: 'GET', |
| path: '/info', |
| ip: '192.168.1.0', |
| }); |
| // ✗ Zu tief verschachtelt |
| event.emit('http:request', { |
| request: { |
| meta: { |
| traceId: 'abc123', |
| }, |
| }, |
| }); |
Arrays sind erlaubt – aber nur als Top-Level-Wert, nicht verschachtelt:
| // ✓ Array als Top-Level |
| event.emit('controller:navigation:collected', { |
| items: [ ... ], |
| count: 5, |
| }); |
Key-Naming
Alle Keys sind camelCase, Englisch (Developer Layer – siehe GLOBAL_adr-013):
| // ✓ |
| { traceId, methodName, filePath, itemCount } |
| // ✗ |
| { trace_id, method_name, file_path, item_count } |
Pflicht-Keys
Kein globaler Pflicht-Key für alle Events – jedes Event definiert seine eigene minimale Payload. Aber es gibt Konventionen nach Event-Typ:
Lifecycle-Events (*:init, *:ready):
| // :init – was wird initialisiert |
| { root?, config? } |
| // :ready – Ergebnis der Initialisierung |
| { count?, routes?, items?, uptime? } |
Request/Response-Events:
| // Immer traceId damit Requests korrelierbar sind |
| { traceId, method, path, ... } |
Discovery-Events:
| // Immer count – wie viele wurden gefunden |
| { count, ... } |
Error-Events (wenn ein Package Fehler als Event kommuniziert):
| // Immer message + optional err (das Error-Objekt selbst) |
| { message: 'template not found', err, context? } |
`before`-Events – Mutable Payload
Events mit :before-Suffix signalisieren dass der Payload mutierbar ist –
Listener können Werte verändern bevor die Operation ausgeführt wird.
| // template:render:before – data ist mutierbar |
| event.on('template:render:before', ({ data }) => { |
| data._nav = navigationRegistry.getItems(); // injizieren |
| data._route = currentRoute; // injizieren |
| }); |
Das ist das einzige Muster wo Mutation explizit erwünscht ist. Bei allen anderen Events ist Mutation des Payloads kein definiertes Verhalten – Listener sollten den Payload als read-only behandeln.
Vollständige Event-Anatomie (Beispiel)
| // Name: controller:navigation:collected |
| // ──────────┬────────── ───────── |
| // namespace │area operation |
| // └─ Unterbereich von controller |
| // Payload: |
| { |
| items: NavItem[], // mutable – Listener können filtern/erweitern |
| count: number, // Anzahl gesammelter Items vor Pipeline |
| } |
| // Verhalten: fired-and-forgotten, Payload ist mutable (Pre-Hook) |
Was Events nicht sind
Kein RPC. Ein emit() ist kein Funktionsaufruf mit Rückgabewert –
wer Daten zurück braucht, nutzt den Mutable-Payload-Pattern (:before)
oder eine direkte API.
Kein Error-Handling-Mechanismus. Fehler werden nicht als Events
kommuniziert außer es gibt einen dedizierten *:error Event. Exceptions
bleiben Exceptions.
Kein Persistence. Events sind fired-and-forgotten. Wer historische
Events braucht, baut einen Collector (@xpulse/debug macht genau das).
Kein Cross-Process. Events leben im selben Node-Prozess / Browser-Tab. Keine IPC, keine WebSockets, keine Message Queues.
Abgrenzung zu Log-Messages
Events und Log-Messages sind zwei verschiedene Systeme mit unterschiedlichen Zwecken:
| Event | Log-Message | |
|---|---|---|
| Zweck | Kommunikation zwischen Packages | Ausgabe für Entwickler/Betrieb |
| Konsument | Andere Packages / App-Code | Mensch / Log-Aggregator |
| Format | name + payload (Objekt) |
Strukturierter Text mit Kontext |
| Persistence | Nein | Ja (Datei / Console) |
| Sprache | Englisch (Keys) | Englisch (Message + Keys) |
Der Logger nutzt selbst Events – logger:write – damit @xpulse/debug
mithören kann. Events und Logs sind komplementär, nicht redundant:
| log.debug('navItem registered', { key: 'info_team' }) |
| ↓ |
| logger:write event → @xpulse/debug sammelt |
| ↓ |
| Console / Datei → Entwickler liest |
Registrierung (optional, aber empfohlen)
@xpulse/event erlaubt optionale Registrierung via event.register() –
das macht Events introspektierbar für @xpulse/debug und debug:events:list.
Jedes @xpulse/* Package sollte seine Events beim Init registrieren:
| // Intern in @xpulse/controller beim init() |
| event.register('controller:navigation:collected', { |
| description: 'navItems collected from all controllers – mutable, pipeline open', |
| emittedBy: '@xpulse/controller', |
| payload: '{ items: NavItem[], count: number }', |
| mutable: true, |
| }); |
| event.register('controller:navigation:ready', { |
| description: 'Navigation finalized – read-only', |
| emittedBy: '@xpulse/controller', |
| payload: '{ items: NavItem[], count: number }', |
| mutable: false, |
| }); |
mutable: true signalisiert dass der Payload in Listenern verändert werden darf.
Das ist Dokumentation – @xpulse/event erzwingt es nicht technisch.