1"""Durable channel reconciliation at gateway boot.
2
3When a host binds a :class:`RelayChannelStoreProtocol`, the gateway
4loads every durable row and merges it over the static configuration by
5name: store rows override same-named static channels and store-only
6channels are appended. An empty store leaves the static table
7byte-for-byte untouched, so the default configuration behavior is
8preserved when no durable store is bound.
9"""
10
11from __future__ import annotations
12
13from typing import TYPE_CHECKING
14
15from lexigram.contracts.ai.relay.gateway import RelayChannel
16from lexigram.logging import get_logger
17
18if TYPE_CHECKING:
19 from lexigram.contracts.ai.relay.store import RelayChannelStoreProtocol
20
21logger = get_logger(__name__)
22
23__all__ = ["DurableChannelLoader"]
24
25
26class DurableChannelLoader:
27 """Merge durable channel rows over a static channel table.
28
29 Args:
30 store: The durable store bound at boot; rows are read once.
31 """
32
33 def __init__(self, store: RelayChannelStoreProtocol) -> None:
34 self._store = store
35
36 async def load(self, static: tuple[RelayChannel, ...]) -> tuple[RelayChannel, ...]:
37 """Return the static table merged with durable store rows.
38
39 Store rows override same-named static channels; store-only
40 channels keep the store order appended after the static
41 channels. An empty store returns *static* unchanged.
42
43 Args:
44 static: The configured channel table.
45
46 Returns:
47 The merged channel tuple, or *static* when the store is
48 empty.
49
50 Raises:
51 ValueError: The store returned duplicate channel names.
52 """
53 rows = await self._store.list_channels()
54 if not rows:
55 return static
56 durable: dict[str, RelayChannel] = {}
57 for snap in rows:
58 name = snap.channel.name
59 if name in durable:
60 raise ValueError(f"duplicate channel name from durable store: {name!r}")
61 durable[name] = snap.channel
62 merged = {ch.name: ch for ch in static}
63 merged.update(durable)
64 return tuple(merged.values())