# API Design for Firebase `AsyncSequence` Event Streams * **Authors** * Peter Friese * **Contributors** * Nick Cooke * Paul Beusterien * **Status**: `In Review` * **Last Updated**: 2025-09-25 ## 1. Abstract This proposal outlines the integration of Swift's `AsyncStream` and `AsyncSequence` APIs into the Firebase Apple SDK. The goal is to provide a modern, developer-friendly way to consume real-time data streams from Firebase APIs, aligning the SDK with Swift's structured concurrency model and improving the overall developer experience. ## 2. Background Many Firebase APIs produce a sequence of asynchronous events, such as authentication state changes, document and collection updates, and remote configuration updates. Currently, the SDK exposes these through completion-handler-based APIs (listeners). ```swift // Current listener-based approach db.collection("cities").document("SF") .addSnapshotListener { documentSnapshot, error in guard let document = documentSnapshot else { /* ... */ } guard let data = document.data() else { /* ... */ } print("Current data: \(data)") } ``` This approach breaks the otherwise linear control flow, requires manual management of listener lifecycles, and complicates error handling. Swift's `AsyncSequence` provides a modern, type-safe alternative that integrates seamlessly with structured concurrency, offering automatic resource management, simplified error handling, and a more intuitive, linear control flow. ## 3. Motivation Adopting `AsyncSequence` will: * **Modernize the SDK:** Align with Swift's modern concurrency approach, making Firebase feel more native to Swift developers. * **Simplify Development:** Eliminate the need for manual listener management and reduce boilerplate code, especially when integrating with SwiftUI. * **Improve Code Quality:** Provide official, high-quality implementations for streaming APIs, reducing ecosystem fragmentation caused by unofficial solutions. * **Enhance Readability:** Leverage structured error handling (`throws`) and a linear `for try await` syntax to make asynchronous code easier to read and maintain. * **Enable Composition:** Allow developers to use a rich set of sequence operators (like `map`, `filter`, `prefix`) to transform and combine streams declaratively. ## 4. Goals * To design and implement an idiomatic, `AsyncSequence`-based API surface for all relevant event-streaming Firebase APIs. * To provide a clear and consistent naming convention that aligns with Apple's own Swift APIs. * To ensure the new APIs automatically manage the lifecycle of underlying listeners, removing this burden from the developer. * To improve the testability of asynchronous Firebase interactions. ## 5. Non-Goals * To deprecate or remove the existing listener-based APIs in the immediate future. The new APIs will be additive. * To introduce `AsyncSequence` wrappers for one-shot asynchronous calls (which are better served by `async/await` functions). This proposal is focused exclusively on event streams. * To provide a custom `AsyncSequence` implementation. We will use Swift's standard `Async(Throwing)Stream` types. ## 6. API Naming Convention The guiding principle is to establish a clear, concise, and idiomatic naming convention that aligns with modern Swift practices and mirrors Apple's own frameworks. ### Recommended Approach: Name the sequence based on its conceptual model. 1. **For sequences of discrete items, use a plural noun.** * This applies when the stream represents a series of distinct objects, like data snapshots. * **Guidance:** Use a computed property for parameter-less access and a method for cases that require parameters. * **Examples:** `url.lines`, `db.collection("users").snapshots`. 2. **For sequences observing a single entity, describe the event with a suffix.** * This applies when the stream represents the changing value of a single property or entity over time. * **Guidance:** Use the entity's name combined with a suffix like `Changes`, `Updates`, or `Events`. * **Example:** `auth.authStateChanges`. This approach was chosen over verb-based (`.streamSnapshots()`) or suffix-based (`.snapshotStream`) alternatives because it aligns most closely with Apple's API design guidelines, leading to a more idiomatic and less verbose call site. ## 7. Proposed API Design ### 7.1. Cloud Firestore Provides an async alternative to `addSnapshotListener`. #### API Design ```swift // Collection snapshots extension CollectionReference { var snapshots: AsyncThrowingStream { get } func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream } // Query snapshots extension Query { var snapshots: AsyncThrowingStream { get } func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream } // Document snapshots extension DocumentReference { var snapshots: AsyncThrowingStream { get } func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream } ``` #### Usage ```swift // Streaming updates on a collection func observeUsers() async throws { for try await snapshot in db.collection("users").snapshots { // ... } } ``` ### 7.2. Realtime Database Provides an async alternative to the `observe(_:with:)` method. #### API Design ```swift /// An enumeration of granular child-level events. public enum DatabaseEvent { case childAdded(DataSnapshot, previousSiblingKey: String?) case childChanged(DataSnapshot, previousSiblingKey: String?) case childRemoved(DataSnapshot) case childMoved(DataSnapshot, previousSiblingKey: String?) } extension DatabaseQuery { /// An asynchronous stream of the entire contents at a location. /// This stream emits a new `DataSnapshot` every time the data changes. var value: AsyncThrowingStream { get } /// An asynchronous stream of child-level events at a location. func events() -> AsyncThrowingStream } ``` #### Usage ```swift // Streaming a single value let scoreRef = Database.database().reference(withPath: "game/score") for try await snapshot in scoreRef.value { // ... } // Streaming child events let messagesRef = Database.database().reference(withPath: "chats/123/messages") for try await event in messagesRef.events() { switch event { case .childAdded(let snapshot, _): // ... // ... } } ``` ### 7.3. Authentication Provides an async alternative to `addStateDidChangeListener`. #### API Design ```swift extension Auth { /// An asynchronous stream of authentication state changes. var authStateChanges: AsyncStream { get } } ``` #### Usage ```swift // Monitoring authentication state for await user in Auth.auth().authStateChanges { if let user = user { // User is signed in } else { // User is signed out } } ``` ### 7.4. Cloud Storage Provides an async alternative to `observe(.progress, ...)`. #### API Design ```swift extension StorageTask { /// An asynchronous stream of progress updates for an ongoing task. var progressUpdates: AsyncThrowingStream { get } } ``` #### Usage ```swift // Monitoring an upload task let uploadTask = ref.putData(data, metadata: nil) do { for try await progress in uploadTask.progress { // Update progress bar } print("Upload complete") } catch { // Handle error } ``` ### 7.5. Remote Config Provides an async alternative to `addOnConfigUpdateListener`. #### API Design ```swift extension RemoteConfig { /// An asynchronous stream of configuration updates. var updates: AsyncThrowingStream { get } } ``` #### Usage ```swift // Listening for real-time config updates for try await update in RemoteConfig.remoteConfig().updates { // Activate new config } ``` ### 7.6. Cloud Messaging (FCM) Provides an async alternative to the delegate-based approach for token updates and foreground messages. #### API Design ```swift extension Messaging { /// An asynchronous stream of FCM registration token updates. var tokenUpdates: AsyncStream { get } /// An asynchronous stream of remote messages received while the app is in the foreground. var foregroundMessages: AsyncStream { get } } ``` #### Usage ```swift // Monitoring FCM token updates for await token in Messaging.messaging().tokenUpdates { // Send token to server } ``` ## 8. Testing Plan The quality and reliability of this new API surface will be ensured through a multi-layered testing strategy, covering unit, integration, and cancellation scenarios. ### 8.1. Unit Tests The primary goal of unit tests is to verify the correctness of the `AsyncStream` wrapping logic in isolation from the network and backend services. * **Mocking:** Each product's stream implementation will be tested against a mocked version of its underlying service (e.g., a mock `Firestore` client). * **Behavior Verification:** * Tests will confirm that initiating a stream correctly registers a listener with the underlying service. * We will use the mock listeners to simulate events (e.g., new snapshots, auth state changes) and assert that the `AsyncStream` yields the corresponding values correctly. * Error conditions will be simulated to ensure that the stream correctly throws errors. * **Teardown Logic:** We will verify that the underlying listener is removed when the stream is either cancelled or finishes naturally. ### 8.2. Integration Tests Integration tests will validate the end-to-end functionality of the async sequences against a live backend environment using the **Firebase Emulator Suite**. * **Environment:** A new integration test suite will be created that configures the Firebase SDK to connect to the local emulators (Firestore, Database, Auth, etc.). * **Validation:** These tests will perform real operations (e.g., writing a document and then listening to its `snapshots` stream) to verify that real-time updates are correctly received and propagated through the `AsyncSequence` API. * **Cross-Product Scenarios:** We will test scenarios that involve multiple Firebase products where applicable. ### 8.3. Cancellation Behavior Tests A specific set of tests will be dedicated to ensuring that resource cleanup (i.e., listener removal) happens correctly and promptly when the consuming task is cancelled. * **Test Scenario:** 1. A stream will be consumed within a Swift `Task`. 2. The `Task` will be cancelled immediately after the stream is initiated. 3. Using a mock or a spy object, we will assert that the `remove()` method on the underlying listener registration is called. * **Importance:** This is critical for preventing resource leaks and ensuring the new API behaves predictably within the Swift structured concurrency model, especially in SwiftUI contexts where tasks are automatically managed. ## 9. Implementation Plan The implementation will be phased, with each product's API being added in a separate Pull Request to facilitate focused reviews. * **Firestore:** [PR #14924: Support AsyncStream in realtime query](https://github.com/firebase/firebase-ios-sdk/pull/14924) * **Authentication:** [Link to PR when available] * **Realtime Database:** [Link to PR when available] * ...and so on. ## 10. Open Questions & Future Work * Should we provide convenience wrappers for common `AsyncSequence` operators? (e.g., a method to directly stream decoded objects instead of snapshots). For now, this is considered a **Non-Goal** but could be revisited.