feat(block2): relay pool — publish to many relays, discover deduped, tolerate failures
Relay-strategy hardening (network-trust §3): don't hang on one relay. - NostrChannel interface: what the transports need (sign, publish, subscribe, reqOnce). Implemented by NostrConnection (single relay) and the new RelayPool. - RelayPool: connects to all configured relays (skips unreachable ones, throws only when none are reachable), publishes to every live relay (accepted if any accepts), merges discovery deduped by event id, and survives a relay dropping. - Transports now depend on NostrChannel, not a concrete connection. - SocialService.openSession takes ALL configured relays (was relays.first); createOffersCubit passes the whole list. commons_core 63 tests green (3 new pool tests over two in-process relays); app_seeds social/market 18 green.
This commit is contained in:
parent
492bc62025
commit
2a6b4b0947
10 changed files with 246 additions and 24 deletions
68
packages/commons_core/test/social/relay_pool_test.dart
Normal file
68
packages/commons_core/test/social/relay_pool_test.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/mini_relay.dart';
|
||||
|
||||
/// Relay-strategy hardening: publish to several relays, discover deduped, and
|
||||
/// keep working when one is down.
|
||||
void main() {
|
||||
Future<NostrIdentity> idFor(int fill) =>
|
||||
NostrKeyDerivation.deriveFromSeed(Uint8List(32)..fillRange(0, 32, fill));
|
||||
|
||||
Offer offer(String id, NostrIdentity who) => Offer(
|
||||
id: id,
|
||||
authorPubkeyHex: who.publicKeyHex,
|
||||
summary: 'Tomate',
|
||||
type: OfferType.gift,
|
||||
approxGeohash: 'sp3e9',
|
||||
);
|
||||
|
||||
test('publishes to every relay and discovers deduped across them', () async {
|
||||
final r1 = await MiniRelay.start();
|
||||
final r2 = await MiniRelay.start();
|
||||
final alice = await idFor(1);
|
||||
final bob = await idFor(2);
|
||||
|
||||
final alicePool = await RelayPool.connect([r1.url, r2.url], identity: alice);
|
||||
await NostrOfferTransport(alicePool).publish(offer('o1', alice));
|
||||
expect(r1.storedCount, 1, reason: 'reached relay 1');
|
||||
expect(r2.storedCount, 1, reason: 'reached relay 2');
|
||||
|
||||
final bobPool = await RelayPool.connect([r1.url, r2.url], identity: bob);
|
||||
final found = await NostrOfferTransport(bobPool)
|
||||
.discoverUntilEose(const DiscoveryQuery(geohashPrefix: 'sp3e9'));
|
||||
expect(found, hasLength(1), reason: 'both relays return it → deduped to one');
|
||||
|
||||
await alicePool.close();
|
||||
await bobPool.close();
|
||||
await r1.stop();
|
||||
await r2.stop();
|
||||
});
|
||||
|
||||
test('tolerates a dead relay (skips it, keeps the live one)', () async {
|
||||
final r1 = await MiniRelay.start();
|
||||
final alice = await idFor(3);
|
||||
|
||||
// Port 1 is unused → connection refused, skipped.
|
||||
final pool =
|
||||
await RelayPool.connect([r1.url, 'ws://127.0.0.1:1'], identity: alice);
|
||||
expect(pool.relayCount, 1);
|
||||
|
||||
final result = await NostrOfferTransport(pool).publish(offer('o2', alice));
|
||||
expect(result.accepted, isTrue);
|
||||
expect(r1.storedCount, 1);
|
||||
|
||||
await pool.close();
|
||||
await r1.stop();
|
||||
});
|
||||
|
||||
test('throws when no relay is reachable (caller treats as offline)', () async {
|
||||
final alice = await idFor(4);
|
||||
expect(
|
||||
RelayPool.connect(['ws://127.0.0.1:1'], identity: alice),
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue