diff --git a/Apollo.podspec b/Apollo.podspec index 828c3bff95..d4b03a2254 100644 --- a/Apollo.podspec +++ b/Apollo.podspec @@ -40,7 +40,7 @@ Pod::Spec.new do |s| s.subspec 'WebSocket' do |ss| ss.source_files = 'Sources/ApolloWebSocket/*.swift' ss.dependency 'Apollo/Core' - ss.dependency 'Starscream', '~>4.0.4' + ss.dependency 'Apollo-Starscream', '~>3.1.2' end end diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 6f38452349..3d6c862f13 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -92,9 +92,8 @@ 9B7BDA9C23FDE94C00ACD198 /* WebSocketTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDA9523FDE94C00ACD198 /* WebSocketTask.swift */; }; 9B7BDA9D23FDE94C00ACD198 /* SplitNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */; }; 9B7BDA9E23FDE94C00ACD198 /* OperationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */; }; - 9B7BDA9F23FDE94C00ACD198 /* ApolloWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDA9823FDE94C00ACD198 /* ApolloWebSocket.swift */; }; + 9B7BDA9F23FDE94C00ACD198 /* WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */; }; 9B7BDAA023FDE94C00ACD198 /* WebSocketTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDA9923FDE94C00ACD198 /* WebSocketTransport.swift */; }; - 9B7BDAAC23FDEA7B00ACD198 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9B7BDAAB23FDEA7B00ACD198 /* Starscream */; }; 9B7BDAD023FDEBE300ACD198 /* SQLiteSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDACD23FDEBE300ACD198 /* SQLiteSerialization.swift */; }; 9B7BDAD223FDEBE300ACD198 /* SQLiteNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BDACF23FDEBE300ACD198 /* SQLiteNormalizedCache.swift */; }; 9B7BDAF623FDEE2600ACD198 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 9B7BDAF523FDEE2600ACD198 /* SQLite */; }; @@ -244,6 +243,7 @@ DE0586362669957800265760 /* CacheReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE664ED92666DF150054DB4F /* CacheReference.swift */; }; DE0586372669958F00265760 /* GraphQLError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */; }; DE0586392669985000265760 /* Dictionary+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0586382669985000265760 /* Dictionary+Helpers.swift */; }; + DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE3A2815268BCE6700A1BDC8 /* Starscream */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE3C7A94260A6C1000D2F4FF /* ApolloUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B68353E2463481A00337AE6 /* ApolloUtils.framework */; }; DE3C7A95260A6C1000D2F4FF /* ApolloUtils.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9B68353E2463481A00337AE6 /* ApolloUtils.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -256,6 +256,7 @@ DE6B156A261505660068D642 /* GraphQLMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6B154A261505450068D642 /* GraphQLMap.swift */; }; DE6B15AF26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6B15AE26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift */; }; DE6B15B126152BE10068D642 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; + DE8C84F5268BC42100C54D02 /* DefaultWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C84F4268BC42100C54D02 /* DefaultWebSocket.swift */; }; DECD46D0262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */; }; DECD46FB262F659500924527 /* ApolloCodegenLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B7B6F47233C26D100F32205 /* ApolloCodegenLib.framework */; }; DECD4736262F668500924527 /* UploadAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */; }; @@ -699,7 +700,7 @@ 9B7BDA9523FDE94C00ACD198 /* WebSocketTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketTask.swift; sourceTree = ""; }; 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitNetworkTransport.swift; sourceTree = ""; }; 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationMessage.swift; sourceTree = ""; }; - 9B7BDA9823FDE94C00ACD198 /* ApolloWebSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloWebSocket.swift; sourceTree = ""; }; + 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketClient.swift; sourceTree = ""; }; 9B7BDA9923FDE94C00ACD198 /* WebSocketTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketTransport.swift; sourceTree = ""; }; 9B7BDA9A23FDE94C00ACD198 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9B7BDAA323FDE98C00ACD198 /* ApolloWebSocket-Project-Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "ApolloWebSocket-Project-Release.xcconfig"; sourceTree = ""; }; @@ -912,6 +913,7 @@ DE6B160B26152D210068D642 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; DE6B160C26152D210068D642 /* Workspace-Packaging.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Workspace-Packaging.xcconfig"; sourceTree = ""; }; DE6B160D26152D210068D642 /* Workspace-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Workspace-Shared.xcconfig"; sourceTree = ""; }; + DE8C84F4268BC42100C54D02 /* DefaultWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWebSocket.swift; sourceTree = ""; }; DEA34AF6260E821F00F95F86 /* Apollo-Target-AnimalKingdomAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-AnimalKingdomAPI.xcconfig"; sourceTree = ""; }; DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarWarsApolloSchemaDownloaderTests.swift; sourceTree = ""; }; DECD490B262F81BF00924527 /* ApolloCodegenTestSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApolloCodegenTestSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -965,7 +967,7 @@ buildActionMask = 2147483647; files = ( 9B7BDAFD23FDEE9300ACD198 /* Apollo.framework in Frameworks */, - 9B7BDAAC23FDEA7B00ACD198 /* Starscream in Frameworks */, + DE3A2816268BCE6700A1BDC8 /* Starscream in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1382,7 +1384,8 @@ 9B7BDA9323FDE94C00ACD198 /* ApolloWebSocket */ = { isa = PBXGroup; children = ( - 9B7BDA9823FDE94C00ACD198 /* ApolloWebSocket.swift */, + 9B7BDA9823FDE94C00ACD198 /* WebSocketClient.swift */, + DE8C84F4268BC42100C54D02 /* DefaultWebSocket.swift */, 9B7BDA9723FDE94C00ACD198 /* OperationMessage.swift */, 9B7BDA9623FDE94C00ACD198 /* SplitNetworkTransport.swift */, 9B7BDA9423FDE94C00ACD198 /* WebSocketError.swift */, @@ -2182,7 +2185,7 @@ ); name = ApolloWebSocket; packageProductDependencies = ( - 9B7BDAAB23FDEA7B00ACD198 /* Starscream */, + DE3A2815268BCE6700A1BDC8 /* Starscream */, ); productName = ApolloWebSocket; productReference = 9B7BDA7D23FDE90400ACD198 /* ApolloWebSocket.framework */; @@ -2312,6 +2315,7 @@ ); name = Apollo; packageProductDependencies = ( + DE8C84F3268BBF8000C54D02 /* Starscream */, ); productName = Apollo; productReference = 9FC750441D2A532C00458D91 /* Apollo.framework */; @@ -2518,10 +2522,10 @@ ); mainGroup = 9FC7503A1D2A532C00458D91; packageReferences = ( - 9B7BDAAA23FDEA7B00ACD198 /* XCRemoteSwiftPackageReference "Starscream" */, 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */, 9B68F04B2413239100E97318 /* XCRemoteSwiftPackageReference "Stencil" */, 9B47515B2575AA4A0001FB87 /* XCRemoteSwiftPackageReference "InflectorKit" */, + DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */, ); productRefGroup = 9FC750451D2A532C00458D91 /* Products */; projectDirPath = ""; @@ -2786,9 +2790,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9B7BDA9F23FDE94C00ACD198 /* ApolloWebSocket.swift in Sources */, + 9B7BDA9F23FDE94C00ACD198 /* WebSocketClient.swift in Sources */, 9B7BDAA023FDE94C00ACD198 /* WebSocketTransport.swift in Sources */, 9B7BDA9C23FDE94C00ACD198 /* WebSocketTask.swift in Sources */, + DE8C84F5268BC42100C54D02 /* DefaultWebSocket.swift in Sources */, 9B7BDA9B23FDE94C00ACD198 /* WebSocketError.swift in Sources */, 9B7BDA9D23FDE94C00ACD198 /* SplitNetworkTransport.swift in Sources */, 9B7BDA9E23FDE94C00ACD198 /* OperationMessage.swift in Sources */, @@ -3807,7 +3812,7 @@ repositoryURL = "https://github.com/daltoniam/Starscream.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 4.0.4; + minimumVersion = 3.1.1; }; }; 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */ = { @@ -3818,6 +3823,14 @@ minimumVersion = 0.12.2; }; }; + DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apollographql/Starscream.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 3.1.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3831,11 +3844,6 @@ package = 9B68F04B2413239100E97318 /* XCRemoteSwiftPackageReference "Stencil" */; productName = Stencil; }; - 9B7BDAAB23FDEA7B00ACD198 /* Starscream */ = { - isa = XCSwiftPackageProductDependency; - package = 9B7BDAAA23FDEA7B00ACD198 /* XCRemoteSwiftPackageReference "Starscream" */; - productName = Starscream; - }; 9B7BDAF523FDEE2600ACD198 /* SQLite */ = { isa = XCSwiftPackageProductDependency; package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; @@ -3851,6 +3859,16 @@ package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; + DE3A2815268BCE6700A1BDC8 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; + DE8C84F3268BBF8000C54D02 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = DE8C84F2268BBF8000C54D02 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; DECD4870262F7A6E00924527 /* InflectorKit */ = { isa = XCSwiftPackageProductDependency; package = 9B47515B2575AA4A0001FB87 /* XCRemoteSwiftPackageReference "InflectorKit" */; diff --git a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ae73f48cea..7295ad10bb 100644 --- a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -39,11 +39,11 @@ }, { "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream.git", + "repositoryURL": "https://github.com/apollographql/Starscream.git", "state": { "branch": null, - "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version": "4.0.4" + "revision": "8cf77babe5901693396436f4f418a6db0f328b78", + "version": "3.1.2" } }, { diff --git a/Package.resolved b/Package.resolved index 028146a8c7..7dc9a17451 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/kylef/Spectre.git", "state": { "branch": null, - "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", - "version": "0.9.0" + "revision": "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53", + "version": "0.9.2" } }, { @@ -39,11 +39,11 @@ }, { "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream", + "repositoryURL": "https://github.com/apollographql/Starscream", "state": { "branch": null, - "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version": "4.0.4" + "revision": "8cf77babe5901693396436f4f418a6db0f328b78", + "version": "3.1.2" } }, { @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/stencilproject/Stencil.git", "state": { "branch": null, - "revision": "94197b3adbbf926348ad8765476a158aa4e54f8a", - "version": "0.14.0" + "revision": "973e190edf5d09274e4a6bc2e636c86899ed84c3", + "version": "0.14.1" } } ] diff --git a/Package.swift b/Package.swift index 616d29c5f5..08b8910c08 100644 --- a/Package.swift +++ b/Package.swift @@ -40,8 +40,8 @@ let package = Package( url: "https://github.com/stephencelis/SQLite.swift.git", .upToNextMinor(from: "0.12.2")), .package( - url: "https://github.com/daltoniam/Starscream", - .upToNextMinor(from: "4.0.4")), + url: "https://github.com/apollographql/Starscream", + .upToNextMinor(from: "3.1.2")), .package( url: "https://github.com/stencilproject/Stencil.git", .upToNextMinor(from: "0.14.0")), diff --git a/Sources/ApolloTestSupport/MockWebSocket.swift b/Sources/ApolloTestSupport/MockWebSocket.swift index 878f1a7da1..20c7cedaa2 100644 --- a/Sources/ApolloTestSupport/MockWebSocket.swift +++ b/Sources/ApolloTestSupport/MockWebSocket.swift @@ -1,57 +1,35 @@ -import Starscream import Foundation @testable import ApolloWebSocket -public class MockWebSocket: ApolloWebSocketClient { +public class MockWebSocket: WebSocketClient { - public var callbackQueue: DispatchQueue = DispatchQueue.main - - // A dummy web socket since we can't just return the client - var webSocketForDelegate: WebSocket public var request: URLRequest + public var callbackQueue: DispatchQueue = DispatchQueue.main + public var delegate: WebSocketClientDelegate? = nil + public var isConnected: Bool = false - public required init(request: URLRequest, - certPinner: CertificatePinning? = FoundationSecurity(), - compressionHandler: CompressionHandler? = nil) { - self.webSocketForDelegate = WebSocket(request: request) + public required init(request: URLRequest) { self.request = request } - public init(request: URLRequest) { - self.request = request - self.webSocketForDelegate = WebSocket(request: request) - } - open func reportDidConnect() { callbackQueue.async { - self.delegate?.didReceive(event: .connected([:]), client: self.webSocketForDelegate) + self.delegate?.websocketDidConnect(socket: self) } } - open func write(string: String, completion: (() -> ())?) { + open func write(string: String) { callbackQueue.async { - self.delegate?.didReceive(event: .text(string), client: self.webSocketForDelegate) + self.delegate?.websocketDidReceiveMessage(socket: self, text: string) } } - open func write(stringData: Data, completion: (() -> ())?) { - } - - open func write(data: Data, completion: (() -> ())?) { - } - open func write(ping: Data, completion: (() -> ())?) { } - - open func write(pong: Data, completion: (() -> ())?) { - } - - public func disconnect(closeCode: UInt16) { + + public func disconnect() { } - public var delegate: WebSocketDelegate? = nil - public var isConnected: Bool = false - public func connect() { } } diff --git a/Sources/ApolloWebSocket/ApolloWebSocket.swift b/Sources/ApolloWebSocket/ApolloWebSocket.swift deleted file mode 100644 index d1bc15e687..0000000000 --- a/Sources/ApolloWebSocket/ApolloWebSocket.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Starscream -import Foundation - -// MARK: - Client protocol - -/// Protocol allowing alternative implementations of web sockets beyond `ApolloWebSocket`. Extends `Starscream`'s `WebSocketClient` protocol. -public protocol ApolloWebSocketClient: WebSocketClient { - - /// Required initializer - /// - /// - Parameters: - /// - request: The URLRequest to use on connection. - /// - certPinner: [optional] The object providing information about certificate pinning. Should default to Starscream's `FoundationSecurity`. - /// - compressionHandler: [optional] The object helping with any compression handling. Should default to nil. - init(request: URLRequest, - certPinner: CertificatePinning?, - compressionHandler: CompressionHandler?) - - /// The URLRequest used on connection. - var request: URLRequest { get set } - - /// Queue where the callbacks are executed - var callbackQueue: DispatchQueue { get set } - - var delegate: WebSocketDelegate? { get set } -} - -extension ApolloWebSocketClient { - init(request: URLRequest) { - self.init(request: request, - certPinner: FoundationSecurity(), - compressionHandler: nil) - } -} - -// MARK: - WebSocket - -/// Included implementation of an `ApolloWebSocketClient`, based on `Starscream`'s `WebSocket`. -public class ApolloWebSocket: WebSocket, ApolloWebSocketClient { - - private var transport: FoundationTransport! - - required public init(request: URLRequest, - certPinner: CertificatePinning? = FoundationSecurity(), - compressionHandler: CompressionHandler? = nil) { - let engine = WSEngine(transport: FoundationTransport(), - certPinner: certPinner, - compressionHandler: compressionHandler) - - super.init(request: request, engine: engine) - } -} diff --git a/Sources/ApolloWebSocket/DefaultWebSocket.swift b/Sources/ApolloWebSocket/DefaultWebSocket.swift new file mode 100644 index 0000000000..3465209936 --- /dev/null +++ b/Sources/ApolloWebSocket/DefaultWebSocket.swift @@ -0,0 +1,70 @@ +import Starscream +import Foundation + +/// Included default implementation of a `WebSocketClient`, based on `Starscream`'s `WebSocket`. +public class DefaultWebSocket: WebSocketClient, Starscream.WebSocketDelegate { + + /// The websocket protocols supported by this websocket client implementation. + static private let wsProtocols = ["graphql-ws"] + + /// The underlying `Starscream` websocket used by this websocket client. + private let underlyingWebsocket: Starscream.WebSocket + + public var request: URLRequest { + get { underlyingWebsocket.request } + set { underlyingWebsocket.request = newValue } + } + + public weak var delegate: WebSocketClientDelegate? + + public var callbackQueue: DispatchQueue { + get { underlyingWebsocket.callbackQueue } + set { underlyingWebsocket.callbackQueue = newValue } + } + + /// Required initializer + /// + /// - Parameters: + /// - request: The URLRequest to use on connection. + /// - certPinner: [optional] The object providing information about certificate pinning. Should default to Starscream's `FoundationSecurity`. + /// - compressionHandler: [optional] The object helping with any compression handling. Should default to nil. + required public init(request: URLRequest) { + self.underlyingWebsocket = Starscream.WebSocket( + request: request, + protocols: Self.wsProtocols, + stream: FoundationStream()) + self.underlyingWebsocket.delegate = self + } + + public func connect() { + self.underlyingWebsocket.connect() + } + + public func disconnect() { + self.underlyingWebsocket.disconnect() + } + + public func write(ping: Data, completion: (() -> Void)?) { + self.underlyingWebsocket.write(ping: ping, completion: completion) + } + + public func write(string: String) { + self.underlyingWebsocket.write(string: string) + } + + public func websocketDidConnect(socket: Starscream.WebSocketClient) { + self.delegate?.websocketDidConnect(socket: self) + } + + public func websocketDidDisconnect(socket: Starscream.WebSocketClient, error: Error?) { + self.delegate?.websocketDidDisconnect(socket: self, error: error) + } + + public func websocketDidReceiveMessage(socket: Starscream.WebSocketClient, text: String) { + self.delegate?.websocketDidReceiveMessage(socket: self, text: text) + } + + public func websocketDidReceiveData(socket: Starscream.WebSocketClient, data: Data) { + self.delegate?.websocketDidReceiveData(socket: self, data: data) + } +} diff --git a/Sources/ApolloWebSocket/WebSocketClient.swift b/Sources/ApolloWebSocket/WebSocketClient.swift new file mode 100644 index 0000000000..2cbb5d8b9d --- /dev/null +++ b/Sources/ApolloWebSocket/WebSocketClient.swift @@ -0,0 +1,57 @@ +import Foundation + +// MARK: - Client protocol + +/// Protocol allowing alternative implementations of websockets beyond `ApolloWebSocket`. +public protocol WebSocketClient: AnyObject { + + /// The URLRequest used on connection. + var request: URLRequest { get set } + + /// The delegate that will receive networking event updates for this websocket client. + var delegate: WebSocketClientDelegate? { get set } + + /// `DispatchQueue` where the websocket client should call all delegate callbacks. + var callbackQueue: DispatchQueue { get set } + + /// Connects to the websocket server. + /// + /// - Note: This should be implemented to connect the websocket on a background thread. + func connect() + + /// Disconnects from the websocket server. + func disconnect() + + /// Writes ping data to the websocket. + func write(ping: Data, completion: (() -> Void)?) + + /// Writes a string to the websocket. + func write(string: String) + +} + +/// The delegate for a `WebSocketClient` to recieve notification of socket events. +public protocol WebSocketClientDelegate: AnyObject { + + /// The websocket client has started a connection to the server. + /// - Parameter socket: The `WebSocketClient` that sent the delegate event. + func websocketDidConnect(socket: WebSocketClient) + + /// The websocket client has disconnected from the server. + /// - Parameters: + /// - socket: The `WebSocketClient` that sent the delegate event. + /// - error: An optional error if an error occured. + func websocketDidDisconnect(socket: WebSocketClient, error: Error?) + + /// The websocket client received message text from the server + /// - Parameters: + /// - socket: The `WebSocketClient` that sent the delegate event. + /// - text: The text received from the server. + func websocketDidReceiveMessage(socket: WebSocketClient, text: String) + + /// The websocket client received data from the server + /// - Parameters: + /// - socket: The `WebSocketClient` that sent the delegate event. + /// - data: The data received from the server. + func websocketDidReceiveData(socket: WebSocketClient, data: Data) +} diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 6818f60ba0..04493a8b2c 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -2,7 +2,6 @@ import Apollo import ApolloUtils #endif -import Starscream import Foundation // MARK: - Transport Delegate @@ -11,8 +10,6 @@ public protocol WebSocketTransportDelegate: AnyObject { func webSocketTransportDidConnect(_ webSocketTransport: WebSocketTransport) func webSocketTransportDidReconnect(_ webSocketTransport: WebSocketTransport) func webSocketTransport(_ webSocketTransport: WebSocketTransport, didDisconnectWithError error:Error?) - func webSocketTransport(_ webSocketTransport: WebSocketTransport, didReceivePingData: Data?) - func webSocketTransport(_ webSocketTransport: WebSocketTransport, didReceivePongData: Data?) } public extension WebSocketTransportDelegate { @@ -25,18 +22,16 @@ public extension WebSocketTransportDelegate { // MARK: - WebSocketTransport -/// A network transport that uses web sockets requests to send GraphQL subscription operations to a server, and that uses the Starscream implementation of web sockets. +/// A network transport that uses web sockets requests to send GraphQL subscription operations to a server. public class WebSocketTransport { public weak var delegate: WebSocketTransportDelegate? let connectOnInit: Bool let reconnect: Atomic - let websocket: ApolloWebSocketClient + let websocket: WebSocketClient let error: Atomic = Atomic(nil) let serializationFormat = JSONSerializationFormat.self private let requestBodyCreator: RequestBodyCreator - - private final let protocols = ["graphql-ws"] /// non-private for testing - you should not use this directly enum SocketConnectionState { @@ -92,7 +87,7 @@ public class WebSocketTransport { /// - connectOnInit: Whether the websocket connects immediately on creation. If false, remember to call `resumeWebSocketConnection()` to connect. Defaults to true. /// - connectingPayload: [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. /// - requestBodyCreator: The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. - public init(websocket: ApolloWebSocketClient, + public init(websocket: WebSocketClient, clientName: String = WebSocketTransport.defaultClientName, clientVersion: String = WebSocketTransport.defaultClientVersion, sendOperationIdentifiers: Bool = false, @@ -109,7 +104,6 @@ public class WebSocketTransport { self.reconnectionInterval = reconnectionInterval self.allowSendingDuplicates = allowSendingDuplicates self.requestBodyCreator = requestBodyCreator - self.websocket.request.setValue(self.protocols.joined(separator: ","), forHTTPHeaderField: "Sec-WebSocket-Protocol") self.clientName = clientName self.clientVersion = clientVersion self.connectOnInit = connectOnInit @@ -384,65 +378,12 @@ extension WebSocketTransport: NetworkTransport { // MARK: - WebSocketDelegate implementation -extension WebSocketTransport: WebSocketDelegate { - - public func didReceive(event: WebSocketEvent, client: WebSocket) { - switch event { - case .connected: - self.handleConnection() - case .disconnected(let reason, let code): - self.socketConnectionState.mutate { $0 = .disconnected } - self.error.mutate { $0 = nil } - debugPrint("websocket is disconnected: \(reason) with code: \(code)") - self.handleDisconnection() - case .text(let text): - self.processMessage(text: text) - case .binary(let data): - self.processMessage(data: data) - case .ping(let pingData): - self.delegate?.webSocketTransport(self, didReceivePingData: pingData) - case .pong(let pongData): - self.delegate?.webSocketTransport(self, didReceivePongData: pongData) - case .viabilityChanged(_): - break - case .reconnectSuggested(let shouldReconnect): - if shouldReconnect { - self.attemptReconnectionIfDesired() - } - case .cancelled: - self.socketConnectionState.mutate { $0 = .disconnected } - self.error.mutate { $0 = nil } - self.handleDisconnection() - case .error(let error): - // Set state to `.failed`, and grab its previous value. - let previousState: SocketConnectionState = self.socketConnectionState.mutate { socketConnectionState in - let previousState = socketConnectionState - socketConnectionState = .failed - return previousState - } - // report any error to all subscribers - if let error = error { - self.error.mutate { $0 = WebSocketError(payload: nil, - error: error, - kind: .networkError) } - self.notifyErrorAllHandlers(error) - } else { - self.error.mutate { $0 = nil } - } - - switch previousState { - case .connected, .disconnected: - self.handleDisconnection() - case .failed: - // Don't attempt at reconnecting if already failed. - // Websockets will sometimes notify several errors in a row, and - // we don't want to perform disconnection handling multiple times. - // This avoids https://github.com/apollographql/apollo-ios/issues/1753 - break - } - } +extension WebSocketTransport: WebSocketClientDelegate { + + public func websocketDidConnect(socket: WebSocketClient) { + self.handleConnection() } - + public func handleConnection() { self.error.mutate { $0 = nil } self.socketConnectionState.mutate { $0 = .connected } @@ -467,18 +408,55 @@ extension WebSocketTransport: WebSocketDelegate { self.reconnected = true } + public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { + self.socketConnectionState.mutate { $0 = .disconnected } + if let error = error { + debugPrint("websocket is disconnected: \(error)") + handleDisconnection(with: error) + } else { + self.error.mutate { $0 = nil } + debugPrint("websocket is disconnected") + self.handleDisconnection() + } + } + + private func handleDisconnection(with error: Error) { + // Set state to `.failed`, and grab its previous value. + let previousState: SocketConnectionState = self.socketConnectionState.mutate { socketConnectionState in + let previousState = socketConnectionState + socketConnectionState = .failed + return previousState + } + // report any error to all subscribers + self.error.mutate { $0 = WebSocketError(payload: nil, + error: error, + kind: .networkError) } + self.notifyErrorAllHandlers(error) + + switch previousState { + case .connected, .disconnected: + self.handleDisconnection() + case .failed: + // Don't attempt at reconnecting if already failed. + // Websockets will sometimes notify several errors in a row, and + // we don't want to perform disconnection handling multiple times. + // This avoids https://github.com/apollographql/apollo-ios/issues/1753 + break + } + } + private func handleDisconnection() { self.delegate?.webSocketTransport(self, didDisconnectWithError: self.error.value) self.acked = false // need new connect and ack before sending self.attemptReconnectionIfDesired() } - + private func attemptReconnectionIfDesired() { guard self.reconnect.value else { return } - + DispatchQueue.main.asyncAfter(deadline: .now() + reconnectionInterval) { [weak self] in guard let self = self else { return } self.socketConnectionState.mutate { socketConnectionState in @@ -495,4 +473,13 @@ extension WebSocketTransport: WebSocketDelegate { self.websocket.connect() } } + + public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + self.processMessage(text: text) + } + + public func websocketDidReceiveData(socket: WebSocketClient, data: Data) { + self.processMessage(data: data) + } + } diff --git a/SwiftScripts/Package.resolved b/SwiftScripts/Package.resolved index f03f0a0231..4dd42e80fb 100644 --- a/SwiftScripts/Package.resolved +++ b/SwiftScripts/Package.resolved @@ -111,11 +111,11 @@ }, { "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream", + "repositoryURL": "https://github.com/apollographql/Starscream", "state": { "branch": null, - "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version": "4.0.4" + "revision": "4c985380c92920be5f75c14577e5c2e3615f2337", + "version": "3.1.2" } }, { diff --git a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 58a024d5fc..33256c0919 100644 --- a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift @@ -22,7 +22,7 @@ class StarWarsSubscriptionTests: XCTestCase { connectionStartedExpectation = self.expectation(description: "Web socket connected") webSocketTransport = WebSocketTransport( - websocket: ApolloWebSocket( + websocket: DefaultWebSocket( request: URLRequest(url: TestServerURL.starWarsWebSocket.url) ) ) diff --git a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift index 852745fb42..b4677de6df 100755 --- a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift @@ -22,7 +22,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheDependentTesting { let store = ApolloStore(cache: cache) let networkTransport = WebSocketTransport( - websocket: ApolloWebSocket( + websocket: DefaultWebSocket( request: URLRequest(url: TestServerURL.starWarsWebSocket.url) ) ) diff --git a/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift b/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift index 7b65312a30..2907b729cd 100644 --- a/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift +++ b/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift @@ -1,7 +1,6 @@ import XCTest import Apollo import ApolloTestSupport -import Starscream @testable import ApolloWebSocket class WebSocketTransportTests: XCTestCase { @@ -73,17 +72,18 @@ class WebSocketTransportTests: XCTestCase { } } -private final class MockWebSocketDelegate: WebSocketDelegate { +private final class MockWebSocketDelegate: WebSocketClientDelegate { var didReceiveMessage: ((String) -> Void)? - func didReceive(event: WebSocketEvent, client: WebSocket) { - switch event { - case .text(let message): - didReceiveMessage?(message) - default: - // No-op, this is a mock socket. - break - } + func websocketDidConnect(socket: WebSocketClient) {} + + func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {} + + func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + didReceiveMessage?(text) } + + func websocketDidReceiveData(socket: WebSocketClient, data: Data) {} + }