fix(market): recover by itself when the shared connection comes up

Fresh installs could sit on 'can't reach the servers' forever: the offers
cubit captured the transport once at build time, the shared connection only
retried on a connectivity CHANGE, and a silently-filtered relay could stall
the pool for minutes.

- SocialConnection: retry with backoff after a failed attempt while started
  and not knowingly offline (injectable schedule for tests)
- OffersCubit: follow connection.sessions, re-attach the transport and re-run
  the last discovery on (re)connect; announce drops via connectionEpoch
- market _init: record the wanted area on the cubit even while offline
- NostrOfferTransport.discoverPage: sort a copy (channel lists may be
  unmodifiable)
This commit is contained in:
vjrj 2026-07-22 12:57:24 +02:00
parent 981cf10048
commit 8a7dc8d3dc
6 changed files with 273 additions and 9 deletions

View file

@ -25,19 +25,36 @@ class SocialConnection {
required SocialSettings settings,
SessionOpener? open,
Stream<bool>? online,
List<Duration>? retrySchedule,
}) : _settings = settings,
_open = open ?? social.openSession,
_online = online;
_online = online,
_retrySchedule = retrySchedule ?? _defaultRetrySchedule;
/// Backoff for self-retries after a failed attempt while started and not
/// knowingly offline (the fresh-install case: online the whole time, first
/// connect fails, so no connectivity change ever retriggers a connect).
static const _defaultRetrySchedule = [
Duration(seconds: 5),
Duration(seconds: 15),
Duration(seconds: 45),
Duration(seconds: 90),
];
final SocialSettings _settings;
final SessionOpener _open;
final Stream<bool>? _online;
final List<Duration> _retrySchedule;
final _sessions = StreamController<SocialSession?>.broadcast();
SocialSession? _current;
Future<SocialSession?>? _pending;
StreamSubscription<bool>? _onlineSub;
bool _disposed = false;
bool _started = false;
bool _knownOffline = false;
Timer? _retryTimer;
int _retryIndex = 0;
/// Emits the live session on each (re)connect, and null when it drops.
Stream<SocialSession?> get sessions => _sessions.stream;
@ -48,10 +65,14 @@ class SocialConnection {
/// Begins watching connectivity (reconnect on regain, drop when offline) and
/// attempts an initial connect. Idempotent-ish; call once at startup.
void start() {
_started = true;
_onlineSub = (_online ?? _connectivityOnline()).listen((isOnline) {
_knownOffline = !isOnline;
if (!isOnline) {
_cancelRetry();
_drop();
} else if (_current == null) {
_retryIndex = 0; // fresh network start the backoff over
unawaited(session());
}
});
@ -75,15 +96,40 @@ class SocialConnection {
return null;
}
_current = s;
_retryIndex = 0;
_cancelRetry();
_sessions.add(s);
return s;
} catch (_) {
return null; // unreachable a later connectivity change retries
_scheduleRetry(); // unreachable retry with backoff (see below)
return null;
} finally {
_pending = null;
}
}
/// After a failed attempt, retries by itself while started and not knowingly
/// offline a connectivity change may never come if the device was online
/// all along. Un-started connections (one-shot [session] callers, tests)
/// never leave a timer behind.
void _scheduleRetry() {
if (!_started || _disposed || _knownOffline || _current != null) return;
_cancelRetry();
final i = _retryIndex < _retrySchedule.length
? _retryIndex
: _retrySchedule.length - 1;
_retryIndex++;
_retryTimer = Timer(_retrySchedule[i], () {
if (_disposed || _current != null) return;
unawaited(session());
});
}
void _cancelRetry() {
_retryTimer?.cancel();
_retryTimer = null;
}
void _drop() {
final s = _current;
_current = null;
@ -95,6 +141,7 @@ class SocialConnection {
Future<void> dispose() async {
_disposed = true;
_cancelRetry();
await _onlineSub?.cancel();
_onlineSub = null;
_drop();