AsyncSequence Event StreamsIn ReviewThis 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.
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.
Adopting AsyncSequence will:
throws) and a linear for try await syntax to make asynchronous code easier to read and maintain.map, filter, prefix) to transform and combine streams declaratively.AsyncSequence-based API surface for all relevant event-streaming Firebase APIs.AsyncSequence wrappers for one-shot asynchronous calls (which are better served by async/await functions). This proposal is focused exclusively on event streams.AsyncSequence implementation. We will use Swift's standard Async(Throwing)Stream types.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.
For sequences of discrete items, use a plural noun.
url.lines, db.collection("users").snapshots.For sequences observing a single entity, describe the event with a suffix.
Changes, Updates, or Events.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.
Provides an async alternative to addSnapshotListener.
// 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>
}
// Streaming updates on a collection
func observeUsers() async throws {
for try await snapshot in db.collection("users").snapshots {
// ...
}
}
Provides an async alternative to the observe(_:with:) method.
/// 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>
}
// 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, _):
// ...
// ...
}
}
Provides an async alternative to addStateDidChangeListener.
extension Auth {
/// An asynchronous stream of authentication state changes.
var authStateChanges: AsyncStream<User?> { get }
}
// Monitoring authentication state
for await user in Auth.auth().authStateChanges {
if let user = user {
// User is signed in
} else {
// User is signed out
}
}
Provides an async alternative to observe(.progress, ...).
extension StorageTask {
/// An asynchronous stream of progress updates for an ongoing task.
var progressUpdates: AsyncThrowingStream<StorageTaskSnapshot, Error> { get }
}
// 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
}
Provides an async alternative to addOnConfigUpdateListener.
extension RemoteConfig {
/// An asynchronous stream of configuration updates.
var updates: AsyncThrowingStream<RemoteConfigUpdate, Error> { get }
}
// Listening for real-time config updates
for try await update in RemoteConfig.remoteConfig().updates {
// Activate new config
}
Provides an async alternative to the delegate-based approach for token updates and foreground messages.
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 }
}
// Monitoring FCM token updates
for await token in Messaging.messaging().tokenUpdates {
// Send token to server
}
The quality and reliability of this new API surface will be ensured through a multi-layered testing strategy, covering unit, integration, and cancellation scenarios.
The primary goal of unit tests is to verify the correctness of the AsyncStream wrapping logic in isolation from the network and backend services.
Firestore client).AsyncStream yields the corresponding values correctly.Integration tests will validate the end-to-end functionality of the async sequences against a live backend environment using the Firebase Emulator Suite.
snapshots stream) to verify that real-time updates are correctly received and propagated through the AsyncSequence API.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.
Task.Task will be cancelled immediately after the stream is initiated.remove() method on the underlying listener registration is called.The implementation will be phased, with each product's API being added in a separate Pull Request to facilitate focused reviews.
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.