"""NixMsg 同步客户端。""" from __future__ import annotations import logging import random import threading import time import urllib.error import urllib.request from collections import OrderedDict from dataclasses import dataclass, field from typing import Any, Optional from .errors import ClosedError, NixMsgError, NotConnectedError from .protocol import dumps, down_topic, loads, normalize_mqtt_ws_url, register_url_from_connect, up_topic from .transport import ConnectParams, FakeTransport, PahoTransport, Transport from .types import ( BACKOFF_INITIAL_S, BACKOFF_JITTER, BACKOFF_MAX_S, CLIENT_NAME, CONNECT_TIMEOUT_S, DEDUP_CAPACITY, DEFAULT_MAX_BODY, DEFAULT_MAX_FRAME, DEFAULT_MAX_META, INFLIGHT_LIMIT, MIN_MAX_RECEIVE, SEND_QUEUE_LIMIT, STABLE_RESET_S, Body, ConnectionEvent, ConnectionHandler, ConnectionState, GroupEvent, GroupEventHandler, HelloLimits, IncomingMessage, MessageHandler, PresenceEvent, PresenceHandler, Receipt, ReceiptHandler, RegisterOptions, RegisterResult, RecallResult, RevokedEvent, RevokedHandler, SendOptions, SendResult, SessionHandler, Target, ) from .uuid7 import new_uuid7 log = logging.getLogger("nixmsg") @dataclass class _PendingReq: rid: str frame: dict[str, Any] event: threading.Event = field(default_factory=threading.Event) response: Optional[dict[str, Any]] = None error: Optional[BaseException] = None is_send: bool = False message_id: str = "" @dataclass class _SendItem: message_id: str frame: dict[str, Any] # 已含 send_at_ms,重交不改 pending: _PendingReq class _DedupState: DELIVERED = "delivered" # 已交应用未确认 ACKED = "acked" class Client: def __init__( self, *, transport: Optional[Transport] = None, auto_ack: bool = True, max_receive_bytes: int = DEFAULT_MAX_FRAME, client_name: str = CLIENT_NAME, connect_timeout_s: float = CONNECT_TIMEOUT_S, ) -> None: self._transport: Transport = transport or PahoTransport() self._auto_ack = auto_ack self._max_receive_bytes = max(MIN_MAX_RECEIVE, max_receive_bytes) self._client_name = client_name self._connect_timeout_s = connect_timeout_s self._session_handler: Optional[SessionHandler] = None self._message_handler: Optional[MessageHandler] = None self._receipt_handler: Optional[ReceiptHandler] = None self._revoked_handler: Optional[RevokedHandler] = None self._presence_handler: Optional[PresenceHandler] = None self._group_handler: Optional[GroupEventHandler] = None self._connection_handler: Optional[ConnectionHandler] = None self._lock = threading.RLock() self._cb_lock = threading.Lock() # 回调串行 self._state = ConnectionState.OFFLINE self._stop_reconnect = False self._closed = False self._user_close = False self._url = "" self._endpoint_id = "" self._password: Optional[str] = None self._session_token: Optional[str] = None self._use_token = False self._use_tcp = False self._limits = HelloLimits() self._clock_skew_ms = 0 self._online_since = 0.0 self._backoff_s = BACKOFF_INITIAL_S self._rid_seq = 0 self._pending: dict[str, _PendingReq] = {} self._send_queue: list[_SendItem] = [] self._inflight_sends = 0 self._dedup: OrderedDict[str, str] = OrderedDict() self._receipt_seen: OrderedDict[str, bool] = OrderedDict() self._watch_ids: Optional[list[str]] = None self._watch_all = False self._conn_event = threading.Event() self._handshake_error: Optional[BaseException] = None self._worker: Optional[threading.Thread] = None self._wake = threading.Event() self._want_connected = False self._transport.set_handlers(self._on_transport_connected, self._on_transport_disconnected, self._on_down) # ---- 回调注册 ---- def on_session(self, handler: SessionHandler) -> None: self._session_handler = handler def on_message(self, handler: MessageHandler) -> None: self._message_handler = handler def on_receipt(self, handler: ReceiptHandler) -> None: self._receipt_handler = handler def on_revoked(self, handler: RevokedHandler) -> None: self._revoked_handler = handler def on_presence(self, handler: PresenceHandler) -> None: self._presence_handler = handler def on_group_event(self, handler: GroupEventHandler) -> None: self._group_handler = handler def on_connection(self, handler: ConnectionHandler) -> None: self._connection_handler = handler # ---- 连接 ---- def connect( self, url: str, endpoint_id: str, *, password: Optional[str] = None, session_token: Optional[str] = None, use_tcp: bool = False, wait: bool = True, ) -> None: if password is None and session_token is None: raise ValueError("需要 password 或 session_token") with self._lock: if self._closed: raise ClosedError() self._url = normalize_mqtt_ws_url(url) if not use_tcp else url self._endpoint_id = endpoint_id self._password = password self._session_token = session_token self._use_token = session_token is not None and password is None self._use_tcp = use_tcp self._stop_reconnect = False self._user_close = False self._want_connected = True self._handshake_error = None self._conn_event.clear() self._set_state(ConnectionState.CONNECTING) if self._worker is None or not self._worker.is_alive(): self._worker = threading.Thread(target=self._run_loop, name="nixmsg-client", daemon=True) self._worker.start() self._wake.set() if wait: if not self._conn_event.wait(self._connect_timeout_s + 5): raise NixMsgError("busy", "连接超时") err = self._handshake_error if err: raise err if self._state not in (ConnectionState.ONLINE,): if self._state == ConnectionState.AUTH_FAILED: raise NixMsgError( getattr(self, "_auth_reason", "bad_credentials"), "认证失败", ) if self._state == ConnectionState.KICKED: raise NixMsgError("taken_over", "会话被接管") raise NixMsgError("busy", f"连接未成功: {self._state.value}") def close(self) -> None: with self._lock: self._user_close = True self._want_connected = False self._stop_reconnect = True self._closed = True self._fail_all_pending(ClosedError()) self._set_state(ConnectionState.OFFLINE) try: self._transport.disconnect() except Exception: pass self._wake.set() def logout(self) -> None: try: self._request({"type": "self.logout"}, wait=True) except Exception: pass with self._lock: self._stop_reconnect = True self._want_connected = False self._session_token = None self._fail_all_pending(NixMsgError("auth_failed", "已退出登录")) try: self._transport.disconnect() except Exception: pass self._set_state(ConnectionState.OFFLINE) self._wake.set() # ---- 发送 / 确认 ---- def send(self, to: Target, body: Body | str | bytes, options: Optional[SendOptions] = None) -> SendResult: options = options or SendOptions() if isinstance(body, str): body = Body(data=body) elif isinstance(body, bytes): import base64 body = Body(data=base64.b64encode(body).decode("ascii"), enc="base64") if options.content_type: body.content_type = options.content_type with self._lock: if self._closed: raise ClosedError() if len(self._send_queue) >= SEND_QUEUE_LIMIT: raise NixMsgError("quota_exceeded", "发送队列已满") max_body = self._limits.max_body_bytes or DEFAULT_MAX_BODY max_meta = self._limits.max_meta_bytes or DEFAULT_MAX_META max_frame = self._limits.max_frame_bytes or DEFAULT_MAX_FRAME if body.decoded_size() > max_body: raise NixMsgError("body_too_large", "正文超限") meta = options.meta or {} meta_bytes = dumps({"meta": meta}) # 近似;真正检查序列化后 meta 对象 # 精确:meta 单独序列化 meta_raw = dumps(meta) if meta else b"{}" if len(meta_raw) > max_meta: raise NixMsgError("meta_too_large", "自定义字段超限") msg_id = options.message_id or new_uuid7() frame: dict[str, Any] = { "v": 1, "type": "send", "id": msg_id, "to": to.to_dict(), "body": body.to_dict(), } if meta: frame["meta"] = meta # send_at_ms 在入队时固定,重交不重算 if options.send_at_ms is not None: frame["send_at_ms"] = int(options.send_at_ms) elif options.send_at is not None: # 本机语义时间 + 偏差 local_ms = int(options.send_at * 1000) if options.send_at < 1e12 else int(options.send_at) frame["send_at_ms"] = local_ms + self._clock_skew_ms elif options.delay_ms is not None: frame["delay_ms"] = int(options.delay_ms) if options.keep: offline: dict[str, Any] = {"keep": True} if options.ttl_seconds is not None: offline["ttl_seconds"] = int(options.ttl_seconds) frame["offline"] = offline if not options.receipt: frame["receipt"] = False if options.talk_password: frame["talk_password"] = options.talk_password # 帧大小本地检查(含 rid 占位) probe = dict(frame) probe["rid"] = "0" * 8 raw = dumps(probe) if len(raw) > max_frame: raise NixMsgError("frame_too_large", "整帧超限") pending = _PendingReq(rid="", frame=frame, is_send=True, message_id=msg_id) item = _SendItem(message_id=msg_id, frame=frame, pending=pending) self._send_queue.append(item) self._wake.set() if not pending.event.wait(timeout=None if self._state == ConnectionState.ONLINE else 3600): raise NixMsgError("busy", "发送等待中断") if pending.error: raise pending.error assert pending.response is not None data = pending.response.get("data") or {} return SendResult(id=data.get("id", msg_id), send_at_ms=int(data.get("send_at_ms", 0)), state=str(data.get("state", ""))) def ack(self, message: IncomingMessage) -> None: self._send_ack(message.from_id, message.id, mark_acked=True) # ---- 其余接口 ---- def recall(self, message_id: str) -> RecallResult: resp = self._request({"type": "recall", "id": message_id}) data = resp.get("data") or {} return RecallResult( result=str(data.get("result", "")), recalled=int(data.get("recalled", 0)), accepted=int(data.get("accepted", 0)), other=int(data.get("other", 0)), ) def status(self, message_id: str, cursor: str = "", limit: int = 100) -> dict[str, Any]: return self._request({"type": "status", "id": message_id, "cursor": cursor, "limit": limit}) def unlock(self, endpoint_id: str, talk_password: str) -> dict[str, Any]: return self._request({"type": "unlock", "endpoint_id": endpoint_id, "talk_password": talk_password}) def presence(self, ids: list[str]) -> dict[str, Any]: return self._request({"type": "presence.get", "ids": ids}) def directory(self, cursor: str = "", query: str = "", limit: int = 100) -> dict[str, Any]: return self._request({"type": "directory.list", "cursor": cursor, "limit": limit, "query": query}) def watch_presence(self, ids: Optional[list[str]] = None, *, all: bool = False) -> dict[str, Any]: with self._lock: self._watch_ids = list(ids) if ids is not None else None self._watch_all = all frame: dict[str, Any] = {"type": "presence.watch", "all": all} if ids is not None: frame["ids"] = ids return self._request(frame) def get_self(self) -> dict[str, Any]: return self._request({"type": "self.get"}) def update_self(self, *, name: Optional[str] = None, default_delay_ms: Optional[int] = None) -> dict[str, Any]: frame: dict[str, Any] = {"type": "self.update"} if name is not None: frame["name"] = name if default_delay_ms is not None: frame["default_delay_ms"] = default_delay_ms return self._request(frame) def set_talk_password(self, talk_password: str) -> dict[str, Any]: return self._request({"type": "self.talk_password", "talk_password": talk_password}) def change_login_password(self, old_password: str, new_password: str) -> dict[str, Any]: resp = self._request({"type": "self.login_password", "old_password": old_password, "new_password": new_password}) data = resp.get("data") or {} token = data.get("session_token") if token: with self._lock: self._session_token = token self._use_token = True self._fire_session(token) return resp def group_create(self, name: str, members: list[dict[str, str]], group_id: str = "") -> dict[str, Any]: frame: dict[str, Any] = {"type": "group.create", "name": name, "members": members} if group_id: frame["id"] = group_id else: frame["id"] = "" return self._request(frame) def group_add(self, group_id: str, members: list[dict[str, str]]) -> dict[str, Any]: return self._request({"type": "group.add", "group_id": group_id, "members": members}) def group_remove(self, group_id: str, endpoint_id: str) -> dict[str, Any]: return self._request({"type": "group.remove", "group_id": group_id, "endpoint_id": endpoint_id}) def group_leave(self, group_id: str) -> dict[str, Any]: return self._request({"type": "group.leave", "group_id": group_id}) def group_transfer(self, group_id: str, endpoint_id: str) -> dict[str, Any]: return self._request({"type": "group.transfer", "group_id": group_id, "endpoint_id": endpoint_id}) def group_rename(self, group_id: str, name: str) -> dict[str, Any]: return self._request({"type": "group.rename", "group_id": group_id, "name": name}) def group_dissolve(self, group_id: str) -> dict[str, Any]: return self._request({"type": "group.dissolve", "group_id": group_id}) def group_list(self, cursor: str = "", limit: int = 100) -> dict[str, Any]: return self._request({"type": "group.list", "cursor": cursor, "limit": limit}) def group_get(self, group_id: str, cursor: str = "", limit: int = 100) -> dict[str, Any]: return self._request({"type": "group.get", "group_id": group_id, "cursor": cursor, "limit": limit}) @staticmethod def register(url: str, registration_code: str, options: Optional[RegisterOptions] = None) -> RegisterResult: options = options or RegisterOptions() reg_url = register_url_from_connect(url) body = { "registration_code": registration_code, "id": options.id or "", "login_password": options.login_password or "", "name": options.name or "", "talk_password": options.talk_password or "", } raw = dumps(body) req = urllib.request.Request( reg_url, data=raw, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=30) as resp: data = loads(resp.read()) except urllib.error.HTTPError as e: try: data = loads(e.read()) except Exception: raise NixMsgError("bad_request", f"HTTP {e.code}") from e err = data.get("error") or {} raise NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", ""))) from e if not data.get("ok"): err = data.get("error") or {} raise NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", ""))) d = data.get("data") or {} return RegisterResult(id=str(d.get("id", "")), login_password=d.get("login_password")) # ---- 内部:连接循环 ---- def _run_loop(self) -> None: while True: with self._lock: if self._closed and not self._want_connected: return want = self._want_connected and not self._stop_reconnect state = self._state if not want: self._wake.wait(0.5) self._wake.clear() continue if state == ConnectionState.ONLINE: self._pump_sends() # 稳定 60 秒后恢复退避 if self._online_since and time.monotonic() - self._online_since >= STABLE_RESET_S: self._backoff_s = BACKOFF_INITIAL_S self._wake.wait(0.2) self._wake.clear() continue # 尝试连接 try: self._attempt_connect() except Exception as e: log.debug("connect attempt failed: %s", e) with self._lock: if self._state == ConnectionState.ONLINE: continue if self._stop_reconnect or not self._want_connected: continue delay = self._jitter(self._backoff_s) self._backoff_s = min(BACKOFF_MAX_S, self._backoff_s * 2) self._set_state(ConnectionState.RECONNECTING) self._wake.wait(delay) self._wake.clear() def _attempt_connect(self) -> None: with self._lock: if self._stop_reconnect or not self._want_connected: return self._set_state(ConnectionState.CONNECTING if self._state == ConnectionState.OFFLINE else ConnectionState.RECONNECTING) self._handshake_error = None url = self._url eid = self._endpoint_id if self._session_token: cred = self._session_token using_token = True else: cred = self._password or "" using_token = False self._connecting_with_token = using_token use_tcp = self._use_tcp timeout = self._connect_timeout_s self._conn_event.clear() params = ConnectParams( url=url, client_id=eid, username=eid, password=cred, clean_start=True, session_expiry=0, timeout_s=timeout, use_tcp=use_tcp, ) self._transport.connect(params) # 等待握手完成或失败 ok = self._conn_event.wait(timeout) if not ok: try: self._transport.disconnect() except Exception: pass with self._lock: self._handshake_error = NixMsgError("busy", "连接超时") return def _on_transport_connected(self) -> None: try: topic = down_topic(self._endpoint_id) self._transport.subscribe(topic) rid = self._next_rid() hello = { "v": 1, "type": "hello", "rid": rid, "max_receive_bytes": self._max_receive_bytes, "client": self._client_name, } t0 = time.time() pending = _PendingReq(rid=rid, frame=hello) with self._lock: self._pending[rid] = pending self._transport.publish(up_topic(self._endpoint_id), dumps(hello)) if not pending.event.wait(self._connect_timeout_s): raise NixMsgError("busy", "握手超时") if pending.error: raise pending.error assert pending.response is not None if not pending.response.get("ok"): err = (pending.response.get("error") or {}) raise NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", ""))) t1 = time.time() data = pending.response.get("data") or {} limits = HelloLimits( server_time_ms=int(data.get("server_time_ms", 0)), server_version=str(data.get("server_version", "")), max_body_bytes=int(data.get("max_body_bytes", DEFAULT_MAX_BODY)), max_meta_bytes=int(data.get("max_meta_bytes", DEFAULT_MAX_META)), max_frame_bytes=int(data.get("max_frame_bytes", DEFAULT_MAX_FRAME)), max_ttl_seconds=int(data.get("max_ttl_seconds", 2592000)), max_schedule_seconds=int(data.get("max_schedule_seconds", 31536000)), ack_timeout_seconds=int(data.get("ack_timeout_seconds", 300)), session_token=str(data.get("session_token", "")), ) skew = limits.server_time_ms - int(((t0 + t1) / 2) * 1000) with self._lock: self._limits = limits self._clock_skew_ms = skew self._online_since = time.monotonic() self._handshake_error = None self._set_state(ConnectionState.ONLINE) token = limits.session_token if token: with self._lock: self._session_token = token self._use_token = True self._fire_session(token) # 重连后恢复 presence.watch if self._watch_all or self._watch_ids is not None: try: frame: dict[str, Any] = {"type": "presence.watch", "all": self._watch_all} if self._watch_ids is not None: frame["ids"] = self._watch_ids self._request(frame, wait=False) except Exception: pass self._conn_event.set() self._wake.set() except Exception as e: with self._lock: self._handshake_error = e try: self._transport.disconnect() except Exception: pass self._conn_event.set() def _on_transport_disconnected(self, reason: Optional[str], stop: bool) -> None: with self._lock: was_online = self._state == ConnectionState.ONLINE using_token = getattr(self, "_connecting_with_token", self._use_token) if reason == "taken_over": self._stop_reconnect = True self._want_connected = False self._fail_all_pending(NixMsgError("taken_over", "会话被接管")) self._set_state(ConnectionState.KICKED, "taken_over") self._conn_event.set() return if stop or reason in ("session_invalid", "bad_credentials", "banned"): # 令牌被拒 -> session_invalid;密码被拒 -> bad_credentials if reason in ("bad_credentials", "session_invalid", "banned") or stop: auth_reason = reason or "bad_credentials" if auth_reason == "bad_credentials" and using_token: auth_reason = "session_invalid" if auth_reason == "banned": auth_reason = "session_invalid" if using_token else "bad_credentials" self._auth_reason = auth_reason self._stop_reconnect = True self._want_connected = False self._fail_all_pending(NixMsgError(auth_reason, "认证失败")) self._handshake_error = NixMsgError(auth_reason, "认证失败") self._set_state(ConnectionState.AUTH_FAILED, auth_reason) self._conn_event.set() return # 网络 / busy:继续重连 if self._user_close or self._closed: self._set_state(ConnectionState.OFFLINE) self._conn_event.set() return if was_online or self._state in (ConnectionState.CONNECTING, ConnectionState.RECONNECTING): self._set_state(ConnectionState.RECONNECTING, reason or "network") self._conn_event.set() self._wake.set() def _on_down(self, payload: bytes) -> None: try: frame = loads(payload) except Exception: return ftype = frame.get("type") if ftype == "resp": rid = str(frame.get("rid", "")) with self._lock: pending = self._pending.pop(rid, None) if pending: pending.response = frame if pending.is_send: with self._lock: self._inflight_sends = max(0, self._inflight_sends - 1) # rate_limited 重交:清状态后不 set event err = (frame.get("error") or {}) if not frame.get("ok") else {} if not frame.get("ok") and str(err.get("code")) == "rate_limited": with self._lock: pending.rid = "" pending.response = None pending.error = None pending.event.clear() self._wake.set() return # 从发送队列移除 with self._lock: self._send_queue = [it for it in self._send_queue if it.pending is not pending] if not frame.get("ok"): pending.error = NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", ""))) pending.event.set() self._wake.set() else: pending.event.set() return if ftype == "msg": self._handle_msg(frame) return if ftype == "receipt": self._handle_receipt(frame) return if ftype == "revoked": self._handle_revoked(frame) return if ftype == "presence": self._fire_presence( PresenceEvent(id=str(frame.get("id", "")), online=bool(frame.get("online")), at_ms=int(frame.get("at_ms", 0))) ) return if ftype == "group_event": self._fire_group( GroupEvent( group_id=str(frame.get("group_id", "")), event=str(frame.get("event", "")), endpoint_id=str(frame.get("endpoint_id", "")), at_ms=int(frame.get("at_ms", 0)), ) ) return if ftype == "fatal": reason = str(frame.get("reason", "protocol")) with self._lock: self._stop_reconnect = True self._want_connected = False self._fail_all_pending(NixMsgError("fatal", reason)) self._set_state(ConnectionState.AUTH_FAILED, reason) try: self._transport.disconnect() except Exception: pass def _handle_msg(self, frame: dict[str, Any]) -> None: mid = str(frame.get("id", "")) from_id = str(frame.get("from", "")) key = f"{from_id}\0{mid}" with self._lock: st = self._dedup.get(key) if st == _DedupState.ACKED: # 已确认再到达:再 ack,不交应用 pass elif st == _DedupState.DELIVERED: # 已交未确认:忽略 return else: st = None if st == _DedupState.ACKED: self._send_ack(from_id, mid, mark_acked=True) return to_raw = frame.get("to") or {} msg = IncomingMessage( id=mid, from_id=from_id, to=Target(kind=str(to_raw.get("kind", "endpoint")), id=str(to_raw.get("id", ""))), body=Body( data=str((frame.get("body") or {}).get("data", "")), enc=str((frame.get("body") or {}).get("enc", "utf8")), content_type=(frame.get("body") or {}).get("content_type"), ), send_at_ms=int(frame.get("send_at_ms", 0)), meta=dict(frame.get("meta") or {}), ) with self._lock: self._dedup_put(key, _DedupState.DELIVERED) if not self._message_handler: if self._auto_ack: self._send_ack(from_id, mid, mark_acked=True) return try: with self._cb_lock: self._message_handler(msg) except Exception: log.exception("on_message 回调错误,等待重推") with self._lock: self._dedup.pop(key, None) return if self._auto_ack: self._send_ack(from_id, mid, mark_acked=True) def _handle_receipt(self, frame: dict[str, Any]) -> None: rid = str(frame.get("receipt_id", "")) with self._lock: if rid in self._receipt_seen: return self._receipt_seen[rid] = True while len(self._receipt_seen) > DEDUP_CAPACITY: self._receipt_seen.popitem(last=False) receipt = Receipt( receipt_id=rid, id=str(frame.get("id", "")), endpoint_id=str(frame.get("endpoint_id", "")), state=str(frame.get("state", "")), reason=str(frame.get("reason", "")), at_ms=int(frame.get("at_ms", 0)), ) self._fire_receipt(receipt) # 自动 receipt_ack try: self._request({"type": "receipt_ack", "receipt_id": rid}, wait=False) except Exception: pass def _handle_revoked(self, frame: dict[str, Any]) -> None: mid = str(frame.get("id", "")) from_id = str(frame.get("from", "")) key = f"{from_id}\0{mid}" with self._lock: st = self._dedup.get(key) if st == _DedupState.ACKED: return if st is None: # 还没交给应用:直接丢弃 return # 已交未确认:发撤回事件并不再确认 self._dedup.pop(key, None) self._fire_revoked(RevokedEvent(id=mid, from_id=from_id, reason=str(frame.get("reason", "")))) def _send_ack(self, from_id: str, message_id: str, *, mark_acked: bool) -> None: try: resp = self._request({"type": "ack", "from": from_id, "id": message_id}) except Exception: return key = f"{from_id}\0{message_id}" data = resp.get("data") or {} result = data.get("result", "accepted") if result != "accepted": with self._lock: self._dedup.pop(key, None) self._fire_revoked(RevokedEvent(id=message_id, from_id=from_id, reason=str(result))) return if mark_acked: with self._lock: self._dedup_put(key, _DedupState.ACKED) def _pump_sends(self) -> None: while True: with self._lock: if self._state != ConnectionState.ONLINE: return if self._inflight_sends >= INFLIGHT_LIMIT: return item = None for it in self._send_queue: if it.pending.rid == "" and it.pending.response is None and it.pending.error is None: item = it break if item is None: return rid = self._next_rid_locked() item.pending.rid = rid frame = dict(item.frame) frame["rid"] = rid self._pending[rid] = item.pending self._inflight_sends += 1 topic = up_topic(self._endpoint_id) try: self._transport.publish(topic, dumps(frame)) except Exception as e: with self._lock: self._pending.pop(rid, None) item.pending.rid = "" self._inflight_sends = max(0, self._inflight_sends - 1) item.pending.error = e item.pending.event.set() self._send_queue = [it for it in self._send_queue if it is not item] continue def _request(self, frame: dict[str, Any], *, wait: bool = True) -> dict[str, Any]: with self._lock: if self._closed: raise ClosedError() if self._state != ConnectionState.ONLINE: raise NotConnectedError() rid = self._next_rid_locked() body = dict(frame) body["v"] = 1 body["rid"] = rid pending = _PendingReq(rid=rid, frame=body) self._pending[rid] = pending topic = up_topic(self._endpoint_id) self._transport.publish(topic, dumps(body)) if not wait: # 仍等一小会拿结果;后台请求也尽量同步完成 pending.event.wait(timeout=30) with self._lock: self._pending.pop(rid, None) if pending.error: raise pending.error if pending.response is None: return {} if not pending.response.get("ok"): err = pending.response.get("error") or {} raise NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", ""))) return pending.response if not pending.event.wait(timeout=60): with self._lock: self._pending.pop(rid, None) raise NixMsgError("busy", "请求超时") if pending.error: raise pending.error assert pending.response is not None if not pending.response.get("ok"): err = pending.response.get("error") or {} raise NixMsgError(str(err.get("code", "bad_request")), str(err.get("message", ""))) return pending.response def _next_rid(self) -> str: with self._lock: return self._next_rid_locked() def _next_rid_locked(self) -> str: self._rid_seq += 1 return str(self._rid_seq) def _dedup_put(self, key: str, state: str) -> None: if key in self._dedup: self._dedup.move_to_end(key) self._dedup[key] = state while len(self._dedup) > DEDUP_CAPACITY: self._dedup.popitem(last=False) def _fail_all_pending(self, err: BaseException) -> None: for p in list(self._pending.values()): p.error = err p.event.set() self._pending.clear() for it in list(self._send_queue): it.pending.error = err it.pending.event.set() self._send_queue.clear() self._inflight_sends = 0 def _set_state(self, state: ConnectionState, reason: str = "") -> None: self._state = state h = self._connection_handler if h: try: with self._cb_lock: h(ConnectionEvent(state=state, reason=reason)) except Exception: log.exception("on_connection 回调错误") def _fire_session(self, token: str) -> None: h = self._session_handler if h: try: with self._cb_lock: h(token) except Exception: log.exception("on_session 回调错误") def _fire_receipt(self, receipt: Receipt) -> None: h = self._receipt_handler if h: try: with self._cb_lock: h(receipt) except Exception: log.exception("on_receipt 回调错误") def _fire_revoked(self, ev: RevokedEvent) -> None: h = self._revoked_handler if h: try: with self._cb_lock: h(ev) except Exception: log.exception("on_revoked 回调错误") def _fire_presence(self, ev: PresenceEvent) -> None: h = self._presence_handler if h: try: with self._cb_lock: h(ev) except Exception: log.exception("on_presence 回调错误") def _fire_group(self, ev: GroupEvent) -> None: h = self._group_handler if h: try: with self._cb_lock: h(ev) except Exception: log.exception("on_group_event 回调错误") @staticmethod def _jitter(base: float) -> float: return max(0.0, base * (1.0 + random.uniform(-BACKOFF_JITTER, BACKOFF_JITTER))) # 测试辅助 @property def state(self) -> ConnectionState: return self._state @property def limits(self) -> HelloLimits: return self._limits @property def clock_skew_ms(self) -> int: return self._clock_skew_ms @property def session_token(self) -> Optional[str]: return self._session_token def _auto_hello_responder(client: Client, transport: FakeTransport, *, session_token: str = "nst_test") -> None: """测试辅助:对 hello 自动回成功(由测试自行调用更清晰)。""" _ = (client, transport, session_token)