"""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