I expected NotificationCenter.MainActorMessage conforming types to be post-able from anywhere and auto-isolate delivery to the main actor. Instead, I get a EXC_BREAKPOINT when this is invoked from a background queue.

If it were required to post on the main actor, the NotificationCenter.post message surely would be MainActor-isolation annotated, wouldn't it?

I wonder what goes on there.

Good news everyone --

-- the issue was the context! The notification was posted from a closure (!) rather far down and it wasn't isolated or sendable, so the compiler didn't complain.

Was an easy fix once I was made aware that this should not happen because post(_:) is isolated to main actor, in fact!

https://github.com/swiftlang/swift-foundation/blob/4a6ef727b5cda1d42c9f56e38adf5b1005826103/Sources/FoundationEssentials/NotificationCenter/MainActorMessage.swift#L196

swift-foundation/Sources/FoundationEssentials/NotificationCenter/MainActorMessage.swift at 4a6ef727b5cda1d42c9f56e38adf5b1005826103 · swiftlang/swift-foundation

The Foundation project. Contribute to swiftlang/swift-foundation development by creating an account on GitHub.

GitHub
@ctietze That’s so bizarre. I love the idea of the Notification Center strongly typed messages, but yeah, I feel like it should be fire from anywhere and forget, or force you to call from the right isolation for specifically isolation messages…
@rhysmorgan @ctietze I have not had the chance to play with this API and now I’m curious!

@mattiem @ctietze I recall playing around with it a while ago and having some weird issues with it, especially around the MainActorMessage variant. https://mastodon.social/@rhysmorgan/115488269582590293

But that's a side-issue to the runtime crash/breakpoint weirdness!

@rhysmorgan @ctietze hmm.

Ok so I *think* a problem here is notifications need to support synchronous delivery. There seems to be a separate api for async notifications.

@mattiem @rhysmorgan @ctietze as long as I can remember, people seem to be surprised that NotificationCenter is not a queue. It has always been the case that posting a notification processes all observers before returning. NotificationQueue exists when you need queuing and coalescing behavior, but I’ve never found it that useful. (It is RunLoop based, not GCD.)

Frustrating that Swift can only catch mismatches at runtime rather than with types though. That would have been nice.

@cocoaphony @mattiem @rhysmorgan @ctietze my favorite NotificationCenter footgun is that when you use the closure-based version of registration, you MUST hang on to the returned token and manually unregister it later. Otherwise it stays around forever and leaks anything it strongly references.

@natep @cocoaphony @mattiem @rhysmorgan 'favorite' yeah; I introduce @ole's token in every project early on for this. https://oleb.net/blog/2018/01/notificationcenter-removeobserver/>

ObservationToken is a great change. Took a while(

Do you have to manually unregister block-based NotificationCenter observers?

tl;dr: yes.

Ole Begemann
@ctietze @natep @cocoaphony @mattiem @ole To be fair, I think Combine fixed this too, with its Cancellable type. Made it much, much nicer to use NotificationCenter!

@rhysmorgan @ctietze @natep @mattiem @ole Much as I’ve tried to reduce the amount of Combine we use, it really has been the best interface to NotificationCenter.

But we have been moving towards a structured concurrency solution, using a method called .runUntilCancelled() that is responsible for observing things in a TaskGroup with for-async loops. I really am liking this approach (but it is a slightly different way of thinking of your object lifetimes).

@cocoaphony @ctietze @natep @mattiem @ole Agreed. There’s a couple of use cases it is still just the best tool for. There’s still, as far as I’m aware, no equivalent to CurrentValueSubject in the AsyncSequence world?

@cocoaphony @rhysmorgan @natep @mattiem @ole Can you share more about that? It sounds like you end up with a pure side-effect for which you keep the lifetime with .runUntilCancelled (the name implies that IMO) -- not unlike Cancellables or RxSwift.Disposables are eventually just lifetime-tracking tokens.

But that also means the actual 'stuff' happens on the inside, and my gut feeling (which is not refined) was that async streams offer a way to not have such an opaque lifetime tracker

@ctietze @rhysmorgan @natep @mattiem @ole

No, there's no token. We tie it to Views generally. Or it can live within a single top-level Task that contains a TaskGroup of services. The power of using `.task` is that it is automatically cancelled when the View goes away.

https://mastodon.social/@cocoaphony/115998829003446439

@ctietze @rhysmorgan @natep @mattiem @ole Here's something I'm doing in a CLI program I'm working on using structured concurrency. I think it works out very nicely.

https://gist.github.com/rnapier/0159168cb44adfdc3f798c2da9d661f4

Structured Concurrency in CLI

Structured Concurrency in CLI. GitHub Gist: instantly share code, notes, and snippets.

Gist

@cocoaphony oh this is really nice, thank you for sharing! It's not a token, it's a bit more complex -- a Task -- but it's still like a Cancellable in a way, because that's all you can do with the task handle later (hand waving a bit)

If you were selling something, you would've sold me with the signal handler setup. That is great. Also love the rest, it's understandable!

@cocoaphony @mattiem @rhysmorgan MainActorMessage works well if you make sure you call it with the correct compiler settings or pay close attention to the call site -- I was posting from a Swift 5 background queue callback and the compiler then couldn't help until I tightened the screws a bit.
https://mastodon.social/@ctietze/116007713848932405

Much better now, works correctly 👍

@mattiem @ctietze Yeah, it just surprised me there wasn’t an equivalent API for MainActorMessage, as effectively they’re still just streams delivered over time. But I guess there’s no way to enforce consumption of that AsyncSequence on the MainActor? Not that I can immediately think of, at least.