Skip to content

Commit 5cb5b43

Browse files
committed
Use concrete AsyncSequence types for snapshots
1 parent d1b2327 commit 5cb5b43

File tree

2 files changed

+168
-26
lines changed

2 files changed

+168
-26
lines changed

Firestore/Swift/Source/AsyncAwait/DocumentReference+AsyncSequence.swift

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,98 @@ public extension DocumentReference {
2727
///
2828
/// This stream emits a new `DocumentSnapshot` every time the underlying data changes.
2929
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
30-
var snapshots: some AsyncSequence<DocumentSnapshot, Error> {
30+
var snapshots: DocumentSnapshotsSequence {
3131
return snapshots(includeMetadataChanges: false)
3232
}
3333

3434
/// An asynchronous sequence of document snapshots.
3535
///
3636
/// - Parameter includeMetadataChanges: Whether to receive events for metadata-only changes.
37-
/// - Returns: An `AsyncThrowingStream` of `DocumentSnapshot` events.
37+
/// - Returns: A `DocumentSnapshotsSequence` of `DocumentSnapshot` events.
3838
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
39-
func snapshots(includeMetadataChanges: Bool) -> some AsyncSequence<DocumentSnapshot, Error> {
40-
return AsyncThrowingStream { continuation in
41-
let listener = self
42-
.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in
43-
if let error = error {
44-
continuation.finish(throwing: error)
45-
} else if let snapshot = snapshot {
46-
continuation.yield(snapshot)
39+
func snapshots(includeMetadataChanges: Bool) -> DocumentSnapshotsSequence {
40+
return DocumentSnapshotsSequence(self, includeMetadataChanges: includeMetadataChanges)
41+
}
42+
43+
/// An `AsyncSequence` that emits `DocumentSnapshot` values whenever the document data changes.
44+
///
45+
/// This struct is the concrete type returned by the `DocumentReference.snapshots` property.
46+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
47+
@frozen
48+
struct DocumentSnapshotsSequence: AsyncSequence, Sendable {
49+
public typealias Element = DocumentSnapshot
50+
public typealias Failure = Error
51+
public typealias AsyncIterator = Iterator
52+
53+
@usableFromInline
54+
internal let documentReference: DocumentReference
55+
@usableFromInline
56+
internal let includeMetadataChanges: Bool
57+
58+
/// Creates a new sequence for monitoring document snapshots.
59+
/// - Parameters:
60+
/// - documentReference: The `DocumentReference` instance to monitor.
61+
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
62+
@inlinable
63+
public init(_ documentReference: DocumentReference, includeMetadataChanges: Bool) {
64+
self.documentReference = documentReference
65+
self.includeMetadataChanges = includeMetadataChanges
66+
}
67+
68+
/// Creates and returns an iterator for this asynchronous sequence.
69+
/// - Returns: An `Iterator` for `DocumentSnapshotsSequence`.
70+
@inlinable
71+
public func makeAsyncIterator() -> Iterator {
72+
Iterator(documentReference: documentReference, includeMetadataChanges: includeMetadataChanges)
73+
}
74+
75+
/// The asynchronous iterator for `DocumentSnapshotsSequence`.
76+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
77+
@frozen
78+
public struct Iterator: AsyncIteratorProtocol {
79+
public typealias Element = DocumentSnapshot
80+
@usableFromInline
81+
internal let stream: AsyncThrowingStream<DocumentSnapshot, Error>
82+
@usableFromInline
83+
internal var streamIterator: AsyncThrowingStream<DocumentSnapshot, Error>.Iterator
84+
85+
/// Initializes the iterator with the provided `DocumentReference` instance.
86+
/// This sets up the `AsyncThrowingStream` and registers the necessary listener.
87+
/// - Parameters:
88+
/// - documentReference: The `DocumentReference` instance to monitor.
89+
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
90+
@inlinable
91+
init(documentReference: DocumentReference, includeMetadataChanges: Bool) {
92+
stream = AsyncThrowingStream { continuation in
93+
let listener = documentReference
94+
.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in
95+
if let error = error {
96+
continuation.finish(throwing: error)
97+
} else if let snapshot = snapshot {
98+
continuation.yield(snapshot)
99+
}
100+
}
101+
102+
continuation.onTermination = { @Sendable _ in
103+
listener.remove()
47104
}
48105
}
49-
continuation.onTermination = { @Sendable _ in
50-
listener.remove()
106+
streamIterator = stream.makeAsyncIterator()
107+
}
108+
109+
/// Produces the next element in the asynchronous sequence.
110+
///
111+
/// Returns a `DocumentSnapshot` value or `nil` if the sequence has terminated.
112+
/// Throws an error if the underlying listener encounters an issue.
113+
/// - Returns: An optional `DocumentSnapshot` object.
114+
@inlinable
115+
public mutating func next() async throws -> Element? {
116+
try await streamIterator.next()
51117
}
52118
}
53119
}
54120
}
121+
122+
// Explicitly mark the Iterator as unavailable for Sendable conformance
123+
@available(*, unavailable)
124+
extension DocumentReference.DocumentSnapshotsSequence.Iterator: Sendable {}

Firestore/Swift/Source/AsyncAwait/Query+AsyncSequence.swift

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,101 @@ public extension Query {
2626
/// An asynchronous sequence of query snapshots.
2727
///
2828
/// This stream emits a new `QuerySnapshot` every time the underlying data changes.
29-
@available(iOS 18.0, *)
30-
var snapshots: some AsyncSequence<QuerySnapshot, Error> {
29+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
30+
var snapshots: QuerySnapshotsSequence {
3131
return snapshots(includeMetadataChanges: false)
3232
}
3333

3434
/// An asynchronous sequence of query snapshots.
3535
///
3636
/// - Parameter includeMetadataChanges: Whether to receive events for metadata-only changes.
37-
/// - Returns: An `AsyncThrowingStream` of `QuerySnapshot` events.
38-
@available(iOS 18.0, *)
39-
func snapshots(includeMetadataChanges: Bool) -> some AsyncSequence<QuerySnapshot, Error> {
40-
return AsyncThrowingStream { continuation in
41-
let listener = self
42-
.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in
43-
if let error = error {
44-
continuation.finish(throwing: error)
45-
} else if let snapshot = snapshot {
46-
continuation.yield(snapshot)
37+
/// - Returns: A `QuerySnapshotsSequence` of `QuerySnapshot` events.
38+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
39+
func snapshots(includeMetadataChanges: Bool) -> QuerySnapshotsSequence {
40+
return QuerySnapshotsSequence(self, includeMetadataChanges: includeMetadataChanges)
41+
}
42+
43+
/// An `AsyncSequence` that emits `QuerySnapshot` values whenever the query data changes.
44+
///
45+
/// This struct is the concrete type returned by the `Query.snapshots` property.
46+
///
47+
/// - Important: This type is marked `Sendable` because `Query` itself is `Sendable`.
48+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
49+
@frozen
50+
struct QuerySnapshotsSequence: AsyncSequence, Sendable {
51+
public typealias Element = QuerySnapshot
52+
public typealias Failure = Error
53+
public typealias AsyncIterator = Iterator
54+
55+
@usableFromInline
56+
internal let query: Query
57+
@usableFromInline
58+
internal let includeMetadataChanges: Bool
59+
60+
/// Creates a new sequence for monitoring query snapshots.
61+
/// - Parameters:
62+
/// - query: The `Query` instance to monitor.
63+
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
64+
@inlinable
65+
public init(_ query: Query, includeMetadataChanges: Bool) {
66+
self.query = query
67+
self.includeMetadataChanges = includeMetadataChanges
68+
}
69+
70+
/// Creates and returns an iterator for this asynchronous sequence.
71+
/// - Returns: An `Iterator` for `QuerySnapshotsSequence`.
72+
@inlinable
73+
public func makeAsyncIterator() -> Iterator {
74+
Iterator(query: query, includeMetadataChanges: includeMetadataChanges)
75+
}
76+
77+
/// The asynchronous iterator for `QuerySnapshotsSequence`.
78+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
79+
@frozen
80+
public struct Iterator: AsyncIteratorProtocol {
81+
public typealias Element = QuerySnapshot
82+
@usableFromInline
83+
internal let stream: AsyncThrowingStream<QuerySnapshot, Error>
84+
@usableFromInline
85+
internal var streamIterator: AsyncThrowingStream<QuerySnapshot, Error>.Iterator
86+
87+
/// Initializes the iterator with the provided `Query` instance.
88+
/// This sets up the `AsyncThrowingStream` and registers the necessary listener.
89+
/// - Parameters:
90+
/// - query: The `Query` instance to monitor.
91+
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
92+
@inlinable
93+
init(query: Query, includeMetadataChanges: Bool) {
94+
stream = AsyncThrowingStream { continuation in
95+
let listener = query
96+
.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in
97+
if let error = error {
98+
continuation.finish(throwing: error)
99+
} else if let snapshot = snapshot {
100+
continuation.yield(snapshot)
101+
}
102+
}
103+
104+
continuation.onTermination = { @Sendable _ in
105+
listener.remove()
47106
}
48107
}
49-
continuation.onTermination = { @Sendable _ in
50-
listener.remove()
108+
streamIterator = stream.makeAsyncIterator()
109+
}
110+
111+
/// Produces the next element in the asynchronous sequence.
112+
///
113+
/// Returns a `QuerySnapshot` value or `nil` if the sequence has terminated.
114+
/// Throws an error if the underlying listener encounters an issue.
115+
/// - Returns: An optional `QuerySnapshot` object.
116+
@inlinable
117+
public mutating func next() async throws -> Element? {
118+
try await streamIterator.next()
51119
}
52120
}
53121
}
54122
}
123+
124+
// Explicitly mark the Iterator as unavailable for Sendable conformance
125+
@available(*, unavailable)
126+
extension Query.QuerySnapshotsSequence.Iterator: Sendable {}

0 commit comments

Comments
 (0)