From e6dd8e08285c7ecd4373e702c2a9bac8d1c3caa0 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 21 Aug 2024 11:26:14 +0200 Subject: [PATCH 01/20] savepoint --- Sources/WalletConnectRelay/Dispatching.swift | 9 ++++++--- Sources/WalletConnectRelay/RelayClient.swift | 6 +++++- .../AutomaticSocketConnectionHandler.swift | 16 ++++++++++++---- .../ManualSocketConnectionHandler.swift | 6 +++++- .../SocketConnectionHandler.swift | 3 +++ 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 3af72ce97..bece016f4 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -6,7 +6,6 @@ protocol Dispatching { var isSocketConnected: Bool { get } var networkConnectionStatusPublisher: AnyPublisher { get } var socketConnectionStatusPublisher: AnyPublisher { get } - func send(_ string: String, completion: @escaping (Error?) -> Void) func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) func protectedSend(_ string: String) async throws func connect() throws @@ -59,7 +58,7 @@ final class Dispatcher: NSObject, Dispatching { setUpSocketConnectionObserving() } - func send(_ string: String, completion: @escaping (Error?) -> Void) { + private func send(_ string: String, completion: @escaping (Error?) -> Void) { guard socket.isConnected else { completion(NetworkError.connectionFailed) return @@ -74,12 +73,16 @@ final class Dispatcher: NSObject, Dispatching { return send(string, completion: completion) } + if !socket.isConnected { + socketConnectionHandler.handleInternalConnect() + } + var cancellable: AnyCancellable? cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) .filter { $0.0 == .connected && $0.1 == .connected } .setFailureType(to: NetworkError.self) .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed }) - .sink(receiveCompletion: { [unowned self] result in + .sink(receiveCompletion: { result in switch result { case .failure(let error): cancellable?.cancel() diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index f51f69c84..63279a80b 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -55,6 +55,7 @@ public final class RelayClient { private var dispatcher: Dispatching private let rpcHistory: RPCHistory private let logger: ConsoleLogging + private let subscriptionsTracker: SubscriptionsTracker private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.relay_client", qos: .utility, attributes: .concurrent) @@ -69,15 +70,18 @@ public final class RelayClient { dispatcher: Dispatching, logger: ConsoleLogging, rpcHistory: RPCHistory, - clientIdStorage: ClientIdStoring + clientIdStorage: ClientIdStoring, + subscriptionsTracker: SubscriptionsTracker ) { self.logger = logger self.dispatcher = dispatcher self.rpcHistory = rpcHistory self.clientIdStorage = clientIdStorage + self.subscriptionsTracker = subscriptionsTracker setUpBindings() } + private func setUpBindings() { dispatcher.onMessage = { [weak self] payload in self?.handlePayloadMessage(payload) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index c9ea12219..763af0025 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -35,9 +35,6 @@ class AutomaticSocketConnectionHandler { setUpStateObserving() setUpNetworkMonitoring() - - connect() - } func connect() { @@ -97,8 +94,9 @@ class AutomaticSocketConnectionHandler { } private func reconnectIfNeeded() { + check if it is subscribed to anything and only then subscribe if !socket.isConnected { - socket.connect() + connect() } } } @@ -106,6 +104,10 @@ class AutomaticSocketConnectionHandler { // MARK: - SocketConnectionHandler extension AutomaticSocketConnectionHandler: SocketConnectionHandler { + func handleInternalConnect() { + connect() + } + func handleConnect() throws { throw Errors.manualSocketConnectionForbidden } @@ -119,3 +121,9 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { reconnectIfNeeded() } } + + +class SubscriptionsTracker { + var subscriptions: [String: String] = [:] + +} diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift index 04152bd21..daf2d1c76 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift @@ -1,7 +1,6 @@ import Foundation class ManualSocketConnectionHandler: SocketConnectionHandler { - private let socket: WebSocketConnecting private let logger: ConsoleLogging private let defaultTimeout: Int = 60 @@ -37,6 +36,11 @@ class ManualSocketConnectionHandler: SocketConnectionHandler { socket.disconnect() } + func handleInternalConnect() { + // No operation + } + + func handleDisconnection() async { // No operation // ManualSocketConnectionHandler does not support reconnection logic diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift index 4ac3046dd..808ee43df 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift @@ -1,7 +1,10 @@ import Foundation protocol SocketConnectionHandler { + /// handles connection request from the sdk consumes func handleConnect() throws + /// handles connection request from sdk's internal function + func handleInternalConnect() func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws func handleDisconnection() async } From e7a29f0bf290eb4a4595fade397b7f8d9fa71e08 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 21 Aug 2024 11:40:15 +0200 Subject: [PATCH 02/20] add SubscriptionsTracker --- Sources/WalletConnectRelay/RelayClient.swift | 25 ++++++-------- .../AutomaticSocketConnectionHandler.swift | 5 --- .../SubscriptionsTracker.swift | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 Sources/WalletConnectRelay/SubscriptionsTracker.swift diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 63279a80b..fd075a6ec 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -187,14 +187,13 @@ public final class RelayClient { } public func unsubscribe(topic: String, completion: ((Error?) -> Void)?) { - guard let subscriptionId = subscriptions[topic] else { + guard let subscriptionId = subscriptionsTracker.getSubscription(for: topic) else { completion?(Errors.subscriptionIdNotFound) return } logger.debug("Unsubscribing from topic: \(topic)") let rpc = Unsubscribe(params: .init(id: subscriptionId, topic: topic)) - let request = rpc - .asRPCRequest() + let request = rpc.asRPCRequest() let message = try! request.asJSONEncodedString() rpcHistory.deleteAll(forTopic: topic) dispatcher.protectedSend(message) { [weak self] error in @@ -202,9 +201,7 @@ public final class RelayClient { self?.logger.debug("Failed to unsubscribe from topic") completion?(error) } else { - self?.concurrentQueue.async(flags: .barrier) { - self?.subscriptions[topic] = nil - } + self?.subscriptionsTracker.removeSubscription(for: topic) completion?(nil) } } @@ -217,15 +214,13 @@ public final class RelayClient { .filter { $0.0 == requestId } .sink { [unowned self] (_, subscriptionIds) in cancellable?.cancel() - concurrentQueue.async(flags: .barrier) { [unowned self] in - logger.debug("Subscribed to topics: \(topics)") - guard topics.count == subscriptionIds.count else { - logger.warn("Number of topics in (batch)subscribe does not match number of subscriptions") - return - } - for i in 0.. String? { + var result: String? + concurrentQueue.sync { + result = self.subscriptions[topic] + } + return result + } + + func removeSubscription(for topic: String) { + concurrentQueue.async(flags: .barrier) { + self.subscriptions[topic] = nil + } + } + + func isSubscribed() -> Bool { + var result = false + concurrentQueue.sync { + result = !self.subscriptions.isEmpty + } + return result + } +} From b82a9b5d57c56f854afe0af6ac88517a61c81dc0 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 21 Aug 2024 12:49:06 +0200 Subject: [PATCH 03/20] fix relay tests --- .../xcschemes/RelayerTests.xcscheme | 54 ++++++++++ Sources/WalletConnectRelay/Dispatching.swift | 3 +- Sources/WalletConnectRelay/RelayClient.swift | 6 +- .../RelayClientFactory.swift | 5 +- .../AutomaticSocketConnectionHandler.swift | 9 +- .../SubscriptionsTracker.swift | 37 ++++++- ...utomaticSocketConnectionHandlerTests.swift | 98 +++++++++++++++++++ Tests/RelayerTests/RelayClientTests.swift | 8 +- 8 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/RelayerTests.xcscheme diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/RelayerTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/RelayerTests.xcscheme new file mode 100644 index 000000000..f4fc9ed05 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/RelayerTests.xcscheme @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index bece016f4..37ae0884e 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -6,6 +6,7 @@ protocol Dispatching { var isSocketConnected: Bool { get } var networkConnectionStatusPublisher: AnyPublisher { get } var socketConnectionStatusPublisher: AnyPublisher { get } + func send(_ string: String, completion: @escaping (Error?) -> Void) func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) func protectedSend(_ string: String) async throws func connect() throws @@ -58,7 +59,7 @@ final class Dispatcher: NSObject, Dispatching { setUpSocketConnectionObserving() } - private func send(_ string: String, completion: @escaping (Error?) -> Void) { + func send(_ string: String, completion: @escaping (Error?) -> Void) { guard socket.isConnected else { completion(NetworkError.connectionFailed) return diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index fd075a6ec..5212b8ccb 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -20,8 +20,6 @@ public final class RelayClient { case subscriptionIdNotFound } - var subscriptions: [String: String] = [:] - public var isSocketConnected: Bool { return dispatcher.isSocketConnected } @@ -55,7 +53,7 @@ public final class RelayClient { private var dispatcher: Dispatching private let rpcHistory: RPCHistory private let logger: ConsoleLogging - private let subscriptionsTracker: SubscriptionsTracker + private let subscriptionsTracker: SubscriptionsTracking private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.relay_client", qos: .utility, attributes: .concurrent) @@ -71,7 +69,7 @@ public final class RelayClient { logger: ConsoleLogging, rpcHistory: RPCHistory, clientIdStorage: ClientIdStoring, - subscriptionsTracker: SubscriptionsTracker + subscriptionsTracker: SubscriptionsTracking ) { self.logger = logger self.dispatcher = dispatcher diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index 2120b720e..f00295fe0 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -61,10 +61,11 @@ public struct RelayClientFactory { if let bundleId = Bundle.main.bundleIdentifier { socket.request.addValue(bundleId, forHTTPHeaderField: "Origin") } + let subscriptionsTracker = SubscriptionsTracker() var socketConnectionHandler: SocketConnectionHandler! switch socketConnectionType { - case .automatic: socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, logger: logger) + case .automatic: socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: subscriptionsTracker, logger: logger) case .manual: socketConnectionHandler = ManualSocketConnectionHandler(socket: socket, logger: logger) } @@ -79,6 +80,6 @@ public struct RelayClientFactory { let rpcHistory = RPCHistoryFactory.createForRelay(keyValueStorage: keyValueStorage) - return RelayClient(dispatcher: dispatcher, logger: logger, rpcHistory: rpcHistory, clientIdStorage: clientIdStorage) + return RelayClient(dispatcher: dispatcher, logger: logger, rpcHistory: rpcHistory, clientIdStorage: clientIdStorage, subscriptionsTracker: subscriptionsTracker) } } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 5789e8a76..0e7b982bf 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -16,6 +16,7 @@ class AutomaticSocketConnectionHandler { private let backgroundTaskRegistrar: BackgroundTaskRegistering private let defaultTimeout: Int = 60 private let logger: ConsoleLogging + private let subscriptionsTracker: SubscriptionsTracking private var publishers = Set() private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.automatic_socket_connection", qos: .utility, attributes: .concurrent) @@ -25,6 +26,7 @@ class AutomaticSocketConnectionHandler { networkMonitor: NetworkMonitoring = NetworkMonitor(), appStateObserver: AppStateObserving = AppStateObserver(), backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar(), + subscriptionsTracker: SubscriptionsTracking, logger: ConsoleLogging ) { self.appStateObserver = appStateObserver @@ -32,6 +34,7 @@ class AutomaticSocketConnectionHandler { self.networkMonitor = networkMonitor self.backgroundTaskRegistrar = backgroundTaskRegistrar self.logger = logger + self.subscriptionsTracker = subscriptionsTracker setUpStateObserving() setUpNetworkMonitoring() @@ -93,9 +96,9 @@ class AutomaticSocketConnectionHandler { } } - private func reconnectIfNeeded() { - check if it is subscribed to anything and only then subscribe - if !socket.isConnected { + func reconnectIfNeeded() { + // Check if client has active subscriptions and only then subscribe + if !socket.isConnected && subscriptionsTracker.isSubscribed() { connect() } } diff --git a/Sources/WalletConnectRelay/SubscriptionsTracker.swift b/Sources/WalletConnectRelay/SubscriptionsTracker.swift index d0a8e777b..71aaeebf5 100644 --- a/Sources/WalletConnectRelay/SubscriptionsTracker.swift +++ b/Sources/WalletConnectRelay/SubscriptionsTracker.swift @@ -1,6 +1,13 @@ import Foundation -public final class SubscriptionsTracker { +protocol SubscriptionsTracking { + func setSubscription(for topic: String, id: String) + func getSubscription(for topic: String) -> String? + func removeSubscription(for topic: String) + func isSubscribed() -> Bool +} + +public final class SubscriptionsTracker: SubscriptionsTracking { private var subscriptions: [String: String] = [:] private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.subscriptions_tracker", attributes: .concurrent) @@ -32,3 +39,31 @@ public final class SubscriptionsTracker { return result } } + +#if DEBUG +final class SubscriptionsTrackerMock: SubscriptionsTracking { + var isSubscribedReturnValue: Bool = false + private var subscriptions: [String: String] = [:] + + func setSubscription(for topic: String, id: String) { + subscriptions[topic] = id + } + + func getSubscription(for topic: String) -> String? { + return subscriptions[topic] + } + + func removeSubscription(for topic: String) { + subscriptions[topic] = nil + } + + func isSubscribed() -> Bool { + return isSubscribedReturnValue + } + + func reset() { + subscriptions.removeAll() + isSubscribedReturnValue = false + } +} +#endif diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 368d25da4..101fdf3ad 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -8,6 +8,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { var networkMonitor: NetworkMonitoringMock! var appStateObserver: AppStateObserverMock! var backgroundTaskRegistrar: BackgroundTaskRegistrarMock! + var subscriptionsTracker: SubscriptionsTrackerMock! override func setUp() { webSocketSession = WebSocketMock() @@ -28,17 +29,21 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { socketAuthenticator: socketAuthenticator ) backgroundTaskRegistrar = BackgroundTaskRegistrarMock() + subscriptionsTracker = SubscriptionsTrackerMock() + sut = AutomaticSocketConnectionHandler( socket: webSocketSession, networkMonitor: networkMonitor, appStateObserver: appStateObserver, backgroundTaskRegistrar: backgroundTaskRegistrar, + subscriptionsTracker: subscriptionsTracker, logger: ConsoleLoggerMock() ) } func testConnectsOnConnectionSatisfied() { webSocketSession.disconnect() + subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions XCTAssertFalse(webSocketSession.isConnected) networkMonitor.networkConnectionStatusPublisherSubject.send(.connected) XCTAssertTrue(webSocketSession.isConnected) @@ -53,11 +58,19 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { } func testReconnectsOnEnterForeground() { + subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions webSocketSession.disconnect() appStateObserver.onWillEnterForeground?() XCTAssertTrue(webSocketSession.isConnected) } + func testReconnectsOnEnterForegroundWhenNoSubscriptions() { + subscriptionsTracker.isSubscribedReturnValue = false // Simulate no active subscriptions + webSocketSession.disconnect() + appStateObserver.onWillEnterForeground?() + XCTAssertFalse(webSocketSession.isConnected) // The connection should not be re-established + } + func testRegisterTaskOnEnterBackground() { XCTAssertNil(backgroundTaskRegistrar.completion) appStateObserver.onWillEnterBackground?() @@ -66,12 +79,15 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { func testDisconnectOnEndBackgroundTask() { appStateObserver.onWillEnterBackground?() + webSocketSession.connect() XCTAssertTrue(webSocketSession.isConnected) backgroundTaskRegistrar.completion!() XCTAssertFalse(webSocketSession.isConnected) } func testReconnectOnDisconnectForeground() async { + subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions + webSocketSession.connect() appStateObserver.currentState = .foreground XCTAssertTrue(webSocketSession.isConnected) webSocketSession.disconnect() @@ -79,11 +95,93 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { XCTAssertTrue(webSocketSession.isConnected) } + func testNotReconnectOnDisconnectForegroundWhenNoSubscriptions() async { + subscriptionsTracker.isSubscribedReturnValue = false // Simulate no active subscriptions + webSocketSession.connect() + appStateObserver.currentState = .foreground + XCTAssertTrue(webSocketSession.isConnected) + webSocketSession.disconnect() + await sut.handleDisconnection() + XCTAssertFalse(webSocketSession.isConnected) // The connection should not be re-established + } + func testReconnectOnDisconnectBackground() async { + subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions + webSocketSession.connect() + appStateObserver.currentState = .background + XCTAssertTrue(webSocketSession.isConnected) + webSocketSession.disconnect() + await sut.handleDisconnection() + XCTAssertFalse(webSocketSession.isConnected) + } + + func testNotReconnectOnDisconnectBackgroundWhenNoSubscriptions() async { + subscriptionsTracker.isSubscribedReturnValue = false // Simulate no active subscriptions + webSocketSession.connect() appStateObserver.currentState = .background XCTAssertTrue(webSocketSession.isConnected) webSocketSession.disconnect() await sut.handleDisconnection() + XCTAssertFalse(webSocketSession.isConnected) // The connection should not be re-established + } + + func testReconnectIfNeededWhenSubscribed() { + // Simulate that there are active subscriptions + subscriptionsTracker.isSubscribedReturnValue = true + + // Ensure socket is disconnected initially + webSocketSession.disconnect() XCTAssertFalse(webSocketSession.isConnected) + + // Trigger reconnect logic + sut.reconnectIfNeeded() + + // Expect the socket to be connected since there are subscriptions + XCTAssertTrue(webSocketSession.isConnected) + } + + func testReconnectIfNeededWhenNotSubscribed() { + // Simulate that there are no active subscriptions + subscriptionsTracker.isSubscribedReturnValue = false + + // Ensure socket is disconnected initially + webSocketSession.disconnect() + XCTAssertFalse(webSocketSession.isConnected) + + // Trigger reconnect logic + sut.reconnectIfNeeded() + + // Expect the socket to remain disconnected since there are no subscriptions + XCTAssertFalse(webSocketSession.isConnected) + } + + func testReconnectsOnConnectionSatisfiedWhenSubscribed() { + // Simulate that there are active subscriptions + subscriptionsTracker.isSubscribedReturnValue = true + + // Ensure socket is disconnected initially + webSocketSession.disconnect() + XCTAssertFalse(webSocketSession.isConnected) + + // Simulate network connection becomes satisfied + networkMonitor.networkConnectionStatusPublisherSubject.send(.connected) + + // Expect the socket to reconnect since there are subscriptions + XCTAssertTrue(webSocketSession.isConnected) + } + + func testReconnectsOnEnterForegroundWhenSubscribed() { + // Simulate that there are active subscriptions + subscriptionsTracker.isSubscribedReturnValue = true + + // Ensure socket is disconnected initially + webSocketSession.disconnect() + XCTAssertFalse(webSocketSession.isConnected) + + // Simulate entering foreground + appStateObserver.onWillEnterForeground?() + + // Expect the socket to reconnect since there are subscriptions + XCTAssertTrue(webSocketSession.isConnected) } } diff --git a/Tests/RelayerTests/RelayClientTests.swift b/Tests/RelayerTests/RelayClientTests.swift index d767623e4..884c8047f 100644 --- a/Tests/RelayerTests/RelayClientTests.swift +++ b/Tests/RelayerTests/RelayClientTests.swift @@ -10,13 +10,15 @@ final class RelayClientTests: XCTestCase { var sut: RelayClient! var dispatcher: DispatcherMock! var publishers = Set() + var subscriptionsTracker: SubscriptionsTrackerMock! override func setUp() { dispatcher = DispatcherMock() let logger = ConsoleLogger() let clientIdStorage = ClientIdStorageMock() let rpcHistory = RPCHistoryFactory.createForRelay(keyValueStorage: RuntimeKeyValueStorage()) - sut = RelayClient(dispatcher: dispatcher, logger: logger, rpcHistory: rpcHistory, clientIdStorage: clientIdStorage) + subscriptionsTracker = SubscriptionsTrackerMock() + sut = RelayClient(dispatcher: dispatcher, logger: logger, rpcHistory: rpcHistory, clientIdStorage: clientIdStorage, subscriptionsTracker: subscriptionsTracker) } override func tearDown() { @@ -50,7 +52,7 @@ final class RelayClientTests: XCTestCase { func testUnsubscribeRequest() { let topic = String.randomTopic() - sut.subscriptions[topic] = "" + subscriptionsTracker.setSubscription(for: topic, id: "") sut.unsubscribe(topic: topic) { error in XCTAssertNil(error) } @@ -78,7 +80,7 @@ final class RelayClientTests: XCTestCase { func testSendOnUnsubscribe() { let topic = "123" - sut.subscriptions[topic] = "" + subscriptionsTracker.setSubscription(for: topic, id: "") sut.unsubscribe(topic: topic) {_ in } XCTAssertTrue(dispatcher.sent) } From 83b3bea0000d438e9e4dff087ea12088ddea8f23 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 21 Aug 2024 13:44:02 +0200 Subject: [PATCH 04/20] fix tests --- Example/RelayIntegrationTests/RelayClientEndToEndTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index e52094049..5e69702cc 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -52,7 +52,7 @@ final class RelayClientEndToEndTests: XCTestCase { socketAuthenticator: socketAuthenticator ) - let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, logger: logger) + let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: SubscriptionsTracker(), logger: logger) let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, From 4df3bb54c7d9e7cdf30dbeecc128dac9c5f7bbdb Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 22 Aug 2024 12:50:54 +0200 Subject: [PATCH 05/20] resubscribe after disconnecting socket --- Sources/WalletConnectRelay/RelayClient.swift | 15 ++++++++++- .../AutomaticSocketConnectionHandler.swift | 2 ++ .../SubscriptionsTracker.swift | 27 ++++++++++++++----- ...thResponseTopicResubscriptionService.swift | 17 +++++------- .../Engine/Common/SessionEngine.swift | 17 +++++------- 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 5212b8ccb..a3088bca9 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -47,6 +47,7 @@ public final class RelayClient { private var requestAcknowledgePublisher: AnyPublisher { requestAcknowledgePublisherSubject.eraseToAnyPublisher() } + private var publishers = [AnyCancellable]() private let clientIdStorage: ClientIdStoring @@ -77,15 +78,27 @@ public final class RelayClient { self.clientIdStorage = clientIdStorage self.subscriptionsTracker = subscriptionsTracker setUpBindings() + setupConnectionSubscriptions() } - private func setUpBindings() { dispatcher.onMessage = { [weak self] payload in self?.handlePayloadMessage(payload) } } + private func setupConnectionSubscriptions() { + socketConnectionStatusPublisher + .sink { [unowned self] status in + guard status == .connected else { return } + let topics = subscriptionsTracker.getTopics() + Task(priority: .high) { + try await batchSubscribe(topics: topics) + } + } + .store(in: &publishers) + } + public func setLogging(level: LoggingLevel) { logger.setLogging(level: level) } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 0e7b982bf..6727c373b 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -96,7 +96,9 @@ class AutomaticSocketConnectionHandler { } } + func reconnectIfNeeded() { + // Check if client has active subscriptions and only then subscribe if !socket.isConnected && subscriptionsTracker.isSubscribed() { connect() diff --git a/Sources/WalletConnectRelay/SubscriptionsTracker.swift b/Sources/WalletConnectRelay/SubscriptionsTracker.swift index 71aaeebf5..2684de202 100644 --- a/Sources/WalletConnectRelay/SubscriptionsTracker.swift +++ b/Sources/WalletConnectRelay/SubscriptionsTracker.swift @@ -5,6 +5,7 @@ protocol SubscriptionsTracking { func getSubscription(for topic: String) -> String? func removeSubscription(for topic: String) func isSubscribed() -> Bool + func getTopics() -> [String] } public final class SubscriptionsTracker: SubscriptionsTracking { @@ -12,32 +13,40 @@ public final class SubscriptionsTracker: SubscriptionsTracking { private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.subscriptions_tracker", attributes: .concurrent) func setSubscription(for topic: String, id: String) { - concurrentQueue.async(flags: .barrier) { + concurrentQueue.async(flags: .barrier) { [unowned self] in self.subscriptions[topic] = id } } func getSubscription(for topic: String) -> String? { var result: String? - concurrentQueue.sync { - result = self.subscriptions[topic] + concurrentQueue.sync { [unowned self] in + result = subscriptions[topic] } return result } func removeSubscription(for topic: String) { - concurrentQueue.async(flags: .barrier) { - self.subscriptions[topic] = nil + concurrentQueue.async(flags: .barrier) { [unowned self] in + subscriptions[topic] = nil } } func isSubscribed() -> Bool { var result = false - concurrentQueue.sync { - result = !self.subscriptions.isEmpty + concurrentQueue.sync { [unowned self] in + result = !subscriptions.isEmpty } return result } + + func getTopics() -> [String] { + var topics: [String] = [] + concurrentQueue.sync { [unowned self] in + topics = Array(subscriptions.keys) + } + return topics + } } #if DEBUG @@ -65,5 +74,9 @@ final class SubscriptionsTrackerMock: SubscriptionsTracking { subscriptions.removeAll() isSubscribedReturnValue = false } + + func getTopics() -> [String] { + return Array(subscriptions.keys) + } } #endif diff --git a/Sources/WalletConnectSign/Auth/Services/AuthResponseTopicResubscriptionService.swift b/Sources/WalletConnectSign/Auth/Services/AuthResponseTopicResubscriptionService.swift index 4d8af4005..e17ef6426 100644 --- a/Sources/WalletConnectSign/Auth/Services/AuthResponseTopicResubscriptionService.swift +++ b/Sources/WalletConnectSign/Auth/Services/AuthResponseTopicResubscriptionService.swift @@ -30,19 +30,14 @@ class AuthResponseTopicResubscriptionService { self.logger = logger self.authResponseTopicRecordsStore = authResponseTopicRecordsStore cleanExpiredRecordsIfNeeded() - setupConnectionSubscriptions() + subscribeResponsTopics() } - func setupConnectionSubscriptions() { - networkingInteractor.socketConnectionStatusPublisher - .sink { [unowned self] status in - guard status == .connected else { return } - let topics = authResponseTopicRecordsStore.getAll().map{$0.topic} - Task(priority: .high) { - try await networkingInteractor.batchSubscribe(topics: topics) - } - } - .store(in: &publishers) + func subscribeResponsTopics() { + let topics = authResponseTopicRecordsStore.getAll().map{$0.topic} + Task(priority: .background) { + try await networkingInteractor.batchSubscribe(topics: topics) + } } func cleanExpiredRecordsIfNeeded() { diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index a18c922c8..7bba80c19 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -45,7 +45,7 @@ final class SessionEngine { self.sessionRequestsProvider = sessionRequestsProvider self.invalidRequestsSanitiser = invalidRequestsSanitiser - setupConnectionSubscriptions() + subscribeActiveSessions() setupRequestSubscriptions() setupResponseSubscriptions() setupUpdateSubscriptions() @@ -88,16 +88,11 @@ final class SessionEngine { private extension SessionEngine { - func setupConnectionSubscriptions() { - networkingInteractor.socketConnectionStatusPublisher - .sink { [unowned self] status in - guard status == .connected else { return } - let topics = sessionStore.getAll().map{$0.topic} - Task(priority: .high) { - try await networkingInteractor.batchSubscribe(topics: topics) - } - } - .store(in: &publishers) + func subscribeActiveSessions() { + let topics = sessionStore.getAll().map{$0.topic} + Task(priority: .background) { + try await networkingInteractor.batchSubscribe(topics: topics) + } } func setupRequestSubscriptions() { From ec680e9082e50052e650fe6e9dbef55a6d8a6142 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 23 Aug 2024 07:00:03 +0200 Subject: [PATCH 06/20] Add SocketStatusProvider --- .../WalletConnectNetworking/Networking.swift | 2 +- Sources/WalletConnectRelay/Dispatching.swift | 33 +++++--- .../AutomaticSocketConnectionHandler.swift | 78 +++++++++++++++---- .../Engine/Common/SessionEngine.swift | 1 - 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/Sources/WalletConnectNetworking/Networking.swift b/Sources/WalletConnectNetworking/Networking.swift index 0b5277ea1..c52a03e89 100644 --- a/Sources/WalletConnectNetworking/Networking.swift +++ b/Sources/WalletConnectNetworking/Networking.swift @@ -40,7 +40,7 @@ public class Networking { /// - socketFactory: web socket factory /// - socketConnectionType: socket connection type static public func configure( - relayHost: String = "relay.walletconnect.com", + relayHost: String = "relayx.walletconnect.com", groupIdentifier: String, projectId: String, socketFactory: WebSocketFactory, diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 37ae0884e..d39453194 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -23,8 +23,6 @@ final class Dispatcher: NSObject, Dispatching { private let networkMonitor: NetworkMonitoring private let logger: ConsoleLogging - private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) - var socketConnectionStatusPublisher: AnyPublisher { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } @@ -56,7 +54,6 @@ final class Dispatcher: NSObject, Dispatching { super.init() setUpWebSocketSession() - setUpSocketConnectionObserving() } func send(_ string: String, completion: @escaping (Error?) -> Void) { @@ -132,18 +129,36 @@ extension Dispatcher { } } + +} + +protocol SocketStatusProviding { + var socketConnectionStatusPublisher: AnyPublisher { get } +} + +class SocketStatusProvider: SocketStatusProviding { + private var socket: WebSocketConnecting + private let logger: ConsoleLogging + private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) + + var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + init(socket: WebSocketConnecting, + logger: ConsoleLogging) { + self.socket = socket + self.logger = logger + setUpSocketConnectionObserving() + } + private func setUpSocketConnectionObserving() { socket.onConnect = { [unowned self] in self.socketConnectionStatusPublisherSubject.send(.connected) } socket.onDisconnect = { [unowned self] error in + logger.debug("Socket disconnected with error: \(error?.localizedDescription ?? "Unknown error")") self.socketConnectionStatusPublisherSubject.send(.disconnected) - if error != nil { - self.socket.request.url = relayUrlFactory.create() - } - Task(priority: .high) { - await self.socketConnectionHandler.handleDisconnection() - } } } } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 6727c373b..8cc57dbc9 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -17,17 +17,25 @@ class AutomaticSocketConnectionHandler { private let defaultTimeout: Int = 60 private let logger: ConsoleLogging private let subscriptionsTracker: SubscriptionsTracking + private let socketStatusProvider: SocketStatusProviding private var publishers = Set() private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.automatic_socket_connection", qos: .utility, attributes: .concurrent) + private var reconnectionAttempts = 0 + private let maxImmediateAttempts = 3 + private let periodicReconnectionInterval: TimeInterval = 5.0 + private var reconnectionTimer: DispatchSourceTimer? + private var isConnecting = false + init( socket: WebSocketConnecting, networkMonitor: NetworkMonitoring = NetworkMonitor(), appStateObserver: AppStateObserving = AppStateObserver(), backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar(), subscriptionsTracker: SubscriptionsTracking, - logger: ConsoleLogging + logger: ConsoleLogging, + socketStatusProvider: SocketStatusProviding ) { self.appStateObserver = appStateObserver self.socket = socket @@ -35,30 +43,65 @@ class AutomaticSocketConnectionHandler { self.backgroundTaskRegistrar = backgroundTaskRegistrar self.logger = logger self.subscriptionsTracker = subscriptionsTracker + self.socketStatusProvider = socketStatusProvider setUpStateObserving() setUpNetworkMonitoring() } func connect() { - // Attempt to handle connection + // Start the connection process + isConnecting = true socket.connect() - // Start a timer for the fallback mechanism - let timer = DispatchSource.makeTimerSource(queue: concurrentQueue) - timer.schedule(deadline: .now() + .seconds(defaultTimeout)) - timer.setEventHandler { [weak self] in - guard let self = self else { - timer.cancel() - return - } - if !self.socket.isConnected { - self.logger.debug("Connection timed out, will rety to connect...") - retryToConnect() + // Monitor the onConnect event to reset flags when connected + socket.onConnect = { [unowned self] in + isConnecting = false + reconnectionAttempts = 0 // Reset reconnection attempts on successful connection + stopPeriodicReconnectionTimer() // Stop any ongoing periodic reconnection attempts + } + + // Monitor the onDisconnect event to handle reconnections + socket.onDisconnect = { [unowned self] error in + logger.debug("Socket disconnected: \(error?.localizedDescription ?? "Unknown error")") + + if isConnecting { + // Handle reconnection logic + handleFailedConnectionAndReconnectIfNeeded() } - timer.cancel() } - timer.resume() + } + + private func stopPeriodicReconnectionTimer() { + reconnectionTimer?.cancel() + reconnectionTimer = nil + } + + private func startPeriodicReconnectionTimer() { + reconnectionTimer?.cancel() // Cancel any existing timer + reconnectionTimer = DispatchSource.makeTimerSource(queue: concurrentQueue) + reconnectionTimer?.schedule(deadline: .now(), repeating: periodicReconnectionInterval) + + reconnectionTimer?.setEventHandler { [weak self] in + guard let self = self else { return } + self.logger.debug("Periodic reconnection attempt...") + self.socket.connect() // Attempt to reconnect + + // The onConnect handler will stop the timer and reset states if connection is successful + } + + reconnectionTimer?.resume() + } + + private func handleFailedConnectionAndReconnectIfNeeded() { + if reconnectionAttempts < maxImmediateAttempts { + reconnectionAttempts += 1 + logger.debug("Immediate reconnection attempt \(reconnectionAttempts) of \(maxImmediateAttempts)") + socket.connect() + } else { + logger.debug("Max immediate reconnection attempts reached. Switching to periodic reconnection every \(periodicReconnectionInterval) seconds.") + startPeriodicReconnectionTimer() + } } private func setUpStateObserving() { @@ -96,10 +139,10 @@ class AutomaticSocketConnectionHandler { } } - func reconnectIfNeeded() { - + // Check if client has active subscriptions and only then subscribe + if !socket.isConnected && subscriptionsTracker.isSubscribed() { connect() } @@ -121,6 +164,7 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { throw Errors.manualSocketDisconnectionForbidden } + no longer called from dispatcher func handleDisconnection() async { guard await appStateObserver.currentState == .foreground else { return } reconnectIfNeeded() diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 7bba80c19..203cd6d16 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -87,7 +87,6 @@ final class SessionEngine { // MARK: - Privates private extension SessionEngine { - func subscribeActiveSessions() { let topics = sessionStore.getAll().map{$0.topic} Task(priority: .background) { From 40c85f3e547a503998a5c5c1c8f932552fbc96e4 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 23 Aug 2024 07:15:19 +0200 Subject: [PATCH 07/20] fix build --- Sources/WalletConnectRelay/Dispatching.swift | 40 ++---------- .../RelayClientFactory.swift | 6 +- .../AutomaticSocketConnectionHandler.swift | 62 +++++++++---------- .../SocketStatusProvider.swift | 34 ++++++++++ 4 files changed, 72 insertions(+), 70 deletions(-) create mode 100644 Sources/WalletConnectRelay/SocketStatusProvider.swift diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index d39453194..9d2198cee 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -22,9 +22,10 @@ final class Dispatcher: NSObject, Dispatching { private let relayUrlFactory: RelayUrlFactory private let networkMonitor: NetworkMonitoring private let logger: ConsoleLogging + private let socketStatusProvider: SocketStatusProviding var socketConnectionStatusPublisher: AnyPublisher { - socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + socketStatusProvider.socketConnectionStatusPublisher } var networkConnectionStatusPublisher: AnyPublisher { @@ -43,14 +44,15 @@ final class Dispatcher: NSObject, Dispatching { networkMonitor: NetworkMonitoring, socket: WebSocketConnecting, logger: ConsoleLogging, - socketConnectionHandler: SocketConnectionHandler + socketConnectionHandler: SocketConnectionHandler, + socketStatusProvider: SocketStatusProviding ) { self.socketConnectionHandler = socketConnectionHandler self.relayUrlFactory = relayUrlFactory self.networkMonitor = networkMonitor self.logger = logger - self.socket = socket + self.socketStatusProvider = socketStatusProvider super.init() setUpWebSocketSession() @@ -71,6 +73,7 @@ final class Dispatcher: NSObject, Dispatching { return send(string, completion: completion) } + // Always connect when there is a message to be sent if !socket.isConnected { socketConnectionHandler.handleInternalConnect() } @@ -131,34 +134,3 @@ extension Dispatcher { } - -protocol SocketStatusProviding { - var socketConnectionStatusPublisher: AnyPublisher { get } -} - -class SocketStatusProvider: SocketStatusProviding { - private var socket: WebSocketConnecting - private let logger: ConsoleLogging - private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) - - var socketConnectionStatusPublisher: AnyPublisher { - socketConnectionStatusPublisherSubject.eraseToAnyPublisher() - } - - init(socket: WebSocketConnecting, - logger: ConsoleLogging) { - self.socket = socket - self.logger = logger - setUpSocketConnectionObserving() - } - - private func setUpSocketConnectionObserving() { - socket.onConnect = { [unowned self] in - self.socketConnectionStatusPublisherSubject.send(.connected) - } - socket.onDisconnect = { [unowned self] error in - logger.debug("Socket disconnected with error: \(error?.localizedDescription ?? "Unknown error")") - self.socketConnectionStatusPublisherSubject.send(.disconnected) - } - } -} diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index f00295fe0..0091d496c 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -63,9 +63,10 @@ public struct RelayClientFactory { } let subscriptionsTracker = SubscriptionsTracker() + let socketStatusProvider = SocketStatusProvider(socket: socket, logger: logger) var socketConnectionHandler: SocketConnectionHandler! switch socketConnectionType { - case .automatic: socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: subscriptionsTracker, logger: logger) + case .automatic: socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: subscriptionsTracker, logger: logger, socketStatusProvider: socketStatusProvider) case .manual: socketConnectionHandler = ManualSocketConnectionHandler(socket: socket, logger: logger) } @@ -75,7 +76,8 @@ public struct RelayClientFactory { networkMonitor: networkMonitor, socket: socket, logger: logger, - socketConnectionHandler: socketConnectionHandler + socketConnectionHandler: socketConnectionHandler, + socketStatusProvider: socketStatusProvider ) let rpcHistory = RPCHistoryFactory.createForRelay(keyValueStorage: keyValueStorage) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 8cc57dbc9..3a4bb774c 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -14,7 +14,6 @@ class AutomaticSocketConnectionHandler { private let appStateObserver: AppStateObserving private let networkMonitor: NetworkMonitoring private let backgroundTaskRegistrar: BackgroundTaskRegistering - private let defaultTimeout: Int = 60 private let logger: ConsoleLogging private let subscriptionsTracker: SubscriptionsTracking private let socketStatusProvider: SocketStatusProviding @@ -47,29 +46,35 @@ class AutomaticSocketConnectionHandler { setUpStateObserving() setUpNetworkMonitoring() + setUpSocketStatusObserving() // Set up to observe socket status changes } func connect() { // Start the connection process isConnecting = true socket.connect() + } - // Monitor the onConnect event to reset flags when connected - socket.onConnect = { [unowned self] in - isConnecting = false - reconnectionAttempts = 0 // Reset reconnection attempts on successful connection - stopPeriodicReconnectionTimer() // Stop any ongoing periodic reconnection attempts - } - - // Monitor the onDisconnect event to handle reconnections - socket.onDisconnect = { [unowned self] error in - logger.debug("Socket disconnected: \(error?.localizedDescription ?? "Unknown error")") - - if isConnecting { - // Handle reconnection logic - handleFailedConnectionAndReconnectIfNeeded() + private func setUpSocketStatusObserving() { + socketStatusProvider.socketConnectionStatusPublisher + .sink { [unowned self] status in + switch status { + case .connected: + isConnecting = false + reconnectionAttempts = 0 // Reset reconnection attempts on successful connection + stopPeriodicReconnectionTimer() // Stop any ongoing periodic reconnection attempts + case .disconnected: + if isConnecting { + // Handle reconnection logic + handleFailedConnectionAndReconnectIfNeeded() + } else { + Task(priority: .high) { + await handleDisconnection() + } + } + } } - } + .store(in: &publishers) } private func stopPeriodicReconnectionTimer() { @@ -82,10 +87,9 @@ class AutomaticSocketConnectionHandler { reconnectionTimer = DispatchSource.makeTimerSource(queue: concurrentQueue) reconnectionTimer?.schedule(deadline: .now(), repeating: periodicReconnectionInterval) - reconnectionTimer?.setEventHandler { [weak self] in - guard let self = self else { return } - self.logger.debug("Periodic reconnection attempt...") - self.socket.connect() // Attempt to reconnect + reconnectionTimer?.setEventHandler { [unowned self] in + logger.debug("Periodic reconnection attempt...") + socket.connect() // Attempt to reconnect // The onConnect handler will stop the timer and reset states if connection is successful } @@ -115,9 +119,9 @@ class AutomaticSocketConnectionHandler { } private func setUpNetworkMonitoring() { - networkMonitor.networkConnectionStatusPublisher.sink { [weak self] networkConnectionStatus in + networkMonitor.networkConnectionStatusPublisher.sink { [unowned self] networkConnectionStatus in if networkConnectionStatus == .connected { - self?.reconnectIfNeeded() + reconnectIfNeeded() } } .store(in: &publishers) @@ -133,16 +137,8 @@ class AutomaticSocketConnectionHandler { socket.disconnect() } - private func retryToConnect() { - if !socket.isConnected { - connect() - } - } - func reconnectIfNeeded() { - - // Check if client has active subscriptions and only then subscribe - + // Check if client has active subscriptions and only then attempt to reconnect if !socket.isConnected && subscriptionsTracker.isSubscribed() { connect() } @@ -155,7 +151,7 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { func handleInternalConnect() { connect() } - + func handleConnect() throws { throw Errors.manualSocketConnectionForbidden } @@ -164,10 +160,8 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { throw Errors.manualSocketDisconnectionForbidden } - no longer called from dispatcher func handleDisconnection() async { guard await appStateObserver.currentState == .foreground else { return } reconnectIfNeeded() } } - diff --git a/Sources/WalletConnectRelay/SocketStatusProvider.swift b/Sources/WalletConnectRelay/SocketStatusProvider.swift new file mode 100644 index 000000000..1da3fa37d --- /dev/null +++ b/Sources/WalletConnectRelay/SocketStatusProvider.swift @@ -0,0 +1,34 @@ + +import Foundation +import Combine + +protocol SocketStatusProviding { + var socketConnectionStatusPublisher: AnyPublisher { get } +} + +class SocketStatusProvider: SocketStatusProviding { + private var socket: WebSocketConnecting + private let logger: ConsoleLogging + private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) + + var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + init(socket: WebSocketConnecting, + logger: ConsoleLogging) { + self.socket = socket + self.logger = logger + setUpSocketConnectionObserving() + } + + private func setUpSocketConnectionObserving() { + socket.onConnect = { [unowned self] in + self.socketConnectionStatusPublisherSubject.send(.connected) + } + socket.onDisconnect = { [unowned self] error in + logger.debug("Socket disconnected with error: \(error?.localizedDescription ?? "Unknown error")") + self.socketConnectionStatusPublisherSubject.send(.disconnected) + } + } +} From 600b408c310323fe214cb71fe124b303c9ee5ad4 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 23 Aug 2024 07:18:01 +0200 Subject: [PATCH 08/20] savepoint --- .../AutomaticSocketConnectionHandler.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 3a4bb774c..b0a2f8706 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -46,7 +46,7 @@ class AutomaticSocketConnectionHandler { setUpStateObserving() setUpNetworkMonitoring() - setUpSocketStatusObserving() // Set up to observe socket status changes + setUpSocketStatusObserving() } func connect() { @@ -77,6 +77,17 @@ class AutomaticSocketConnectionHandler { .store(in: &publishers) } + private func handleFailedConnectionAndReconnectIfNeeded() { + if reconnectionAttempts < maxImmediateAttempts { + reconnectionAttempts += 1 + logger.debug("Immediate reconnection attempt \(reconnectionAttempts) of \(maxImmediateAttempts)") + socket.connect() + } else { + logger.debug("Max immediate reconnection attempts reached. Switching to periodic reconnection every \(periodicReconnectionInterval) seconds.") + startPeriodicReconnectionTimer() + } + } + private func stopPeriodicReconnectionTimer() { reconnectionTimer?.cancel() reconnectionTimer = nil @@ -97,17 +108,6 @@ class AutomaticSocketConnectionHandler { reconnectionTimer?.resume() } - private func handleFailedConnectionAndReconnectIfNeeded() { - if reconnectionAttempts < maxImmediateAttempts { - reconnectionAttempts += 1 - logger.debug("Immediate reconnection attempt \(reconnectionAttempts) of \(maxImmediateAttempts)") - socket.connect() - } else { - logger.debug("Max immediate reconnection attempts reached. Switching to periodic reconnection every \(periodicReconnectionInterval) seconds.") - startPeriodicReconnectionTimer() - } - } - private func setUpStateObserving() { appStateObserver.onWillEnterBackground = { [unowned self] in registerBackgroundTask() From d1bbfe83fc3111afafc2eec171d53caa81c51adc Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 23 Aug 2024 08:09:05 +0200 Subject: [PATCH 09/20] clean up tests --- .../SocketConnectionHandler/WebSocket.swift | 40 +++++++++++++ .../SocketStatusProvider.swift | 14 +++++ ...utomaticSocketConnectionHandlerTests.swift | 14 ++--- Tests/RelayerTests/DispatcherTests.swift | 57 +++---------------- 4 files changed, 66 insertions(+), 59 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/WebSocket.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/WebSocket.swift index fd9d96a56..d4042ee19 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/WebSocket.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/WebSocket.swift @@ -14,3 +14,43 @@ public protocol WebSocketConnecting: AnyObject { public protocol WebSocketFactory { func create(with url: URL) -> WebSocketConnecting } + +#if DEBUG +class WebSocketMock: WebSocketConnecting { + var request: URLRequest = URLRequest(url: URL(string: "wss://relay.walletconnect.com")!) + + var onText: ((String) -> Void)? + var onConnect: (() -> Void)? + var onDisconnect: ((Error?) -> Void)? + var sendCallCount: Int = 0 + var isConnected: Bool = false + + func connect() { + isConnected = true + onConnect?() + } + + func disconnect() { + isConnected = false + onDisconnect?(nil) + } + + func write(string: String, completion: (() -> Void)?) { + sendCallCount+=1 + } +} +#endif + +#if DEBUG +class WebSocketFactoryMock: WebSocketFactory { + private let webSocket: WebSocketMock + + init(webSocket: WebSocketMock) { + self.webSocket = webSocket + } + + func create(with url: URL) -> WebSocketConnecting { + return webSocket + } +} +#endif diff --git a/Sources/WalletConnectRelay/SocketStatusProvider.swift b/Sources/WalletConnectRelay/SocketStatusProvider.swift index 1da3fa37d..1003fe01e 100644 --- a/Sources/WalletConnectRelay/SocketStatusProvider.swift +++ b/Sources/WalletConnectRelay/SocketStatusProvider.swift @@ -32,3 +32,17 @@ class SocketStatusProvider: SocketStatusProviding { } } } + +#if DEBUG +final class SocketStatusProviderMock: SocketStatusProviding { + private var socketConnectionStatusPublisherSubject = PassthroughSubject() + + var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + func simulateConnectionStatus(_ status: SocketConnectionStatus) { + socketConnectionStatusPublisherSubject.send(status) + } +} +#endif diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 101fdf3ad..0da827d1b 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -9,35 +9,31 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { var appStateObserver: AppStateObserverMock! var backgroundTaskRegistrar: BackgroundTaskRegistrarMock! var subscriptionsTracker: SubscriptionsTrackerMock! + var socketStatusProviderMock: SocketStatusProviderMock! override func setUp() { webSocketSession = WebSocketMock() networkMonitor = NetworkMonitoringMock() appStateObserver = AppStateObserverMock() - let webSocket = WebSocketMock() let defaults = RuntimeKeyValueStorage() let logger = ConsoleLoggerMock() let keychainStorageMock = DispatcherKeychainStorageMock() let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychainStorageMock, logger: logger) - - let socketAuthenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage) - let relayUrlFactory = RelayUrlFactory( - relayHost: "relay.walletconnect.com", - projectId: "1012db890cf3cfb0c1cdc929add657ba", - socketAuthenticator: socketAuthenticator - ) backgroundTaskRegistrar = BackgroundTaskRegistrarMock() subscriptionsTracker = SubscriptionsTrackerMock() + socketStatusProviderMock = SocketStatusProviderMock() + sut = AutomaticSocketConnectionHandler( socket: webSocketSession, networkMonitor: networkMonitor, appStateObserver: appStateObserver, backgroundTaskRegistrar: backgroundTaskRegistrar, subscriptionsTracker: subscriptionsTracker, - logger: ConsoleLoggerMock() + logger: logger, + socketStatusProvider: socketStatusProviderMock // Use the mock ) } diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index e8b0de168..3b41c4600 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -14,48 +14,13 @@ class DispatcherKeychainStorageMock: KeychainStorageProtocol { func deleteAll() throws {} } -class WebSocketMock: WebSocketConnecting { - var request: URLRequest = URLRequest(url: URL(string: "wss://relay.walletconnect.com")!) - - var onText: ((String) -> Void)? - var onConnect: (() -> Void)? - var onDisconnect: ((Error?) -> Void)? - var sendCallCount: Int = 0 - var isConnected: Bool = false - - func connect() { - isConnected = true - onConnect?() - } - - func disconnect() { - isConnected = false - onDisconnect?(nil) - } - - func write(string: String, completion: (() -> Void)?) { - sendCallCount+=1 - } -} - -class WebSocketFactoryMock: WebSocketFactory { - private let webSocket: WebSocketMock - - init(webSocket: WebSocketMock) { - self.webSocket = webSocket - } - - func create(with url: URL) -> WebSocketConnecting { - return webSocket - } -} - final class DispatcherTests: XCTestCase { var publishers = Set() var sut: Dispatcher! var webSocket: WebSocketMock! var networkMonitor: NetworkMonitoringMock! - + var socketStatusProviderMock: SocketStatusProviderMock! + override func setUp() { webSocket = WebSocketMock() let webSocketFactory = WebSocketFactoryMock(webSocket: webSocket) @@ -72,13 +37,15 @@ final class DispatcherTests: XCTestCase { socketAuthenticator: socketAuthenticator ) let socketConnectionHandler = ManualSocketConnectionHandler(socket: webSocket, logger: logger) + socketStatusProviderMock = SocketStatusProviderMock() sut = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: relayUrlFactory, networkMonitor: networkMonitor, socket: webSocket, logger: ConsoleLoggerMock(), - socketConnectionHandler: socketConnectionHandler + socketConnectionHandler: socketConnectionHandler, + socketStatusProvider: socketStatusProviderMock ) } @@ -88,16 +55,6 @@ final class DispatcherTests: XCTestCase { XCTAssertEqual(webSocket.sendCallCount, 1) } -// func testTextFramesSentAfterReconnectingSocket() { -// try! sut.disconnect(closeCode: .normalClosure) -// sut.send("1"){_ in} -// sut.send("2"){_ in} -// XCTAssertEqual(webSocketSession.sendCallCount, 0) -// try! sut.connect() -// socketConnectionObserver.onConnect?() -// XCTAssertEqual(webSocketSession.sendCallCount, 2) -// } - func testOnMessage() { let expectation = expectation(description: "on message") sut.onMessage = { message in @@ -114,7 +71,7 @@ final class DispatcherTests: XCTestCase { guard status == .connected else { return } expectation.fulfill() }.store(in: &publishers) - webSocket.onConnect?() + socketStatusProviderMock.simulateConnectionStatus(.connected) waitForExpectations(timeout: 0.001) } @@ -125,7 +82,7 @@ final class DispatcherTests: XCTestCase { guard status == .disconnected else { return } expectation.fulfill() }.store(in: &publishers) - webSocket.onDisconnect?(nil) + socketStatusProviderMock.simulateConnectionStatus(.disconnected) waitForExpectations(timeout: 0.001) } } From 4f7591ea3c8dd9659f12942427bcb433c7b1fbed Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 23 Aug 2024 09:57:32 +0200 Subject: [PATCH 10/20] test periodic reconnection --- .../AutomaticSocketConnectionHandler.swift | 10 +-- ...utomaticSocketConnectionHandlerTests.swift | 62 ++++++++++++++++++- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index b0a2f8706..ec16596db 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -21,11 +21,11 @@ class AutomaticSocketConnectionHandler { private var publishers = Set() private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.automatic_socket_connection", qos: .utility, attributes: .concurrent) - private var reconnectionAttempts = 0 - private let maxImmediateAttempts = 3 - private let periodicReconnectionInterval: TimeInterval = 5.0 - private var reconnectionTimer: DispatchSourceTimer? - private var isConnecting = false + var reconnectionAttempts = 0 + let maxImmediateAttempts = 3 + var periodicReconnectionInterval: TimeInterval = 5.0 + var reconnectionTimer: DispatchSourceTimer? + var isConnecting = false init( socket: WebSocketConnecting, diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 0da827d1b..6b1809b35 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -33,7 +33,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { backgroundTaskRegistrar: backgroundTaskRegistrar, subscriptionsTracker: subscriptionsTracker, logger: logger, - socketStatusProvider: socketStatusProviderMock // Use the mock + socketStatusProvider: socketStatusProviderMock ) } @@ -180,4 +180,64 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { // Expect the socket to reconnect since there are subscriptions XCTAssertTrue(webSocketSession.isConnected) } + + func testSwitchesToPeriodicReconnectionAfterMaxImmediateAttempts() { + sut.connect() // Start connection process + + // Simulate immediate reconnection attempts + for _ in 0...sut.maxImmediateAttempts { + socketStatusProviderMock.simulateConnectionStatus(.disconnected) + } + + // Now we should be switching to periodic reconnection attempts + // Check reconnectionAttempts is set to maxImmediateAttempts + XCTAssertEqual(sut.reconnectionAttempts, sut.maxImmediateAttempts) + XCTAssertNotNil(sut.reconnectionTimer) // Periodic reconnection timer should be started + } + + func testPeriodicReconnectionStopsAfterSuccessfulConnection() { + sut.connect() // Start connection process + + // Simulate immediate reconnection attempts + for _ in 0...sut.maxImmediateAttempts { + socketStatusProviderMock.simulateConnectionStatus(.disconnected) + } + + // Check that periodic reconnection starts + XCTAssertNotNil(sut.reconnectionTimer) + + // Now simulate the connection being successful + socketStatusProviderMock.simulateConnectionStatus(.connected) + + // Periodic reconnection timer should stop + XCTAssertNil(sut.reconnectionTimer) + XCTAssertEqual(sut.reconnectionAttempts, 0) // Attempts should be reset + } + + func testPeriodicReconnectionAttempts() { + subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions + webSocketSession.disconnect() + sut.periodicReconnectionInterval = 0.0001 + sut.connect() // Start connection process + + // Simulate immediate reconnection attempts to switch to periodic + for _ in 0...sut.maxImmediateAttempts { + socketStatusProviderMock.simulateConnectionStatus(.disconnected) + } + + // Ensure we have switched to periodic reconnection + XCTAssertNotNil(sut.reconnectionTimer) + + // Simulate the periodic timer firing without waiting for real time + let expectation = XCTestExpectation(description: "Periodic reconnection attempt made") + sut.reconnectionTimer?.setEventHandler { + self.socketStatusProviderMock.simulateConnectionStatus(.connected) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + + // Check that the periodic reconnection attempt was made + XCTAssertTrue(webSocketSession.isConnected) // Assume that connection would have been attempted + } } From 8edd4a1034ff55976a592dcd677b5cd83fff023f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 23 Aug 2024 12:55:22 +0200 Subject: [PATCH 11/20] fix tests --- .../RelayIntegrationTests/RelayClientEndToEndTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 5e69702cc..4eccebd9b 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -52,14 +52,16 @@ final class RelayClientEndToEndTests: XCTestCase { socketAuthenticator: socketAuthenticator ) - let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: SubscriptionsTracker(), logger: logger) + let socketStatusProvider = SocketStatusProvider(socket: socket, logger: logger) + let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: SubscriptionsTracker(), logger: logger, socketStatusProvider: socketStatusProvider) let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, networkMonitor: networkMonitor, socket: socket, logger: logger, - socketConnectionHandler: socketConnectionHandler + socketConnectionHandler: socketConnectionHandler, + socketStatusProvider: socketStatusProvider ) let keychain = KeychainStorageMock() let relayClient = RelayClientFactory.create( From 6df02aa579fd55cbab5ee2fa6bdead3c4943fa6f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 26 Aug 2024 09:23:14 +0200 Subject: [PATCH 12/20] fix reconnection issue --- Sources/WalletConnectNetworking/Networking.swift | 2 +- .../AutomaticSocketConnectionHandler.swift | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectNetworking/Networking.swift b/Sources/WalletConnectNetworking/Networking.swift index c52a03e89..0b5277ea1 100644 --- a/Sources/WalletConnectNetworking/Networking.swift +++ b/Sources/WalletConnectNetworking/Networking.swift @@ -40,7 +40,7 @@ public class Networking { /// - socketFactory: web socket factory /// - socketConnectionType: socket connection type static public func configure( - relayHost: String = "relayx.walletconnect.com", + relayHost: String = "relay.walletconnect.com", groupIdentifier: String, projectId: String, socketFactory: WebSocketFactory, diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index ec16596db..ccafbbb8b 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -94,15 +94,18 @@ class AutomaticSocketConnectionHandler { } private func startPeriodicReconnectionTimer() { - reconnectionTimer?.cancel() // Cancel any existing timer + guard reconnectionTimer == nil else {return} + reconnectionTimer = DispatchSource.makeTimerSource(queue: concurrentQueue) - reconnectionTimer?.schedule(deadline: .now(), repeating: periodicReconnectionInterval) + let initialDelay: DispatchTime = .now() + periodicReconnectionInterval + + reconnectionTimer?.schedule(deadline: initialDelay, repeating: periodicReconnectionInterval) reconnectionTimer?.setEventHandler { [unowned self] in logger.debug("Periodic reconnection attempt...") socket.connect() // Attempt to reconnect - // The onConnect handler will stop the timer and reset states if connection is successful + // The socketConnectionStatusPublisher handler will stop the timer and reset states if connection is successful } reconnectionTimer?.resume() From daf5258c19c604d31feb23131a5364dabb39bba1 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 26 Aug 2024 12:29:04 +0200 Subject: [PATCH 13/20] rename method --- .../AutomaticSocketConnectionHandler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index ccafbbb8b..e1edd574b 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -84,7 +84,7 @@ class AutomaticSocketConnectionHandler { socket.connect() } else { logger.debug("Max immediate reconnection attempts reached. Switching to periodic reconnection every \(periodicReconnectionInterval) seconds.") - startPeriodicReconnectionTimer() + startPeriodicReconnectionTimerIfNeeded() } } @@ -93,7 +93,7 @@ class AutomaticSocketConnectionHandler { reconnectionTimer = nil } - private func startPeriodicReconnectionTimer() { + private func startPeriodicReconnectionTimerIfNeeded() { guard reconnectionTimer == nil else {return} reconnectionTimer = DispatchSource.makeTimerSource(queue: concurrentQueue) From 2ed2d17a9dd7333c1981868bfe5f9dcfdaca30c2 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 27 Aug 2024 08:59:30 +0200 Subject: [PATCH 14/20] add init event --- Sources/Events/EventsClient.swift | 47 +++++++++++++++++++----- Sources/Events/EventsClientFactory.swift | 3 +- Sources/Events/InitEvent.swift | 25 +++++++++++++ Sources/Events/InitEventsStorage.swift | 42 +++++++++++++++++++++ 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 Sources/Events/InitEvent.swift create mode 100644 Sources/Events/InitEventsStorage.swift diff --git a/Sources/Events/EventsClient.swift b/Sources/Events/EventsClient.swift index da0dd30bd..5d1664c29 100644 --- a/Sources/Events/EventsClient.swift +++ b/Sources/Events/EventsClient.swift @@ -14,25 +14,28 @@ public class EventsClient: EventsClientProtocol { private let logger: ConsoleLogging private var stateStorage: TelemetryStateStorage private let messageEventsStorage: MessageEventsStorage + private let initEventsStorage: InitEventsStorage init( eventsCollector: EventsCollector, eventsDispatcher: EventsDispatcher, logger: ConsoleLogging, stateStorage: TelemetryStateStorage, - messageEventsStorage: MessageEventsStorage + messageEventsStorage: MessageEventsStorage, + initEventsStorage: InitEventsStorage ) { self.eventsCollector = eventsCollector self.eventsDispatcher = eventsDispatcher self.logger = logger self.stateStorage = stateStorage self.messageEventsStorage = messageEventsStorage + self.initEventsStorage = initEventsStorage - if stateStorage.telemetryEnabled { - Task { await sendStoredEvents() } - } else { + if !stateStorage.telemetryEnabled { self.eventsCollector.storage.clearErrorEvents() } + saveInitEvent() + Task { await sendStoredEvents() } } public func setLogging(level: LoggingLevel) { @@ -63,6 +66,30 @@ public class EventsClient: EventsClientProtocol { messageEventsStorage.saveMessageEvent(event) } + public func saveInitEvent() { + logger.debug("Will store an init event") + + let bundleId = Bundle.main.bundleIdentifier ?? "Unknown" + let clientId = (try? Networking.interactor.getClientId()) ?? "Unknown" + let userAgent = EnvironmentInfo.userAgent + + let props = InitEvent.Props( + properties: InitEvent.Properties( + clientId: clientId, + userAgent: userAgent + ) + ) + + let event = InitEvent( + eventId: UUID().uuidString, + bundleId: bundleId, + timestamp: Int64(Date().timeIntervalSince1970 * 1000), + props: props + ) + + initEventsStorage.saveInitEvent(event) + } + // Public method to set telemetry enabled or disabled public func setTelemetryEnabled(_ enabled: Bool) { stateStorage.telemetryEnabled = enabled @@ -78,24 +105,26 @@ public class EventsClient: EventsClientProtocol { let traceEvents = eventsCollector.storage.fetchErrorEvents() let messageEvents = messageEventsStorage.fetchMessageEvents() + let initEvents = initEventsStorage.fetchInitEvents() - guard !traceEvents.isEmpty || !messageEvents.isEmpty else { return } + guard !traceEvents.isEmpty || !messageEvents.isEmpty || !initEvents.isEmpty else { return } var combinedEvents: [AnyCodable] = [] - // Wrap trace events combinedEvents.append(contentsOf: traceEvents.map { AnyCodable($0) }) - // Wrap message events combinedEvents.append(contentsOf: messageEvents.map { AnyCodable($0) }) + combinedEvents.append(contentsOf: initEvents.map { AnyCodable($0) }) + logger.debug("Will send combined events") do { let success: Bool = try await eventsDispatcher.executeWithRetry(events: combinedEvents) if success { logger.debug("Combined events sent successfully") - self.eventsCollector.storage.clearErrorEvents() - self.messageEventsStorage.clearMessageEvents() + eventsCollector.storage.clearErrorEvents() + messageEventsStorage.clearMessageEvents() + initEventsStorage.clearInitEvents() } } catch { logger.debug("Failed to send events after multiple attempts: \(error)") diff --git a/Sources/Events/EventsClientFactory.swift b/Sources/Events/EventsClientFactory.swift index b45df2f26..c7cf66f2d 100644 --- a/Sources/Events/EventsClientFactory.swift +++ b/Sources/Events/EventsClientFactory.swift @@ -19,7 +19,8 @@ public class EventsClientFactory { eventsDispatcher: eventsDispatcher, logger: logger, stateStorage: UserDefaultsTelemetryStateStorage(), - messageEventsStorage: UserDefaultsMessageEventsStorage() + messageEventsStorage: UserDefaultsMessageEventsStorage(), + initEventsStorage: UserDefaultsInitEventsStorage() ) } } diff --git a/Sources/Events/InitEvent.swift b/Sources/Events/InitEvent.swift new file mode 100644 index 000000000..9c1718d29 --- /dev/null +++ b/Sources/Events/InitEvent.swift @@ -0,0 +1,25 @@ +import Foundation + +struct InitEvent: Codable { + struct Props: Codable { + let event: String = "INIT" + let type: String = "None" + let properties: Properties + } + + struct Properties: Codable { + let clientId: String + let userAgent: String + + // Custom CodingKeys to map Swift property names to JSON keys + enum CodingKeys: String, CodingKey { + case clientId = "client_id" + case userAgent = "user_agent" + } + } + + let eventId: String + let bundleId: String + let timestamp: Int64 + let props: Props +} diff --git a/Sources/Events/InitEventsStorage.swift b/Sources/Events/InitEventsStorage.swift new file mode 100644 index 000000000..ae6216554 --- /dev/null +++ b/Sources/Events/InitEventsStorage.swift @@ -0,0 +1,42 @@ +import Foundation + +protocol InitEventsStorage { + func saveInitEvent(_ event: InitEvent) + func fetchInitEvents() -> [InitEvent] + func clearInitEvents() +} + + +class UserDefaultsInitEventsStorage: InitEventsStorage { + private let initEventsKey = "com.walletconnect.sdk.initEvents" + private let maxEvents = 100 + + func saveInitEvent(_ event: InitEvent) { + // Fetch existing events from UserDefaults + var existingEvents = fetchInitEvents() + existingEvents.append(event) + + // Ensure we keep only the last 100 events + if existingEvents.count > maxEvents { + existingEvents = Array(existingEvents.suffix(maxEvents)) + } + + // Save updated events back to UserDefaults + if let encoded = try? JSONEncoder().encode(existingEvents) { + UserDefaults.standard.set(encoded, forKey: initEventsKey) + } + } + + func fetchInitEvents() -> [InitEvent] { + if let data = UserDefaults.standard.data(forKey: initEventsKey), + let events = try? JSONDecoder().decode([InitEvent].self, from: data) { + // Return only the last 100 events + return Array(events.suffix(maxEvents)) + } + return [] + } + + func clearInitEvents() { + UserDefaults.standard.removeObject(forKey: initEventsKey) + } +} From 9b6eb1ac59562c955469f4b3a1a8b7f8f0b9dc7c Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 26 Aug 2024 13:14:26 +0200 Subject: [PATCH 15/20] savepoint --- Sources/WalletConnectRelay/Dispatching.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 9d2198cee..cd239e12c 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -73,12 +73,18 @@ final class Dispatcher: NSObject, Dispatching { return send(string, completion: completion) } + var cancellable: AnyCancellable? + // Always connect when there is a message to be sent if !socket.isConnected { - socketConnectionHandler.handleInternalConnect() + do { + try socketConnectionHandler.handleInternalConnect() + } catch { + cancellable?.cancel() + completion(error) + } } - var cancellable: AnyCancellable? cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) .filter { $0.0 == .connected && $0.1 == .connected } .setFailureType(to: NetworkError.self) From bc10874c33bc3e80ae7bfbaf2328708c7c2a9eb0 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 27 Aug 2024 15:29:56 +0200 Subject: [PATCH 16/20] savepoint --- Sources/Events/InitEvent.swift | 1 + Sources/WalletConnectRelay/Dispatching.swift | 35 +++--- .../AutomaticSocketConnectionHandler.swift | 51 ++++++++- .../SocketConnectionHandler.swift | 2 +- ...utomaticSocketConnectionHandlerTests.swift | 100 ++++++++++++++++++ 5 files changed, 169 insertions(+), 20 deletions(-) diff --git a/Sources/Events/InitEvent.swift b/Sources/Events/InitEvent.swift index 9c1718d29..a3bd7f7fa 100644 --- a/Sources/Events/InitEvent.swift +++ b/Sources/Events/InitEvent.swift @@ -1,3 +1,4 @@ + import Foundation struct InitEvent: Codable { diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index cd239e12c..79d29b5c8 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -18,7 +18,7 @@ final class Dispatcher: NSObject, Dispatching { var socket: WebSocketConnecting var socketConnectionHandler: SocketConnectionHandler - private let defaultTimeout: Int = 5 + private let defaultTimeout: Int = 15 private let relayUrlFactory: RelayUrlFactory private let networkMonitor: NetworkMonitoring private let logger: ConsoleLogging @@ -78,28 +78,29 @@ final class Dispatcher: NSObject, Dispatching { // Always connect when there is a message to be sent if !socket.isConnected { do { - try socketConnectionHandler.handleInternalConnect() + Task { try await socketConnectionHandler.handleInternalConnect() } } catch { cancellable?.cancel() completion(error) } } - cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) - .filter { $0.0 == .connected && $0.1 == .connected } - .setFailureType(to: NetworkError.self) - .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed }) - .sink(receiveCompletion: { result in - switch result { - case .failure(let error): - cancellable?.cancel() - completion(error) - case .finished: break - } - }, receiveValue: { [unowned self] _ in - cancellable?.cancel() - send(string, completion: completion) - }) + // in progress +// cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) +// .filter { $0.0 == .connected && $0.1 == .connected } +// .setFailureType(to: NetworkError.self) +// .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed }) +// .sink(receiveCompletion: { result in +// switch result { +// case .failure(let error): +// cancellable?.cancel() +// completion(error) +// case .finished: break +// } +// }, receiveValue: { [unowned self] _ in +// cancellable?.cancel() +// send(string, completion: completion) +// }) } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index e1edd574b..64ae45838 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -151,10 +151,57 @@ class AutomaticSocketConnectionHandler { // MARK: - SocketConnectionHandler extension AutomaticSocketConnectionHandler: SocketConnectionHandler { - func handleInternalConnect() { - connect() + func handleInternalConnect() async throws { + let maxAttempts = maxImmediateAttempts + let timeout: TimeInterval = 15 + var attempts = 0 + + // Start the connection process immediately + connect() // This will set isConnecting = true and attempt to connect + + // Use Combine publisher to monitor connection status + let connectionStatusPublisher = socketStatusProvider.socketConnectionStatusPublisher + .share() + .makeConnectable() + + let connection = connectionStatusPublisher.connect() + + // Ensure connection is canceled when done + defer { connection.cancel() } + + // Use a Combine publisher to monitor disconnection and timeout + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + var cancellable: AnyCancellable? + + cancellable = connectionStatusPublisher.sink(receiveCompletion: { completion in + switch completion { + case .finished: + continuation.resume() // Connection successful + case .failure(let error): + continuation.resume(throwing: error) // Timeout or connection failure + } + }, receiveValue: { [weak self] status in + guard let self = self else { return } + + if status == .connected { + continuation.resume() // Successfully connected + } else if status == .disconnected { + attempts += 1 + self.logger.debug("Disconnection observed, incrementing attempts to \(attempts)") + + if attempts >= maxAttempts { + self.logger.debug("Max attempts reached. Failing with connection failed error.") + continuation.resume(throwing: NetworkError.connectionFailed) + } + } + }) + + // Store cancellable to keep it alive + self.publishers.insert(cancellable!) + } } + func handleConnect() throws { throw Errors.manualSocketConnectionForbidden } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift index 808ee43df..9b47eb4a0 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift @@ -4,7 +4,7 @@ protocol SocketConnectionHandler { /// handles connection request from the sdk consumes func handleConnect() throws /// handles connection request from sdk's internal function - func handleInternalConnect() + func handleInternalConnect() async throws func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws func handleDisconnection() async } diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 6b1809b35..346722b82 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -240,4 +240,104 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { // Check that the periodic reconnection attempt was made XCTAssertTrue(webSocketSession.isConnected) // Assume that connection would have been attempted } + + func testHandleInternalConnectThrowsAfterThreeDisconnections() async { + // Start a task to call handleInternalConnect and await its result + let handleConnectTask = Task { + do { + try await sut.handleInternalConnect() + XCTFail("Expected handleInternalConnect to throw NetworkError.connectionFailed after three disconnections") + } catch NetworkError.connectionFailed { + // Expected behavior + XCTAssertEqual(sut.reconnectionAttempts, sut.maxImmediateAttempts) + } catch { + XCTFail("Expected NetworkError.connectionFailed, but got \(error)") + } + } + + let startObservingExpectation = XCTestExpectation(description: "Start observing connection status") + + // Allow handleInternalConnect() to start observing + DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { + startObservingExpectation.fulfill() + } + await fulfillment(of: [startObservingExpectation], timeout: 0.02) + + // Simulate three disconnections + for _ in 0.. Date: Tue, 27 Aug 2024 15:56:04 +0200 Subject: [PATCH 17/20] add more tests --- .../AutomaticSocketConnectionHandler.swift | 55 ++++++++++--------- ...utomaticSocketConnectionHandlerTests.swift | 29 ++++++++++ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 64ae45838..3b7ae52f1 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -146,6 +146,8 @@ class AutomaticSocketConnectionHandler { connect() } } + var requestTimeout: TimeInterval = 15 + } // MARK: - SocketConnectionHandler @@ -153,11 +155,12 @@ class AutomaticSocketConnectionHandler { extension AutomaticSocketConnectionHandler: SocketConnectionHandler { func handleInternalConnect() async throws { let maxAttempts = maxImmediateAttempts - let timeout: TimeInterval = 15 var attempts = 0 - // Start the connection process immediately - connect() // This will set isConnecting = true and attempt to connect + // Start the connection process immediately if not already connecting + if !isConnecting { + connect() // This will set isConnecting = true and attempt to connect + } // Use Combine publisher to monitor connection status let connectionStatusPublisher = socketStatusProvider.socketConnectionStatusPublisher @@ -173,35 +176,37 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in var cancellable: AnyCancellable? - cancellable = connectionStatusPublisher.sink(receiveCompletion: { completion in - switch completion { - case .finished: - continuation.resume() // Connection successful - case .failure(let error): - continuation.resume(throwing: error) // Timeout or connection failure - } - }, receiveValue: { [weak self] status in - guard let self = self else { return } - - if status == .connected { - continuation.resume() // Successfully connected - } else if status == .disconnected { - attempts += 1 - self.logger.debug("Disconnection observed, incrementing attempts to \(attempts)") - - if attempts >= maxAttempts { - self.logger.debug("Max attempts reached. Failing with connection failed error.") - continuation.resume(throwing: NetworkError.connectionFailed) + cancellable = connectionStatusPublisher + .setFailureType(to: NetworkError.self) // Now set failure type after makeConnectable + .timeout(.seconds(requestTimeout), scheduler: concurrentQueue, customError: { NetworkError.connectionFailed }) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + continuation.resume() // Connection successful + case .failure(let error): + continuation.resume(throwing: error) // Timeout or connection failure } - } - }) + }, receiveValue: { [weak self] status in + guard let self = self else { return } + + if status == .connected { + continuation.resume() // Successfully connected + } else if status == .disconnected { + attempts += 1 + self.logger.debug("Disconnection observed, incrementing attempts to \(attempts)") + + if attempts >= maxAttempts { + self.logger.debug("Max attempts reached. Failing with connection failed error.") + continuation.resume(throwing: NetworkError.connectionFailed) + } + } + }) // Store cancellable to keep it alive self.publishers.insert(cancellable!) } } - func handleConnect() throws { throw Errors.manualSocketConnectionForbidden } diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 346722b82..54e5fd0ae 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -340,4 +340,33 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { XCTAssertNil(sut.reconnectionTimer) XCTAssertEqual(sut.reconnectionAttempts, 0) // Attempts should reset after success } + + func testHandleInternalConnectTimeout() async { + // Set a short timeout for testing purposes + sut.requestTimeout = 0.001 + // Start a task to call handleInternalConnect and await its result + let handleConnectTask = Task { + do { + try await sut.handleInternalConnect() + XCTFail("Expected handleInternalConnect to throw NetworkError.connectionFailed due to timeout") + } catch NetworkError.connectionFailed { + // Expected behavior + XCTAssertEqual(sut.reconnectionAttempts, 0) // No reconnection attempts should be recorded for timeout + } catch { + XCTFail("Expected NetworkError.connectionFailed due to timeout, but got \(error)") + } + } + + // Expectation to ensure handleInternalConnect() is observing + let startObservingExpectation = XCTestExpectation(description: "Start observing connection status") + + // Allow handleInternalConnect() to start observing + DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { + startObservingExpectation.fulfill() + } + await fulfillment(of: [startObservingExpectation], timeout: 0.02) + + // No connection simulation to allow timeout to trigger + await handleConnectTask.value + } } From d81ec99ff469737dd81c2b96bb3de41e7ec1d517 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 27 Aug 2024 20:10:45 +0200 Subject: [PATCH 18/20] update dispatcher --- Sources/WalletConnectRelay/Dispatching.swift | 39 +++++++------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 79d29b5c8..48010907f 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -69,40 +69,27 @@ final class Dispatcher: NSObject, Dispatching { } func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) { - guard !socket.isConnected || !networkMonitor.isConnected else { - return send(string, completion: completion) + // Check if the socket is already connected and ready to send + if socket.isConnected && networkMonitor.isConnected { + send(string, completion: completion) + return } - var cancellable: AnyCancellable? - - // Always connect when there is a message to be sent - if !socket.isConnected { + // Start the connection process if not already connected + Task { do { - Task { try await socketConnectionHandler.handleInternalConnect() } + // Await the connection handler to establish the connection + try await socketConnectionHandler.handleInternalConnect() + + // If successful, send the message + send(string, completion: completion) } catch { - cancellable?.cancel() + // If an error occurs during connection, complete with that error completion(error) } } - - // in progress -// cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) -// .filter { $0.0 == .connected && $0.1 == .connected } -// .setFailureType(to: NetworkError.self) -// .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed }) -// .sink(receiveCompletion: { result in -// switch result { -// case .failure(let error): -// cancellable?.cancel() -// completion(error) -// case .finished: break -// } -// }, receiveValue: { [unowned self] _ in -// cancellable?.cancel() -// send(string, completion: completion) -// }) } - + func protectedSend(_ string: String) async throws { var isResumed = false From 086116a4a663c755dc90b02eb3f3ce99dbfb8ac1 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 27 Aug 2024 20:27:56 +0200 Subject: [PATCH 19/20] update socket connection --- .../AutomaticSocketConnectionHandler.swift | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 3b7ae52f1..6b17bd42d 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -156,6 +156,8 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { func handleInternalConnect() async throws { let maxAttempts = maxImmediateAttempts var attempts = 0 + var isResumed = false // Track if continuation has been resumed + let requestTimeout = self.requestTimeout // Timeout set at the class level // Start the connection process immediately if not already connecting if !isConnecting { @@ -177,26 +179,30 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { var cancellable: AnyCancellable? cancellable = connectionStatusPublisher - .setFailureType(to: NetworkError.self) // Now set failure type after makeConnectable + .setFailureType(to: NetworkError.self) // Set failure type to NetworkError .timeout(.seconds(requestTimeout), scheduler: concurrentQueue, customError: { NetworkError.connectionFailed }) .sink(receiveCompletion: { completion in - switch completion { - case .finished: - continuation.resume() // Connection successful - case .failure(let error): + guard !isResumed else { return } // Ensure continuation is only resumed once + isResumed = true + cancellable?.cancel() // Cancel the subscription to prevent further events + + // Handle only the failure case, as .finished is not expected to be meaningful here + if case .failure(let error) = completion { continuation.resume(throwing: error) // Timeout or connection failure } - }, receiveValue: { [weak self] status in - guard let self = self else { return } - + }, receiveValue: { [unowned self] status in + guard !isResumed else { return } // Ensure continuation is only resumed once if status == .connected { + isResumed = true + cancellable?.cancel() // Cancel the subscription to prevent further events continuation.resume() // Successfully connected } else if status == .disconnected { attempts += 1 - self.logger.debug("Disconnection observed, incrementing attempts to \(attempts)") + logger.debug("Disconnection observed, incrementing attempts to \(attempts)") if attempts >= maxAttempts { - self.logger.debug("Max attempts reached. Failing with connection failed error.") + isResumed = true + cancellable?.cancel() // Cancel the subscription to prevent further events continuation.resume(throwing: NetworkError.connectionFailed) } } From 1b45c8fdab11f2fc3aa06e7f8043cd7b936cd41f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 29 Aug 2024 08:09:38 +0200 Subject: [PATCH 20/20] savepoint --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 34a63bc5b..f5b588c63 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/getsentry/sentry-cocoa.git", "state": { "branch": null, - "revision": "d2ced2d961b34573ebd2ea0567a2f1408e90f0ae", - "version": "8.34.0" + "revision": "5575af93efb776414f243e93d6af9f6258dc539a", + "version": "8.36.0" } }, {