Skip to content

Commit 45b8a27

Browse files
committed
Initial implementation of AsyncStream for RC + basic test
1 parent 8a6390c commit 45b8a27

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
@available(iOS 13.0.0, macOS 10.15.0, macCatalyst 13.0.0, tvOS 13.0.0, watchOS 7.0.0, *)
18+
extension RemoteConfig {
19+
/// Returns an `AsyncThrowingStream` that provides real-time updates to the configuration.
20+
///
21+
/// You can listen for updates by iterating over the stream using a `for try await` loop.
22+
/// The stream will yield a `RemoteConfigUpdate` whenever a change is pushed from the
23+
/// Remote Config backend. After receiving an update, you must call `activate()` to make the
24+
/// new configuration available to your app.
25+
///
26+
/// The underlying listener is automatically added when you begin iterating and is removed when
27+
/// the iteration is cancelled or finishes.
28+
///
29+
/// - Throws: `RemoteConfigUpdateError` if the listener encounters a server-side error or another
30+
/// issue, causing the stream to terminate.
31+
///
32+
/// ### Example Usage
33+
///
34+
/// ```swift
35+
/// func listenForRealtimeUpdates() {
36+
/// Task {
37+
/// do {
38+
/// for try await configUpdate in remoteConfig.updates {
39+
/// print("Updated keys: \(configUpdate.updatedKeys)")
40+
/// // Activate the new config to make it available
41+
/// let status = try await remoteConfig.activate()
42+
/// print("Config activated with status: \(status)")
43+
/// }
44+
/// } catch {
45+
/// print("Error listening for remote config updates: \(error)")
46+
/// }
47+
/// }
48+
/// }
49+
/// ```
50+
public var updates: AsyncThrowingStream<RemoteConfigUpdate, Error> {
51+
return AsyncThrowingStream { continuation in
52+
let listener = addOnConfigUpdateListener { update, error in
53+
switch (update, error) {
54+
case (let update?, _):
55+
// If there's an update, yield it. We prioritize the update over a potential error.
56+
continuation.yield(update)
57+
case (_, let error?):
58+
// If there's no update but there is an error, terminate the stream with the error.
59+
continuation.finish(throwing: error)
60+
case (nil, nil):
61+
// If both are nil (the "should not happen" case), gracefully finish the stream.
62+
continuation.finish()
63+
}
64+
}
65+
66+
continuation.onTermination = { @Sendable _ in
67+
listener.remove()
68+
}
69+
}
70+
}
71+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache-Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing-software
10+
// distributed under the License is distributed on an "AS IS" BASIS-
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND-either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseCore
16+
@testable import FirebaseRemoteConfig
17+
18+
import XCTest
19+
20+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
21+
class AsyncStreamTests: APITestBase {
22+
func testConfigUpdateStreamReceivesUpdates() async throws {
23+
guard APITests.useFakeConfig else { return }
24+
25+
let expectation = self.expectation(description: #function)
26+
27+
Task {
28+
for try await update in config.updates {
29+
expectation.fulfill()
30+
}
31+
}
32+
33+
fakeConsole.config[Constants.key1] = Constants.value1
34+
await fulfillment(of: [expectation], timeout: 5)
35+
}
36+
}

0 commit comments

Comments
 (0)