-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add AsyncSequence support for Realtime Database #15596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
peterfriese
wants to merge
1
commit into
main
Choose a base branch
from
peterfriese/asyncsequences/rtdb
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+248
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
226 changes: 226 additions & 0 deletions
226
FirebaseDatabase/Swift/Sources/Database+AsyncSequences.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| import FirebaseCore | ||
| import Foundation | ||
|
|
||
| // MARK: - DatabaseEvent | ||
|
|
||
| /// 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?) | ||
| } | ||
|
|
||
| // MARK: - DatabaseQuery + AsyncSequence | ||
|
|
||
| public extension DatabaseQuery { | ||
| /// An asynchronous stream of the entire contents at a location. | ||
| /// This stream emits a new `DataSnapshot` every time the data changes. | ||
| var snapshots: DatabaseQuerySnapshotsSequence { | ||
| DatabaseQuerySnapshotsSequence(self) | ||
| } | ||
|
|
||
| /// An asynchronous stream of child-level events at a location. | ||
| func childEvents() -> DatabaseChildEventsSequence { | ||
| DatabaseChildEventsSequence(self) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - DatabaseQuerySnapshotsSequence | ||
|
|
||
| /// An asynchronous sequence that emits `DataSnapshot` values whenever the query data changes. | ||
| /// | ||
| /// This struct is the concrete type returned by the `DatabaseQuery.snapshots` property. | ||
| /// | ||
| /// - Important: This type is marked `Sendable` because `DatabaseQuery` itself is `Sendable`. | ||
| public struct DatabaseQuerySnapshotsSequence: AsyncSequence, Sendable { | ||
| public typealias Element = DataSnapshot | ||
| public typealias Failure = Error | ||
| public typealias AsyncIterator = Iterator | ||
|
|
||
| @usableFromInline | ||
| let query: DatabaseQuery | ||
|
|
||
| /// Creates a new sequence for monitoring query snapshots. | ||
| /// - Parameter query: The `DatabaseQuery` instance to monitor. | ||
| @inlinable | ||
| public init(_ query: DatabaseQuery) { | ||
| self.query = query | ||
| } | ||
|
|
||
| /// Creates and returns an iterator for this asynchronous sequence. | ||
| /// - Returns: An `Iterator` for `DatabaseQuerySnapshotsSequence`. | ||
| @inlinable | ||
| public func makeAsyncIterator() -> Iterator { | ||
| Iterator(query: query) | ||
| } | ||
|
|
||
| /// The asynchronous iterator for `DatabaseQuerySnapshotsSequence`. | ||
| public struct Iterator: AsyncIteratorProtocol { | ||
| public typealias Element = DataSnapshot | ||
|
|
||
| @usableFromInline | ||
| let stream: AsyncThrowingStream<DataSnapshot, Error> | ||
| @usableFromInline | ||
| var streamIterator: AsyncThrowingStream<DataSnapshot, Error>.Iterator | ||
|
|
||
| /// Initializes the iterator with the provided `DatabaseQuery` instance. | ||
| /// This sets up the `AsyncThrowingStream` and registers the necessary listener. | ||
| /// - Parameter query: The `DatabaseQuery` instance to monitor. | ||
| @inlinable | ||
| init(query: DatabaseQuery) { | ||
| stream = AsyncThrowingStream { continuation in | ||
| let handle = query.observe(.value) { snapshot in | ||
| continuation.yield(snapshot) | ||
| } withCancel: { error in | ||
| continuation.finish(throwing: error) | ||
| } | ||
|
|
||
| continuation.onTermination = { @Sendable _ in | ||
| query.removeObserver(withHandle: handle) | ||
| } | ||
| } | ||
| streamIterator = stream.makeAsyncIterator() | ||
| } | ||
|
|
||
| /// Produces the next element in the asynchronous sequence. | ||
| /// | ||
| /// Returns a `DataSnapshot` value or `nil` if the sequence has terminated. | ||
| /// Throws an error if the underlying listener encounters an issue. | ||
| /// - Returns: An optional `DataSnapshot` object. | ||
| @inlinable | ||
| public mutating func next() async throws -> Element? { | ||
| try await streamIterator.next() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - DatabaseChildEventsSequence | ||
|
|
||
| /// An asynchronous sequence that emits `DatabaseEvent` values whenever the query's child data | ||
| /// changes. | ||
| /// | ||
| /// This struct is the concrete type returned by the `DatabaseQuery.childEvents()` method. | ||
| /// | ||
| /// - Important: This type is marked `Sendable` because `DatabaseQuery` itself is `Sendable`. | ||
| public struct DatabaseChildEventsSequence: AsyncSequence, Sendable { | ||
| public typealias Element = DatabaseEvent | ||
| public typealias Failure = Error | ||
| public typealias AsyncIterator = Iterator | ||
|
|
||
| @usableFromInline | ||
| let query: DatabaseQuery | ||
|
|
||
| /// Creates a new sequence for monitoring child events. | ||
| /// - Parameter query: The `DatabaseQuery` instance to monitor. | ||
| @inlinable | ||
| public init(_ query: DatabaseQuery) { | ||
| self.query = query | ||
| } | ||
|
|
||
| /// Creates and returns an iterator for this asynchronous sequence. | ||
| /// - Returns: An `Iterator` for `DatabaseChildEventsSequence`. | ||
| @inlinable | ||
| public func makeAsyncIterator() -> Iterator { | ||
| Iterator(query: query) | ||
| } | ||
|
|
||
| /// The asynchronous iterator for `DatabaseChildEventsSequence`. | ||
| public struct Iterator: AsyncIteratorProtocol { | ||
| public typealias Element = DatabaseEvent | ||
|
|
||
| @usableFromInline | ||
| let stream: AsyncThrowingStream<DatabaseEvent, Error> | ||
| @usableFromInline | ||
| var streamIterator: AsyncThrowingStream<DatabaseEvent, Error>.Iterator | ||
|
|
||
| /// Initializes the iterator with the provided `DatabaseQuery` instance. | ||
| /// This sets up the `AsyncThrowingStream` and registers the necessary listeners. | ||
| /// - Parameter query: The `DatabaseQuery` instance to monitor. | ||
| @inlinable | ||
| init(query: DatabaseQuery) { | ||
| stream = AsyncThrowingStream { continuation in | ||
| var handles = [DatabaseHandle]() | ||
|
|
||
| // Child Added | ||
| let childAddedHandle = query.observe( | ||
| .childAdded, | ||
| andPreviousSiblingKeyWith: { snapshot, previousKey in | ||
| continuation.yield(.childAdded(snapshot, previousSiblingKey: previousKey)) | ||
| }, | ||
| withCancel: { error in | ||
| continuation.finish(throwing: error) | ||
| } | ||
| ) | ||
| handles.append(childAddedHandle) | ||
|
|
||
| // Child Changed | ||
| let childChangedHandle = query.observe( | ||
| .childChanged, | ||
| andPreviousSiblingKeyWith: { snapshot, previousKey in | ||
| continuation.yield(.childChanged(snapshot, previousSiblingKey: previousKey)) | ||
| }, | ||
| withCancel: { error in | ||
| continuation.finish(throwing: error) | ||
| } | ||
| ) | ||
| handles.append(childChangedHandle) | ||
|
|
||
| // Child Removed | ||
| let childRemovedHandle = query.observe(.childRemoved, with: { snapshot in | ||
| continuation.yield(.childRemoved(snapshot)) | ||
| }, withCancel: { error in | ||
| continuation.finish(throwing: error) | ||
| }) | ||
| handles.append(childRemovedHandle) | ||
|
|
||
| // Child Moved | ||
| let childMovedHandle = query.observe( | ||
| .childMoved, | ||
| andPreviousSiblingKeyWith: { snapshot, previousKey in | ||
| continuation.yield(.childMoved(snapshot, previousSiblingKey: previousKey)) | ||
| }, | ||
| withCancel: { error in | ||
| continuation.finish(throwing: error) | ||
| } | ||
| ) | ||
| handles.append(childMovedHandle) | ||
|
|
||
| // We capture `handles` (the array of handles we just populated) | ||
| // by value in the capture list `[handles]`. | ||
|
|
||
| // This ensures the closure uses an immutable copy of the array, preventing data races. | ||
| continuation.onTermination = { @Sendable [handles] _ in | ||
| for handle in handles { | ||
| query.removeObserver(withHandle: handle) | ||
| } | ||
| } | ||
| } | ||
| streamIterator = stream.makeAsyncIterator() | ||
| } | ||
|
|
||
| /// Produces the next element in the asynchronous sequence. | ||
| /// | ||
| /// Returns a `DatabaseEvent` value or `nil` if the sequence has terminated. | ||
| /// Throws an error if the underlying listener encounters an issue. | ||
| /// - Returns: An optional `DatabaseEvent` object. | ||
| @inlinable | ||
| public mutating func next() async throws -> Element? { | ||
| try await streamIterator.next() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Sendable Conformance | ||
|
|
||
| // `DatabaseQuery` is thread-safe, so we can mark it as `@unchecked Sendable`. | ||
| // We use `@retroactive` to silence Swift 6 warnings about conforming a type from another module. | ||
| extension DatabaseQuery: @retroactive @unchecked Sendable {} | ||
|
Check warning on line 218 in FirebaseDatabase/Swift/Sources/Database+AsyncSequences.swift
|
||
|
|
||
| // Explicitly mark the Iterator as unavailable for Sendable conformance | ||
| @available(*, unavailable) | ||
| extension DatabaseQuerySnapshotsSequence.Iterator: Sendable {} | ||
|
|
||
| // Explicitly mark the Iterator as unavailable for Sendable conformance | ||
| @available(*, unavailable) | ||
| extension DatabaseChildEventsSequence.Iterator: Sendable {} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
DatabaseEventenum should explicitly conform toSendable. Since it is a public enum used as the element type of anAsyncSequence, explicit conformance ensures it can be safely used across concurrency boundaries in strict mode. Note thatDataSnapshotis already markedNS_SWIFT_SENDABLEin its header, so the enum will satisfy the requirements.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion.
Interesting: given the symbol,
DataSnapshot, in a Swift context, it recognized that the symbol was being bridged from Objective-C and the source-of-truth definition was marked sendable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, from the trace the agent did read
FIRDataSnapshot.hso it had the context.