11 KiB
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).
// 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 linearfor try awaitsyntax 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
AsyncSequencewrappers for one-shot asynchronous calls (which are better served byasync/awaitfunctions). This proposal is focused exclusively on event streams. - To provide a custom
AsyncSequenceimplementation. We will use Swift's standardAsync(Throwing)Streamtypes.
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.
-
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.
-
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, orEvents. - 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
// Collection snapshots
extension CollectionReference {
var snapshots: AsyncThrowingStream<QuerySnapshot, Error> { get }
func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream<QuerySnapshot, Error>
}
// Query snapshots
extension Query {
var snapshots: AsyncThrowingStream<QuerySnapshot, Error> { get }
func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream<QuerySnapshot, Error>
}
// Document snapshots
extension DocumentReference {
var snapshots: AsyncThrowingStream<DocumentSnapshot, Error> { get }
func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream<DocumentSnapshot, Error>
}
Usage
// 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
/// 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<DataSnapshot, Error> { get }
/// An asynchronous stream of child-level events at a location.
func events() -> AsyncThrowingStream<DatabaseEvent, Error>
}
Usage
// 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
extension Auth {
/// An asynchronous stream of authentication state changes.
var authStateChanges: AsyncStream<User?> { get }
}
Usage
// 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
extension StorageTask {
/// An asynchronous stream of progress updates for an ongoing task.
var progressUpdates: AsyncThrowingStream<StorageTaskSnapshot, Error> { get }
}
Usage
// 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
extension RemoteConfig {
/// An asynchronous stream of configuration updates.
var updates: AsyncThrowingStream<RemoteConfigUpdate, Error> { get }
}
Usage
// 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
extension Messaging {
/// An asynchronous stream of FCM registration token updates.
var tokenUpdates: AsyncStream<String> { get }
/// An asynchronous stream of remote messages received while the app is in the foreground.
var foregroundMessages: AsyncStream<MessagingRemoteMessage> { get }
}
Usage
// 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
Firestoreclient). - 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
AsyncStreamyields 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
snapshotsstream) to verify that real-time updates are correctly received and propagated through theAsyncSequenceAPI. - 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:
- A stream will be consumed within a Swift
Task. - The
Taskwill be cancelled immediately after the stream is initiated. - Using a mock or a spy object, we will assert that the
remove()method on the underlying listener registration is called.
- A stream will be consumed within a Swift
- 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
- 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
AsyncSequenceoperators? (e.g., a method to directly stream decoded objects instead of snapshots). For now, this is considered a Non-Goal but could be revisited.