damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 9091cb1aae43633673794009bdc576faddc6ed35
parent e0984aab3463aab58c6dd9bdcb10e7b5ff0b82dd
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 17 Mar 2023 07:54:29 -0600

Revert "Reduce battery usage by using exp backoff on connections"

This is causing pretty bad fail to reconnect issues

This reverts commit 252a77fd975c6b74becbf1ba47f899e971ad2011, reversing
changes made to a611a5d25240220a3302e28e0b7db57fbb7c82c9.

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4----
Mdamus/Models/EventsModel.swift | 6+++---
Mdamus/Models/FollowersModel.swift | 2+-
Mdamus/Models/FollowingModel.swift | 2+-
Mdamus/Models/ProfileModel.swift | 4++--
Mdamus/Models/SearchHomeModel.swift | 2+-
Mdamus/Models/ThreadModel.swift | 4++--
Mdamus/Models/ZapsModel.swift | 2+-
Mdamus/Nostr/RelayConnection.swift | 87+++++++++++++++++++++++++++++++++++++------------------------------------------
Mdamus/Nostr/RelayPool.swift | 177++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mdamus/Views/Relays/RelayStatus.swift | 24+++++++++++-------------
DdamusTests/RelayPoolTests.swift | 170-------------------------------------------------------------------------------
12 files changed, 161 insertions(+), 323 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -223,7 +223,6 @@ 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; }; 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; - 5023E76329AA3627007D3D50 /* RelayPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5023E76229AA3627007D3D50 /* RelayPoolTests.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; @@ -594,7 +593,6 @@ 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; }; 4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; }; 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; - 5023E76229AA3627007D3D50 /* RelayPoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPoolTests.swift; sourceTree = "<group>"; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; }; @@ -1087,7 +1085,6 @@ 4CE6DEF627F7A08200C66700 /* damusTests */ = { isa = PBXGroup; children = ( - 5023E76229AA3627007D3D50 /* RelayPoolTests.swift */, 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */, DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */, 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */, @@ -1594,7 +1591,6 @@ DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */, 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */, 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */, - 5023E76329AA3627007D3D50 /* RelayPoolTests.swift in Sources */, 4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */, 4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */, 4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */, diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift @@ -31,9 +31,9 @@ class EventsModel: ObservableObject { } func subscribe() { - state.pool.subscribe_to(sub_id: sub_id, - filters: [get_filter()], - handler: handle_nostr_event) + state.pool.subscribe(sub_id: sub_id, + filters: [get_filter()], + handler: handle_nostr_event) } func unsubscribe() { diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift @@ -40,7 +40,7 @@ class FollowersModel: ObservableObject { let filter = get_filter() let filters = [filter] print_filters(relay_id: "following", filters: [filters]) - self.damus_state.pool.subscribe_to(sub_id: sub_id, filters: filters, handler: handle_event) + self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event) } func unsubscribe() { diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift @@ -41,7 +41,7 @@ class FollowingModel { } let filters = [filter] print_filters(relay_id: "following", filters: [filters]) - self.damus_state.pool.subscribe_to(sub_id: sub_id, filters: filters, handler: handle_event) + self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event) } func unsubscribe() { diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -83,8 +83,8 @@ class ProfileModel: ObservableObject, Equatable { print("subscribing to profile \(pubkey) with sub_id \(sub_id)") print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]]) - damus.pool.subscribe_to(sub_id: sub_id, filters: [text_filter], handler: handle_event) - damus.pool.subscribe_to(sub_id: prof_subid, filters: [profile_filter], handler: handle_event) + damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event) + damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event) } func handle_profile_contact_event(_ ev: NostrEvent) { diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -38,7 +38,7 @@ class SearchHomeModel: ObservableObject { func subscribe() { loading = true let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters) - damus_state.pool.subscribe_to(sub_id: base_subid, filters: [get_base_filter()], to: to_relays, handler: handle_event) + damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays) } func unsubscribe(to: String? = nil) { diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -104,8 +104,8 @@ class ThreadModel: ObservableObject { print("subscribing to thread \(event.id) with sub_id \(base_subid)") loading = true - damus_state.pool.subscribe_to(sub_id: base_subid, filters: base_filters, handler: handle_event) - damus_state.pool.subscribe_to(sub_id: meta_subid, filters: meta_filters, handler: handle_event) + damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event) + damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event) } func add_event(_ ev: NostrEvent, privkey: String?) { diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift @@ -29,7 +29,7 @@ class ZapsModel: ObservableObject { case .note(let note_target): filter.referenced_ids = [note_target.note_id] } - state.pool.subscribe_to(sub_id: zaps_subid, filters: [filter], handler: handle_event) + state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event) } func unsubscribe() { diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -14,15 +14,9 @@ enum NostrConnectionEvent { } final class RelayConnection: WebSocketDelegate { - enum State { - case notConnected - case connecting - case reconnecting - case connected - case failed - } - - private(set) var state: State = .notConnected + private(set) var isConnected = false + private(set) var isConnecting = false + private(set) var isReconnecting = false private(set) var last_connection_attempt: TimeInterval = 0 private lazy var socket = { @@ -31,36 +25,38 @@ final class RelayConnection: WebSocketDelegate { socket.delegate = self return socket }() - private let eventHandler: (NostrConnectionEvent) -> () - let url: URL - - init(url: URL, eventHandler: @escaping (NostrConnectionEvent) -> ()) { + private var handleEvent: (NostrConnectionEvent) -> () + private let url: URL + + init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) { self.url = url - self.eventHandler = eventHandler + self.handleEvent = handleEvent } func reconnect() { - if state == .connected { - state = .reconnecting + if isConnected { + isReconnecting = true disconnect() } else { // we're already disconnected, so just connect - connect() + connect(force: true) } } func connect(force: Bool = false) { - if !force && (state == .connected || state == .connecting) { + if !force && (isConnected || isConnecting) { return } - state = .connecting + isConnecting = true last_connection_attempt = Date().timeIntervalSince1970 socket.connect() } func disconnect() { socket.disconnect() + isConnected = false + isConnecting = false } func send(_ req: NostrRequest) { @@ -72,52 +68,51 @@ final class RelayConnection: WebSocketDelegate { socket.write(string: req) } - private func decodeEvent(_ txt: String) throws -> NostrConnectionEvent { - if let ev = decode_nostr_event(txt: txt) { - return .nostr_event(ev) - } else { - throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "decoding event failed")) - } - } - - @MainActor - private func handleEvent(_ event: NostrConnectionEvent) async { - eventHandler(event) - } - // MARK: - WebSocketDelegate func didReceive(event: WebSocketEvent, client: WebSocket) { switch event { case .connected: - state = .connected + self.isConnected = true + self.isConnecting = false case .disconnected: - if state == .reconnecting { - connect() - } else { - state = .notConnected + self.isConnecting = false + self.isConnected = false + if self.isReconnecting { + self.isReconnecting = false + self.connect() } case .cancelled, .error: - state = .failed + self.isConnecting = false + self.isConnected = false case .text(let txt): - Task(priority: .userInitiated) { - do { - let event = try decodeEvent(txt) - await handleEvent(event) - } catch { - print("decode failed for \(txt): \(error)") - // TODO: trigger event error + if txt.count > 2000 { + DispatchQueue.global(qos: .default).async { + if let ev = decode_nostr_event(txt: txt) { + DispatchQueue.main.async { + self.handleEvent(.nostr_event(ev)) + } + return + } + } + } else { + if let ev = decode_nostr_event(txt: txt) { + handleEvent(.nostr_event(ev)) + return } } + print("decode failed for \(txt)") + // TODO: trigger event error + default: break } - eventHandler(.ws_event(event)) + handleEvent(.ws_event(event)) } } diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -7,6 +7,22 @@ import Foundation +struct SubscriptionId: Identifiable, CustomStringConvertible { + let id: String + + var description: String { + id + } +} + +struct RelayId: Identifiable, CustomStringConvertible { + let id: String + + var description: String { + id + } +} + struct RelayHandler { let sub_id: String let callback: (String, NostrConnectionEvent) -> () @@ -17,58 +33,58 @@ struct QueuedRequest { let relay: String } -final class RelayPool { - enum Constants { - /// Used for an exponential backoff algorithm when retrying stale connections - /// Each retry attempt will be delayed by raising this base delay to an exponent - /// equal to the number of previous retries. - static let base_reconnect_delay: TimeInterval = 2 - static let max_queued_requests = 10 - static let max_retry_attempts = 3 - } - - private(set) var relays: [Relay] = [] - private(set) var handlers: [RelayHandler] = [] - private var request_queue: [QueuedRequest] = [] - private(set) var seen: Set<String> = Set() - private(set) var counts: [String: UInt64] = [:] - private var retry_attempts_per_relay: [URL: Int] = [:] +struct NostrRequestId: Equatable, Hashable { + let relay: String? + let sub_id: String +} + +class RelayPool { + var relays: [Relay] = [] + var handlers: [RelayHandler] = [] + var request_queue: [QueuedRequest] = [] + var seen: Set<String> = Set() + var counts: [String: UInt64] = [:] var descriptors: [RelayDescriptor] { relays.map { $0.descriptor } } var num_connecting: Int { - relays.reduce(0) { n, r in n + (r.connection.state == .connecting ? 1 : 0) } + return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) } } func remove_handler(sub_id: String) { - guard let index = handlers.firstIndex(where: { $0.sub_id == sub_id }) else { - return - } - handlers.remove(at: index) + self.handlers = handlers.filter { $0.sub_id != sub_id } print("removing \(sub_id) handler, current: \(handlers.count)") } func register_handler(sub_id: String, handler: @escaping (String, NostrConnectionEvent) -> ()) { - guard !handlers.contains(where: { $0.sub_id == sub_id }) else { - return // don't add duplicate handlers + for handler in handlers { + // don't add duplicate handlers + if handler.sub_id == sub_id { + return + } } - - handlers.append(RelayHandler(sub_id: sub_id, callback: handler)) + self.handlers.append(RelayHandler(sub_id: sub_id, callback: handler)) print("registering \(sub_id) handler, current: \(self.handlers.count)") } func remove_relay(_ relay_id: String) { - disconnect(from: [relay_id]) + var i: Int = 0 - if let index = relays.firstIndex(where: { $0.id == relay_id }) { - relays.remove(at: index) + self.disconnect(to: [relay_id]) + + for relay in relays { + if relay.id == relay_id { + relays.remove(at: i) + break + } + + i += 1 } } - @discardableResult - func add_relay(_ url: URL, info: RelayInfo) throws -> Relay { + func add_relay(_ url: URL, info: RelayInfo) throws { let relay_id = get_relay_id(url) if get_relay(relay_id) != nil { throw RelayError.RelayAlreadyExists @@ -78,57 +94,40 @@ final class RelayPool { } let descriptor = RelayDescriptor(url: url, info: info) let relay = Relay(descriptor: descriptor, connection: conn) - relays.append(relay) - return relay + self.relays.append(relay) } /// This is used to retry dead connections func connect_to_disconnected() { - for relay in relays where !relay.is_broken && relay.connection.state != .connected { + for relay in relays { let c = relay.connection - let is_connecting = c.state == .reconnecting || c.state == .connecting - - let retry_attempts = retry_attempts_per_relay[c.url] ?? 0 + let is_connecting = c.isReconnecting || c.isConnecting - let delay = pow(Constants.base_reconnect_delay, TimeInterval(retry_attempts + 1)) // the + 1 helps us avoid a 1-second delay for the first retry - if is_connecting && (Date.now.timeIntervalSince1970 - c.last_connection_attempt) > delay { - if retry_attempts > Constants.max_retry_attempts { - if c.state != .notConnected { - c.disconnect() - print("exceeded max connection attempts with \(relay.descriptor.url.absoluteString)") - relay.mark_broken() - } - continue - } else { - print("stale connection detected (\(relay.descriptor.url.absoluteString)). retrying after \(delay) seconds...") - c.connect(force: true) - retry_attempts_per_relay[c.url] = retry_attempts + 1 - } - } else if is_connecting { + if is_connecting && (Date.now.timeIntervalSince1970 - c.last_connection_attempt) > 5 { + print("stale connection detected (\(relay.descriptor.url.absoluteString)). retrying...") + relay.connection.connect(force: true) + } else if relay.is_broken || is_connecting || c.isConnected { continue } else { - c.reconnect() + relay.connection.reconnect() } + } } - func reconnect(to relay_ids: [String]? = nil) { - let relays: [Relay] - if let relay_ids { - relays = get_relays(relay_ids) - } else { - relays = self.relays - } - - for relay in relays where !relay.is_broken { + func reconnect(to: [String]? = nil) { + let relays = to.map{ get_relays($0) } ?? self.relays + for relay in relays { // don't try to reconnect to broken relays relay.connection.reconnect() } } func mark_broken(_ relay_id: String) { - relays.first(where: { $0.id == relay_id })?.mark_broken() + for relay in relays { + relay.mark_broken() + } } func connect(to: [String]? = nil) { @@ -138,8 +137,8 @@ final class RelayPool { } } - private func disconnect(from: [String]? = nil) { - let relays = from.map{ get_relays($0) } ?? self.relays + func disconnect(to: [String]? = nil) { + let relays = to.map{ get_relays($0) } ?? self.relays for relay in relays { relay.connection.disconnect() } @@ -147,23 +146,35 @@ final class RelayPool { func unsubscribe(sub_id: String, to: [String]? = nil) { if to == nil { - remove_handler(sub_id: sub_id) + self.remove_handler(sub_id: sub_id) } - send(.unsubscribe(sub_id), to: to) + self.send(.unsubscribe(sub_id), to: to) + } + + func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) { + register_handler(sub_id: sub_id, handler: handler) + send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to) } - func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]? = nil, handler: @escaping (String, NostrConnectionEvent) -> ()) { + func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) { register_handler(sub_id: sub_id, handler: handler) send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to) } func count_queued(relay: String) -> Int { - request_queue.filter({ $0.relay == relay }).count + var c = 0 + for request in request_queue { + if request.relay == relay { + c += 1 + } + } + + return c } func queue_req(r: NostrRequest, relay: String) { let count = count_queued(relay: relay) - guard count < Constants.max_queued_requests else { + guard count <= 10 else { print("can't queue, too many queued events for \(relay)") return } @@ -173,10 +184,10 @@ final class RelayPool { } func send(_ req: NostrRequest, to: [String]? = nil) { - let relays = to.map { get_relays($0) } ?? self.relays - + let relays = to.map{ get_relays($0) } ?? self.relays + for relay in relays { - guard relay.connection.state == .connected else { + guard relay.connection.isConnected else { queue_req(r: req, relay: relay.id) continue } @@ -196,14 +207,17 @@ final class RelayPool { func record_last_pong(relay_id: String, event: NostrConnectionEvent) { if case .ws_event(let ws_event) = event { if case .pong = ws_event { - if let relay = relays.first(where: { $0.id == relay_id }) { - relay.last_pong = UInt32(Date.now.timeIntervalSince1970) + for relay in relays { + if relay.id == relay_id { + relay.last_pong = UInt32(Date.now.timeIntervalSince1970) + return + } } } } } - private func run_queue(_ relay_id: String) { + func run_queue(_ relay_id: String) { self.request_queue = request_queue.reduce(into: Array<QueuedRequest>()) { (q, req) in guard req.relay == relay_id else { q.append(req) @@ -221,14 +235,17 @@ final class RelayPool { let k = relay_id + nev.id if !seen.contains(k) { seen.insert(k) - let prev_count = counts[relay_id] ?? 0 - counts[relay_id] = prev_count + 1 + if counts[relay_id] == nil { + counts[relay_id] = 1 + } else { + counts[relay_id] = (counts[relay_id] ?? 0) + 1 + } } } } } - private func handle_event(relay_id: String, event: NostrConnectionEvent) { + func handle_event(relay_id: String, event: NostrConnectionEvent) { record_last_pong(relay_id: relay_id, event: event) record_seen(relay_id: relay_id, event: event) @@ -248,5 +265,7 @@ final class RelayPool { func add_rw_relay(_ pool: RelayPool, _ url: String) { let url_ = URL(string: url)! - let _ = try? pool.add_relay(url_, info: RelayInfo.rw) + try? pool.add_relay(url_, info: RelayInfo.rw) } + + diff --git a/damus/Views/Relays/RelayStatus.swift b/damus/Views/Relays/RelayStatus.swift @@ -7,16 +7,6 @@ import SwiftUI -extension RelayConnection.State { - var indicatorColor: Color { - switch self { - case .connected: return .green - case .connecting, .reconnecting: return .yellow - default: return .red - } - } -} - struct RelayStatus: View { let pool: RelayPool let relay: String @@ -26,10 +16,18 @@ struct RelayStatus: View { @State var conn_color: Color = .gray func update_connection_color() { - guard let relay = pool.relays.first(where: { $0.id == relay }) else { - return + for relay in pool.relays { + if relay.id == self.relay { + let c = relay.connection + if c.isConnected { + conn_color = .green + } else if c.isConnecting || c.isReconnecting { + conn_color = .yellow + } else { + conn_color = .red + } + } } - conn_color = relay.connection.state.indicatorColor } var body: some View { diff --git a/damusTests/RelayPoolTests.swift b/damusTests/RelayPoolTests.swift @@ -1,170 +0,0 @@ -// -// RelayPoolTests.swift -// damusTests -// -// Created by Bryan Montz on 2/25/23. -// - -import XCTest -@testable import damus - -final class RelayPoolTests: XCTestCase { - - private let fakeRelayURL = URL(string: "wss://some.relay.com")! - - private func setUpPool() throws -> RelayPool { - let pool = RelayPool() - XCTAssertTrue(pool.relays.isEmpty) - - try pool.add_relay(fakeRelayURL, info: RelayInfo.rw) - return pool - } - - // MARK: - Relay Add/Remove - - func testAddRelay() throws { - let pool = try setUpPool() - - XCTAssertEqual(pool.relays.count, 1) - } - - func testRejectDuplicateRelay() throws { - let pool = try setUpPool() - - XCTAssertThrowsError(try pool.add_relay(fakeRelayURL, info: RelayInfo.rw)) { error in - XCTAssertEqual(error as? RelayError, RelayError.RelayAlreadyExists) - } - } - - func testRemoveRelay() throws { - let pool = try setUpPool() - - XCTAssertEqual(pool.relays.count, 1) - - pool.remove_relay(fakeRelayURL.absoluteString) - - XCTAssertTrue(pool.relays.isEmpty) - } - - func testMarkRelayBroken() throws { - let pool = try setUpPool() - - let relay = try XCTUnwrap(pool.relays.first(where: { $0.id == fakeRelayURL.absoluteString })) - XCTAssertFalse(relay.is_broken) - - pool.mark_broken(fakeRelayURL.absoluteString) - XCTAssertTrue(relay.is_broken) - } - - func testGetRelay() throws { - let pool = try setUpPool() - XCTAssertNotNil(pool.get_relay(fakeRelayURL.absoluteString)) - } - - func testGetRelays() throws { - let pool = try setUpPool() - - try pool.add_relay(URL(string: "wss://second.relay.com")!, info: RelayInfo.rw) - - let allRelays = pool.get_relays([fakeRelayURL.absoluteString, "wss://second.relay.com"]) - XCTAssertEqual(allRelays.count, 2) - - let relays = pool.get_relays(["wss://second.relay.com"]) - XCTAssertEqual(relays.count, 1) - } - - // MARK: - Handler Add/Remove - - private func setUpPoolWithHandler(sub_id: String) -> RelayPool { - let pool = RelayPool() - XCTAssertTrue(pool.handlers.isEmpty) - - pool.register_handler(sub_id: sub_id) { _, _ in } - return pool - } - - func testAddHandler() { - let sub_id = "123" - let pool = setUpPoolWithHandler(sub_id: sub_id) - - XCTAssertEqual(pool.handlers.count, 1) - } - - func testRejectDuplicateHandler() { - let sub_id = "123" - let pool = setUpPoolWithHandler(sub_id: sub_id) - XCTAssertEqual(pool.handlers.count, 1) - - pool.register_handler(sub_id: sub_id) { _, _ in } - - XCTAssertEqual(pool.handlers.count, 1) - } - - func testRemoveHandler() { - let sub_id = "123" - let pool = setUpPoolWithHandler(sub_id: sub_id) - XCTAssertEqual(pool.handlers.count, 1) - pool.remove_handler(sub_id: sub_id) - XCTAssertTrue(pool.handlers.isEmpty) - } - - func testRecordLastPong() throws { - let pool = try setUpPool() - let relayId = fakeRelayURL.absoluteString - let relay = try XCTUnwrap(pool.get_relay(relayId)) - XCTAssertEqual(relay.last_pong, 0) - - let pongEvent = NostrConnectionEvent.ws_event(.pong(nil)) - pool.record_last_pong(relay_id: relayId, event: pongEvent) - XCTAssertNotEqual(relay.last_pong, 0) - } - - func testSeenAndCounts() throws { - let pool = try setUpPool() - - XCTAssertTrue(pool.seen.isEmpty) - XCTAssertTrue(pool.counts.isEmpty) - - let event = NostrEvent(id: "123", content: "", pubkey: "") - let connectionEvent = NostrConnectionEvent.nostr_event(NostrResponse.event("", event)) - let relay_id = fakeRelayURL.absoluteString - pool.record_seen(relay_id: relay_id, event: connectionEvent) - - XCTAssertTrue(pool.seen.contains("wss://some.relay.com123")) - - XCTAssertEqual(pool.counts[relay_id], 1) - - pool.record_seen(relay_id: relay_id, event: connectionEvent) - // don't count the same event twice - XCTAssertEqual(pool.counts[relay_id], 1) - } - - func testAddQueuedRequest() throws { - let pool = try setUpPool() - - XCTAssertEqual(pool.count_queued(relay: fakeRelayURL.absoluteString), 0) - - let req = NostrRequest.unsubscribe("") - pool.queue_req(r: req, relay: fakeRelayURL.absoluteString) - - XCTAssertEqual(pool.count_queued(relay: fakeRelayURL.absoluteString), 1) - } - - func testRejectTooManyQueuedRequests() throws { - let pool = try setUpPool() - - let maxRequests = RelayPool.Constants.max_queued_requests - for _ in 0..<maxRequests { - let req = NostrRequest.unsubscribe("") - pool.queue_req(r: req, relay: fakeRelayURL.absoluteString) - } - - XCTAssertEqual(pool.count_queued(relay: fakeRelayURL.absoluteString), maxRequests) - - // try to add one beyond the maximum - let req = NostrRequest.unsubscribe("") - pool.queue_req(r: req, relay: fakeRelayURL.absoluteString) - - XCTAssertEqual(pool.count_queued(relay: fakeRelayURL.absoluteString), maxRequests) - } -}