Skip to content

Commit 6bcf978

Browse files
authored
Add design document for Swift AsyncSequence / AsyncStream support for Firebase's realtime APIs (#15350)
1 parent 63083d8 commit 6bcf978

File tree

1 file changed

+301
-0
lines changed

1 file changed

+301
-0
lines changed
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# API Design for Firebase `AsyncSequence` Event Streams
2+
3+
* **Authors**
4+
* Peter Friese
5+
* **Contributors**
6+
* Nick Cooke
7+
* Paul Beusterien
8+
* **Status**: `In Review`
9+
* **Last Updated**: 2025-09-25
10+
11+
## 1. Abstract
12+
13+
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.
14+
15+
## 2. Background
16+
17+
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).
18+
19+
```swift
20+
// Current listener-based approach
21+
db.collection("cities").document("SF")
22+
.addSnapshotListener { documentSnapshot, error in
23+
guard let document = documentSnapshot else { /* ... */ }
24+
guard let data = document.data() else { /* ... */ }
25+
print("Current data: \(data)")
26+
}
27+
```
28+
29+
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.
30+
31+
## 3. Motivation
32+
33+
Adopting `AsyncSequence` will:
34+
35+
* **Modernize the SDK:** Align with Swift's modern concurrency approach, making Firebase feel more native to Swift developers.
36+
* **Simplify Development:** Eliminate the need for manual listener management and reduce boilerplate code, especially when integrating with SwiftUI.
37+
* **Improve Code Quality:** Provide official, high-quality implementations for streaming APIs, reducing ecosystem fragmentation caused by unofficial solutions.
38+
* **Enhance Readability:** Leverage structured error handling (`throws`) and a linear `for try await` syntax to make asynchronous code easier to read and maintain.
39+
* **Enable Composition:** Allow developers to use a rich set of sequence operators (like `map`, `filter`, `prefix`) to transform and combine streams declaratively.
40+
41+
## 4. Goals
42+
43+
* To design and implement an idiomatic, `AsyncSequence`-based API surface for all relevant event-streaming Firebase APIs.
44+
* To provide a clear and consistent naming convention that aligns with Apple's own Swift APIs.
45+
* To ensure the new APIs automatically manage the lifecycle of underlying listeners, removing this burden from the developer.
46+
* To improve the testability of asynchronous Firebase interactions.
47+
48+
## 5. Non-Goals
49+
50+
* To deprecate or remove the existing listener-based APIs in the immediate future. The new APIs will be additive.
51+
* 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.
52+
* To provide a custom `AsyncSequence` implementation. We will use Swift's standard `Async(Throwing)Stream` types.
53+
54+
## 6. API Naming Convention
55+
56+
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.
57+
58+
### Recommended Approach: Name the sequence based on its conceptual model.
59+
60+
1. **For sequences of discrete items, use a plural noun.**
61+
* This applies when the stream represents a series of distinct objects, like data snapshots.
62+
* **Guidance:** Use a computed property for parameter-less access and a method for cases that require parameters.
63+
* **Examples:** `url.lines`, `db.collection("users").snapshots`.
64+
65+
2. **For sequences observing a single entity, describe the event with a suffix.**
66+
* This applies when the stream represents the changing value of a single property or entity over time.
67+
* **Guidance:** Use the entity's name combined with a suffix like `Changes`, `Updates`, or `Events`.
68+
* **Example:** `auth.authStateChanges`.
69+
70+
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.
71+
72+
## 7. Proposed API Design
73+
74+
### 7.1. Cloud Firestore
75+
76+
Provides an async alternative to `addSnapshotListener`.
77+
78+
#### API Design
79+
80+
```swift
81+
// Collection snapshots
82+
extension CollectionReference {
83+
var snapshots: AsyncThrowingStream<QuerySnapshot, Error> { get }
84+
func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream<QuerySnapshot, Error>
85+
}
86+
87+
// Query snapshots
88+
extension Query {
89+
var snapshots: AsyncThrowingStream<QuerySnapshot, Error> { get }
90+
func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream<QuerySnapshot, Error>
91+
}
92+
93+
// Document snapshots
94+
extension DocumentReference {
95+
var snapshots: AsyncThrowingStream<DocumentSnapshot, Error> { get }
96+
func snapshots(includeMetadataChanges: Bool = false) -> AsyncThrowingStream<DocumentSnapshot, Error>
97+
}
98+
```
99+
100+
#### Usage
101+
102+
```swift
103+
// Streaming updates on a collection
104+
func observeUsers() async throws {
105+
for try await snapshot in db.collection("users").snapshots {
106+
// ...
107+
}
108+
}
109+
```
110+
111+
### 7.2. Realtime Database
112+
113+
Provides an async alternative to the `observe(_:with:)` method.
114+
115+
#### API Design
116+
117+
```swift
118+
/// An enumeration of granular child-level events.
119+
public enum DatabaseEvent {
120+
case childAdded(DataSnapshot, previousSiblingKey: String?)
121+
case childChanged(DataSnapshot, previousSiblingKey: String?)
122+
case childRemoved(DataSnapshot)
123+
case childMoved(DataSnapshot, previousSiblingKey: String?)
124+
}
125+
126+
extension DatabaseQuery {
127+
/// An asynchronous stream of the entire contents at a location.
128+
/// This stream emits a new `DataSnapshot` every time the data changes.
129+
var value: AsyncThrowingStream<DataSnapshot, Error> { get }
130+
131+
/// An asynchronous stream of child-level events at a location.
132+
func events() -> AsyncThrowingStream<DatabaseEvent, Error>
133+
}
134+
```
135+
136+
#### Usage
137+
138+
```swift
139+
// Streaming a single value
140+
let scoreRef = Database.database().reference(withPath: "game/score")
141+
for try await snapshot in scoreRef.value {
142+
// ...
143+
}
144+
145+
// Streaming child events
146+
let messagesRef = Database.database().reference(withPath: "chats/123/messages")
147+
for try await event in messagesRef.events() {
148+
switch event {
149+
case .childAdded(let snapshot, _):
150+
// ...
151+
// ...
152+
}
153+
}
154+
```
155+
156+
### 7.3. Authentication
157+
158+
Provides an async alternative to `addStateDidChangeListener`.
159+
160+
#### API Design
161+
162+
```swift
163+
extension Auth {
164+
/// An asynchronous stream of authentication state changes.
165+
var authStateChanges: AsyncStream<User?> { get }
166+
}
167+
```
168+
169+
#### Usage
170+
171+
```swift
172+
// Monitoring authentication state
173+
for await user in Auth.auth().authStateChanges {
174+
if let user = user {
175+
// User is signed in
176+
} else {
177+
// User is signed out
178+
}
179+
}
180+
```
181+
182+
### 7.4. Cloud Storage
183+
184+
Provides an async alternative to `observe(.progress, ...)`.
185+
186+
#### API Design
187+
188+
```swift
189+
extension StorageTask {
190+
/// An asynchronous stream of progress updates for an ongoing task.
191+
var progressUpdates: AsyncThrowingStream<StorageTaskSnapshot, Error> { get }
192+
}
193+
```
194+
195+
#### Usage
196+
197+
```swift
198+
// Monitoring an upload task
199+
let uploadTask = ref.putData(data, metadata: nil)
200+
do {
201+
for try await progress in uploadTask.progress {
202+
// Update progress bar
203+
}
204+
print("Upload complete")
205+
} catch {
206+
// Handle error
207+
}
208+
```
209+
210+
### 7.5. Remote Config
211+
212+
Provides an async alternative to `addOnConfigUpdateListener`.
213+
214+
#### API Design
215+
216+
```swift
217+
extension RemoteConfig {
218+
/// An asynchronous stream of configuration updates.
219+
var updates: AsyncThrowingStream<RemoteConfigUpdate, Error> { get }
220+
}
221+
```
222+
223+
#### Usage
224+
225+
```swift
226+
// Listening for real-time config updates
227+
for try await update in RemoteConfig.remoteConfig().updates {
228+
// Activate new config
229+
}
230+
```
231+
232+
### 7.6. Cloud Messaging (FCM)
233+
234+
Provides an async alternative to the delegate-based approach for token updates and foreground messages.
235+
236+
#### API Design
237+
238+
```swift
239+
extension Messaging {
240+
/// An asynchronous stream of FCM registration token updates.
241+
var tokenUpdates: AsyncStream<String> { get }
242+
243+
/// An asynchronous stream of remote messages received while the app is in the foreground.
244+
var foregroundMessages: AsyncStream<MessagingRemoteMessage> { get }
245+
}
246+
```
247+
248+
#### Usage
249+
250+
```swift
251+
// Monitoring FCM token updates
252+
for await token in Messaging.messaging().tokenUpdates {
253+
// Send token to server
254+
}
255+
```
256+
257+
## 8. Testing Plan
258+
259+
The quality and reliability of this new API surface will be ensured through a multi-layered testing strategy, covering unit, integration, and cancellation scenarios.
260+
261+
### 8.1. Unit Tests
262+
263+
The primary goal of unit tests is to verify the correctness of the `AsyncStream` wrapping logic in isolation from the network and backend services.
264+
265+
* **Mocking:** Each product's stream implementation will be tested against a mocked version of its underlying service (e.g., a mock `Firestore` client).
266+
* **Behavior Verification:**
267+
* Tests will confirm that initiating a stream correctly registers a listener with the underlying service.
268+
* 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.
269+
* Error conditions will be simulated to ensure that the stream correctly throws errors.
270+
* **Teardown Logic:** We will verify that the underlying listener is removed when the stream is either cancelled or finishes naturally.
271+
272+
### 8.2. Integration Tests
273+
274+
Integration tests will validate the end-to-end functionality of the async sequences against a live backend environment using the **Firebase Emulator Suite**.
275+
276+
* **Environment:** A new integration test suite will be created that configures the Firebase SDK to connect to the local emulators (Firestore, Database, Auth, etc.).
277+
* **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.
278+
* **Cross-Product Scenarios:** We will test scenarios that involve multiple Firebase products where applicable.
279+
280+
### 8.3. Cancellation Behavior Tests
281+
282+
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.
283+
284+
* **Test Scenario:**
285+
1. A stream will be consumed within a Swift `Task`.
286+
2. The `Task` will be cancelled immediately after the stream is initiated.
287+
3. Using a mock or a spy object, we will assert that the `remove()` method on the underlying listener registration is called.
288+
* **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.
289+
290+
## 9. Implementation Plan
291+
292+
The implementation will be phased, with each product's API being added in a separate Pull Request to facilitate focused reviews.
293+
294+
* **Firestore:** [PR #14924: Support AsyncStream in realtime query](https://github.com/firebase/firebase-ios-sdk/pull/14924)
295+
* **Authentication:** [Link to PR when available]
296+
* **Realtime Database:** [Link to PR when available]
297+
* ...and so on.
298+
299+
## 10. Open Questions & Future Work
300+
301+
* 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.

0 commit comments

Comments
 (0)