diff --git a/AppSyncRealTimeClient/Connection/AppSyncConnection/AppSyncSubscriptionConnection+Connection.swift b/AppSyncRealTimeClient/Connection/AppSyncConnection/AppSyncSubscriptionConnection+Connection.swift index 9d96374f..660aea59 100644 --- a/AppSyncRealTimeClient/Connection/AppSyncConnection/AppSyncSubscriptionConnection+Connection.swift +++ b/AppSyncRealTimeClient/Connection/AppSyncConnection/AppSyncSubscriptionConnection+Connection.swift @@ -13,7 +13,7 @@ extension AppSyncSubscriptionConnection { // we should retry the connection if connectionState == .notConnected && subscriptionState == .inProgress { - let connectionError = ConnectionProviderError.connection + let connectionError = ConnectionProviderError.connection(nil, nil) handleError(error: connectionError) return } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+StaleConnection.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+StaleConnection.swift index 15e443b5..0572b006 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+StaleConnection.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+StaleConnection.swift @@ -67,7 +67,7 @@ extension RealtimeConnectionProvider { self.status = .notConnected self.isStaleConnection = false self.websocket.disconnect() - self.updateCallback(event: .error(ConnectionProviderError.connection)) + self.updateCallback(event: .error(ConnectionProviderError.connection(nil, nil))) } } } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+Websocket.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+Websocket.swift index 384ae37f..7a944f47 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+Websocket.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+Websocket.swift @@ -27,7 +27,16 @@ extension RealtimeConnectionProvider: AppSyncWebsocketDelegate { self.updateCallback(event: .connection(self.status)) return } - self.updateCallback(event: .error(ConnectionProviderError.connection)) + #if os(watchOS) + AppSyncLogger.debug( + "[RealtimeConnectionProvider] on watchOS, received disconnect." + ) + self.updateCallback(event: .error(ConnectionProviderError.connection( + "This API uses low-level networking (websockets). Running on watchOS only works for specific circumstances.", + error))) + #else + self.updateCallback(event: .error(ConnectionProviderError.connection(nil, error))) + #endif } } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider.swift index 0cd88a17..3bca0979 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider.swift @@ -85,7 +85,13 @@ public class RealtimeConnectionProvider: ConnectionProvider { self.serialCallbackQueue = serialCallbackQueue self.connectivityMonitor = connectivityMonitor + // On a physical watchOS device, it is showing "unsatisfied" despite connected to the internet + // according to https://developer.apple.com/forums/thread/729568 + // To avoid an incorrect network status state for the system, do not use connectivity monitor + // for watchOS apps. + #if !os(watchOS) connectivityMonitor.start(onUpdates: handleConnectivityUpdates(connectivity:)) + #endif if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { subscribeToLimitExceededThrottle() @@ -249,6 +255,6 @@ public class RealtimeConnectionProvider: ConnectionProvider { /// - Warning: This must be invoked from the `connectionQueue` private func receivedConnectionInit() { status = .notConnected - updateCallback(event: .error(ConnectionProviderError.connection)) + updateCallback(event: .error(ConnectionProviderError.connection(nil, nil))) } } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProviderResponse.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProviderResponse.swift index 337a3a1b..0f087619 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProviderResponse.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProviderResponse.swift @@ -76,7 +76,7 @@ extension RealtimeConnectionProviderResponse { // If it is in-progress, return `.connection`. guard connectionState != .inProgress else { - return .connection + return .connection(nil, nil) } if isLimitExceededError() || isMaxSubscriptionReachedError() { diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+StaleConnection.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+StaleConnection.swift index bf654a19..6ed9a2e3 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+StaleConnection.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+StaleConnection.swift @@ -70,7 +70,7 @@ extension RealtimeConnectionProviderAsync { self.status = .notConnected self.isStaleConnection = false self.websocket.disconnect() - self.updateCallback(event: .error(ConnectionProviderError.connection)) + self.updateCallback(event: .error(ConnectionProviderError.connection(nil, nil))) } } } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+Websocket.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+Websocket.swift index 968d0764..f2d91e3d 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+Websocket.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync+Websocket.swift @@ -30,7 +30,16 @@ extension RealtimeConnectionProviderAsync: AppSyncWebsocketDelegate { self.updateCallback(event: .connection(self.status)) return } - self.updateCallback(event: .error(ConnectionProviderError.connection)) + #if os(watchOS) + AppSyncLogger.debug( + "[RealtimeConnectionProvider] on watchOS, received disconnect." + ) + self.updateCallback(event: .error(ConnectionProviderError.connection( + "This API uses low-level networking (websockets). Running on watchOS only works for specific circumstances.", + error))) + #else + self.updateCallback(event: .error(ConnectionProviderError.connection(nil, error))) + #endif } } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync.swift index 577f75b9..8cb3dd50 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnectionAsync/RealtimeConnectionProviderAsync.swift @@ -80,7 +80,14 @@ public class RealtimeConnectionProviderAsync: ConnectionProvider { self.serialCallbackQueue = serialCallbackQueue self.connectivityMonitor = connectivityMonitor + // On a physical watchOS device, it is showing "unsatisfied" despite connected to the internet + // according to https://developer.apple.com/forums/thread/729568 + // To avoid an incorrect network status state for the system, do not use connectivity monitor + // for watchOS apps. + #if !os(watchOS) connectivityMonitor.start(onUpdates: handleConnectivityUpdates(connectivity:)) + #endif + subscribeToLimitExceededThrottle() } @@ -236,7 +243,7 @@ public class RealtimeConnectionProviderAsync: ConnectionProvider { /// - Warning: This must be invoked from the `taskQueue` private func receivedConnectionInit() { status = .notConnected - updateCallback(event: .error(ConnectionProviderError.connection)) + updateCallback(event: .error(ConnectionProviderError.connection(nil, nil))) } } #endif diff --git a/AppSyncRealTimeClient/ConnectionProvider/ConnectionProviderError.swift b/AppSyncRealTimeClient/ConnectionProvider/ConnectionProviderError.swift index 081c8f92..28b0ec7e 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/ConnectionProviderError.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/ConnectionProviderError.swift @@ -10,7 +10,7 @@ import Foundation public enum ConnectionProviderError: Error { /// Caused by connection error - case connection + case connection(String?, Error?) /// Caused by JSON parse error. The first optional String will be the connection identifier if available. case jsonParse(String?, Error?) diff --git a/AppSyncRealTimeClient/Websocket/Starscream/StarscreamAdapter.swift b/AppSyncRealTimeClient/Websocket/Starscream/StarscreamAdapter.swift index a75591c8..23fab3db 100644 --- a/AppSyncRealTimeClient/Websocket/Starscream/StarscreamAdapter.swift +++ b/AppSyncRealTimeClient/Websocket/Starscream/StarscreamAdapter.swift @@ -23,6 +23,8 @@ public class StarscreamAdapter: AppSyncWebsocketProvider { } } + let watchOSConnectivityTimer: CountdownTimer + public init() { let serialQueue = DispatchQueue(label: "com.amazonaws.StarscreamAdapter.serialQueue") let callbackQueue = DispatchQueue( @@ -32,6 +34,7 @@ public class StarscreamAdapter: AppSyncWebsocketProvider { self._isConnected = false self.serialQueue = serialQueue self.callbackQueue = callbackQueue + self.watchOSConnectivityTimer = CountdownTimer() } public func connect(urlRequest: URLRequest, protocols: [String], delegate: AppSyncWebsocketDelegate?) { @@ -49,6 +52,33 @@ public class StarscreamAdapter: AppSyncWebsocketProvider { self.socket?.delegate = self self.socket?.callbackQueue = self.callbackQueue self.socket?.connect() + #if os(watchOS) + AppSyncLogger.debug( + "[StarscreamAdapter] Starting connectivity timer for watchOS for 3s." + ) + self.watchOSConnectivityTimer.start(interval: 3) { + AppSyncLogger.debug( + "[StarscreamAdapter] watchOS connectivity timer is up." + ) + self.serialQueue.async { + if !self._isConnected { + AppSyncLogger.debug( + "[StarscreamAdapter] watchOS subscriptions not connected after 3s." + ) + AppSyncLogger.debug( + "[StarscreamAdapter] Manually send disconnect." + ) + let error = ConnectionProviderError.connection("WatchOS Error", nil) + delegate?.websocketDidDisconnect(provider: self, error: error) + } else { + AppSyncLogger.debug( + "[StarscreamAdapter] watchOS subscriptions are connected within 3s." + ) + } + } + + } + #endif } } diff --git a/AppSyncRealTimeClientTests/Connection/AppSyncSubscriptionConnectionErrorHandlerTests.swift b/AppSyncRealTimeClientTests/Connection/AppSyncSubscriptionConnectionErrorHandlerTests.swift index 5b38b857..d9441570 100644 --- a/AppSyncRealTimeClientTests/Connection/AppSyncSubscriptionConnectionErrorHandlerTests.swift +++ b/AppSyncRealTimeClientTests/Connection/AppSyncSubscriptionConnectionErrorHandlerTests.swift @@ -232,7 +232,7 @@ class AppSyncSubscriptionConnectionErrorHandlerTests: XCTestCase { XCTAssertEqual(connection.subscriptionState, .subscribed) XCTAssertNotNil(connectionProvider.listener) - let connectionError = ConnectionProviderError.connection + let connectionError = ConnectionProviderError.connection(nil, nil) connection.handleError(error: connectionError) wait(for: [failedEvent], timeout: 5) XCTAssertEqual(connection.subscriptionState, .notSubscribed) @@ -304,7 +304,7 @@ class AppSyncSubscriptionConnectionErrorHandlerTests: XCTestCase { XCTAssertEqual(connection.subscriptionState, .subscribed) XCTAssertNotNil(connectionProvider.listener) - let connectionError = ConnectionProviderError.connection + let connectionError = ConnectionProviderError.connection(nil, nil) connection.handleError(error: connectionError) wait(for: [connectedOnRetryExpectation], timeout: 5) XCTAssertEqual(connection.subscriptionState, .subscribed) diff --git a/AppSyncRealTimeClientTests/Mocks/MockConnectionProvider.swift b/AppSyncRealTimeClientTests/Mocks/MockConnectionProvider.swift index 45b7115c..06462e5b 100644 --- a/AppSyncRealTimeClientTests/Mocks/MockConnectionProvider.swift +++ b/AppSyncRealTimeClientTests/Mocks/MockConnectionProvider.swift @@ -24,7 +24,7 @@ class MockConnectionProvider: ConnectionProvider { func connect() { guard validConnection else { - listener?(.error(ConnectionProviderError.connection)) + listener?(.error(ConnectionProviderError.connection(nil, nil))) return } @@ -39,7 +39,7 @@ class MockConnectionProvider: ConnectionProvider { func write(_ message: AppSyncMessage) { guard validConnection else { - listener?(.error(ConnectionProviderError.connection)) + listener?(.error(ConnectionProviderError.connection(nil, nil))) return } @@ -70,7 +70,7 @@ class MockConnectionProvider: ConnectionProvider { func disconnect() { guard validConnection else { - listener?(.error(ConnectionProviderError.connection)) + listener?(.error(ConnectionProviderError.connection(nil, nil))) return }