Skip to content

Commit 2b26846

Browse files
cthieleniCharlesHu
andauthored
Update concurrency-safe notifications proposal (v4) (#1295)
* Update concurrency-safe notifications proposal (v4) * Update status for SF-0011 for second review --------- Co-authored-by: Charles Hu <[email protected]>
1 parent 5164280 commit 2b26846

File tree

1 file changed

+136
-16
lines changed

1 file changed

+136
-16
lines changed

Proposals/0011-concurrency-safe-notifications.md

+136-16
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
* Proposal: SF-0011
44
* Author(s): [Philippe Hausler](https://github.com/phausler), [Christopher Thielen](https://github.com/cthielen)
55
* Review Manager: [Charles Hu](https://github.com/iCharlesHu)
6-
* Status: **Accepted**
6+
* Status: **2nd Review** May. 15, 2025 ... May. 22, 2025
77

88
## Revision history
99

1010
* **v1** Initial version
1111
* **v2** Remove `static` from `NotificationCenter.Message.isolation` to better support actor instances
1212
* **v3** Remove generic isolation pattern in favor of dedicated `MainActorMessage` and `AsyncMessage` types. Apply SE-0299-style static member lookups for `addObserver()`. Provide default value for `Message.name`.
13+
* **v4** Add `AsyncSequence` APIs for observing. Expand `Message.Subject` conformance to take either `AnyObject` or `Identifiable` where `Identifiable.ID == ObjectIdentifier`. Document `ObservationToken` automatic de-registration behavior. Drop `with` label on `post()` methods in favor of `subject` for clarity.
1314

1415
## Introduction
1516

@@ -162,7 +163,7 @@ And it could be posted using:
162163
```swift
163164
NotificationCenter.default.post(
164165
NSWorkspace.WillLaunchApplication(application: launchedApplication),
165-
with: workspace
166+
subject: workspace
166167
)
167168
```
168169

@@ -176,7 +177,7 @@ The `NotificationCenter.Message` protocol acts as a base for `NotificationCenter
176177
@available(FoundationPreview 0.5, *)
177178
extension NotificationCenter {
178179
public protocol Message {
179-
associatedtype Subject: AnyObject
180+
associatedtype Subject
180181
static var name: Notification.Name { get }
181182

182183
static func makeMessage(_ notification: Notification) -> Self?
@@ -194,10 +195,14 @@ The protocol specifies `makeMessage(:Notification)` and `makeNotification(:Self)
194195

195196
For `Message` types that do not need to interoperate with existing `Notification` uses, the `name` property does not need to be specified, and will default to the fully qualified name of the `Message` type, e.g. `MyModule.MyMessage`. Note that when using this default, renaming the type or relocating it to another module has a similar effect as changing ABI, as any code that was compiled separately will not be aware of the name change until recompiled. Developers can control this effect by explicitly setting the `name` property if needed.
196197

198+
Each `Message` specifies a specific *subject* variable or metatype to observe, similar to the existing `Notification.object`, e.g. an `NSWindow` instance or the `NSWindow.self` metatype. `Message.Subject` has no conformance requirements in its protocol, but `addObserver()` and `post()` both refine `Message.Subject` to either conform to `AnyObject` or confirm to `Identifiable` where `Identifiable.ID == ObjectIdentifier`.
199+
197200
### Observing messages
198201

199202
Observing messages can be done with new overloads to `addObserver`. Clients do not need to know whether a message conforms to `MainActorMessage` or `AsyncMessage`.
200203

204+
Overloads are provided both for `Message.Subject: AnyObject` and `Message.Subject: Identifiable where ID == ObjectIdentifier`. This allows the observation of both reference types and value types which can provide an `ObjectIdentifier`.
205+
201206
For `MainActorMessage`:
202207

203208
```swift
@@ -207,19 +212,33 @@ extension NotificationCenter {
207212
public func addObserver<I: MessageIdentifier, M: MainActorMessage>(of subject: M.Subject,
208213
for identifier: I,
209214
using observer: @escaping @MainActor (M) -> Void)
210-
-> ObservationToken where I.MessageType == M
215+
-> ObservationToken where I.MessageType == M,
216+
M.Subject: AnyObject
217+
218+
public func addObserver<I: MessageIdentifier, M: MainActorMessage>(of subject: M.Subject,
219+
for identifier: I,
220+
using observer: @escaping @MainActor (M) -> Void)
221+
-> ObservationToken where I.MessageType == M,
222+
M.Subject: Identifiable,
223+
M.Subject.ID == ObjectIdentifier
211224

212225
// e.g. addObserver(of: NSWorkspace.self, for: .willLaunchApplication) { message in ... }
213226
public func addObserver<I: MessageIdentifier, M: MainActorMessage>(of subject: M.Subject.Type,
214227
for identifier: I,
215228
using observer: @escaping @MainActor (M) -> Void)
216229
-> ObservationToken where I.MessageType == M
217230

218-
// e.g. addObserver(NSWorkspace.WillLaunchApplication.self) { message in ... }
219-
public func addObserver<M: MainActorMessage>(_ messageType: M.Type,
220-
subject: M.Subject? = nil,
231+
// e.g. addObserver(for: NSWorkspace.WillLaunchApplication.self) { message in ... }
232+
public func addObserver<M: MainActorMessage>(of subject: M.Subject? = nil,
233+
for messageType: M.Type,
234+
using observer: @escaping @MainActor (M) -> Void)
235+
-> ObservationToken where M.Subject: AnyObject
236+
237+
public func addObserver<M: MainActorMessage>(of subject: M.Subject? = nil,
238+
for messageType: M.Type,
221239
using observer: @escaping @MainActor (M) -> Void)
222-
-> ObservationToken
240+
-> ObservationToken where M.Subject: Identifiable,
241+
M.Subject.ID == ObjectIdentifier
223242
}
224243
```
225244

@@ -231,17 +250,31 @@ extension NotificationCenter {
231250
public func addObserver<I: MessageIdentifier, M: AsyncMessage>(of subject: M.Subject,
232251
for identifier: I,
233252
using observer: @escaping @Sendable (M) async -> Void)
234-
-> ObservationToken where I.MessageType == M
253+
-> ObservationToken where I.MessageType == M,
254+
M.Subject: AnyObject
255+
256+
public func addObserver<I: MessageIdentifier, M: AsyncMessage>(of subject: M.Subject,
257+
for identifier: I,
258+
using observer: @escaping @Sendable (M) async -> Void)
259+
-> ObservationToken where I.MessageType == M,
260+
M.Subject: Identifiable,
261+
M.Subject.ID == ObjectIdentifier
235262

236263
public func addObserver<I: MessageIdentifier, M: AsyncMessage>(of subject: M.Subject.Type,
237264
for identifier: I,
238265
using observer: @escaping @Sendable (M) async -> Void)
239266
-> ObservationToken where I.MessageType == M
240267

241-
public func addObserver<M: AsyncMessage>(_ messageType: M.Type,
242-
subject: M.Subject? = nil,
268+
public func addObserver<M: AsyncMessage>(of subject: M.Subject? = nil,
269+
for messageType: M.Type,
243270
using observer: @escaping @Sendable (M) async -> Void)
244-
-> ObservationToken
271+
-> ObservationToken where M.Subject: AnyObject
272+
273+
public func addObserver<M: AsyncMessage>(of subject: M.Subject? = nil,
274+
for messageType: M.Type,
275+
using observer: @escaping @Sendable (M) async -> Void)
276+
-> ObservationToken where M.Subject: Identifiable,
277+
M.Subject.ID == ObjectIdentifier
245278
}
246279
```
247280

@@ -258,15 +291,102 @@ extension NotificationCenter {
258291
}
259292
```
260293

294+
When an `ObservationToken` goes out of scope, the corresponding observer will be removed from its center automatically if it is still registered. This behavior helps prevent memory leaks from tokens which are accidentally dropped by the user.
295+
296+
Messages conforming to `AsyncMessage` can also be observed using a set of `AsyncSequence`-conforming APIs, similar to the existing `notifications(named:object:)` method:
297+
298+
```swift
299+
@available(macOS 16, iOS 19, tvOS 19, watchOS 12, visionOS 3, *)
300+
extension NotificationCenter {
301+
public func messages<Identifier: MessageIdentifier, Message: AsyncMessage>(
302+
of subject: Message.Subject,
303+
for identifier: Identifier,
304+
bufferSize limit: Int = 10
305+
)
306+
-> some AsyncSequence<Message, Never> where Identifier.MessageType == Message,
307+
Message.Subject: AnyObject
308+
309+
public func messages<Identifier: MessageIdentifier, Message: AsyncMessage>(
310+
of subject: Message.Subject,
311+
for identifier: Identifier,
312+
bufferSize limit: Int = 10
313+
)
314+
-> some AsyncSequence<Message, Never> where Identifier.MessageType == Message,
315+
Message.Subject: Identifiable,
316+
Message.Subject.ID == ObjectIdentifier {}
317+
318+
public func messages<Identifier: MessageIdentifier, Message: AsyncMessage>(
319+
of subject: Message.Subject.Type,
320+
for identifier: Identifier,
321+
bufferSize limit: Int = 10
322+
)
323+
-> some AsyncSequence<Message, Never> where Identifier.MessageType == Message
324+
325+
public func messages<Message: AsyncMessage>(
326+
of subject: Message.Subject? = nil,
327+
for messageType: Message.Type,
328+
bufferSize limit: Int = 10
329+
)
330+
-> some AsyncSequence<Message, Never> where Message.Subject: AnyObject
331+
332+
public func messages<Message: AsyncMessage>(
333+
of subject: Message.Subject? = nil,
334+
for messageType: Message.Type,
335+
bufferSize limit: Int = 10
336+
)
337+
-> some AsyncSequence<Message, Never> where Message.Subject: Identifiable,
338+
Message.Subject.ID == ObjectIdentifier
339+
}
340+
```
341+
342+
These allow for the familiar `for await in` syntax:
343+
344+
```swift
345+
for await message in center.messages(of: anObject, for: .anAsyncMessage) {
346+
// ...
347+
}
348+
349+
for await message in center.messages(for: AnAsyncMessage.self) {
350+
// ...
351+
}
352+
353+
// etc.
354+
```
355+
356+
The `messages()` sequence uses a reasonably-sized buffer to reduce the likelihood of dropped messages caused by the interaction of synchronous and asynchronous code. When a `Message` is dropped, the implementation will log to aid in debugging. Message frequency in practice is typically 0-2x / second / message type and therefore unlikely to result in dropped messages. Certain UI-related messages can post in practice as often as 40 - 50x / second / message, but these are typically `MainActorMessage` and would not be subject to dropping nor available for use with `messages()`.
357+
261358
### Posting messages
262359

263360
Posting messages can be done with new overloads on the existing `post` method:
264361

265362
```swift
266363
@available(FoundationPreview 0.5, *)
267364
extension NotificationCenter {
268-
public func post<M: Message>(_ message: M, with subject: M.Subject)
269-
public func post<M: Message>(_ message: M, with subject: M.Subject.Type)
365+
366+
// MainActorMessage post()
367+
368+
@MainActor
369+
public func post<M: MainActorMessage>(_ message: M, subject: M.Subject)
370+
where M.Subject: AnyObject
371+
372+
@MainActor
373+
public func post<M: MainActorMessage>(_ message: M, subject: M.Subject)
374+
where M.Subject: Identifiable,
375+
M.Subject.ID == ObjectIdentifier
376+
377+
@MainActor
378+
public func post<M: MainActorMessage>(_ message: M, subject: M.Subject.Type = M.Subject.self)
379+
380+
// AsyncMessage post()
381+
382+
public func post<M: AsyncMessage>(_ message: M, subject: M.Subject)
383+
where M.Subject: AnyObject
384+
385+
public func post<M: AsyncMessage>(_ message: M, subject: M.Subject)
386+
where M.Subject: Identifiable,
387+
M.Subject.ID == ObjectIdentifier
388+
389+
public func post<M: AsyncMessage>(_ message: M, subject: M.Subject.Type = M.Subject.self)
270390
}
271391
```
272392

@@ -289,7 +409,7 @@ struct EventDidOccur: NotificationCenter.Message {
289409
}
290410

291411
static func makeNotification(_ message: Self) -> Notification {
292-
return Notification(name: Self.name, userInfo: ["foo": self.foo])
412+
return Notification(name: Self.name, object: object, userInfo: ["foo": self.foo])
293413
}
294414
}
295415
```
@@ -376,7 +496,7 @@ We could alternatively ferry `subject` in both the observer closure and `post()`
376496
center.addObserver(of: someSubject, for: .someMessage) { message, subject in ... }
377497

378498
// Nor post() ...
379-
center.post(SomeMessage(), with: someSubject)
499+
center.post(SomeMessage(), subject: someSubject)
380500
```
381501

382502
However, not all messages have subject instances (e.g. `addObserver(of: NSWindow.self, for: .willMove)`). While `post()` could take a default parameter for an optional `subject`, the `addObserver()` closure would always have to specify a `subject` parameter even for messages without subject instances.

0 commit comments

Comments
 (0)