damus

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

commit 0e94c48e26f448a01773ff02e397f014b6ff6449
parent 6ac68b5a737cdabb4e2c6c15dfabc620cadaa532
Author: Bryan Montz <bryanmontz@me.com>
Date:   Thu, 13 Apr 2023 09:12:16 -0500

Replace Starscream with URLSessionWebSocketTask

Changelog-Fixed: Fix slow reconnection issues

Diffstat:
MPackage.swift | 1-
Mdamus.xcodeproj/project.pbxproj | 21++++-----------------
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 9---------
Mdamus/ContentView.swift | 26--------------------------
Mdamus/Models/HomeModel.swift | 9++-------
Mdamus/Nostr/Relay.swift | 2--
Mdamus/Nostr/RelayConnection.swift | 106+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mdamus/Nostr/RelayPool.swift | 36++++++++++++++++++++----------------
Adamus/Nostr/WebSocket.swift | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Relays/RelayStatus.swift | 2+-
Mdamus/Views/SaveKeysView.swift | 2+-
11 files changed, 175 insertions(+), 126 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -1,4 +1,3 @@ dependencies: [ - .Package(url: "https://github.com/daltoniam/Starscream.git", majorVersion: 4), .Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main") ] diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -216,7 +216,6 @@ 4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.swift */; }; 4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0127F7A08200C66700 /* damusUITests.swift */; }; 4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; }; - 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; }; 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; }; 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; }; 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; }; @@ -253,6 +252,7 @@ 4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; }; 4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; + 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; @@ -669,6 +669,7 @@ 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; }; 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; }; 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; + 50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; }; @@ -708,7 +709,6 @@ files = ( 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, 6C7DE41F2955169800E66263 /* Vault in Frameworks */, - 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -973,6 +973,7 @@ 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */, 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */, 4C363A8F28247A1D006E126D /* NostrLink.swift */, + 50088DA029E8271A008A1FDF /* WebSocket.swift */, ); path = Nostr; sourceTree = "<group>"; @@ -1348,7 +1349,6 @@ ); name = damus; packageProductDependencies = ( - 4CE6DF1127F7A2B300C66700 /* Starscream */, 4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C06670328FC7EC500038D2A /* Kingfisher */, 6C7DE41E2955169800E66263 /* Vault */, @@ -1454,7 +1454,6 @@ ); mainGroup = 4CE6DEDA27F7A08100C66700; packageReferences = ( - 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */, 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */, 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */, 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */, @@ -1661,6 +1660,7 @@ 4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */, 4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */, 4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */, + 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */, 4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */, 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */, 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, @@ -2259,14 +2259,6 @@ revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9; }; }; - 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/daltoniam/Starscream"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.0.0; - }; - }; 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SparrowTek/Vault"; @@ -2288,11 +2280,6 @@ package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; - 4CE6DF1127F7A2B300C66700 /* Starscream */ = { - isa = XCSwiftPackageProductDependency; - package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */; - productName = Starscream; - }; 6C7DE41E2955169800E66263 /* Vault */ = { isa = XCSwiftPackageProductDependency; package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,15 +18,6 @@ } }, { - "identity" : "starscream", - "kind" : "remoteSourceControl", - "location" : "https://github.com/daltoniam/Starscream", - "state" : { - "revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version" : "4.0.4" - } - }, - { "identity" : "vault", "kind" : "remoteSourceControl", "location" : "https://github.com/SparrowTek/Vault", diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Starscream struct TimestampedProfile { let profile: Profile @@ -686,31 +685,6 @@ func get_since_time(last_event: NostrEvent?) -> Int64? { return nil } -func ws_nostr_event(relay: String, ev: WebSocketEvent) -> NostrEvent? { - switch ev { - case .binary(let dat): - return NostrEvent(content: "binary data? \(dat.count) bytes", pubkey: relay) - case .cancelled: - return NostrEvent(content: "cancelled", pubkey: relay) - case .connected: - return NostrEvent(content: "connected", pubkey: relay) - case .disconnected: - return NostrEvent(content: "disconnected", pubkey: relay) - case .error(let err): - return NostrEvent(content: "error \(err.debugDescription)", pubkey: relay) - case .text(let txt): - return NostrEvent(content: "text \(txt)", pubkey: relay) - case .pong: - return NostrEvent(content: "pong", pubkey: relay) - case .ping: - return NostrEvent(content: "ping", pubkey: relay) - case .viabilityChanged(let b): - return NostrEvent(content: "viabilityChanged \(b)", pubkey: relay) - case .reconnectSuggested(let b): - return NostrEvent(content: "reconnectSuggested \(b)", pubkey: relay) - } -} - func is_notification(ev: NostrEvent, pubkey: String) -> Bool { if ev.pubkey == pubkey { return false diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -290,17 +290,12 @@ class HomeModel: ObservableObject { send_home_filters(relay_id: relay_id) } case .error(let merr): - let desc = merr.debugDescription + let desc = String(describing: merr) if desc.contains("Software caused connection abort") { pool.reconnect(to: [relay_id]) } - case .disconnected: fallthrough - case .cancelled: + case .disconnected: pool.reconnect(to: [relay_id]) - case .reconnectSuggested(let t): - if t { - pool.reconnect(to: [relay_id]) - } default: break } diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift @@ -52,14 +52,12 @@ class Relay: Identifiable { let descriptor: RelayDescriptor let connection: RelayConnection - var last_pong: UInt32 var flags: Int init(descriptor: RelayDescriptor, connection: RelayConnection) { self.flags = 0 self.descriptor = descriptor self.connection = connection - self.last_pong = 0 } func mark_broken() { diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -5,26 +5,22 @@ // Created by William Casarin on 2022-04-02. // +import Combine import Foundation -import Starscream enum NostrConnectionEvent { case ws_event(WebSocketEvent) case nostr_event(NostrResponse) } -final class RelayConnection: WebSocketDelegate { +final class RelayConnection { 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 = { - let req = URLRequest(url: url) - let socket = WebSocket(request: req, compressionHandler: .none) - socket.delegate = self - return socket - }() + private lazy var socket = WebSocket(url) + private var subscriptionToken: AnyCancellable? + private var handleEvent: (NostrConnectionEvent) -> () private let url: URL @@ -33,16 +29,6 @@ final class RelayConnection: WebSocketDelegate { self.handleEvent = handleEvent } - func reconnect() { - if isConnected { - isReconnecting = true - disconnect() - } else { - // we're already disconnected, so just connect - connect(force: true) - } - } - func connect(force: Bool = false) { if !force && (isConnected || isConnecting) { return @@ -50,11 +36,27 @@ final class RelayConnection: WebSocketDelegate { isConnecting = true last_connection_attempt = Date().timeIntervalSince1970 + + subscriptionToken = socket.subject + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + switch completion { + case .failure(let error): + self?.receive(event: .error(error)) + case .finished: + self?.receive(event: .disconnected(.normalClosure, nil)) + } + } receiveValue: { [weak self] event in + self?.receive(event: event) + } + socket.connect() } func disconnect() { socket.disconnect() + subscriptionToken = nil + isConnected = false isConnecting = false } @@ -64,34 +66,46 @@ final class RelayConnection: WebSocketDelegate { print("failed to encode nostr req: \(req)") return } - - socket.write(string: req) + socket.send(.string(req)) } - // MARK: - WebSocketDelegate - - func didReceive(event: WebSocketEvent, client: WebSocket) { + private func receive(event: WebSocketEvent) { switch event { case .connected: self.isConnected = true self.isConnecting = false - - case .disconnected: - self.isConnecting = false - self.isConnected = false - if self.isReconnecting { - self.isReconnecting = false - self.connect() + case .message(let message): + self.receive(message: message) + case .disconnected(let closeCode, let reason): + if closeCode != .normalClosure { + print("⚠️ Warning: RelayConnection (\(self.url)) closed with code \(closeCode), reason: \(String(describing: reason))") } - - case .cancelled, .error: - self.isConnecting = false - self.isConnected = false - - case .text(let txt): - if txt.utf8.count > 2000 { + isConnected = false + isConnecting = false + reconnect() + case .error(let error): + print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)") + isConnected = false + isConnecting = false + reconnect() + } + self.handleEvent(.ws_event(event)) + } + + func reconnect() { + guard !isConnecting else { + return // we're already trying to connect + } + disconnect() + connect() + } + + private func receive(message: URLSessionWebSocketTask.Message) { + switch message { + case .string(let messageString): + if messageString.utf8.count > 2000 { DispatchQueue.global(qos: .default).async { - if let ev = decode_nostr_event(txt: txt) { + if let ev = decode_nostr_event(txt: messageString) { DispatchQueue.main.async { self.handleEvent(.nostr_event(ev)) } @@ -99,18 +113,18 @@ final class RelayConnection: WebSocketDelegate { } } } else { - if let ev = decode_nostr_event(txt: txt) { + if let ev = decode_nostr_event(txt: messageString) { handleEvent(.nostr_event(ev)) return } } - - - default: - break + case .data(let messageData): + if let messageString = String(data: messageData, encoding: .utf8) { + receive(message: .string(messageString)) + } + @unknown default: + print("An unexpected URLSessionWebSocketTask.Message was received.") } - - handleEvent(.ws_event(event)) } } diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -6,6 +6,7 @@ // import Foundation +import Network struct SubscriptionId: Identifiable, CustomStringConvertible { let id: String @@ -44,7 +45,24 @@ class RelayPool { var request_queue: [QueuedRequest] = [] var seen: Set<String> = Set() var counts: [String: UInt64] = [:] + + private let network_monitor = NWPathMonitor() + private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor") + private var last_network_status: NWPath.Status = .unsatisfied + init() { + network_monitor.pathUpdateHandler = { [weak self] path in + if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status { + DispatchQueue.main.async { + self?.connect_to_disconnected() + } + } + + self?.last_network_status = path.status + } + network_monitor.start(queue: network_monitor_queue) + } + var descriptors: [RelayDescriptor] { relays.map { $0.descriptor } } @@ -106,11 +124,11 @@ class RelayPool { for relay in relays { let c = relay.connection - let is_connecting = c.isReconnecting || c.isConnecting + let is_connecting = c.isConnecting 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) + relay.connection.reconnect() } else if relay.is_broken || is_connecting || c.isConnected { continue } else { @@ -208,19 +226,6 @@ class RelayPool { relays.first(where: { $0.id == id }) } - func record_last_pong(relay_id: String, event: NostrConnectionEvent) { - if case .ws_event(let ws_event) = event { - if case .pong = ws_event { - for relay in relays { - if relay.id == relay_id { - relay.last_pong = UInt32(Date.now.timeIntervalSince1970) - return - } - } - } - } - } - func run_queue(_ relay_id: String) { self.request_queue = request_queue.reduce(into: Array<QueuedRequest>()) { (q, req) in guard req.relay == relay_id else { @@ -250,7 +255,6 @@ class RelayPool { } 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) // run req queue when we reconnect diff --git a/damus/Nostr/WebSocket.swift b/damus/Nostr/WebSocket.swift @@ -0,0 +1,87 @@ +// +// WebSocket.swift +// damus +// +// Created by Bryan Montz on 4/13/23. +// + +import Combine +import Foundation + +enum WebSocketEvent { + case connected + case message(URLSessionWebSocketTask.Message) + case disconnected(URLSessionWebSocketTask.CloseCode, String?) + case error(Error) +} + +final class WebSocket: NSObject, URLSessionWebSocketDelegate { + + private let url: URL + private let session: URLSession + private lazy var webSocketTask: URLSessionWebSocketTask = { + let task = session.webSocketTask(with: url) + task.delegate = self + return task + }() + + let subject = PassthroughSubject<WebSocketEvent, Never>() + + init(_ url: URL, session: URLSession = .shared) { + self.url = url + self.session = session + } + + func connect() { + resume() + } + + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure, reason: Data? = nil) { + webSocketTask.cancel(with: closeCode, reason: reason) + + // reset after disconnecting to be ready for reconnecting + let task = session.webSocketTask(with: url) + task.delegate = self + webSocketTask = task + + let reason_str: String? + if let reason { + reason_str = String(data: reason, encoding: .utf8) + } else { + reason_str = nil + } + subject.send(.disconnected(closeCode, reason_str)) + } + + func send(_ message: URLSessionWebSocketTask.Message) { + webSocketTask.send(message) { [weak self] error in + if let error { + self?.subject.send(.error(error)) + } + } + } + + private func resume() { + webSocketTask.receive { [weak self] result in + switch result { + case .success(let message): + self?.subject.send(.message(message)) + self?.resume() + case .failure(let error): + self?.subject.send(.error(error)) + } + } + + webSocketTask.resume() + } + + // MARK: - URLSessionWebSocketDelegate + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol theProtocol: String?) { + subject.send(.connected) + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + disconnect(closeCode: closeCode, reason: reason) + } +} diff --git a/damus/Views/Relays/RelayStatus.swift b/damus/Views/Relays/RelayStatus.swift @@ -24,7 +24,7 @@ struct RelayStatus: View { if c.isConnected { conn_image = "network" conn_color = .green - } else if c.isConnecting || c.isReconnecting { + } else if c.isConnecting { connecting = true } else { conn_image = "exclamationmark.circle.fill" diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift @@ -127,7 +127,7 @@ struct SaveKeysView: View { case .error(let err): self.loading = false - self.error = "\(err.debugDescription)" + self.error = String(describing: err) default: break }