Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Law aefe583213 code clean up 2022-01-31 16:06:48 -05:00
Michael Law bf25b04f9d feat: Attempt to reconnect on connectivity 2022-01-28 11:49:12 -05:00
7 changed files with 275 additions and 3 deletions

View File

@ -10,7 +10,7 @@
--elseposition same-line --elseposition same-line
--enable fileHeader --enable fileHeader
--header "//\n// Copyright 2018-{created.year} Amazon.com,\n// Inc. or its affiliates. All Rights Reserved.\n//\n// SPDX-License-Identifier: Apache-2.0\n//" --header "//\n// Copyright 2018-2021 Amazon.com,\n// Inc. or its affiliates. All Rights Reserved.\n//\n// SPDX-License-Identifier: Apache-2.0\n//"
--disable hoistPatternLet --disable hoistPatternLet
--patternlet inline --patternlet inline

View File

@ -42,6 +42,9 @@
217F39F12406EA4000F1A0B3 /* MockConnectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217F39EB2406EA3F00F1A0B3 /* MockConnectionProvider.swift */; }; 217F39F12406EA4000F1A0B3 /* MockConnectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217F39EB2406EA3F00F1A0B3 /* MockConnectionProvider.swift */; };
217F39F22406EA4000F1A0B3 /* ConnectionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217F39ED2406EA4000F1A0B3 /* ConnectionProviderTests.swift */; }; 217F39F22406EA4000F1A0B3 /* ConnectionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217F39ED2406EA4000F1A0B3 /* ConnectionProviderTests.swift */; };
217F39F32406EA4000F1A0B3 /* RealtimeGatewayURLInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217F39EF2406EA4000F1A0B3 /* RealtimeGatewayURLInterceptorTests.swift */; }; 217F39F32406EA4000F1A0B3 /* RealtimeGatewayURLInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217F39EF2406EA4000F1A0B3 /* RealtimeGatewayURLInterceptorTests.swift */; };
219BFF3B27A38902000FC148 /* schema.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 219BFF3A27A38902000FC148 /* schema.graphql */; };
219BFF4927A3B238000FC148 /* ConnectivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 219BFF4827A3B237000FC148 /* ConnectivityMonitor.swift */; };
219BFF4B27A3B24F000FC148 /* ConnectivityPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 219BFF4A27A3B24F000FC148 /* ConnectivityPath.swift */; };
21D38B412409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D38B402409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.swift */; }; 21D38B412409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D38B402409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.swift */; };
21D38B432409AFBD00EC2A8D /* AppSyncRealTimeClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 217F398F2405D9D500F1A0B3 /* AppSyncRealTimeClient.framework */; }; 21D38B432409AFBD00EC2A8D /* AppSyncRealTimeClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 217F398F2405D9D500F1A0B3 /* AppSyncRealTimeClient.framework */; };
21D38B4C2409B6C000EC2A8D /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 21D38B4B2409B6C000EC2A8D /* amplifyconfiguration.json */; }; 21D38B4C2409B6C000EC2A8D /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 21D38B4B2409B6C000EC2A8D /* amplifyconfiguration.json */; };
@ -159,6 +162,9 @@
217F39EB2406EA3F00F1A0B3 /* MockConnectionProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockConnectionProvider.swift; sourceTree = "<group>"; }; 217F39EB2406EA3F00F1A0B3 /* MockConnectionProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockConnectionProvider.swift; sourceTree = "<group>"; };
217F39ED2406EA4000F1A0B3 /* ConnectionProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionProviderTests.swift; sourceTree = "<group>"; }; 217F39ED2406EA4000F1A0B3 /* ConnectionProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionProviderTests.swift; sourceTree = "<group>"; };
217F39EF2406EA4000F1A0B3 /* RealtimeGatewayURLInterceptorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealtimeGatewayURLInterceptorTests.swift; sourceTree = "<group>"; }; 217F39EF2406EA4000F1A0B3 /* RealtimeGatewayURLInterceptorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealtimeGatewayURLInterceptorTests.swift; sourceTree = "<group>"; };
219BFF3A27A38902000FC148 /* schema.graphql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.graphql; sourceTree = "<group>"; };
219BFF4827A3B237000FC148 /* ConnectivityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMonitor.swift; sourceTree = "<group>"; };
219BFF4A27A3B24F000FC148 /* ConnectivityPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityPath.swift; sourceTree = "<group>"; };
21D38B3E2409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppSyncRealTimeClientIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 21D38B3E2409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppSyncRealTimeClientIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
21D38B402409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncRealTimeClientIntegrationTests.swift; sourceTree = "<group>"; }; 21D38B402409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncRealTimeClientIntegrationTests.swift; sourceTree = "<group>"; };
21D38B422409AFBD00EC2A8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 21D38B422409AFBD00EC2A8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -292,9 +298,10 @@
217F39912405D9D500F1A0B3 /* AppSyncRealTimeClient */ = { 217F39912405D9D500F1A0B3 /* AppSyncRealTimeClient */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
217F39932405D9D500F1A0B3 /* Info.plist */,
217F39B82406E98300F1A0B3 /* Connection */, 217F39B82406E98300F1A0B3 /* Connection */,
217F39A92406E98300F1A0B3 /* ConnectionProvider */, 217F39A92406E98300F1A0B3 /* ConnectionProvider */,
219BFF4727A3B223000FC148 /* ConnectivityMonitor */,
217F39932405D9D500F1A0B3 /* Info.plist */,
21D38B6E240A272F00EC2A8D /* Interceptor */, 21D38B6E240A272F00EC2A8D /* Interceptor */,
217F39C62406E98400F1A0B3 /* Support */, 217F39C62406E98400F1A0B3 /* Support */,
217F39C12406E98400F1A0B3 /* Websocket */, 217F39C12406E98400F1A0B3 /* Websocket */,
@ -437,6 +444,15 @@
path = Support; path = Support;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
219BFF4727A3B223000FC148 /* ConnectivityMonitor */ = {
isa = PBXGroup;
children = (
219BFF4827A3B237000FC148 /* ConnectivityMonitor.swift */,
219BFF4A27A3B24F000FC148 /* ConnectivityPath.swift */,
);
path = ConnectivityMonitor;
sourceTree = "<group>";
};
21D38B3F2409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests */ = { 21D38B3F2409AFBD00EC2A8D /* AppSyncRealTimeClientIntegrationTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -497,6 +513,7 @@
21D38B95240C4DC200EC2A8D /* Support */ = { 21D38B95240C4DC200EC2A8D /* Support */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
219BFF3A27A38902000FC148 /* schema.graphql */,
21D38B98240C4E1C00EC2A8D /* ConfigurationHelper.swift */, 21D38B98240C4E1C00EC2A8D /* ConfigurationHelper.swift */,
21D38B96240C4DCF00EC2A8D /* Error+Extension.swift */, 21D38B96240C4DCF00EC2A8D /* Error+Extension.swift */,
21D38B9C240C540D00EC2A8D /* TestCommonConstants.swift */, 21D38B9C240C540D00EC2A8D /* TestCommonConstants.swift */,
@ -753,6 +770,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
21D38B4E2409B8B200EC2A8D /* README.md in Resources */, 21D38B4E2409B8B200EC2A8D /* README.md in Resources */,
219BFF3B27A38902000FC148 /* schema.graphql in Resources */,
21D38B4C2409B6C000EC2A8D /* amplifyconfiguration.json in Resources */, 21D38B4C2409B6C000EC2A8D /* amplifyconfiguration.json in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -1103,6 +1121,7 @@
217F39CD2406E98400F1A0B3 /* InterceptableConnection.swift in Sources */, 217F39CD2406E98400F1A0B3 /* InterceptableConnection.swift in Sources */,
21D38B8E240A3C2300EC2A8D /* ConnectionProviderFactory.swift in Sources */, 21D38B8E240A3C2300EC2A8D /* ConnectionProviderFactory.swift in Sources */,
217F39E02406E98400F1A0B3 /* AppSyncWebsocketProvider.swift in Sources */, 217F39E02406E98400F1A0B3 /* AppSyncWebsocketProvider.swift in Sources */,
219BFF4927A3B238000FC148 /* ConnectivityMonitor.swift in Sources */,
FAB7E91224D2644E00DF1EA1 /* RealtimeConnectionProvider+StaleConnection.swift in Sources */, FAB7E91224D2644E00DF1EA1 /* RealtimeConnectionProvider+StaleConnection.swift in Sources */,
217F39D32406E98400F1A0B3 /* RealtimeConnectionProvider.swift in Sources */, 217F39D32406E98400F1A0B3 /* RealtimeConnectionProvider.swift in Sources */,
217F39D12406E98400F1A0B3 /* AppSyncConnectionRequest.swift in Sources */, 217F39D12406E98400F1A0B3 /* AppSyncConnectionRequest.swift in Sources */,
@ -1126,6 +1145,7 @@
217F39D42406E98400F1A0B3 /* RealtimeConnectionProvider+Websocket.swift in Sources */, 217F39D42406E98400F1A0B3 /* RealtimeConnectionProvider+Websocket.swift in Sources */,
217F39DC2406E98400F1A0B3 /* AppSyncSubscriptionConnection.swift in Sources */, 217F39DC2406E98400F1A0B3 /* AppSyncSubscriptionConnection.swift in Sources */,
21D38B6D240A262800EC2A8D /* AppSyncJSONHelper.swift in Sources */, 21D38B6D240A262800EC2A8D /* AppSyncJSONHelper.swift in Sources */,
219BFF4B27A3B24F000FC148 /* ConnectivityPath.swift in Sources */,
217F39CE2406E98400F1A0B3 /* ConnectionProviderError.swift in Sources */, 217F39CE2406E98400F1A0B3 /* ConnectionProviderError.swift in Sources */,
217F39E12406E98400F1A0B3 /* StarscreamAdapter.swift in Sources */, 217F39E12406E98400F1A0B3 /* StarscreamAdapter.swift in Sources */,
217F39D72406E98400F1A0B3 /* RealtimeConnectionProvider+ConnectionInterceptable.swift in Sources */, 217F39D72406E98400F1A0B3 /* RealtimeConnectionProvider+ConnectionInterceptable.swift in Sources */,

View File

@ -45,6 +45,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableThreadSanitizer = "YES"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"

View File

@ -33,6 +33,28 @@ extension RealtimeConnectionProvider {
staleConnectionTimer?.resetCountdown() staleConnectionTimer?.resetCountdown()
} }
/// Handle updates from the ConnectivityMonitor
func handleConnectivityUpdates(connectivity: ConnectivityPath) {
connectionQueue.async {[weak self] in
guard let self = self else {
return
}
AppSyncLogger.debug("[RealtimeConnectionProvider] Status: \(self.status). Connectivity status: \(connectivity.status)")
if self.status == .connected && connectivity.status == .unsatisfied && !self.isStaleConnection {
AppSyncLogger.debug("[RealtimeConnectionProvider] Connetion is stale. Pending reconnect on connectivity.")
self.isStaleConnection = true
} else if self.status == .connected && self.isStaleConnection && connectivity.status == .satisfied {
AppSyncLogger.debug("[RealtimeConnectionProvider] Connetion is stale. Disconnecting to begin reconnect.")
if self.staleConnectionTimer != nil {
self.stopStaleConnectionTimer()
}
self.disconnectStaleConnection()
}
}
}
/// Fired when the stale connection timer expires /// Fired when the stale connection timer expires
private func disconnectStaleConnection() { private func disconnectStaleConnection() {
connectionQueue.async {[weak self] in connectionQueue.async {[weak self] in
@ -41,9 +63,9 @@ extension RealtimeConnectionProvider {
} }
AppSyncLogger.error("[RealtimeConnectionProvider] Realtime connection is stale, disconnecting.") AppSyncLogger.error("[RealtimeConnectionProvider] Realtime connection is stale, disconnecting.")
self.status = .notConnected self.status = .notConnected
self.isStaleConnection = false
self.websocket.disconnect() self.websocket.disconnect()
self.updateCallback(event: .error(ConnectionProviderError.connection)) self.updateCallback(event: .error(ConnectionProviderError.connection))
} }
} }
} }

View File

@ -28,12 +28,18 @@ public class RealtimeConnectionProvider: ConnectionProvider {
/// alive" message will cause the timer to be reset to the full interval. /// alive" message will cause the timer to be reset to the full interval.
var staleConnectionTimer: CountdownTimer? var staleConnectionTimer: CountdownTimer?
/// Intermediate state when the connection is connected and connectivity updates to unsatisfied (offline)
var isStaleConnection: Bool
/// Manages concurrency for socket connections, disconnections, writes, and status reports. /// Manages concurrency for socket connections, disconnections, writes, and status reports.
/// ///
/// Each connection request will be sent to this queue. Connection request are /// Each connection request will be sent to this queue. Connection request are
/// handled one at a time. /// handled one at a time.
let connectionQueue: DispatchQueue let connectionQueue: DispatchQueue
/// Monitor for connectivity updates to disconnect the current connection if it flips between connectivity statuses
let connectivityMonitor: ConnectivityMonitor
/// The serial queue on which status & message callbacks from the web socket are invoked. /// The serial queue on which status & message callbacks from the web socket are invoked.
private let serialCallbackQueue = DispatchQueue( private let serialCallbackQueue = DispatchQueue(
label: "com.amazonaws.AppSyncRealTimeConnectionProvider.callbackQueue" label: "com.amazonaws.AppSyncRealTimeConnectionProvider.callbackQueue"
@ -51,6 +57,9 @@ public class RealtimeConnectionProvider: ConnectionProvider {
self.connectionQueue = DispatchQueue( self.connectionQueue = DispatchQueue(
label: "com.amazonaws.AppSyncRealTimeConnectionProvider.serialQueue" label: "com.amazonaws.AppSyncRealTimeConnectionProvider.serialQueue"
) )
self.isStaleConnection = false
self.connectivityMonitor = ConnectivityMonitor()
connectivityMonitor.start(connectivityUpdates: handleConnectivityUpdates(connectivity:))
} }
// MARK: - ConnectionProvider methods // MARK: - ConnectionProvider methods

View File

@ -0,0 +1,95 @@
//
// Copyright 2018-2021 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
import Network
typealias ConnectivityUpdates = (ConnectivityPath) -> Void
protocol AnyConnectivityMonitor {
func start(connectivityUpdatesQueue: DispatchQueue, onConnectivityUpdates: @escaping ConnectivityUpdates)
func cancel()
}
class ConnectivityMonitor {
var connectivity: ConnectivityPath?
private let connectivityUpdatesQueue = DispatchQueue(
label: "com.amazonaws.ConnectivityMonitor.connectivityUpdatesQueue",
qos: .background
)
private var monitor: AnyConnectivityMonitor?
init(connectivityMonitor: AnyConnectivityMonitor? = nil) {
self.monitor = connectivityMonitor
}
func start(connectivityUpdates: @escaping ConnectivityUpdates) {
if let monitor = monitor {
monitor.start(
connectivityUpdatesQueue: connectivityUpdatesQueue,
onConnectivityUpdates: connectivityUpdates
)
} else if #available(iOS 12.0, *) {
let monitor = NetworkMonitor()
self.monitor = monitor
monitor.start(
connectivityUpdatesQueue: connectivityUpdatesQueue,
onConnectivityUpdates: connectivityUpdates
)
}
}
func cancel() {
guard let monitor = monitor else {
return
}
monitor.cancel()
self.monitor = nil
}
deinit {
cancel()
}
}
@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
class NetworkMonitor: AnyConnectivityMonitor {
private var monitor: NWPathMonitor?
private var onConnectivityUpdates: ConnectivityUpdates?
private var connectivityUpdatesQueue: DispatchQueue?
private let queue = DispatchQueue(label: "com.amazonaws.NetworkMonitor.queue", qos: .background)
func start(connectivityUpdatesQueue: DispatchQueue, onConnectivityUpdates: @escaping ConnectivityUpdates) {
self.connectivityUpdatesQueue = connectivityUpdatesQueue
self.onConnectivityUpdates = onConnectivityUpdates
// A new instance is required each time a monitor is started
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = didUpdate(path:)
monitor.start(queue: queue)
self.monitor = monitor
}
func cancel() {
guard let monitor = monitor else { return }
defer {
self.monitor = nil
}
monitor.cancel()
}
func didUpdate(path: NWPath) {
guard let onConnectivityUpdates = onConnectivityUpdates,
let connectivityUpdatesQueue = connectivityUpdatesQueue else {
return
}
let connectivityPath = ConnectivityPath(path: path)
connectivityUpdatesQueue.async {
onConnectivityUpdates(connectivityPath)
}
}
}

View File

@ -0,0 +1,125 @@
//
// Copyright 2018-2021 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
import Network
struct ConnectivityPath {
let status: ConnectivityStatus
let availableInterfaces: [ConnectivityInterface]
let isExpensive: Bool
let supportsDNS: Bool
let supportsIPv4: Bool
let supportsIPv6: Bool
init(
status: ConnectivityStatus = .unsatisfied,
availableInterfaces: [ConnectivityInterface] = [],
isExpensive: Bool = false,
supportsDNS: Bool = false,
supportsIPv4: Bool = false,
supportsIPv6: Bool = false
) {
self.status = status
self.availableInterfaces = availableInterfaces
self.isExpensive = isExpensive
self.supportsDNS = supportsDNS
self.supportsIPv4 = supportsIPv4
self.supportsIPv6 = supportsIPv6
}
}
extension ConnectivityPath: CustomStringConvertible {
var description: String {
[
"\(status): \(availableInterfaces.description)",
"Expensive = \(isExpensive ? "YES" : "NO")",
"DNS = \(supportsDNS ? "YES" : "NO")",
"IPv4 = \(supportsIPv4 ? "YES" : "NO")",
"IPv6 = \(supportsIPv6 ? "YES" : "NO")"
].joined(separator: "; ")
}
}
extension ConnectivityPath {
@available(iOS 12.0, *)
init(path: NWPath) {
self.status = ConnectivityStatus(status: path.status)
self.availableInterfaces = path.availableInterfaces.map { ConnectivityInterface(interface: $0) }
self.isExpensive = path.isExpensive
self.supportsDNS = path.supportsDNS
self.supportsIPv4 = path.supportsIPv4
self.supportsIPv6 = path.supportsIPv6
}
}
enum ConnectivityInterfaceType: String {
case other
case wifi
case cellular
case wiredEthernet
case loopback
}
extension ConnectivityInterfaceType {
@available(iOS 12.0, *)
init(interfaceType: NWInterface.InterfaceType) {
switch interfaceType {
case .other:
self = .other
case .wifi:
self = .wifi
case .cellular:
self = .cellular
case .wiredEthernet:
self = .wiredEthernet
case .loopback:
self = .loopback
@unknown default:
self = .other
}
}
}
struct ConnectivityInterface {
public let name: String
public let type: ConnectivityInterfaceType
public init(name: String, type: ConnectivityInterfaceType) {
self.name = name
self.type = type
}
}
extension ConnectivityInterface {
@available(iOS 12.0, *)
init(interface: NWInterface) {
self.name = interface.name
self.type = ConnectivityInterfaceType(interfaceType: interface.type)
}
}
enum ConnectivityStatus: String {
case satisfied
case unsatisfied
case requiresConnection
}
extension ConnectivityStatus {
@available(iOS 12.0, *)
init(status: NWPath.Status) {
switch status {
case .satisfied:
self = .satisfied
case .unsatisfied:
self = .unsatisfied
case .requiresConnection:
self = .requiresConnection
@unknown default:
self = .unsatisfied
}
}
}