Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions docs/AsyncStreams/swift-async-sequence-api-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# 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<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

```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<DataSnapshot, Error> { get }

/// An asynchronous stream of child-level events at a location.
func events() -> AsyncThrowingStream<DatabaseEvent, Error>
}
```

#### 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<User?> { 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<StorageTaskSnapshot, Error> { 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<RemoteConfigUpdate, Error> { 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<String> { get }

/// An asynchronous stream of remote messages received while the app is in the foreground.
var foregroundMessages: AsyncStream<MessagingRemoteMessage> { 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.
Loading