91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
|
|
"""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
|