From 0f442a8f3b6afece7481f288cf886ec2965face1 Mon Sep 17 00:00:00 2001 From: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Date: Sun, 2 Aug 2026 19:56:19 -0700 Subject: [PATCH] fix(mobile): reconnect after backward clock jumps Co-authored-by: Tom Brow Signed-off-by: Tom Brow --- mobile/lib/shared/relay/relay_session.dart | 8 ++- .../test/shared/relay/relay_session_test.dart | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/mobile/lib/shared/relay/relay_session.dart b/mobile/lib/shared/relay/relay_session.dart index 1c5c305b4c..9e0f5180d5 100644 --- a/mobile/lib/shared/relay/relay_session.dart +++ b/mobile/lib/shared/relay/relay_session.dart @@ -422,9 +422,13 @@ class RelaySessionNotifier extends Notifier { _backgroundGraceTimer?.cancel(); _backgroundGraceTimer = null; + final backgroundedDuration = backgroundedAt == null + ? null + : _now().difference(backgroundedAt); final backgroundedLongEnoughToRequireReconnect = - backgroundedAt != null && - _now().difference(backgroundedAt) >= _backgroundGraceDuration; + backgroundedDuration != null && + (backgroundedDuration.isNegative || + backgroundedDuration >= _backgroundGraceDuration); if (!backgroundedLongEnoughToRequireReconnect && state.status == SessionStatus.connected) { return; diff --git a/mobile/test/shared/relay/relay_session_test.dart b/mobile/test/shared/relay/relay_session_test.dart index 826e234400..270297d20f 100644 --- a/mobile/test/shared/relay/relay_session_test.dart +++ b/mobile/test/shared/relay/relay_session_test.dart @@ -546,6 +546,63 @@ void main() { }, ); + test( + 'resume reconnects a stale connected session after a backward clock jump', + () async { + final sockets = <_ControlledRelaySocket>[]; + final keychain = nostr.Keys.generate(); + var now = DateTime(2026, 8, 2, 12); + final session = RelaySessionNotifier( + now: () => now, + socketFactory: + ({ + required wsUrl, + required nsec, + required onMessage, + required onConnected, + required onDisconnected, + }) { + final socket = _ControlledRelaySocket( + wsUrl: wsUrl, + nsec: nsec, + onMessage: onMessage, + onConnected: onConnected, + onDisconnected: onDisconnected, + ); + sockets.add(socket); + return socket; + }, + ); + final container = ProviderContainer( + overrides: [ + relaySessionProvider.overrideWith(() => session), + relayConfigProvider.overrideWith( + () => _FakeRelayConfigNotifier( + baseUrl: 'https://relay.example', + nsec: keychain.nsec, + ), + ), + authProvider.overrideWith(() => _AuthenticatedAuthNotifier()), + ], + ); + addTearDown(container.dispose); + await container.read(authProvider.future); + final subscription = container.listen(relaySessionProvider, (_, _) {}); + addTearDown(subscription.close); + await Future.delayed(Duration.zero); + sockets.single.connectSuccessfully(); + + session.onAppPaused(); + now = now.subtract(const Duration(minutes: 5)); + session.onAppResumed(); + await Future.delayed(Duration.zero); + + expect(sockets, hasLength(2)); + expect(sockets.first.disposeCalls, 1); + expect(session.state.status, SessionStatus.reconnecting); + }, + ); + test('delivers the same live event to each matching subscription', () async { final session = RelaySessionNotifier(); final firstEvents = [];