- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
29 KiB
SSE System Documentation
Server-Sent Events (SSE) Integration für das Custom PHP Framework - Technische Dokumentation.
Übersicht
Das SSE-System ermöglicht Echtzeit-Updates von Server zu Client über HTTP-basierte unidirektionale Streams. LiveComponents können automatisch Updates empfangen und sich selbst neu rendern, ohne dass JavaScript-Code geschrieben werden muss.
Kernfeatures
- ✅ Auto-Connection: Components mit
getSseChannel()verbinden sich automatisch - ✅ Channel-Based Routing: Typed channels für verschiedene Update-Typen
- ✅ Event Broadcasting: EventDispatcher Integration für nahtlose Updates
- ✅ Automatic Reconnection: Exponential Backoff mit Jitter (1s → 30s max)
- ✅ Heartbeat Monitoring: 45s Client-Timeout-Erkennung
- ✅ DOM Patching: Fragment-basierte partielle Rendering
- ✅ Connection Pooling: Effizientes Teilen von Connections zwischen Components
Architektur
┌─────────────────────────────────────────────────────────────┐
│ Browser (Frontend) │
├─────────────────────────────────────────────────────────────┤
│ LiveComponentManager │
│ ├─ setupSseConnection() - Auto-detect data-sse-channel │
│ ├─ handleSseComponentUpdate() - Full component refresh │
│ └─ handleSseComponentFragments() - Partial DOM updates │
│ │
│ SseClient (EventSource Wrapper) │
│ ├─ connect() - EventSource mit exponential backoff │
│ ├─ disconnect() - Graceful shutdown │
│ └─ Reference counting für shared connections │
└─────────────────────────────────────────────────────────────┘
↓ SSE Stream (text/event-stream)
┌─────────────────────────────────────────────────────────────┐
│ PHP Backend │
├─────────────────────────────────────────────────────────────┤
│ ComponentRegistry │
│ └─ renderWithWrapper() - Auto-inject data-sse-channel │
│ │
│ SseEventBroadcaster (EventListener) │
│ ├─ onComponentUpdated() - Broadcast component updates │
│ └─ Filters by channel subscription │
│ │
│ SseConnectionPool │
│ ├─ Connection Management │
│ ├─ Heartbeat Scheduling (30s interval) │
│ └─ Broadcast to filtered connections │
│ │
│ /sse/events/{channel} Route │
│ ├─ text/event-stream Response │
│ ├─ Generator-based streaming │
│ └─ Connection lifecycle management │
└─────────────────────────────────────────────────────────────┘
Backend-Komponenten
1. SseChannel (Value Object)
Location: src/Framework/Sse/ValueObjects/SseChannel.php
Typed Channel-Identifier mit Validierung:
final readonly class SseChannel
{
public function __construct(
public string $value
) {
if (empty($value)) {
throw new InvalidArgumentException('Channel cannot be empty');
}
}
public static function global(): self
{
return new self('global');
}
public static function user(string $userId): self
{
return new self("user:$userId");
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
2. SseConnection (Value Object)
Location: src/Framework/Sse/ValueObjects/SseConnection.php
Repräsentiert eine aktive SSE-Verbindung:
final readonly class SseConnection
{
public function __construct(
public string $id, // Unique connection ID
public SseChannel $channel, // Subscribed channel
public \DateTimeImmutable $connectedAt,
public ?\DateTimeImmutable $lastHeartbeat = null,
public array $metadata = [] // User-Agent, IP, etc.
) {}
public function isExpired(int $timeoutSeconds = 60): bool
{
$lastActivity = $this->lastHeartbeat ?? $this->connectedAt;
$now = new \DateTimeImmutable();
return ($now->getTimestamp() - $lastActivity->getTimestamp()) > $timeoutSeconds;
}
public function withHeartbeat(\DateTimeImmutable $timestamp): self
{
return new self(
id: $this->id,
channel: $this->channel,
connectedAt: $this->connectedAt,
lastHeartbeat: $timestamp,
metadata: $this->metadata
);
}
}
3. SseConnectionPool
Location: src/Framework/Sse/SseConnectionPool.php
Verwaltet alle aktiven SSE-Verbindungen:
final class SseConnectionPool
{
/** @var array<string, SseConnection> */
private array $connections = [];
/** @var array<string, Generator> */
private array $generators = [];
public function add(SseConnection $connection, Generator $generator): void
{
$this->connections[$connection->id] = $connection;
$this->generators[$connection->id] = $generator;
}
public function remove(string $connectionId): void
{
unset($this->connections[$connectionId]);
unset($this->generators[$connectionId]);
}
/**
* Send event to all connections subscribed to channel
*/
public function broadcast(SseChannel $channel, SseEvent $event): void
{
foreach ($this->connections as $connectionId => $connection) {
if ($connection->channel->equals($channel)) {
$generator = $this->generators[$connectionId];
$generator->send($event);
}
}
}
/**
* Send heartbeat to all connections
*/
public function sendHeartbeats(): void
{
$heartbeat = SseEvent::heartbeat();
foreach ($this->generators as $generator) {
$generator->send($heartbeat);
}
}
/**
* Remove expired connections
*/
public function cleanupExpired(int $timeoutSeconds = 60): int
{
$removed = 0;
foreach ($this->connections as $connectionId => $connection) {
if ($connection->isExpired($timeoutSeconds)) {
$this->remove($connectionId);
$removed++;
}
}
return $removed;
}
}
4. SseEvent (Value Object)
Location: src/Framework/Sse/ValueObjects/SseEvent.php
SSE-Event mit Typ und Daten:
final readonly class SseEvent
{
public function __construct(
public SseEventType $type,
public array $data = [],
public ?string $id = null,
public ?int $retry = null
) {}
public static function heartbeat(): self
{
return new self(
type: SseEventType::HEARTBEAT,
data: ['timestamp' => time()]
);
}
public static function componentUpdate(
string $componentId,
array $state,
string $html
): self {
return new self(
type: SseEventType::COMPONENT_UPDATE,
data: [
'componentId' => $componentId,
'state' => $state,
'html' => $html
]
);
}
public static function componentFragments(
string $componentId,
array $fragments
): self {
return new self(
type: SseEventType::COMPONENT_FRAGMENTS,
data: [
'componentId' => $componentId,
'fragments' => $fragments
]
);
}
public function toSseFormat(): string
{
$output = '';
// Event type
if ($this->type !== SseEventType::MESSAGE) {
$output .= "event: {$this->type->value}\n";
}
// Event ID (for reconnection)
if ($this->id !== null) {
$output .= "id: {$this->id}\n";
}
// Retry interval (milliseconds)
if ($this->retry !== null) {
$output .= "retry: {$this->retry}\n";
}
// Data (JSON-encoded)
$output .= "data: " . json_encode($this->data) . "\n\n";
return $output;
}
}
5. SseEventBroadcaster (Event Listener)
Location: src/Framework/Sse/SseEventBroadcaster.php
Hört auf ComponentUpdatedEvent und broadcastet SSE-Events:
#[EventHandler]
final readonly class SseEventBroadcaster
{
public function __construct(
private SseConnectionPool $connectionPool,
private ComponentRegistry $componentRegistry
) {}
public function onComponentUpdated(ComponentUpdatedEvent $event): void
{
// Get component instance to extract SSE channel
$component = $this->componentRegistry->get($event->componentId);
// Check if component supports SSE
if (!method_exists($component, 'getSseChannel')) {
return; // Not an SSE-capable component
}
$channel = new SseChannel($component->getSseChannel());
// Render updated component
$html = $this->componentRegistry->render($component);
// Create SSE event
$sseEvent = SseEvent::componentUpdate(
componentId: $event->componentId->toString(),
state: $event->newState->toArray(),
html: $html
);
// Broadcast to all subscribed connections
$this->connectionPool->broadcast($channel, $sseEvent);
}
}
6. SSE Route Controller
Location: src/Application/Api/SseController.php
final readonly class SseController
{
#[Route(path: '/sse/events/{channel}', method: Method::GET)]
public function events(string $channel, HttpRequest $request): StreamedResponse
{
$sseChannel = new SseChannel($channel);
$connectionId = uniqid('sse_', true);
$connection = new SseConnection(
id: $connectionId,
channel: $sseChannel,
connectedAt: new \DateTimeImmutable(),
metadata: [
'user_agent' => $request->headers->getFirst(HeaderKey::USER_AGENT),
'ip' => $request->server->getClientIp()
]
);
// Generator für SSE-Stream
$generator = $this->createEventGenerator();
// Connection registrieren
$this->connectionPool->add($connection, $generator);
// Cleanup bei Verbindungsabbruch
register_shutdown_function(function() use ($connectionId) {
$this->connectionPool->remove($connectionId);
});
return new StreamedResponse(
generator: $generator,
headers: [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no'
]
);
}
private function createEventGenerator(): Generator
{
// Initial connection event
yield SseEvent::heartbeat();
// Keep connection open and wait for events
while (true) {
// Generator wird von außen mit send() gefüttert
$event = yield;
if ($event === null) {
// Heartbeat alle 30s
sleep(30);
yield SseEvent::heartbeat();
} else {
yield $event;
}
}
}
}
Frontend-Komponenten
1. SseClient
Location: resources/js/modules/sse/index.js
EventSource-Wrapper mit Reconnection-Logik:
export class SseClient {
constructor(channels = [], options = {}) {
this.channels = channels;
this.baseUrl = options.baseUrl || '/sse/events';
this.autoReconnect = options.autoReconnect ?? true;
this.heartbeatTimeout = options.heartbeatTimeout || 45000;
this.eventSource = null;
this.reconnectAttempts = 0;
this.maxReconnectDelay = 30000;
this.heartbeatTimer = null;
this.listeners = new Map();
this.isConnected = false;
}
connect() {
if (this.eventSource) {
return; // Already connected
}
const channel = this.channels[0] || 'global';
const url = `${this.baseUrl}/${channel}`;
this.eventSource = new EventSource(url);
this.eventSource.onopen = () => {
console.log(`[SSE] Connected to ${channel}`);
this.isConnected = true;
this.reconnectAttempts = 0;
this.resetHeartbeatTimer();
this.emit('connect', { channel });
};
this.eventSource.onerror = (error) => {
console.error('[SSE] Connection error:', error);
this.isConnected = false;
this.clearHeartbeatTimer();
if (this.eventSource.readyState === EventSource.CLOSED) {
this.eventSource = null;
if (this.autoReconnect) {
this.scheduleReconnect();
}
}
};
// Event-Listener registrieren
this.eventSource.addEventListener('heartbeat', () => {
this.resetHeartbeatTimer();
});
this.eventSource.addEventListener('component-update', (event) => {
const data = JSON.parse(event.data);
this.emit('component-update', data);
});
this.eventSource.addEventListener('component-fragments', (event) => {
const data = JSON.parse(event.data);
this.emit('component-fragments', data);
});
}
disconnect() {
this.autoReconnect = false;
this.clearHeartbeatTimer();
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.isConnected = false;
this.reconnectAttempts = 0;
}
scheduleReconnect() {
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
this.maxReconnectDelay
);
console.log(`[SSE] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
resetHeartbeatTimer() {
this.clearHeartbeatTimer();
this.heartbeatTimer = setTimeout(() => {
console.warn('[SSE] Heartbeat timeout - connection lost');
this.disconnect();
if (this.autoReconnect) {
this.scheduleReconnect();
}
}, this.heartbeatTimeout);
}
clearHeartbeatTimer() {
if (this.heartbeatTimer) {
clearTimeout(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
on(eventType, handler) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType).push(handler);
}
off(eventType, handler) {
if (this.listeners.has(eventType)) {
const handlers = this.listeners.get(eventType);
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
emit(eventType, data) {
if (this.listeners.has(eventType)) {
this.listeners.get(eventType).forEach(handler => handler(data));
}
}
}
2. LiveComponentManager SSE Integration
Location: resources/js/modules/livecomponent/index.js
class LiveComponentManager {
constructor() {
this.components = new Map();
this.sseClients = new Map(); // channel → SseClient
this.componentChannels = new Map(); // componentId → channel
}
register(element) {
const componentId = element.dataset.liveComponent;
// ... existing registration logic ...
// Setup SSE if component has data-sse-channel attribute
this.setupSseConnection(element, componentId);
}
setupSseConnection(element, componentId) {
const sseChannel = element.dataset.sseChannel;
if (!sseChannel) {
return; // Component doesn't support SSE
}
console.log(`[LiveComponent] Setting up SSE for ${componentId} on channel ${sseChannel}`);
// Track component → channel mapping
this.componentChannels.set(componentId, sseChannel);
// Reuse existing SSE client for this channel or create new one
let sseClient = this.sseClients.get(sseChannel);
if (!sseClient) {
sseClient = new SseClient([sseChannel], {
autoReconnect: true,
heartbeatTimeout: 45000
});
// Register event handlers
this.registerSseHandlers(sseClient, sseChannel);
// Connect to SSE stream
sseClient.connect();
// Store for reuse
this.sseClients.set(sseChannel, sseClient);
}
}
registerSseHandlers(sseClient, channel) {
// Handle component updates
sseClient.on('component-update', (data) => {
this.handleSseComponentUpdate(data, channel);
});
// Handle partial fragment updates
sseClient.on('component-fragments', (data) => {
this.handleSseComponentFragments(data, channel);
});
// Connection status
sseClient.on('connect', () => {
console.log(`[LiveComponent] SSE connected: ${channel}`);
});
}
handleSseComponentUpdate(data, channel) {
const { componentId, state, html, events } = data;
const config = this.components.get(componentId);
if (!config) {
console.warn(`[LiveComponent] Component ${componentId} not found for SSE update`);
return;
}
console.log(`[LiveComponent] SSE update received for ${componentId}`);
// Update component HTML
if (html) {
config.element.innerHTML = html;
this.setupActionHandlers(config.element);
this.setupFileUploadHandlers(config.element);
}
// Update component state
if (state) {
config.element.dataset.state = JSON.stringify(state);
}
// Dispatch server-side events
if (events) {
this.handleServerEvents(events);
}
}
handleSseComponentFragments(data, channel) {
const { componentId, fragments } = data;
const config = this.components.get(componentId);
if (!config) {
return;
}
console.log(`[LiveComponent] SSE fragment update for ${componentId}`, fragments);
// Use DomPatcher for partial updates
this.updateFragments(config.element, fragments);
}
cleanup(componentId) {
const channel = this.componentChannels.get(componentId);
if (!channel) {
return;
}
// Remove channel mapping
this.componentChannels.delete(componentId);
// Check if any other component uses this channel
const stillInUse = Array.from(this.componentChannels.values())
.some(ch => ch === channel);
// Only disconnect if no other components use this channel
if (!stillInUse) {
const sseClient = this.sseClients.get(channel);
if (sseClient) {
console.log(`[LiveComponent] Disconnecting SSE: ${channel}`);
sseClient.disconnect();
this.sseClients.delete(channel);
}
}
}
}
Channel-Strategien
Global Channel
Verwendung: System-weite Updates für alle Benutzer
public function getSseChannel(): string
{
return 'global';
}
Beispiele:
- System-Announcements
- Public Activity Feeds
- Global Notification Center
User-Specific Channels
Verwendung: Benutzer-spezifische Updates
public function getSseChannel(): string
{
$instanceId = $this->id->instanceId; // "user-123"
if ($instanceId === 'guest' || $instanceId === 'global') {
return 'global';
}
return "user:{$instanceId}";
}
Beispiele:
- Private Benachrichtigungen
- User-spezifische Activity Feeds
- Personal Dashboards
Context-Based Channels
Verwendung: Kontext-spezifische Updates (Posts, Articles, Rooms)
public function getSseChannel(): string
{
$context = $this->id->instanceId; // "post-123", "article-456"
return "comments:{$context}";
}
Beispiele:
- Post/Article Comments
- Chat Rooms
- Document Collaboration
Presence Channels
Verwendung: Real-time Präsenz-Tracking
public function getSseChannel(): string
{
$room = $this->id->instanceId; // "lobby", "room-5"
return "presence:{$room}";
}
Beispiele:
- Room Presence
- Active User Lists
- Collaborative Editing Sessions
Event-System Integration
ComponentUpdatedEvent
Wird automatisch dispatched wenn eine Component-Action ausgeführt wird:
final readonly class LiveComponentActionHandler
{
public function handleAction(
string $componentId,
string $actionName,
array $parameters
): ComponentData {
// Action ausführen
$newState = $component->$actionName(...$parameters);
// Event dispatchen für SSE Broadcasting
$this->eventDispatcher->dispatch(
new ComponentUpdatedEvent(
componentId: ComponentId::fromString($componentId),
actionName: $actionName,
oldState: $component->getData(),
newState: $newState
)
);
return $newState;
}
}
Custom Events
Du kannst auch manuelle SSE-Updates dispatchen:
// In deinem Service/Controller
$event = new ComponentUpdatedEvent(
componentId: ComponentId::fromString('notification-center:user-123'),
actionName: 'external_notification',
oldState: $oldState,
newState: $newState
);
$this->eventDispatcher->dispatch($event);
Performance-Aspekte
Connection Pooling
- Shared Connections: Mehrere Components auf demselben Channel teilen eine SSE-Verbindung
- Reference Counting: Connection wird erst geschlossen wenn keine Components mehr subscriben
- Memory Efficiency: Nur eine EventSource pro Channel
Heartbeat Strategy
- Server-Side: 30s Heartbeat-Interval
- Client-Side: 45s Timeout-Erkennung
- Graceful Degradation: Automatic Reconnection mit Exponential Backoff
Reconnection Strategy
// Exponential Backoff mit Jitter
delay = min(
1000 * 2^attempts + random(0, 1000),
30000 // max 30s
)
Reconnection Times:
- Attempt 1: ~1s + jitter
- Attempt 2: ~2s + jitter
- Attempt 3: ~4s + jitter
- Attempt 4: ~8s + jitter
- Attempt 5: ~16s + jitter
- Attempt 6+: 30s (capped)
Bandwidth Optimization
- Partial Updates: Fragment-basierte Updates statt Full-Render
- JSON Compression: Minimale Payload-Größe
- Conditional Rendering: Nur betroffene Components updaten
Error Handling
Server-Side Errors
try {
$sseEvent = SseEvent::componentUpdate(...);
$this->connectionPool->broadcast($channel, $sseEvent);
} catch (\Exception $e) {
$this->logger->error('SSE broadcast failed', [
'channel' => $channel->value,
'error' => $e->getMessage()
]);
// Connection wird im Pool behalten, Retry bei nächstem Event
}
Client-Side Errors
sseClient.on('error', (error) => {
console.error('[SSE] Error:', error);
// Automatic reconnection wenn enabled
if (this.autoReconnect) {
this.scheduleReconnect();
}
});
Connection Timeout
- Heartbeat Monitoring: 45s Client-Timeout
- Server Cleanup: Expired Connections werden nach 60s entfernt
- Automatic Reconnect: Client reconnected automatisch
Best Practices
1. Channel Design
✅ DO: Spezifische Channels für verschiedene Kontexte
return "comments:post-{$postId}"; // Spezifisch
❌ DON'T: Über-generalisierte Channels
return "all-updates"; // Zu breit
2. Event Payload
✅ DO: Minimale Payloads mit nur notwendigen Daten
SseEvent::componentUpdate(
componentId: $id,
state: $state,
html: $html // Nur wenn Full-Render notwendig
);
❌ DON'T: Große Payloads mit unnötigen Daten
3. Connection Management
✅ DO: Reference Counting für Shared Connections
// Automatisch in LiveComponentManager
❌ DON'T: Eine Connection pro Component
// Vermeiden: new SseClient() für jede Component
4. Error Handling
✅ DO: Graceful Degradation
sseClient.on('error', () => {
// Continue operation without real-time updates
this.fallbackToPolling();
});
❌ DON'T: Hard Failures bei SSE-Problemen
5. Testing
✅ DO: Mock SSE in Tests
$mockBroadcaster = new class implements EventListener {
public function onComponentUpdated($event): void {
// Track calls in test
}
};
Debugging
Server-Side Logging
// In SseEventBroadcaster
$this->logger->debug('SSE broadcast', [
'channel' => $channel->value,
'component' => $event->componentId->toString(),
'action' => $event->actionName,
'connections' => count($this->connectionPool->getByChannel($channel))
]);
Client-Side Logging
// Enable verbose logging
const sseClient = new SseClient(['user:123'], {
debug: true // Logs alle Events
});
Browser DevTools
- Network Tab: SSE-Stream als
eventsourceType sichtbar - Console: SSE-Events werden geloggt
- EventSource Status:
readyStateProperty prüfen
Troubleshooting
Component Updates kommen nicht an
Diagnose:
- Prüfe ob Component
getSseChannel()implementiert - Prüfe ob
data-sse-channelAttribute im HTML vorhanden ist - Prüfe Browser Console für SSE Connection-Status
- Prüfe Server-Logs für Broadcasting-Events
Lösung:
// Component muss getSseChannel() haben
public function getSseChannel(): string {
return "user:{$this->userId}";
}
// ComponentRegistry fügt automatisch data-sse-channel hinzu
Reconnection-Loops
Ursache: Server wirft Exceptions bei SSE-Route
Lösung: Prüfe Server-Logs und fixe Exception-Ursache
Memory Leaks
Ursache: Connections werden nicht cleanup'd
Lösung:
// Automatisches Cleanup alle 5 Minuten
$this->scheduler->schedule(
'sse-cleanup',
IntervalSchedule::every(Duration::fromMinutes(5)),
fn() => $this->connectionPool->cleanupExpired()
);
Sicherheit
Channel-Isolation
- Channels sind nicht authentifiziert - verwende Server-Side Filtering
- User-spezifische Channels müssen Server-Side validiert werden
#[Route('/sse/events/{channel}')]
public function events(string $channel, HttpRequest $request): StreamedResponse
{
// Validiere dass User auf Channel zugreifen darf
if (str_starts_with($channel, 'user:')) {
$userId = substr($channel, 5);
if ($userId !== $request->user->id) {
throw new UnauthorizedException();
}
}
// ... rest of implementation
}
XSS Prevention
- Alle HTML aus SSE-Updates wird escaped
- Verwende DOMPurify für User-Generated Content
Rate Limiting
- Implementiere Rate Limiting für SSE-Connections
- Limit: Max 5 Connections pro User
Framework Integration
Mit Event System
// EventDispatcher → SseEventBroadcaster → SseConnectionPool
$this->eventDispatcher->dispatch(new ComponentUpdatedEvent(...));
Mit Queue System
// Für verzögerte Broadcasts
$this->queue->push(new BroadcastSseEventJob(
channel: $channel,
event: $event
));
Mit Cache System
// Cache letzte Events für neue Connections (Replay)
$this->cache->remember(
CacheKey::fromString("sse:last-events:{$channel}"),
fn() => $this->getLastEvents($channel),
Duration::fromMinutes(5)
);
Zusammenfassung
Das SSE-System bietet:
✅ Echtzeit-Updates ohne Polling oder WebSockets ✅ Zero-Config für Components mit getSseChannel() ✅ Auto-Reconnection mit intelligenter Backoff-Strategie ✅ Channel-Based Routing für flexible Update-Strategien ✅ Event-Integration für nahtlose Backend-Updates ✅ Connection Pooling für Performance-Optimierung ✅ Framework-Compliant mit Value Objects und Readonly Classes
Next Steps:
- Siehe SSE Integration Guide für praktische Implementierung
- Siehe LiveComponents System für Component-Entwicklung