Why is AsyncSequence<Car, Never> not Sendable when Car is Sendable?
How can I make this work?
Code
import AsyncAlgorithms
struct Car: Sendable {
init(notification: Notification) {}
}
let testNotificationName = Notification.Name(rawValue: "Test")
let notifications = NotificationCenter.default.notifications(named: testNotificationName)
// Conformance of 'Notification' to 'Sendable' is unavailable in iOS
let cars: some AsyncSequence<Car, Never> & Sendable = notifications.map { Car(notification: $0) }
// How can I make cars Sendable?
for await car in cars.debounce(for: .seconds(2)) {
}
Notifications isn't Sendable because Notification isn't Sendable.
AsyncMapSequence<Notifications, Car> isn't Sendable because it contains a Notifications, which isn't Sendable.
Technically, there's "nothing" you can do about this.
If you can use the latest & greatest stuffs, this year's OSes seem to have support for SF-0011 which has some new stuff for dealing with notifications in concurrency-safe ways.
If you don't care about the Notification itself, you can reimplement Notifications easily enough to be Sendable and emit any Sendable data you can extract from the Notification.
My real world scenario is I was observing some notifications posted by Apple framework. Those specific notifications have Async message from OS 27 onwards.
Question
For framework notifications which are unavailable on current OS version, is it ok to fall back to my Combine implementation?
Bridging Combine and AsyncSequence is not a trivial task. Claude & Co. might help you, but be prepared for wasting tokens on this.
Above that, AsyncMessage is available from iOS26. You can always implement your own AsyncMessage conformance in order to retroactively subscribe to notifications that don't come with an AsyncMessage implementation by default.
For this, implement makeMessage and ensure the notification name matches.
Manually implementing the Notifications stream would just be wrapping the closure-based NotificationCenter API with something like AsyncStream and an immediate map closure that transforms the Notification before it enters the stream.
Which is the approach taken here, but since debounce requires both the parent sequence and the value to be Sendable, mapping out to a custom type doesn't actually help. The mapping needs to be done before the AsyncSequence is created in the first place, so all of them have the opportunity to be Sendable before they reach debounce.
If testNotificationName notifications are always sent on the main queue, that's fine (but your .receive(on:) is redundant. If they're not, then you're sending their Notification from wherever they're posted to the main queue, which is a bug.