Every feature (offers, messaging, trust, profile, the inbox listener) used to open its OWN RelayPool via social.openSession — several sockets to the same relays, more battery, and reconnection logic living only in the inbox listener. Add SocialConnection: ONE shared session per identity, lazily connected and reused by all. It watches connectivity — dropping the session when the network goes and reconnecting when it returns, announcing each change on a sessions stream so the inbox listener re-subscribes automatically. Callers get the session via connection.session() and never close it (the connection owns its lifecycle); recreated/disposed on an identity switch. - InboxService now consumes the shared connection (subscribes on its sessions stream) instead of owning its own socket + connectivity code. - createOffersCubit + the chat/market/profile screens use the shared connection; dead createMessagesCubit/createTrustCubit factories removed. - DI registers SocialConnection per identity; Bootstrap starts it (after the inbox subscribes); switchSocialAccount disposes + recreates it. Tests: SocialConnection lifecycle (reuse, concurrent connect, offline drop + reconnect, unreachable retry) with a fake opener + online stream; inbox test updated. nostr added as a dev_dependency for the channel fake.
119 lines
3.8 KiB
Dart
119 lines
3.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:commons_core/commons_core.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:nostr/nostr.dart';
|
|
import 'package:tane/services/social_connection.dart';
|
|
import 'package:tane/services/social_service.dart';
|
|
import 'package:tane/services/social_settings.dart';
|
|
|
|
import '../support/test_support.dart';
|
|
|
|
/// A no-op [NostrChannel] that only tracks whether it was closed — enough to
|
|
/// build a [SocialSession] and assert the connection's lifecycle.
|
|
class FakeChannel implements NostrChannel {
|
|
bool closed = false;
|
|
|
|
@override
|
|
String get privateKeyHex => '00' * 32;
|
|
@override
|
|
String get publicKeyHex => 'ab' * 32;
|
|
@override
|
|
Future<({bool accepted, String message})> publish(Event event) async =>
|
|
(accepted: true, message: '');
|
|
@override
|
|
Stream<Event> subscribe(Filter filter) => const Stream.empty();
|
|
@override
|
|
Future<List<Event>> reqOnce(Filter filter) async => const [];
|
|
@override
|
|
Future<void> close() async => closed = true;
|
|
}
|
|
|
|
void main() {
|
|
const seedHex =
|
|
'000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f';
|
|
|
|
late SocialService social;
|
|
late SocialSettings settings; // relayUrls() falls back to defaults (non-empty)
|
|
|
|
setUp(() async {
|
|
social = await SocialService.fromRootSeedHex(seedHex);
|
|
settings = SocialSettings(InMemorySecretStore());
|
|
});
|
|
|
|
SocialConnection make({
|
|
required List<FakeChannel> opened,
|
|
Stream<bool>? online,
|
|
bool Function()? fail,
|
|
}) =>
|
|
SocialConnection(
|
|
social: social,
|
|
settings: settings,
|
|
online: online,
|
|
open: (_) async {
|
|
if (fail?.call() ?? false) throw StateError('unreachable');
|
|
final ch = FakeChannel();
|
|
opened.add(ch);
|
|
return SocialSession(ch);
|
|
},
|
|
);
|
|
|
|
test('connects once and reuses the shared session', () async {
|
|
final opened = <FakeChannel>[];
|
|
final conn = make(opened: opened);
|
|
final a = await conn.session();
|
|
final b = await conn.session();
|
|
expect(a, isNotNull);
|
|
expect(identical(a, b), isTrue); // same shared instance
|
|
expect(opened, hasLength(1)); // only one connection opened
|
|
await conn.dispose();
|
|
});
|
|
|
|
test('concurrent callers share a single connect', () async {
|
|
final opened = <FakeChannel>[];
|
|
final conn = make(opened: opened);
|
|
final results = await Future.wait([conn.session(), conn.session()]);
|
|
expect(identical(results[0], results[1]), isTrue);
|
|
expect(opened, hasLength(1));
|
|
await conn.dispose();
|
|
});
|
|
|
|
test('drops when offline and reconnects when back online', () async {
|
|
final opened = <FakeChannel>[];
|
|
final online = StreamController<bool>.broadcast();
|
|
final conn = make(opened: opened, online: online.stream);
|
|
final emitted = <SocialSession?>[];
|
|
conn.sessions.listen(emitted.add);
|
|
|
|
conn.start(); // watch connectivity + initial connect
|
|
final first = await conn.session();
|
|
expect(first, isNotNull);
|
|
expect(opened, hasLength(1));
|
|
|
|
online.add(false); // network lost
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(conn.current, isNull);
|
|
expect(opened.first.closed, isTrue); // old session closed
|
|
expect(emitted.last, isNull); // announced the drop
|
|
|
|
online.add(true); // network back
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(conn.current, isNotNull);
|
|
expect(opened, hasLength(2)); // reconnected
|
|
expect(identical(emitted.last, conn.current), isTrue);
|
|
|
|
await conn.dispose();
|
|
await online.close();
|
|
});
|
|
|
|
test('returns null when the relay is unreachable, and retries later',
|
|
() async {
|
|
final opened = <FakeChannel>[];
|
|
var down = true;
|
|
final conn = make(opened: opened, fail: () => down);
|
|
expect(await conn.session(), isNull); // unreachable now
|
|
down = false;
|
|
expect(await conn.session(), isNotNull); // succeeds on retry
|
|
await conn.dispose();
|
|
});
|
|
}
|