|
1 | 1 | /*
|
2 |
| - * Copyright 2022 LiveKit |
| 2 | + * Copyright 2022-2023 LiveKit |
3 | 3 | *
|
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | 5 | * you may not use this file except in compliance with the License.
|
|
17 | 17 | import Foundation
|
18 | 18 | import Promises
|
19 | 19 |
|
20 |
| -internal class WebSocket: NSObject, URLSessionWebSocketDelegate, Loggable { |
| 20 | +internal typealias WebSocketStream = AsyncThrowingStream<URLSessionWebSocketTask.Message, Error> |
21 | 21 |
|
22 |
| - private let queue = DispatchQueue(label: "LiveKitSDK.webSocket", qos: .default) |
| 22 | +internal class WebSocket: NSObject, Loggable, AsyncSequence, URLSessionWebSocketDelegate { |
23 | 23 |
|
24 |
| - typealias OnMessage = (URLSessionWebSocketTask.Message) -> Void |
25 |
| - typealias OnDisconnect = (_ reason: DisconnectReason?) -> Void |
| 24 | + typealias AsyncIterator = WebSocketStream.Iterator |
| 25 | + typealias Element = URLSessionWebSocketTask.Message |
26 | 26 |
|
27 |
| - public var onMessage: OnMessage? |
28 |
| - public var onDisconnect: OnDisconnect? |
| 27 | + private var streamContinuation: WebSocketStream.Continuation? |
| 28 | + private var connectContinuation: CheckedContinuation<Void, Error>? |
29 | 29 |
|
30 |
| - private let operationQueue = OperationQueue() |
31 | 30 | private let request: URLRequest
|
32 | 31 |
|
33 |
| - private var disconnected = false |
34 |
| - private var connectPromise: Promise<WebSocket>? |
35 |
| - |
36 |
| - private lazy var session: URLSession = { |
| 32 | + private lazy var urlSession: URLSession = { |
37 | 33 | let config = URLSessionConfiguration.default
|
38 | 34 | // explicitly set timeout intervals
|
39 | 35 | config.timeoutIntervalForRequest = TimeInterval(60)
|
40 | 36 | config.timeoutIntervalForResource = TimeInterval(604_800)
|
41 |
| - log("URLSessionConfiguration.timeoutIntervalForRequest: \(config.timeoutIntervalForRequest)") |
42 |
| - log("URLSessionConfiguration.timeoutIntervalForResource: \(config.timeoutIntervalForResource)") |
43 |
| - return URLSession(configuration: config, |
44 |
| - delegate: self, |
45 |
| - delegateQueue: operationQueue) |
| 37 | + return URLSession(configuration: config, delegate: self, delegateQueue: nil) |
46 | 38 | }()
|
47 | 39 |
|
48 | 40 | private lazy var task: URLSessionWebSocketTask = {
|
49 |
| - session.webSocketTask(with: request) |
| 41 | + urlSession.webSocketTask(with: request) |
50 | 42 | }()
|
51 | 43 |
|
52 |
| - static func connect(url: URL, |
53 |
| - onMessage: OnMessage? = nil, |
54 |
| - onDisconnect: OnDisconnect? = nil) -> Promise<WebSocket> { |
55 |
| - |
56 |
| - return WebSocket(url: url, |
57 |
| - onMessage: onMessage, |
58 |
| - onDisconnect: onDisconnect).connect() |
59 |
| - } |
| 44 | + private lazy var stream: WebSocketStream = { |
| 45 | + return WebSocketStream { continuation in |
| 46 | + streamContinuation = continuation |
| 47 | + waitForNextValue() |
| 48 | + } |
| 49 | + }() |
60 | 50 |
|
61 |
| - private init(url: URL, |
62 |
| - onMessage: OnMessage? = nil, |
63 |
| - onDisconnect: OnDisconnect? = nil) { |
| 51 | + init(url: URL) { |
64 | 52 |
|
65 | 53 | request = URLRequest(url: url,
|
66 | 54 | cachePolicy: .useProtocolCachePolicy,
|
67 | 55 | timeoutInterval: .defaultSocketConnect)
|
68 |
| - |
69 |
| - self.onMessage = onMessage |
70 |
| - self.onDisconnect = onDisconnect |
71 |
| - super.init() |
72 |
| - task.resume() |
73 | 56 | }
|
74 | 57 |
|
75 | 58 | deinit {
|
76 |
| - log() |
| 59 | + reset() |
77 | 60 | }
|
78 | 61 |
|
79 |
| - private func connect() -> Promise<WebSocket> { |
80 |
| - connectPromise = Promise<WebSocket>.pending() |
81 |
| - return connectPromise! |
82 |
| - } |
83 |
| - |
84 |
| - internal func cleanUp(reason: DisconnectReason?, notify: Bool = true) { |
85 |
| - |
86 |
| - log("reason: \(String(describing: reason))") |
| 62 | + public func connect() async throws { |
87 | 63 |
|
88 |
| - guard !disconnected else { |
89 |
| - log("dispose can be called only once", .warning) |
90 |
| - return |
91 |
| - } |
92 |
| - |
93 |
| - // mark as disconnected, this instance cannot be re-used |
94 |
| - disconnected = true |
95 |
| - |
96 |
| - task.cancel() |
97 |
| - session.invalidateAndCancel() |
98 |
| - |
99 |
| - if let promise = connectPromise { |
100 |
| - let sdkError = NetworkError.disconnected(message: "WebSocket disconnected") |
101 |
| - promise.reject(sdkError) |
102 |
| - connectPromise = nil |
103 |
| - } |
104 |
| - |
105 |
| - if notify { |
106 |
| - onDisconnect?(reason) |
| 64 | + try await withCheckedThrowingContinuation { continuation in |
| 65 | + connectContinuation = continuation |
| 66 | + task.resume() |
107 | 67 | }
|
108 | 68 | }
|
109 | 69 |
|
110 |
| - public func send(data: Data) -> Promise<Void> { |
111 |
| - let message = URLSessionWebSocketTask.Message.data(data) |
112 |
| - return Promise(on: queue) { resolve, fail in |
113 |
| - self.task.send(message) { error in |
114 |
| - if let error = error { |
115 |
| - fail(error) |
116 |
| - return |
117 |
| - } |
118 |
| - resolve(()) |
119 |
| - } |
120 |
| - } |
| 70 | + func reset() { |
| 71 | + task.cancel(with: .goingAway, reason: nil) |
| 72 | + connectContinuation?.resume(throwing: SignalClientError.socketError(rawError: nil)) |
| 73 | + connectContinuation = nil |
| 74 | + streamContinuation?.finish() |
| 75 | + streamContinuation = nil |
121 | 76 | }
|
122 | 77 |
|
123 |
| - private func receive(task: URLSessionWebSocketTask, |
124 |
| - result: Result<URLSessionWebSocketTask.Message, Error>) { |
125 |
| - switch result { |
126 |
| - case .failure(let error): |
127 |
| - log("Failed to receive \(error)", .error) |
| 78 | + // MARK: - AsyncSequence |
128 | 79 |
|
129 |
| - case .success(let message): |
130 |
| - onMessage?(message) |
131 |
| - queue.async { task.receive { self.receive(task: task, result: $0) } } |
132 |
| - } |
| 80 | + func makeAsyncIterator() -> AsyncIterator { |
| 81 | + return stream.makeAsyncIterator() |
133 | 82 | }
|
134 | 83 |
|
135 |
| - // MARK: - URLSessionWebSocketDelegate |
136 |
| - |
137 |
| - internal func urlSession(_ session: URLSession, |
138 |
| - webSocketTask: URLSessionWebSocketTask, |
139 |
| - didOpenWithProtocol protocol: String?) { |
140 |
| - |
141 |
| - guard !disconnected else { |
| 84 | + private func waitForNextValue() { |
| 85 | + guard task.closeCode == .invalid else { |
| 86 | + streamContinuation?.finish() |
| 87 | + streamContinuation = nil |
142 | 88 | return
|
143 | 89 | }
|
144 | 90 |
|
145 |
| - if let promise = connectPromise { |
146 |
| - promise.fulfill(self) |
147 |
| - connectPromise = nil |
148 |
| - } |
| 91 | + task.receive(completionHandler: { [weak self] result in |
| 92 | + guard let continuation = self?.streamContinuation else { |
| 93 | + return |
| 94 | + } |
149 | 95 |
|
150 |
| - queue.async { webSocketTask.receive { self.receive(task: webSocketTask, result: $0) } } |
| 96 | + do { |
| 97 | + let message = try result.get() |
| 98 | + continuation.yield(message) |
| 99 | + self?.waitForNextValue() |
| 100 | + } catch { |
| 101 | + continuation.finish(throwing: error) |
| 102 | + self?.streamContinuation = nil |
| 103 | + } |
| 104 | + }) |
151 | 105 | }
|
152 | 106 |
|
153 |
| - internal func urlSession(_ session: URLSession, |
154 |
| - webSocketTask: URLSessionWebSocketTask, |
155 |
| - didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, |
156 |
| - reason: Data?) { |
| 107 | + // MARK: - Send |
157 | 108 |
|
158 |
| - guard !disconnected else { |
159 |
| - return |
160 |
| - } |
| 109 | + public func send(data: Data) async throws { |
| 110 | + let message = URLSessionWebSocketTask.Message.data(data) |
| 111 | + try await task.send(message) |
| 112 | + } |
161 | 113 |
|
162 |
| - let sdkError = NetworkError.disconnected(message: "WebSocket did close with code: \(closeCode) reason: \(String(describing: reason))") |
| 114 | + // MARK: - URLSessionWebSocketDelegate |
163 | 115 |
|
164 |
| - cleanUp(reason: .networkError(sdkError)) |
| 116 | + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { |
| 117 | + connectContinuation?.resume() |
| 118 | + connectContinuation = nil |
165 | 119 | }
|
166 | 120 |
|
167 |
| - internal func urlSession(_ session: URLSession, |
168 |
| - task: URLSessionTask, |
169 |
| - didCompleteWithError error: Error?) { |
170 |
| - |
171 |
| - guard !disconnected else { |
172 |
| - return |
173 |
| - } |
| 121 | + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { |
| 122 | + log("didCompleteWithError: \(String(describing: error))", .error) |
| 123 | + let error = error ?? NetworkError.disconnected(message: "WebSocket didCompleteWithError") |
| 124 | + connectContinuation?.resume(throwing: error) |
| 125 | + connectContinuation = nil |
| 126 | + streamContinuation?.finish() |
| 127 | + streamContinuation = nil |
| 128 | + } |
| 129 | +} |
174 | 130 |
|
175 |
| - let sdkError = NetworkError.disconnected(message: "WebSocket disconnected", rawError: error) |
| 131 | +internal extension WebSocket { |
176 | 132 |
|
177 |
| - cleanUp(reason: .networkError(sdkError)) |
| 133 | + // Deprecate |
| 134 | + func send(data: Data) -> Promise<Void> { |
| 135 | + Promise { [self] resolve, fail in |
| 136 | + Task { |
| 137 | + do { |
| 138 | + try await self.send(data: data) |
| 139 | + resolve(()) |
| 140 | + } catch { |
| 141 | + fail(error) |
| 142 | + } |
| 143 | + } |
| 144 | + } |
178 | 145 | }
|
179 | 146 | }
|
0 commit comments