proto-socket/python/proto_socket/base_client.py
toki d0754a353a refactor: rename toki_socket to proto_socket across all languages
- Rename package/module from toki_socket to proto_socket in Dart, Kotlin, Python
- Update crosstest implementations to use renamed packages
- Add new proto_socket skill, deprecate add-toki-socket-crosstest-language skill
- Update domain rules for all languages
- Update documentation (README, PORTING_GUIDE, PROTOCOL, VERSIONING)
2026-05-02 07:19:12 +09:00

103 lines
3.3 KiB
Python

from __future__ import annotations
import asyncio
import inspect
from collections.abc import Awaitable, Callable
from contextlib import suppress
from typing import Any
from proto_socket.communicator import Communicator
from proto_socket.packets.message_common_pb2 import HeartBeat
class _HbTimer:
def __init__(self, delay: float, callback: Callable[[], Awaitable[None]]) -> None:
self._task = asyncio.create_task(self._run(delay, callback))
async def _run(self, delay: float, callback: Callable[[], Awaitable[None]]) -> None:
await asyncio.sleep(delay)
await callback()
def cancel(self) -> None:
self._task.cancel()
class BaseClient:
def __init__(
self,
interval_sec: int,
wait_sec: int,
do_close: Callable[[], Awaitable[None]],
) -> None:
self.communicator = Communicator()
self._interval_sec = interval_sec
self._wait_sec = wait_sec
self._do_close = do_close
self._close_called = False
self._close_lock = asyncio.Lock()
self._disconnect_listeners: list[Callable[[Any], Any]] = []
self._hb_timer: _HbTimer | None = None
self._waiting_hb_response = False
def _init_base(self) -> None:
self._close_lock = asyncio.Lock()
async def close(self) -> None:
async with self._close_lock:
if self._close_called:
return
self._close_called = True
self.communicator.shutdown()
self._stop_heartbeat()
await self._do_close()
await self._notify_disconnected()
def add_disconnect_listener(self, fn: Callable[[Any], Any]) -> None:
self._disconnect_listeners.append(fn)
def remove_disconnect_listeners(self) -> None:
self._disconnect_listeners.clear()
async def _notify_disconnected(self) -> None:
listeners = list(self._disconnect_listeners)
self._disconnect_listeners.clear()
for listener in listeners:
result = listener(self)
if inspect.isawaitable(result):
await result
async def _on_disconnected(self) -> None:
await self.close()
async def _send_heartbeat(self) -> None:
if not self.communicator.is_alive() or self._interval_sec <= 0:
return
self._stop_heartbeat()
self._hb_timer = _HbTimer(self._interval_sec, self._heartbeat_interval_elapsed)
async def _heartbeat_interval_elapsed(self) -> None:
if not self.communicator.is_alive():
return
self._waiting_hb_response = True
with suppress(Exception):
await self.communicator.send(HeartBeat())
self._stop_heartbeat()
if self._wait_sec > 0:
self._hb_timer = _HbTimer(self._wait_sec, self._heartbeat_wait_elapsed)
async def _heartbeat_wait_elapsed(self) -> None:
if self.communicator.is_alive():
await self._on_disconnected()
async def _on_heartbeat(self) -> None:
if self._waiting_hb_response:
self._waiting_hb_response = False
return
with suppress(Exception):
await self.communicator.send(HeartBeat())
def _stop_heartbeat(self) -> None:
if self._hb_timer is not None:
self._hb_timer.cancel()
self._hb_timer = None