Last active
February 16, 2025 13:54
-
-
Save thomsmed/b06d50a189f6ed25e52cef166441d025 to your computer and use it in GitHub Desktop.
An `AsyncSequence` that accept a closure to produce its values at a optional `TimeInterval`, with a `Context` that can be used to track relevant information between each iteration.
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
// | |
// AsyncValues.swift | |
// | |
/// An `AsyncSequence` that accept a closure to produce its values at an optional `TimeInterval`. | |
/// | |
/// The `Context` can be used to track relevant information between each iteration of the `AsyncSequence`. | |
public struct AsyncValues<Value: Sendable, Context: Sendable>: AsyncSequence { | |
public struct Iterator: AsyncIteratorProtocol { | |
private let interval: TimeInterval | |
private let askForFirstValueImmediately: Bool | |
private let nextValue: (inout Context) async throws -> Value? | |
private var firstIterationPassed: Bool = false | |
private var context: Context | |
init( | |
interval: TimeInterval, | |
askForFirstValueImmediately: Bool, | |
initialContext: Context, | |
nextValue: @escaping (inout Context) async throws -> Value? | |
) { | |
self.interval = interval | |
self.askForFirstValueImmediately = askForFirstValueImmediately | |
self.context = initialContext | |
self.nextValue = nextValue | |
} | |
mutating public func next() async throws -> Value? { | |
do { | |
if firstIterationPassed { | |
try await Task.sleep(for: .seconds(interval)) | |
} else { | |
firstIterationPassed = true | |
if !askForFirstValueImmediately { | |
try await Task.sleep(for: .seconds(interval)) | |
} | |
} | |
return try await nextValue(&context) | |
} catch is CancellationError { | |
return nil | |
} | |
} | |
} | |
private let interval: TimeInterval | |
private let askForFirstValueImmediately: Bool | |
private let initialContext: Context | |
private let nextValue: (inout Context) async throws -> Value? | |
public init( | |
of _: Value.Type, | |
every interval: TimeInterval = .zero, | |
askForFirstValueImmediately: Bool = true, | |
initialContext: Context, | |
_ nextValue: @escaping (inout Context) async throws -> Value? | |
) { | |
self.interval = interval | |
self.askForFirstValueImmediately = askForFirstValueImmediately | |
self.initialContext = initialContext | |
self.nextValue = nextValue | |
} | |
public func makeAsyncIterator() -> Iterator { | |
Iterator( | |
interval: interval, | |
askForFirstValueImmediately: askForFirstValueImmediately, | |
initialContext: initialContext, | |
nextValue: nextValue | |
) | |
} | |
} | |
public extension AsyncValues where Context == Void { | |
init( | |
of type: Value.Type, | |
every interval: TimeInterval = .zero, | |
askForFirstValueImmediately: Bool = true, | |
_ nextValue: @escaping (inout Context) async throws -> Value? | |
) { | |
self.init( | |
of: type, | |
every: interval, | |
askForFirstValueImmediately: askForFirstValueImmediately, | |
initialContext: Void(), | |
nextValue | |
) | |
} | |
} | |
// MARK: Usage/Tests | |
import Testing | |
struct AsyncValuesTests { | |
@Test func test_next() async throws { | |
let asyncValues = AsyncValues(of: Void.self) { _ in | |
Void() | |
} | |
var iterator = asyncValues.makeAsyncIterator() | |
#expect(try await iterator.next() != nil) | |
} | |
@Test func test_nextTwice() async throws { | |
let asyncValues = AsyncValues(of: Void.self) { _ in | |
Void() | |
} | |
var iterator = asyncValues.makeAsyncIterator() | |
#expect(try await iterator.next() != nil) | |
#expect(try await iterator.next() != nil) | |
} | |
@Test func test_oneValue() async throws { | |
let asyncValues = AsyncValues(of: Void.self) { _ in | |
Void() | |
} | |
var count = 0 | |
for try await _ in asyncValues { | |
if count > 0 { | |
return | |
} else { | |
count += 1 | |
} | |
} | |
} | |
@Test func test_twoValues() async throws { | |
let asyncValues = AsyncValues(of: Void.self) { _ in | |
Void() | |
} | |
var count = 0 | |
for try await _ in asyncValues { | |
if count > 1 { | |
return | |
} else { | |
count += 1 | |
} | |
} | |
} | |
@Test func test_valuesEveryHalfSecond() async throws { | |
let asyncValues = AsyncValues(of: Void.self, every: 0.5) { _ in | |
Void() | |
} | |
var count = 0 | |
for try await _ in asyncValues { | |
if count > 1 { | |
return | |
} else { | |
count += 1 | |
} | |
} | |
} | |
@Test func test_valuesEveryHalfSecondForOneSecond() async throws { | |
let count = try await withThrowingTaskGroup(of: Int.self) { group in | |
group.addTask { | |
let asyncValues = AsyncValues(of: Void.self, every: 0.5) { _ in | |
Void() | |
} | |
var count = 0 | |
for try await _ in asyncValues { | |
count += 1 | |
} | |
return count | |
} | |
group.addTask { | |
try await Task.sleep(for: .seconds(1.1)) | |
return 0 | |
} | |
_ = try await group.next() | |
group.cancelAll() | |
return try await group.next() | |
} | |
#expect(count == 3) | |
} | |
@Test func test_stringsEveryHalfSecondForOneSecond() async throws { | |
struct CounterContext { | |
var count: Int | |
} | |
let values = try await withThrowingTaskGroup(of: [String].self) { group in | |
group.addTask { | |
let asyncValues = AsyncValues( | |
of: String.self, | |
every: 0.5, | |
initialContext: CounterContext(count: 0) | |
) { context in | |
defer { context.count += 1 } | |
return "Hello \(context.count)!" | |
} | |
var values: [String] = [] | |
for try await value in asyncValues { | |
values.append(value) | |
} | |
return values | |
} | |
group.addTask { | |
try await Task.sleep(for: .seconds(1.1)) | |
return [] | |
} | |
_ = try await group.next() | |
group.cancelAll() | |
return try await group.next() | |
} | |
#expect(values == ["Hello 0!", "Hello 1!", "Hello 2!"]) | |
} | |
@Test func test_valuesEveryHalfSecondForOneSecondNotAskingForFirstValueImmediately() async throws { | |
let count = try await withThrowingTaskGroup(of: Int.self) { group in | |
group.addTask { | |
let asyncValues = AsyncValues( | |
of: Void.self, | |
every: 0.5, | |
askForFirstValueImmediately: false | |
) { _ in | |
Void() | |
} | |
var count = 0 | |
for try await _ in asyncValues { | |
count += 1 | |
} | |
return count | |
} | |
group.addTask { | |
try await Task.sleep(for: .seconds(1.1)) | |
return 0 | |
} | |
_ = try await group.next() | |
group.cancelAll() | |
return try await group.next() | |
} | |
#expect(count == 2) | |
} | |
@Test func test_stringsEveryHalfSecondForOneSecondNotAskingForFirstValueImmediately() async throws { | |
struct CounterContext { | |
var count: Int | |
} | |
let values = try await withThrowingTaskGroup(of: [String].self) { group in | |
group.addTask { | |
let asyncValues = AsyncValues( | |
of: String.self, | |
every: 0.5, | |
askForFirstValueImmediately: false, | |
initialContext: CounterContext(count: 0) | |
) { context in | |
defer { context.count += 1 } | |
return "Hello \(context.count)!" | |
} | |
var values: [String] = [] | |
for try await value in asyncValues { | |
values.append(value) | |
} | |
return values | |
} | |
group.addTask { | |
try await Task.sleep(for: .seconds(1.1)) | |
return [] | |
} | |
_ = try await group.next() | |
group.cancelAll() | |
return try await group.next() | |
} | |
#expect(values == ["Hello 0!", "Hello 1!"]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment