xPulse
🇩🇪 DE

Event – Message Structure Concept

Status: CONCEPT · Erstellt März 2026 Übergeordnet: COMP_event_spec.md, GLOBAL_adr-010-event-driven.md, GLOBAL_concept-ecosystem.md Verwandt: 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}:initInitialisierung beginnt
{namespace}:readyInitialisierung 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.

de/concept/message-structure.md 2026-03-30