Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Networking improvements #1348

Merged
merged 10 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Example/DApp/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
)

Sign.instance.logger.setLogging(level: .debug)
Networking.instance.setLogging(level: .debug)

Sign.instance.logsPublisher.sink { log in
switch log {
Expand Down
17 changes: 14 additions & 3 deletions Example/RelayIntegrationTests/RelayClientEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,26 @@ final class RelayClientEndToEndTests: XCTestCase {
projectId: InputConfig.projectId,
socketAuthenticator: socketAuthenticator
)
let socket = WebSocket(url: urlFactory.create(fallback: false))
let socket = WebSocket(url: urlFactory.create())
let webSocketFactory = WebSocketFactoryMock(webSocket: socket)
let networkMonitor = NetworkMonitor()

let relayUrlFactory = RelayUrlFactory(
relayHost: "relay.walletconnect.com",
projectId: "1012db890cf3cfb0c1cdc929add657ba",
socketAuthenticator: socketAuthenticator
)

let socketUrlFallbackHandler = SocketUrlFallbackHandler(relayUrlFactory: relayUrlFactory, logger: logger, socket: socket, networkMonitor: networkMonitor)

let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, logger: logger, socketUrlFallbackHandler: socketUrlFallbackHandler)
let dispatcher = Dispatcher(
socketFactory: webSocketFactory,
relayUrlFactory: urlFactory,
networkMonitor: networkMonitor,
socketConnectionType: .manual,
logger: logger
socket: socket,
logger: logger,
socketConnectionHandler: socketConnectionHandler
)
let keychain = KeychainStorageMock()
let relayClient = RelayClientFactory.create(
Expand Down
59 changes: 20 additions & 39 deletions Sources/WalletConnectRelay/Dispatching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,12 @@ final class Dispatcher: NSObject, Dispatching {
var onMessage: ((String) -> Void)?
var socket: WebSocketConnecting
var socketConnectionHandler: SocketConnectionHandler


private let defaultTimeout: Int = 5
private let relayUrlFactory: RelayUrlFactory
private let networkMonitor: NetworkMonitoring
private let logger: ConsoleLogging

private let defaultTimeout: Int = 5
/// The property is used to determine whether relay.walletconnect.org will be used
/// in case relay.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location).
private var fallback = false

private let socketConnectionStatusPublisherSubject = CurrentValueSubject<SocketConnectionStatus, Never>(.disconnected)

var socketConnectionStatusPublisher: AnyPublisher<SocketConnectionStatus, Never> {
Expand All @@ -42,25 +38,17 @@ final class Dispatcher: NSObject, Dispatching {
socketFactory: WebSocketFactory,
relayUrlFactory: RelayUrlFactory,
networkMonitor: NetworkMonitoring,
socketConnectionType: SocketConnectionType,
logger: ConsoleLogging
socket: WebSocketConnecting,
logger: ConsoleLogging,
socketConnectionHandler: SocketConnectionHandler
) {
self.socketConnectionHandler = socketConnectionHandler
self.relayUrlFactory = relayUrlFactory
self.networkMonitor = networkMonitor
self.logger = logger

let socket = socketFactory.create(with: relayUrlFactory.create(fallback: fallback))
socket.request.addValue(EnvironmentInfo.userAgent, forHTTPHeaderField: "User-Agent")
if let bundleId = Bundle.main.bundleIdentifier {
socket.request.addValue(bundleId, forHTTPHeaderField: "Origin")
}

self.socket = socket

switch socketConnectionType {
case .automatic: socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket)
case .manual: socketConnectionHandler = ManualSocketConnectionHandler(socket: socket)
}


super.init()
setUpWebSocketSession()
setUpSocketConnectionObserving()
Expand Down Expand Up @@ -90,9 +78,6 @@ final class Dispatcher: NSObject, Dispatching {
switch result {
case .failure(let error):
cancellable?.cancel()
if !socket.isConnected {
handleFallbackIfNeeded(error: error)
}
completion(error)
case .finished: break
}
Expand All @@ -101,23 +86,30 @@ final class Dispatcher: NSObject, Dispatching {
send(string, completion: completion)
})
}


func protectedSend(_ string: String) async throws {
var isResumed = false
return try await withCheckedThrowingContinuation { continuation in
protectedSend(string) { error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
if !isResumed {
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
isResumed = true
}
}
}
}

func connect() throws {
// Attempt to handle connection
try socketConnectionHandler.handleConnect()
}


func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws {
try socketConnectionHandler.handleDisconnect(closeCode: closeCode)
}
Expand All @@ -138,22 +130,11 @@ extension Dispatcher {
socket.onDisconnect = { [unowned self] error in
self.socketConnectionStatusPublisherSubject.send(.disconnected)
if error != nil {
self.socket.request.url = relayUrlFactory.create(fallback: fallback)
self.socket.request.url = relayUrlFactory.create()
}
Task(priority: .high) {
await self.socketConnectionHandler.handleDisconnection()
}
}
}

private func handleFallbackIfNeeded(error: NetworkError) {
if error == .connectionFailed && socket.request.url?.host == NetworkConstants.defaultUrl {
logger.debug("[WebSocket] - Fallback to \(NetworkConstants.fallbackUrl)")
fallback = true
socket.request.url = relayUrlFactory.create(fallback: fallback)
Task(priority: .high) {
await self.socketConnectionHandler.handleDisconnection()
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/WalletConnectRelay/RelayClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public final class RelayClient {

logger.debug("[Publish] Sending payload on topic: \(topic)")

Task {try await dispatcher.protectedSend(message)}
try await dispatcher.protectedSend(message)

return try await withUnsafeThrowingContinuation { continuation in
var cancellable: AnyCancellable?
Expand Down
23 changes: 20 additions & 3 deletions Sources/WalletConnectRelay/RelayClientFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,30 @@ public struct RelayClientFactory {
projectId: projectId,
socketAuthenticator: socketAuthenticator
)
let socket = socketFactory.create(with: relayUrlFactory.create())
socket.request.addValue(EnvironmentInfo.userAgent, forHTTPHeaderField: "User-Agent")
if let bundleId = Bundle.main.bundleIdentifier {
socket.request.addValue(bundleId, forHTTPHeaderField: "Origin")
}
let socketFallbackHandler = SocketUrlFallbackHandler(
relayUrlFactory: relayUrlFactory,
logger: logger,
socket: socket,
networkMonitor: networkMonitor
)
var socketConnectionHandler: SocketConnectionHandler!
switch socketConnectionType {
case .automatic: socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, logger: logger, socketUrlFallbackHandler: socketFallbackHandler)
case .manual: socketConnectionHandler = ManualSocketConnectionHandler(socket: socket, logger: logger, socketUrlFallbackHandler: socketFallbackHandler)
}

let dispatcher = Dispatcher(
socketFactory: socketFactory,
relayUrlFactory: relayUrlFactory,
relayUrlFactory: relayUrlFactory,
networkMonitor: networkMonitor,
socketConnectionType: socketConnectionType,
logger: logger
socket: socket,
logger: logger,
socketConnectionHandler: socketConnectionHandler
)

let rpcHistory = RPCHistoryFactory.createForRelay(keyValueStorage: keyValueStorage)
Expand Down
13 changes: 10 additions & 3 deletions Sources/WalletConnectRelay/RelayURLFactory.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import Foundation

struct RelayUrlFactory {
class RelayUrlFactory {
private let relayHost: String
private let projectId: String
private let socketAuthenticator: ClientIdAuthenticating

/// The property is used to determine whether relay.walletconnect.org will be used
/// in case relay.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location).
private var fallback: Bool = false

init(
relayHost: String,
projectId: String,
Expand All @@ -15,7 +18,11 @@ struct RelayUrlFactory {
self.socketAuthenticator = socketAuthenticator
}

func create(fallback: Bool) -> URL {
func setFallback() {
self.fallback = true
}

func create() -> URL {
var components = URLComponents()
components.scheme = "wss"
components.host = fallback ? NetworkConstants.fallbackUrl : relayHost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,60 @@ class AutomaticSocketConnectionHandler {
private let appStateObserver: AppStateObserving
private let networkMonitor: NetworkMonitoring
private let backgroundTaskRegistrar: BackgroundTaskRegistering
private let defaultTimeout: Int = 5
private let logger: ConsoleLogging
private var socketUrlFallbackHandler: SocketUrlFallbackHandler

private var publishers = Set<AnyCancellable>()
private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.automatic_socket_connection", attributes: .concurrent)

init(
socket: WebSocketConnecting,
networkMonitor: NetworkMonitoring = NetworkMonitor(),
appStateObserver: AppStateObserving = AppStateObserver(),
backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar()
backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar(),
logger: ConsoleLogging,
socketUrlFallbackHandler: SocketUrlFallbackHandler
) {
self.appStateObserver = appStateObserver
self.socket = socket
self.networkMonitor = networkMonitor
self.backgroundTaskRegistrar = backgroundTaskRegistrar
self.logger = logger
self.socketUrlFallbackHandler = socketUrlFallbackHandler

setUpStateObserving()
setUpNetworkMonitoring()

socketUrlFallbackHandler.onTryReconnect = { [unowned self] in
Task(priority: .high) {
await tryReconect()
}
}

connect()

}

func connect() {
// Attempt to handle connection
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, initiating fallback...")
self.socketUrlFallbackHandler.handleFallbackIfNeeded(error: .connectionFailed)
}
timer.cancel()
}
timer.resume()
}

private func setUpStateObserving() {
Expand Down Expand Up @@ -73,6 +109,10 @@ class AutomaticSocketConnectionHandler {
// MARK: - SocketConnectionHandler

extension AutomaticSocketConnectionHandler: SocketConnectionHandler {
func tryReconect() async {
guard await appStateObserver.currentState == .foreground else { return }
reconnectIfNeeded()
}

func handleConnect() throws {
throw Errors.manualSocketConnectionForbidden
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import Foundation

class ManualSocketConnectionHandler: SocketConnectionHandler {
var socket: WebSocketConnecting

init(socket: WebSocketConnecting) {
self.socket = socket
}
private let socket: WebSocketConnecting
private let logger: ConsoleLogging
private let defaultTimeout: Int = 5
private var socketUrlFallbackHandler: SocketUrlFallbackHandler
private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.manual_socket_connection", attributes: .concurrent)

init(
socket: WebSocketConnecting,
logger: ConsoleLogging,
socketUrlFallbackHandler: SocketUrlFallbackHandler) {
self.socket = socket
self.logger = logger
self.socketUrlFallbackHandler = socketUrlFallbackHandler

socketUrlFallbackHandler.onTryReconnect = { [unowned self] in
Task(priority: .high) {
await tryReconect()
}
}
}

func handleConnect() throws {
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, initiating fallback...")
self.socketUrlFallbackHandler.handleFallbackIfNeeded(error: .connectionFailed)
}
timer.cancel()
}
timer.resume()
}

func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws {
Expand All @@ -19,4 +50,10 @@ class ManualSocketConnectionHandler: SocketConnectionHandler {
// No operation
// ManualSocketConnectionHandler does not support reconnection logic
}

func tryReconect() async {
if !socket.isConnected {
socket.connect()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ protocol SocketConnectionHandler {
func handleConnect() throws
func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws
func handleDisconnection() async
func tryReconect() async
}
29 changes: 29 additions & 0 deletions Sources/WalletConnectRelay/SocketUrlFallbackHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

class SocketUrlFallbackHandler {
private let relayUrlFactory: RelayUrlFactory
private var logger: ConsoleLogging
private var socket: WebSocketConnecting
private let networkMonitor: NetworkMonitoring
var onTryReconnect: (()->())?

init(
relayUrlFactory: RelayUrlFactory,
logger: ConsoleLogging,
socket: WebSocketConnecting,
networkMonitor: NetworkMonitoring) {
self.relayUrlFactory = relayUrlFactory
self.logger = logger
self.socket = socket
self.networkMonitor = networkMonitor
}

func handleFallbackIfNeeded(error: NetworkError) {
if error == .connectionFailed && socket.request.url?.host == NetworkConstants.defaultUrl {
logger.debug("[WebSocket] - Fallback to \(NetworkConstants.fallbackUrl)")
relayUrlFactory.setFallback()
socket.request.url = relayUrlFactory.create()
onTryReconnect?()
}
}
}
Loading
Loading