"""Redis bridge — device registry + pub/sub routing. All inter-service communication passes through Redis: - Device registry: HSET/HDEL ws:devices:{user_id} - Outbound frames: Subscribe ws:out:{user_id} - Chat requests: Publish chat:request:{user_id} - Batch requests: Publish batch:request:{user_id} - Tool results: LPUSH tool:result:{call_id} """ from __future__ import annotations import json import logging from shared.redis import ( batch_request_channel, chat_request_channel, device_key, redis_client, tool_result_key, ws_out_channel, ) logger = logging.getLogger(__name__) # Instance ID for this gateway replica (set on startup) _GATEWAY_ID: str = "" def set_gateway_id(gid: str) -> None: global _GATEWAY_ID _GATEWAY_ID = gid def get_gateway_id() -> str: return _GATEWAY_ID # ── Device Registry ────────────────────────────────────────────────── async def register_device(user_id: str, device_id: str) -> None: """Register a connected device in Redis.""" key = device_key(user_id) await redis_client.hset(key, mapping={ "device_id": device_id, "gateway_id": _GATEWAY_ID, }) logger.info("redis_bridge: registered user=%s device=%s gateway=%s", user_id, device_id, _GATEWAY_ID) async def unregister_device(user_id: str) -> None: """Remove device registration from Redis.""" key = device_key(user_id) await redis_client.delete(key) logger.info("redis_bridge: unregistered user=%s", user_id) async def is_device_online(user_id: str) -> bool: """Check if a device is registered.""" key = device_key(user_id) return await redis_client.exists(key) > 0 # ── Frame Routing ──────────────────────────────────────────────────── async def publish_chat_request(user_id: str, frame: dict) -> None: """Forward a chat request frame to the Chat Service via Redis.""" channel = chat_request_channel(user_id) await redis_client.publish(channel, json.dumps(frame)) logger.debug("redis_bridge: published chat_request user=%s", user_id) async def publish_batch_request(user_id: str, frame: dict) -> None: """Forward a batch request frame to the Batch Agent Service via Redis.""" channel = batch_request_channel(user_id) await redis_client.publish(channel, json.dumps(frame)) logger.debug("redis_bridge: published batch_request user=%s", user_id) async def push_tool_result(call_id: str, result: dict) -> None: """Push a tool_result to the Redis list for the waiting service. Chat/Batch services do BRPOP on this key with a 30s timeout. """ key = tool_result_key(call_id) await redis_client.lpush(key, json.dumps(result)) # Auto-expire after 60s to prevent stale keys await redis_client.expire(key, 60) logger.debug("redis_bridge: pushed tool_result call_id=%s", call_id) async def subscribe_outbound(user_id: str): """Return an async pubsub subscription for frames to send to Electron. Chat/Batch services publish to ws:out:{user_id} and this gateway forwards them to the connected WebSocket. """ channel = ws_out_channel(user_id) pubsub = redis_client.pubsub() await pubsub.subscribe(channel) return pubsub