import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'social_service.dart'; import 'social_settings.dart'; /// Opens a session against the given relays. Injectable so the connection logic /// can be unit-tested without a real relay. typedef SessionOpener = Future Function(List relays); /// ONE shared relay connection per identity, reused by every social feature /// (offers, messaging, trust, profile, the inbox listener) instead of each /// opening its own `RelayPool`. Fewer sockets, less battery, one place to manage /// reconnection. /// /// Lazily connects on first [session] call; reconnects when the network returns /// and drops the session when it goes away, announcing each change on [sessions] /// so long-lived consumers (the inbox listener) can re-subscribe. Callers must /// NOT close the session they get — this owns its lifecycle. Recreated on an /// identity switch (the old one is disposed). class SocialConnection { SocialConnection({ required SocialService social, required SocialSettings settings, SessionOpener? open, Stream? online, }) : _settings = settings, _open = open ?? social.openSession, _online = online; final SocialSettings _settings; final SessionOpener _open; final Stream? _online; final _sessions = StreamController.broadcast(); SocialSession? _current; Future? _pending; StreamSubscription? _onlineSub; bool _disposed = false; /// Emits the live session on each (re)connect, and null when it drops. Stream get sessions => _sessions.stream; /// The current shared session, or null if not connected right now. SocialSession? get current => _current; /// Begins watching connectivity (reconnect on regain, drop when offline) and /// attempts an initial connect. Idempotent-ish; call once at startup. void start() { _onlineSub = (_online ?? _connectivityOnline()).listen((isOnline) { if (!isOnline) { _drop(); } else if (_current == null) { unawaited(session()); } }); unawaited(session()); // initial attempt (also connects if already online) } /// The shared session, connecting on first use. Returns null when offline or /// no relay is reachable; the feature then degrades gracefully. Future session() { if (_current != null) return Future.value(_current); return _pending ??= _connect(); } Future _connect() async { try { final relays = await _settings.relayUrls(); if (relays.isEmpty) return null; final s = await _open(relays); if (_disposed) { await s.close(); return null; } _current = s; _sessions.add(s); return s; } catch (_) { return null; // unreachable — a later connectivity change retries } finally { _pending = null; } } void _drop() { final s = _current; _current = null; if (s != null) { unawaited(s.close()); if (!_sessions.isClosed) _sessions.add(null); } } Future dispose() async { _disposed = true; await _onlineSub?.cancel(); _onlineSub = null; _drop(); if (!_sessions.isClosed) await _sessions.close(); } static Stream _connectivityOnline() => Connectivity().onConnectivityChanged.map((results) => results.any((r) => r != ConnectivityResult.none)); }