Skip to content

Commit 5cd140b

Browse files
committed
Add design document
1 parent a5bc774 commit 5cd140b

File tree

1 file changed

+303
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)