Files
hms-mediaengine/apps/control_server/hms_control_server/renderer_link.py
T
HMS MediaEngine Agent b2610e35e5 Phase 2 abgeschlossen: Renderer-State-Sync über echte IPC (§6.2, §6.4)
- RemoteStateMirror (Renderer): Snapshot-Pflicht nach (Re-)Connect,
  Deltas mit neu/geaendert/geloescht, Deltas vor erstem Snapshot werden
  abgelehnt (Re-Sync), Duplikate/Veraltete idempotent
- RendererStateLink (Control Core): connect_and_sync mit Pflicht-Snapshot,
  sync_if_changed versendet Deltas seit letzter gesendeter Revision,
  Reconnect loest immer neuen Snapshot aus (kein halber Zustand)
- End-to-End-Integrationstests ueber echtes TCP-Loopback ohne Mocks:
  Snapshot->Delta-Kette, Reconnect-Re-Sync, Delta-vor-Snapshot-Ablehnung,
  Duplikat-No-Op
- Gesamtsuite 297 gruen, Ruff gruen
2026-09-11 01:33:47 +02:00

91 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""RendererStateLink auf der Control-Core-Seite (PLAN.md §6.2, §6.4).
Verbindet ProjectStateStore (autoritativ) mit dem Renderer über IPC:
- connect_and_sync(): Handshake + vollständiger Snapshot (§6.2 Pflicht)
- sync_if_changed(): Delta seit letzter gesendeter Revision
- Reconnect: immer neuer Snapshot, danach erst wieder Deltas (§6.2)
Der Link kennt den letzten beim Renderer angekommenen Zustand und
berechnet Deltas daraus keine halben Zustände (§11.4, §6.4).
"""
from __future__ import annotations
from hms_persistence.state_store import ProjectStateStore
from hms_protocol import Envelope, IpcClient, MessageType
class RendererStateLink:
"""Synchronisiert den Showzustand Control Core → Renderer über IPC."""
def __init__(self, client: IpcClient, store: ProjectStateStore) -> None:
self._client = client
self._store = store
self._last_sent_revision = 0
self._renderer_known_state: dict[str, float] = {}
@property
def last_sent_revision(self) -> int:
return self._last_sent_revision
@property
def in_sync(self) -> bool:
"""True, wenn der Renderer die aktuelle Revision besitzt."""
return self._last_sent_revision == self._store.state_revision
# ---------- Verbindung (§6.2) ----------
async def connect_and_sync(self) -> None:
"""Verbindung aufbauen und vollständigen Snapshot senden.
Nach jedem (Re-)Connect wird immer zuerst der vollständige Snapshot
übertragen; Deltas folgen erst danach (§6.2: „Re-Sync nach
Reconnect", „Deltas erst nach erfolgreichem Re-Sync akzeptiert").
"""
await self._client.connect()
await self.send_full_snapshot()
async def send_full_snapshot(self) -> int:
"""Sendet den vollständigen Zustand; liefert die gesendete Revision."""
snap = self._store.snapshot()
self._last_sent_revision = snap["state_revision"]
self._renderer_known_state = dict(snap["values"])
envelope = Envelope(
type=MessageType.SNAPSHOT,
revision=snap["state_revision"],
payload=snap,
)
await self._client.send(envelope)
return snap["state_revision"]
# ---------- Delta-Versand (§6.4) ----------
async def sync_if_changed(self) -> bool:
"""Sendet ein Delta, falls sich die Revision seit dem letzten Versand
geändert hat. Rückgabe: True, wenn etwas gesendet wurde.
Das Delta wird aus dem zuletzt bekannten Renderer-Zustand berechnet
(neu/geändert/gelöscht); die Semantik folgt StateDelta (§6.4).
"""
if self._store.state_revision == self._last_sent_revision:
return False # nichts Neues
delta = self._store.delta_since(
self._last_sent_revision, self._renderer_known_state
)
if delta is None:
return False
# Buchhaltung: was weiß der Renderer ab jetzt?
for path, value in delta.changes.items():
if value is None:
self._renderer_known_state.pop(path, None)
else:
self._renderer_known_state[path] = value
self._last_sent_revision = delta.state_revision
envelope = Envelope(
type=MessageType.EVENT,
revision=delta.state_revision,
payload=delta.to_dict(),
)
await self._client.send(envelope)
return True