Skip to content

Instantly share code, notes, and snippets.

@thomsmed
Last active February 16, 2025 13:54
Show Gist options
  • Save thomsmed/b06d50a189f6ed25e52cef166441d025 to your computer and use it in GitHub Desktop.
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.
//
// 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