import 'dart:async'; /// Error types that can occur in peer services. /// /// This is a superset covering both Watch Together relay errors and /// Companion Remote direct-connection errors. enum PeerErrorType { connectionFailed, peerDisconnected, dataChannelError, serverError, timeout, invalidSession, authFailed, networkError, unknown, } class PeerError { final PeerErrorType type; final String message; final dynamic originalError; /// Server-provided error code (e.g. 'room_not_found', 'room_full'). final String? serverCode; const PeerError({required this.type, required this.message, this.originalError, this.serverCode}); @override String toString() => 'PeerError($type): $message'; } /// Mixin that provides WebSocket keepalive ping/pong timer management. /// /// Subclasses must implement [sendPing] to send the actual ping message /// over their specific transport. The mixin manages periodic pings and /// an optional pong timeout that closes the connection if no data arrives. mixin KeepaliveMixin { Timer? _pingTimer; Timer? _pongTimer; DateTime? _lastPongReset; /// How often to send a keepalive ping. Duration get pingInterval; /// How long to wait for any incoming data before considering the /// connection dead. Set to [Duration.zero] to disable pong timeout. Duration get pongTimeout; /// Send a keepalive ping over the transport. void sendPing(); /// Called when the pong timeout fires (no data received within [pongTimeout]). /// Override to close the underlying socket/channel. void onPongTimeout(); /// Start the periodic ping timer and arm the pong timeout. void startKeepalive() { _pingTimer?.cancel(); _pingTimer = Timer.periodic(pingInterval, (_) => sendPing()); _lastPongReset = null; // Force first reset resetPongTimer(); } /// Reset the pong timeout (call on every incoming message). /// Coalesced: skips if last reset was <5s ago (timeout is 30s, still safe). void resetPongTimer() { if (pongTimeout == Duration.zero) return; final now = DateTime.now(); if (_lastPongReset != null && now.difference(_lastPongReset!) < const Duration(seconds: 5)) { return; } _lastPongReset = now; _pongTimer?.cancel(); _pongTimer = Timer(pongTimeout, onPongTimeout); } /// Stop all keepalive timers. void stopKeepalive() { _pingTimer?.cancel(); _pingTimer = null; _pongTimer?.cancel(); _pongTimer = null; _lastPongReset = null; } }