In a variation of Cy-4AH’s answer (subsequently deleted), which uses an actor
for synchronization, I would make sure to add an onTermination
handler to remove the associated continuation if the asynchronous sequence was canceled. E.g.:
actor Notifier<Output> {
private var continuations: [UUID: AsyncStream<Output>.Continuation] = [:]
func values() -> AsyncStream<Output> {
AsyncStream { continuation in
let id = UUID()
continuations[id] = continuation
continuation.onTermination = { _ in
Task { await self.cancel(id) }
}
}
}
func send(_ value: Output) {
for continuation in continuations.values {
continuation.yield(value)
}
}
}
private extension Notifier {
func cancel(_ id: UUID) {
continuations[id] = nil
}
}
There are tons of variations on the theme, but the details of the implementation matter less than the general observations of (a) the use of an actor
; and (b) the use of the onTermination
handler to clean up in case the notifier object might outlive the individual sequences.
FWIW, if I really wanted to create a singleton for String
notifications:
final class StringNotifier: Sendable {
static let shared = StringNotifier()
private init() { }
private let notifier = Notifier<String>()
func values() async -> AsyncStream<String> {
await notifier.values()
}
func send(_ value: String) {
Task { await notifier.send(value) }
}
}
As an aside, I generally prefer to use AsyncChannel
for these sorts of “subject” like behaviors, but a single channel does not allow multiple observers and if you try to have a collection of these channels, it does not (currently) provide the required onTermination
-like handler.
FWIW, if you were to use Combine PassthroughSubject
, it might look like:
actor CombineNotifier<Output> {
private let subject = PassthroughSubject<Output, Never>()
private var cancellables: [UUID: AnyCancellable] = [:]
func values() -> AsyncStream<Output> {
AsyncStream { continuation in
let id = UUID()
cancellables[id] = subject.sink { _ in
continuation.finish()
} receiveValue: { value in
continuation.yield(value)
}
continuation.onTermination = { _ in
Task { await self.cancel(id) }
}
}
}
func send(_ value: Output) {
subject.send(value)
}
}
private extension CombineNotifier {
func cancel(_ id: UUID) {
cancellables[id] = nil
}
}
Again, it is an actor
to provide thread-safe interaction and uses onTermination
to clean up individual sequences.