Seiten
Inhalt
@xpulse/app – Service Loader
Status: ACCEPTED · März 2026 Ersetzt: hartcodiertes
bootstrap/services.js
Problem
bootstrap/services.js ist eine hartcodierte Liste von tryInit()-Aufrufen in fester Reihenfolge.
Jedes neue @xpulse/*-Package erfordert eine manuelle Änderung in @xpulse/app.
Das widerspricht dem Auto-Discovery-Prinzip, das der Rest des Frameworks befolgt.
Lösung: Auto-Discovery Service Loader
Der Service Loader liest die xpulse.json jedes installierten @xpulse/*-Packages,
baut daraus einen Abhängigkeitsgraph, sortiert ihn topologisch
und ruft das init() jedes Packages in der richtigen Reihenfolge auf.
Kein manuelles Registrieren. Ein Package installieren reicht.
Service-Metadata in `xpulse.json`
Jedes @xpulse/*-Package deklariert sich in seiner eigenen xpulse.json:
| { |
| "name": "xpulse-router", |
| "type": "component", |
| "service": { |
| "xpulse-router": { |
| "load": true, |
| "load-after": ["xpulse-http"] |
| } |
| } |
| } |
Regeln:
- Der Key innerhalb von
serviceist der vollständige Package-Name (z.B.xpulse-router). load: true— dieses Package nimmt am Service Loader teil.load-after— Liste von Package-Namen, die vorher initialisiert sein müssen.- Packages ohne
service-Section werden vom Loader ignoriert. @xpulse/eventund@xpulse/confighaben keineservice-Section — sie sind immer verfügbar bevor der Loader startet.
Abhängigkeitsgraph (Referenz)
| xpulse-logger |
| xpulse-http |
| └── (keine) |
| xpulse-router |
| └── xpulse-http |
| xpulse-template |
| └── (keine) |
| xpulse-theme |
| └── xpulse-router, xpulse-config |
| xpulse-controller |
| └── xpulse-router, xpulse-template |
| xpulse-session |
| └── xpulse-router, xpulse-crypto |
| xpulse-debug |
| └── nicht Teil des Service Loaders — siehe unten |
| xpulse-doc |
| └── xpulse-router |
App-eigene Overrides
Die xpulse.json der Applikation kann jeden Service-Eintrag überschreiben.
Der Loader führt einen Deep-Merge durch: App-Config gewinnt über Package-Defaults.
Service deaktivieren:
| { |
| "service": { |
| "xpulse-theme": { "load": false } |
| } |
| } |
App-lokalen Service registrieren (aus src/services/):
| { |
| "service": { |
| "my-auth": { "load": true, "load-after": ["xpulse-router", "xpulse-session"] } |
| } |
| } |
App-lokale Services werden aus src/services/*/xpulse.json discovert
und vor dem App-Override gemergt.
Merge-Reihenfolge
| 1. node_modules/@xpulse/*/xpulse.json ← Package-Defaults |
| 2. src/services/*/xpulse.json ← App-lokale Services |
| 3. xpulse.json { "service": { … } } ← App-Overrides (letzter gewinnt) |
Da jedes Package nur seinen eigenen Namen als Key deklariert, gibt es in Schritt 1 keine Konflikte. Schritte 2 und 3 können alles aus Schritt 1 überschreiben.
Loader-Algorithmus
| 1. readdir(node_modules/@xpulse/*) |
| 2. Pro Package: xpulse.json lesen → service-Section extrahieren |
| 3. readdir(src/services/*) → gleiche Logik |
| 4. App-eigene xpulse.json service-Section darüber mergen |
| 5. Filtern: load !== false |
| 6. Topologischer Sort nach load-after[] |
| 7. Pro Service (in Reihenfolge): |
| - import('@xpulse/<name>') bzw. import('src/services/<name>') |
| - mod.default.init() aufrufen falls vorhanden |
| - app:service:ready / app:service:error / app:service:skipped emittieren |
Eine zirkuläre Abhängigkeit wirft einen Fehler mit einer klaren Meldung, die den Zyklus nennt.
Eine fehlende Abhängigkeit (in load-after gelistet, aber load: false oder nicht installiert)
emittiert app:service:skipped und macht weiter — resilient by default.
CLI-Verhalten
Beim Start via npx xpulse <command> wird app.init() ohne app.start() aufgerufen.
Der gesamte Service-Stack bootet — identisch zu einem normalen App-Start —
aber der HTTP-Server beginnt nicht, auf einem Port zu lauschen.
Das ist das bestehende Verhalten und ändert sich mit dem neuen Loader nicht.
Events
| Event | Payload | Wann |
|---|---|---|
app:service:ready |
{ service } |
Service erfolgreich initialisiert |
app:service:skipped |
{ service, reason } |
Nicht installiert oder load: false |
app:service:error |
{ service, error } |
Init hat geworfen, Loader macht weiter |
@xpulse/debug — Sonderstellung
@xpulse/debug ist nicht Teil des Service Loaders.
Es hat einen eigenen dedizierten Bootstrap-Schritt der vor dem Loader läuft,
damit jeder nachfolgende Service beim Init bereits debuggbar ist.
Konfiguriert wie gehabt via xpulse.json:
| { "debug": { "enabled": true } } |
Die service-Section von @xpulse/debug/xpulse.json hat keinen load-Eintrag.
Der Loader ignoriert es vollständig.
Bootstrap-Ablauf in app.init():
| 1. config.load() ← immer |
| 2. debug.bootstrapDebug() ← nur wenn debug.enabled — vor dem Loader |
| 3. Service Loader ← alle anderen @xpulse/*-Packages |
Implementierungshinweise
bootstrap/services.jswird vollständig durch den neuen Loader ersetzt.- Der Loader liegt in
bootstrap/loader.js—services.jswird gelöscht. - Keine neuen externen Abhängigkeiten — reines Node.js mit
readdirund dynamischemimport.