diff --git a/Sources/DevFoundation/Utility Types/CurrentValueMulticaster.swift b/Sources/DevFoundation/Utility Types/CurrentValueMulticaster.swift index b4b2706..088d6fd 100644 --- a/Sources/DevFoundation/Utility Types/CurrentValueMulticaster.swift +++ b/Sources/DevFoundation/Utility Types/CurrentValueMulticaster.swift @@ -101,9 +101,10 @@ public final class CurrentValueMulticaster: Sendable where Element: Sen /// Returns a sequence that emits the current value immediately, then every subsequent update. /// - /// Each call returns an independent sequence with its own buffer, governed by the multicaster’s buffering policy. - /// The sequence finishes when the consumer’s task is cancelled or when the multicaster is deallocated. - public func values() -> some AsyncSequence & Sendable { + /// Each call returns an independent ``Values`` sequence with its own buffer, governed by the multicaster’s + /// buffering policy. The sequence finishes when the consumer’s task is cancelled or when the multicaster is + /// deallocated. + public func values() -> Values { let id = UUID() // `makeStream` is used rather than the closure-based `AsyncStream` initializer on purpose: that initializer’s @@ -125,7 +126,37 @@ public final class CurrentValueMulticaster: Sendable where Element: Sen state.continuations[id] = continuation } - return stream + return Values(stream: stream) + } +} + + +extension CurrentValueMulticaster { + /// An async sequence of a multicaster’s current value followed by its subsequent updates. + /// + /// Create instances with ``CurrentValueMulticaster/values()``. Each instance is independent, with its own buffer + /// governed by the multicaster’s buffering policy, and finishes when the consumer’s task is cancelled or when the + /// multicaster is deallocated. + public struct Values: AsyncSequence, Sendable { + /// The async iterator for a ``Values`` sequence. + public struct AsyncIterator: AsyncIteratorProtocol { + /// The iterator of the underlying stream. + fileprivate var base: AsyncStream.Iterator + + + public mutating func next() async -> Element? { + await base.next() + } + } + + + /// The underlying stream that the sequence vends values from. + fileprivate let stream: AsyncStream + + + public func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(base: stream.makeAsyncIterator()) + } } } diff --git a/Tests/DevFoundationTests/Utility Types/CurrentValueMulticasterTests.swift b/Tests/DevFoundationTests/Utility Types/CurrentValueMulticasterTests.swift index 5e85be1..b505818 100644 --- a/Tests/DevFoundationTests/Utility Types/CurrentValueMulticasterTests.swift +++ b/Tests/DevFoundationTests/Utility Types/CurrentValueMulticasterTests.swift @@ -43,6 +43,26 @@ struct CurrentValueMulticasterTests: RandomValueGenerating { // MARK: - values() + @Test + mutating func valuesReturnsNameablePublicSequenceType() async { + // set up a multicaster with a random initial value + let initialValue = randomInt(in: .min ... .max) + let multicaster = CurrentValueMulticaster(initialValue) + + // exercise by storing the returned sequence using its explicit, public type + let values: CurrentValueMulticaster.Values = multicaster.values() + + // expect the named type behaves as an async sequence emitting the current value + var firstValue: Int? + for await value in values { + firstValue = value + break + } + + #expect(firstValue == initialValue) + } + + @Test mutating func valuesEmitsCurrentValueOnSubscribe() async { // set up a multicaster with a random initial value