|
15 | 15 | import FirebaseSharedSwift
|
16 | 16 | import Foundation
|
17 | 17 |
|
18 |
| -/// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions. |
| 18 | +/// A `Callable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. |
| 19 | +/// |
| 20 | +/// - Note: If the Callable HTTPS trigger accepts no parameters, ``Never`` can be used for |
| 21 | +/// iOS 17.0+. Otherwise, a simple encodable placeholder type (e.g., |
| 22 | +/// `struct EmptyRequest: Encodable {}`) can be used. |
19 | 23 | public struct Callable<Request: Encodable, Response: Decodable> {
|
20 | 24 | /// The timeout to use when calling the function. Defaults to 70 seconds.
|
21 | 25 | public var timeoutInterval: TimeInterval {
|
@@ -160,3 +164,175 @@ public struct Callable<Request: Encodable, Response: Decodable> {
|
160 | 164 | return try await call(data)
|
161 | 165 | }
|
162 | 166 | }
|
| 167 | + |
| 168 | +/// Used to determine when a `StreamResponse<_, _>` is being decoded. |
| 169 | +private protocol StreamResponseProtocol {} |
| 170 | + |
| 171 | +/// A convenience type used to receive both the streaming callable function's yielded messages and |
| 172 | +/// its return value. |
| 173 | +/// |
| 174 | +/// This can be used as the generic `Response` parameter to ``Callable`` to receive both the |
| 175 | +/// yielded messages and final return value of the streaming callable function. |
| 176 | +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) |
| 177 | +public enum StreamResponse<Message: Decodable, Result: Decodable>: Decodable, |
| 178 | + StreamResponseProtocol { |
| 179 | + /// The message yielded by the callable function. |
| 180 | + case message(Message) |
| 181 | + /// The final result returned by the callable function. |
| 182 | + case result(Result) |
| 183 | + |
| 184 | + private enum CodingKeys: String, CodingKey { |
| 185 | + case message |
| 186 | + case result |
| 187 | + } |
| 188 | + |
| 189 | + public init(from decoder: any Decoder) throws { |
| 190 | + do { |
| 191 | + let container = try decoder |
| 192 | + .container(keyedBy: Self<Message, Result>.CodingKeys.self) |
| 193 | + guard let onlyKey = container.allKeys.first, container.allKeys.count == 1 else { |
| 194 | + throw DecodingError |
| 195 | + .typeMismatch( |
| 196 | + Self<Message, |
| 197 | + Result>.self, |
| 198 | + DecodingError.Context( |
| 199 | + codingPath: container.codingPath, |
| 200 | + debugDescription: "Invalid number of keys found, expected one.", |
| 201 | + underlyingError: nil |
| 202 | + ) |
| 203 | + ) |
| 204 | + } |
| 205 | + |
| 206 | + switch onlyKey { |
| 207 | + case .message: |
| 208 | + self = try Self |
| 209 | + .message(container.decode(Message.self, forKey: .message)) |
| 210 | + case .result: |
| 211 | + self = try Self |
| 212 | + .result(container.decode(Result.self, forKey: .result)) |
| 213 | + } |
| 214 | + } catch { |
| 215 | + throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error]) |
| 216 | + } |
| 217 | + } |
| 218 | +} |
| 219 | + |
| 220 | +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) |
| 221 | +public extension Callable where Request: Sendable, Response: Sendable { |
| 222 | + /// Creates a stream that yields responses from the streaming callable function. |
| 223 | + /// |
| 224 | + /// The request to the Cloud Functions backend made by this method automatically includes a FCM |
| 225 | + /// token to identify the app instance. If a user is logged in with Firebase Auth, an auth ID |
| 226 | + /// token for the user is included. If App Check is integrated, an app check token is included. |
| 227 | + /// |
| 228 | + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect |
| 229 | + /// information regarding the app instance. To stop this, see `Messaging.deleteData()`. It |
| 230 | + /// resumes with a new FCM Token the next time you call this method. |
| 231 | + /// |
| 232 | + /// - Important: The final result returned by the callable function is only accessible when |
| 233 | + /// using `StreamResponse` as the `Response` generic type. |
| 234 | + /// |
| 235 | + /// Example of using `stream` _without_ `StreamResponse`: |
| 236 | + /// ```swift |
| 237 | + /// let callable: Callable<MyRequest, MyResponse> = // ... |
| 238 | + /// let request: MyRequest = // ... |
| 239 | + /// let stream = try callable.stream(request) |
| 240 | + /// for try await response in stream { |
| 241 | + /// // Process each `MyResponse` message |
| 242 | + /// print(response) |
| 243 | + /// } |
| 244 | + /// ``` |
| 245 | + /// |
| 246 | + /// Example of using `stream` _with_ `StreamResponse`: |
| 247 | + /// ```swift |
| 248 | + /// let callable: Callable<MyRequest, StreamResponse<MyMessage, MyResult>> = // ... |
| 249 | + /// let request: MyRequest = // ... |
| 250 | + /// let stream = try callable.stream(request) |
| 251 | + /// for try await response in stream { |
| 252 | + /// switch response { |
| 253 | + /// case .message(let message): |
| 254 | + /// // Process each `MyMessage` |
| 255 | + /// print(message) |
| 256 | + /// case .result(let result): |
| 257 | + /// // Process the final `MyResult` |
| 258 | + /// print(result) |
| 259 | + /// } |
| 260 | + /// } |
| 261 | + /// ``` |
| 262 | + /// |
| 263 | + /// - Parameter data: The `Request` data to pass to the callable function. |
| 264 | + /// - Throws: A ``FunctionsError`` if the parameter `data` cannot be encoded. |
| 265 | + /// - Returns: A stream wrapping responses yielded by the streaming callable function or |
| 266 | + /// a ``FunctionsError`` if an error occurred. |
| 267 | + func stream(_ data: Request? = nil) throws -> AsyncThrowingStream<Response, Error> { |
| 268 | + let encoded: Any |
| 269 | + do { |
| 270 | + encoded = try encoder.encode(data) |
| 271 | + } catch { |
| 272 | + throw FunctionsError(.invalidArgument, userInfo: [NSUnderlyingErrorKey: error]) |
| 273 | + } |
| 274 | + |
| 275 | + return AsyncThrowingStream { continuation in |
| 276 | + Task { |
| 277 | + do { |
| 278 | + for try await response in callable.stream(encoded) { |
| 279 | + do { |
| 280 | + // This response JSON should only be able to be decoded to an `StreamResponse<_, _>` |
| 281 | + // instance. If the decoding succeeds and the decoded response conforms to |
| 282 | + // `StreamResponseProtocol`, we know the `Response` generic argument |
| 283 | + // is `StreamResponse<_, _>`. |
| 284 | + let responseJSON = switch response { |
| 285 | + case .message(let json), .result(let json): json |
| 286 | + } |
| 287 | + let response = try decoder.decode(Response.self, from: responseJSON) |
| 288 | + if response is StreamResponseProtocol { |
| 289 | + continuation.yield(response) |
| 290 | + } else { |
| 291 | + // `Response` is a custom type that matched the decoding logic as the |
| 292 | + // `StreamResponse<_, _>` type. Only the `StreamResponse<_, _>` type should decode |
| 293 | + // successfully here to avoid exposing the `result` value in a custom type. |
| 294 | + throw FunctionsError(.internal) |
| 295 | + } |
| 296 | + } catch let error as FunctionsError where error.code == .dataLoss { |
| 297 | + // `Response` is of type `StreamResponse<_, _>`, but failed to decode. Rethrow. |
| 298 | + throw error |
| 299 | + } catch { |
| 300 | + // `Response` is *not* of type `StreamResponse<_, _>`, and needs to be unboxed and |
| 301 | + // decoded. |
| 302 | + guard case let .message(messageJSON) = response else { |
| 303 | + // Since `Response` is not a `StreamResponse<_, _>`, only messages should be |
| 304 | + // decoded. |
| 305 | + continue |
| 306 | + } |
| 307 | + |
| 308 | + do { |
| 309 | + let boxedMessage = try decoder.decode( |
| 310 | + StreamResponseMessage.self, |
| 311 | + from: messageJSON |
| 312 | + ) |
| 313 | + continuation.yield(boxedMessage.message) |
| 314 | + } catch { |
| 315 | + throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error]) |
| 316 | + } |
| 317 | + } |
| 318 | + } |
| 319 | + } catch { |
| 320 | + continuation.finish(throwing: error) |
| 321 | + } |
| 322 | + continuation.finish() |
| 323 | + } |
| 324 | + } |
| 325 | + } |
| 326 | + |
| 327 | + /// A container type for the type-safe decoding of the message object from the generic `Response` |
| 328 | + /// type. |
| 329 | + private struct StreamResponseMessage: Decodable { |
| 330 | + let message: Response |
| 331 | + } |
| 332 | +} |
| 333 | + |
| 334 | +/// A container type for differentiating between message and result responses. |
| 335 | +enum JSONStreamResponse { |
| 336 | + case message([String: Any]) |
| 337 | + case result([String: Any]) |
| 338 | +} |
0 commit comments