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:
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
}