diff --git a/CMakeLists.txt b/CMakeLists.txt index 92e6c5c68..2b293fafa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,7 +86,7 @@ else() message(STATUS "_SwiftCollections_SourceDIR not provided, checking out local copy of swift-collections") FetchContent_Declare(SwiftCollections GIT_REPOSITORY https://github.com/apple/swift-collections.git - GIT_TAG 1.1.2) + GIT_TAG 1.1.6) endif() FetchContent_MakeAvailable(SwiftFoundationICU SwiftCollections) diff --git a/Package.swift b/Package.swift index fa7801ae2..f3d70bdb3 100644 --- a/Package.swift +++ b/Package.swift @@ -110,6 +110,7 @@ let package = Package( "_FoundationCShims", "FoundationMacros", .product(name: "_RopeModule", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections"), .product(name: "OrderedCollections", package: "swift-collections"), ], exclude: [ @@ -128,7 +129,8 @@ let package = Package( "CMakeLists.txt", "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", - "URL/CMakeLists.txt" + "URL/CMakeLists.txt", + "NotificationCenter/CMakeLists.txt" ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index c25d493c6..a5a1e9c79 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -42,6 +42,7 @@ add_subdirectory(FileManager) add_subdirectory(Formatting) add_subdirectory(JSON) add_subdirectory(Locale) +add_subdirectory(NotificationCenter) add_subdirectory(Predicate) add_subdirectory(ProcessInfo) add_subdirectory(PropertyList) diff --git a/Sources/FoundationEssentials/NotificationCenter/ActorQueueManager.swift b/Sources/FoundationEssentials/NotificationCenter/ActorQueueManager.swift new file mode 100644 index 000000000..0dad1f0c7 --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/ActorQueueManager.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal class _NotificationCenterActorQueueManagerNSObjectWrapper: NSObject, @unchecked Sendable {} +#else +internal class _NotificationCenterActorQueueManagerNSObjectWrapper: @unchecked Sendable {} +#endif + +#if FOUNDATION_FRAMEWORK +@objc(_NotificationCenterActorQueueManager) +#endif +internal final class _NotificationCenterActorQueueManager: _NotificationCenterActorQueueManagerNSObjectWrapper, @unchecked Sendable { +#if !NO_FILESYSTEM + struct State { + var buffer = [@Sendable () async -> Void]() + var continuation: UnsafeContinuation<(@Sendable () async -> Void)?, Never>? + var isCancelled: Bool = false + + static func waitForWork(_ state: LockedState) async -> (@Sendable () async -> Void)? { + return await withTaskCancellationHandler { + return await withUnsafeContinuation { continuation in + let (work, resumeContinuation) = state.withLock { state -> ((@Sendable () async -> Void)?, Bool) in + if state.isCancelled { + return (nil, true) + } else { + if state.buffer.isEmpty { + assert(state.continuation == nil) + state.continuation = continuation + return (nil, false) + } else { + return (state.buffer.removeFirst(), true) + } + } + } + if resumeContinuation { + continuation.resume(returning: work) + } + } + } onCancel: { + state.withLock { state in + state.isCancelled = true + defer { + state.continuation = nil + } + return state.continuation + }?.resume(returning: nil) + } + } + } + + let state: LockedState + let workerTask: Task<(), Never> + + override init() { + state = LockedState(initialState: State()) + workerTask = Task.detached { [state] in + await withDiscardingTaskGroup { group in + while let work = await State.waitForWork(state) { + group.addTask(operation: work) + } + } + } + super.init() + } + + deinit { + workerTask.cancel() + } + + func enqueue(_ work: @escaping @Sendable () async -> Void) { + state.withLock { state in + state.buffer.append(work) + if let continuation = state.continuation { + state.continuation = nil + let item = state.buffer.removeFirst() + continuation.resume(returning: item) + } + } + } +#endif +} diff --git a/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift b/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift new file mode 100644 index 000000000..aedab64d0 --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift @@ -0,0 +1,200 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +internal import CollectionsInternal +#elseif canImport(DequeModule) +internal import DequeModule +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif +#if canImport(os) +internal import os.log +#endif + +@available(FoundationPreview 6.2, *) +extension NotificationCenter { + /// Returns an asynchronous sequence of messages produced by this center for a given subject and identifier. + /// - Parameters: + /// - subject: The subject to observe. Specify a metatype to observe all values for a given type. + /// - identifier: An identifier representing a specific message type. + /// - limit: The maximum number of messages allowed to buffer. + /// - Returns: An asynchronous sequence of messages produced by this center. + public func messages( + of subject: Message.Subject, + for identifier: Identifier, + bufferSize limit: Int = 10 + ) -> some AsyncSequence where Identifier.MessageType == Message, Message.Subject: AnyObject { + return AsyncMessageSequence(self, subject, limit) + } + + /// Returns an asynchronous sequence of messages produced by this center for a given subject type and identifier. + /// - Parameters: + /// - subject: The metatype to observe all values for a given type. + /// - identifier: An identifier representing a specific message type. + /// - limit: The maximum number of messages allowed to buffer. + /// - Returns: An asynchronous sequence of messages produced by this center. + public func messages( + of subject: Message.Subject.Type, + for identifier: Identifier, + bufferSize limit: Int = 10 + ) -> some AsyncSequence where Identifier.MessageType == Message { + return AsyncMessageSequence(self, nil, limit) + } + + /// Returns an asynchronous sequence of messages produced by this center for a given subject and message type. + /// - Parameters: + /// - subject: The subject to observe. Specify a metatype to observe all values for a given type. + /// - messageType: The message type to be observed. + /// - limit: The maximum number of messages allowed to buffer. + /// - Returns: An asynchronous sequence of messages produced by this center. + public func messages( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + bufferSize limit: Int = 10 + ) -> some AsyncSequence where Message.Subject: AnyObject { + return AsyncMessageSequence(self, subject, limit) + } +} + +extension NotificationCenter { + fileprivate struct AsyncMessageSequence: AsyncSequence, Sendable { + let center: NotificationCenter + nonisolated(unsafe) weak var object: AnyObject? + let bufferSize: Int + + init(_ center: NotificationCenter, _ object: AnyObject?, _ bufferSize: Int) { + self.center = center + self.object = object + self.bufferSize = bufferSize + } + + func makeAsyncIterator() -> AsyncMessageSequenceIterator { + return AsyncMessageSequenceIterator(center: center, object: object, bufferSize: bufferSize) + } + } +} + +extension NotificationCenter { + fileprivate final class AsyncMessageSequenceIterator: AsyncIteratorProtocol, Sendable { + typealias Element = Message + typealias Failure = Never + + struct State { + var observer: NotificationCenter.ObservationToken? + var continuations: [UnsafeContinuation] = [] + var buffer = Deque(minimumCapacity: 1) + let bufferSize: Int + } + + struct Resumption { + let message: Message? + let continuations: [UnsafeContinuation] + + init(message: Message?, continuation: UnsafeContinuation) { + self.message = message + self.continuations = [continuation] + } + + init(cancelling: [UnsafeContinuation]) { + self.message = nil + self.continuations = cancelling + } + + func resume() { + for continuation in continuations { + continuation.resume(returning: message) + } + } + } + + let state: LockedState + + init(center: NotificationCenter, object: AnyObject?, bufferSize: Int) { + self.state = LockedState(initialState: State(bufferSize: bufferSize)) + +#if FOUNDATION_FRAMEWORK + let observerBlock: @Sendable (Notification) -> Void = { [weak self] notification in + guard let message: Message = NotificationCenter._messageFromNotification(notification) else { return } + + self?.observationCallback(message) + } +#else + let observerBlock: @Sendable (Message) -> Void = { [weak self] message in + self?.observationCallback(message) + } +#endif + + let token = center._addObserver(Message.name, object: object, using: observerBlock) + + self.state.withLock { _state in + _state.observer = ObservationToken(center: center, token: token) + } + } + + deinit { + teardown() + } + + func teardown() { + let (observer, resumption) = state.withLock { _state -> (NotificationCenter.ObservationToken?, Resumption) in + let observer = _state.observer + _state.observer = nil + _state.buffer.removeAll(keepingCapacity: false) + defer { _state.continuations.removeAll(keepingCapacity: false) } + return (observer, Resumption(cancelling: _state.continuations)) + } + + resumption.resume() + + if let observer { + observer.remove() + } + } + + func observationCallback(_ message: Message) { + state.withLock { _state -> Resumption? in + if _state.buffer.count + 1 > _state.bufferSize { + _state.buffer.removeFirst() +#if canImport(os) + NotificationCenter.logger.fault("Notification center message dropped due to buffer limit. Check sequence iterator frequently or increase buffer size. Message: \(String(describing: Message.self))") +#endif + } + _state.buffer.append(message) + + if _state.continuations.isEmpty { + return nil + } else { + return Resumption(message: _state.buffer.removeFirst(), continuation: _state.continuations.removeFirst()) + } + }?.resume() + } + + func next() async -> Message? { + await withTaskCancellationHandler { + return await withUnsafeContinuation { (continuation: UnsafeContinuation) in + state.withLock { _state -> Resumption? in + _state.continuations.append(continuation) + if _state.buffer.isEmpty { + return nil + } else { + return Resumption(message: _state.buffer.removeFirst(), continuation: _state.continuations.removeFirst()) + } + }?.resume() + } + } onCancel: { + teardown() + } + } + } +} diff --git a/Sources/FoundationEssentials/NotificationCenter/AsyncMessage.swift b/Sources/FoundationEssentials/NotificationCenter/AsyncMessage.swift new file mode 100644 index 000000000..5ba4baa77 --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/AsyncMessage.swift @@ -0,0 +1,261 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +#endif +#if canImport(os) +internal import os.log +#endif + +@available(FoundationPreview 6.2, *) +extension NotificationCenter { + /// A protocol for creating types that you can post to a notification center, which posts them to an arbitrary isolation. + /// + /// You post types conforming to `AsyncMessage` to a notification center using `post(_:subject:)` and observe them with `addObserver(of:for:using:)`. + /// + /// The notification center delivers `AsyncMessage` types asynchronously when posted. Asynchronous delivery isn't suitable + /// for messages with time-critical deliveries, such as a message that must have its observers called before a certain + /// action takes place. + /// + /// For types that post on the main actor, use ``MainActorMessage``. + /// + /// Each `AsyncMessage` is associated with a specific `Subject` type. + /// + /// For example, an `AsyncMessage` associated with the type `Event` could use the following declaration: + /// + /// ```swift + /// struct EventDidStart: NotificationCenter.AsyncMessage { + /// typealias Subject = Event + /// } + /// ``` + /// + /// `AsyncMessage` can use an optional ``MessageIdentifier`` type for context-aware observer registration: + /// + /// ```swift + /// extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + /// static var didStart: Self { .init() } + /// } + /// ``` + /// + /// With this identifier, observers can receive information about a specific instance by registering for this message with a ``NotificationCenter``: + /// + /// ```swift + /// let observerToken = NotificationCenter.default.addObserver(of: importantEvent, for: .didStart) + /// ```` + /// + /// Or an observer can receive information about any instance with: + /// + /// ```swift + /// let observerToken = NotificationCenter.default.addObserver(of: Event.self, for: .didStart) + /// ``` + /// + /// The notification center ties observation the lifetime of the returned ``NotificationCenter/ObservationToken`` and automatically de-registers the observer if the token + /// goes out of scope. You can also remove observation explicitly: + /// + /// ```swift + /// NotificationCenter.default.removeObserver(observerToken) + /// ``` + /// + /// ### Notification Interoperability + /// + /// `AsyncMessage` includes optional interoperability with ``Notification``, enabling posters and observers of both types + /// to pass information. + /// + /// It does this by offering a ``makeMessage(_:)`` method that collects values from a ``Notification``'s ``Notification/userInfo`` and populates properties on a new message. + /// In the other direction, a ``makeNotification(_:)`` method collects the message's defined properties and loads them into a new notification's ``Notification/userInfo`` dictionary. + /// + /// For example, if there exists a ``Notification`` posted on an arbitrary isolation identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo`` + /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``AsyncMessage``: + /// + /// ```swift + /// struct EventDidFinish: NotificationCenter.AsyncMessage { + /// typealias Subject = Event + /// static var name: Notification.Name { Notification.Name("eventDidFinish") } + /// + /// var duration: Int + /// + /// static func makeNotification(_ message: Self) -> Notification { + /// return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)]) + /// } + /// + /// static func makeMessage(_ notification: Notification) -> Self? { + /// guard let userInfo = notification.userInfo, + /// let duration = userInfo["duration"] as? Int + /// else { + /// return nil + /// } + /// + /// return Self(duration: duration) + /// } + /// } + /// ``` + /// + /// With this definition, an observer for this `AsyncMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa. + public protocol AsyncMessage: Sendable { + /// A type which you can optionally post and observe along with this `AsyncMessage`. + associatedtype Subject + +#if FOUNDATION_FRAMEWORK + /// A optional name corresponding to this type, used to interoperate with notification posters and observers. + static var name: Notification.Name { get } + + /// Converts a posted notification into this asynchronous message type for any observers. + /// + /// To implement this method in your own `AsyncMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message. + /// - Parameter notification: The posted ``Notification``. + /// - Returns: The converted `AsyncMessage`, or `nil` if conversion is not possible. + static func makeMessage(_ notification: Notification) -> Self? + + /// Converts a posted asynchronous message into a notification for any observers. + /// + /// To implement this method in your own `AsyncMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``. + /// - Parameters: + /// - message: The posted `AsyncMessage`. + /// - Returns: The converted ``Notification``. + static func makeNotification(_ message: Self) -> Notification +#endif + } +} + +@available(FoundationPreview 6.2, *) +extension NotificationCenter.AsyncMessage { +#if FOUNDATION_FRAMEWORK + public static func makeMessage(_ notification: Notification) -> Self? { return nil } + public static func makeNotification(_ message: Self) -> Notification { return Notification(name: Self.name) } + + // Default Message name is the fully-qualified type name, suitable when Notification-compatibility isn't needed + public static var name: Notification.Name { + // Similar to String(describing:) + return Notification.Name(rawValue: _typeName(Self.self)) + } +#else + internal static var name: String { + // Similar to String(describing:) + return _typeName(Self.self) + } +#endif +} + +@available(FoundationPreview 6.2, *) +extension NotificationCenter { + /// Adds an observer to a center for messages delivered asynchronously with a given subject and identifier. + /// - Parameters: + /// - subject: The subject to observe. Specify a metatype to observe all values for a given type. + /// - identifier: An identifier representing a specific message type. + /// - observer: A closure to execute when receving a message. + /// - Returns: A token representing the observation registration with the given notification center. Retain this token for as long as you need to receive messages. + public func addObserver( + of subject: Message.Subject, + for identifier: Identifier, + using observer: @escaping @Sendable (Message) async -> Void) + -> ObservationToken where Identifier.MessageType == Message, + Message.Subject: AnyObject + { + _addAsyncObserver(Identifier.MessageType.self, subject: subject, observer: observer) + } + + /// Adds an observer to a center for messages delivered asynchronously with a given subject and message type. + /// - Parameters: + /// - subject: The metatype to observe all values for a given type. + /// - identifier: An identifier representing a specific message type. + /// - observer: A closure to execute when receving a message. + /// - Returns: A token representing the observation registration with the given notification center. Retain this token for as long as you need to receive messages. + public func addObserver( + of subject: Message.Subject.Type, + for identifier: Identifier, + using observer: @escaping @Sendable (Message) async -> Void) + -> ObservationToken where Identifier.MessageType == Message { + _addAsyncObserver(Identifier.MessageType.self, subject: nil, observer: observer) + } + + /// Adds an observer to a center for messages delivered asynchronously with a given subject and message type. + /// - Parameters: + /// - subject: The subject to observe. Specify a metatype to observe all values for a given type. + /// - messageType: The message type to be observed. + /// - observer: A closure to execute when receving a message. + /// - Returns: A token representing the observation registration with the given notification center. Retain this token for as long as you need to receive messages. + public func addObserver( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + using observer: @escaping @Sendable (Message) async -> Void) + -> ObservationToken where Message.Subject: AnyObject { + _addAsyncObserver(Message.self, subject: subject, observer: observer) + } + + /// Posts a given asynchronous message to the notification center. + /// - Parameters: + /// - message: The message to post. + /// - subject: The subject instance that corresponds to the message. + public func post(_ message: Message, subject: Message.Subject) where Message.Subject: AnyObject { + _post(message: message, subject: subject) + } + + /// Posts a given asynchronous message to the notification center. + /// - Parameters: + /// - message: The message to post. + public func post(_ message: Message) { + _post(message: message) + } +} + +extension NotificationCenter { + fileprivate func _addAsyncObserver( + _ messageType: Message.Type, + subject: Message.Subject?, + observer: @escaping @Sendable (Message) async -> Void + ) -> ObservationToken { + ObservationToken(center: self, token: _addObserver(Message.name, object: subject) { payload in +#if FOUNDATION_FRAMEWORK + guard + let payload: Message = NotificationCenter._messageFromNotification(payload) + else { return } +#endif + self.asyncObserverQueue.enqueue { + await observer(payload) + } + }) + } + +#if FOUNDATION_FRAMEWORK + internal static func _messageFromNotification(_ notification: Notification) -> Message? { + if let m = notification.userInfo?[NotificationMessageKey.key] as? Message { + // Message posted, message observed + return m + } else if let m = Message.makeMessage(notification) { + // Notification posted, message observed + return m + } else { + // Notification posted, unable to make a message + os_log(.fault, log: _NSRuntimeIssuesLog(), "Unable to deliver Notification to Message observer because \(String(describing: Message.self)).makeMessage() returned nil. If this is unexpected, check or provide an implementation of makeMessage() which returns a non-nil value for this notification's payload.") + return nil + } + } +#endif + + fileprivate func _post(message: M, subject: M.Subject? = nil) { +#if FOUNDATION_FRAMEWORK + var notification = M.makeNotification(message) + + notification.name = M.name + notification.object = subject + + var userInfo = notification.userInfo.take() ?? [:] + userInfo[NotificationMessageKey.key] = message + notification.userInfo = userInfo + + post(notification) +#else + _post(M.name, subject: subject, message: message) +#endif + } +} diff --git a/Sources/FoundationEssentials/NotificationCenter/CMakeLists.txt b/Sources/FoundationEssentials/NotificationCenter/CMakeLists.txt new file mode 100644 index 000000000..c5c0d54a4 --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/CMakeLists.txt @@ -0,0 +1,21 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +target_sources(FoundationEssentials PRIVATE + ActorQueueManager.swift + AsyncMessage.swift + AsyncMessage+AsyncSequence.swift + MainActorMessage.swift + NotificationCenter.swift + NotificationCenterMessage.swift) diff --git a/Sources/FoundationEssentials/NotificationCenter/MainActorMessage.swift b/Sources/FoundationEssentials/NotificationCenter/MainActorMessage.swift new file mode 100644 index 000000000..66e80d9eb --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/MainActorMessage.swift @@ -0,0 +1,266 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +#endif +#if canImport(os) +internal import os.log +#endif + +@available(FoundationPreview 6.2, *) +extension NotificationCenter { + /// A protocol for creating types that you can post to a notification center and bind to the main actor. + /// + /// You post types conforming to `MainActorMessage` to a notification center using `post(_:subject:)` and observe them with `addObserver(of:for:using:)`. The notification center delivers `MainActorMessage` types synchronously when posted. + /// + /// For types that post on an arbitrary isolation, use ``NotificationCenter/AsyncMessage``. + /// + /// Each `MainActorMessage` is associated with a specific `Subject` type. + /// + /// For example, a `MainActorMessage` associated with the type `Event` could use the following declaration: + /// + /// ```swift + /// struct EventDidStart: NotificationCenter.MainActorMessage { + /// typealias Subject = Event + /// } + /// ``` + /// + /// `MainActorMessage` can use an optional ``MessageIdentifier`` type for context-aware observer registration: + /// + /// ```swift + /// extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + /// static var didStart: Self { .init() } + /// } + /// ``` + /// + /// With this identifier, observers can receive information about a specific instance by registering for this message with a ``NotificationCenter``: + /// + /// ```swift + /// let observerToken = NotificationCenter.default.addObserver(of: importantEvent, for: .didStart) + /// ``` + /// + /// Or an observer can receive information about any instance with: + /// + /// ```swift + /// let observerToken = NotificationCenter.default.addObserver(of: Event.self, for: .didStart) + /// ``` + /// + /// The notification center ties observation the lifetime of the returned ``NotificationCenter/ObservationToken`` and automatically de-registers the observer if the token + /// goes out of scope. You can also remove observation explicitly: + /// + /// ```swift + /// NotificationCenter.default.removeObserver(observerToken) + /// ``` + /// ### Notification Interoperability + /// + /// `MainActorMessage` includes optional interoperability with ``Notification``, enabling posters and observers of both types + /// to pass information. + /// + /// It does this by offering a ``makeMessage(_:)`` method that collects values from a ``Notification``'s ``Notification/userInfo`` and populates properties on a new message. + /// In the other direction, a ``makeNotification(_:)`` method collects the message's defined properties and loads them into a new notification's ``Notification/userInfo`` dictionary. + /// + /// For example, if there exists a ``Notification`` posted on `MainActor` identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo`` + /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``MainActorMessage``: + /// + /// ```swift + /// struct EventDidFinish: NotificationCenter.MainActorMessage { + /// typealias Subject = Event + /// static var name: Notification.Name { Notification.Name("eventDidFinish") } + /// + /// var duration: Int + /// + /// static func makeNotification(_ message: Self) -> Notification { + /// return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)]) + /// } + /// + /// static func makeMessage(_ notification: Notification) -> Self? { + /// guard let userInfo = notification.userInfo, + /// let duration = userInfo["duration"] as? Int + /// else { + /// return nil + /// } + /// + /// return Self(duration: duration) + /// } + /// } + /// ``` + /// + /// With this definition, an observer for this `MainActorMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa. + public protocol MainActorMessage: SendableMetatype { + /// A type which you can optionally post and observe along with this `MainActorMessage`. + associatedtype Subject + +#if FOUNDATION_FRAMEWORK + /// A optional name corresponding to this type, used to interoperate with notification posters and observers. + static var name: Notification.Name { get } + + /// Converts a posted notification into this main actor message type for any observers. + /// + /// To implement this method in your own `MainActorMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message. + /// - Parameter notification: The posted ``Notification``. + /// - Returns: The converted `MainActorMessage` or `nil` if conversion is not possible. + @MainActor static func makeMessage(_ notification: Notification) -> Self? + + /// Converts a posted main actor message into a notification for any observers. + /// + /// To implement this method in your own `MainActorMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``. + /// - Parameters: + /// - message: The posted `MainActorMessage`. + /// - Returns: The converted ``Notification``. + @MainActor static func makeNotification(_ message: Self) -> Notification +#endif + } +} + +@available(FoundationPreview 6.2, *) +extension NotificationCenter.MainActorMessage { +#if FOUNDATION_FRAMEWORK + @MainActor public static func makeMessage(_ notification: Notification) -> Self? { return nil } + @MainActor public static func makeNotification(_ message: Self) -> Notification { return Notification(name: Self.name) } + + // Default Message name is the fully-qualified type name, suitable when Notification-compatibility isn't needed + public static var name: Notification.Name { + // Similar to String(describing:) + return Notification.Name(rawValue: _typeName(Self.self)) + } +#else + internal static var name: String { + // Similar to String(describing:) + return _typeName(Self.self) + } +#endif +} + +@available(FoundationPreview 6.2, *) +extension NotificationCenter { + /// Adds an observer to a center for messages delivered on the main actor with a given subject and identifier. + /// + /// - Parameters: + /// - subject: The subject to observe. Specify a metatype to observe all values for a given type. + /// - identifier: An identifier representing a specific message type. + /// - observer: A closure to execute when receving a message. + /// - Returns: A token representing the observation registration with the given notification center. + public func addObserver( + of subject: Message.Subject, + for identifier: Identifier, + using observer: @escaping @MainActor (Message) -> Void) + -> ObservationToken where Identifier.MessageType == Message, + Message.Subject: AnyObject { + _addMainActorObserver(subject: subject, observer: observer) + } + + /// Adds an observer to a center for messages delivered on the main actor with a given subject and identifier. + /// + /// - Parameters: + /// - subject: The metatype to observe all values for a given type. + /// - identifier: An identifier representing a specific message type. + /// - observer: A closure to execute when receving a message. + /// - Returns: A token representing the observation registration with the given notification center. + public func addObserver( + of subject: Message.Subject.Type, + for identifier: Identifier, + using observer: @escaping @MainActor (Message) -> Void) + -> ObservationToken where Identifier.MessageType == Message { + _addMainActorObserver(subject: nil, observer: observer) + } + + /// Adds an observer to a center for messages delivered on the main actor with a given subject and message type. + /// - Parameters: + /// - subject: The subject to be observed. Specify a metatype to observe all values for a given type. + /// - messageType: The message type to be observed. + /// - observer: A closure to execute when receving a message. + /// - Returns: A token representing the observation registration with the given notification center. + public func addObserver( + of subject: Message.Subject? = nil, + for messageType: Message.Type, + using observer: @escaping @MainActor (Message) -> Void) + -> ObservationToken where Message.Subject: AnyObject { + _addMainActorObserver(subject: subject, observer: observer) + } + + /// Posts a given main actor message to the notification center. + /// - Parameters: + /// - message: The message to post. + /// - subject: The subject instance that corresponds to the message. + @MainActor + public func post(_ message: Message, subject: Message.Subject) + where Message.Subject: AnyObject { + MainActor.assertIsolated() + _post(message: message, subject: subject) + } + + /// Posts a given main actor message to the notification center. + /// - Parameters: + /// - message: The message to post. + @MainActor + public func post(_ message: Message) { + MainActor.assertIsolated() + _post(message: message) + } +} + +extension NotificationCenter { + fileprivate func _addMainActorObserver( + subject: Message.Subject?, + observer: @escaping @MainActor (Message) -> Void + ) -> ObservationToken { +#if FOUNDATION_FRAMEWORK + nonisolated(unsafe) let observer = observer + return ObservationToken(center: self, token: _addObserver(Message.name, object: subject) { notification in + nonisolated(unsafe) let notification = notification + MainActor.assumeIsolated { + if let message: Message = NotificationCenter._messageFromNotification(notification) { + observer(message) + } + } + }) +#else + return ObservationToken(center: self, token: _addObserver(Message.name, object: subject, using: observer)) +#endif + } + +#if FOUNDATION_FRAMEWORK + @MainActor + fileprivate static func _messageFromNotification(_ notification: Notification) -> Message? { + if let m = notification.userInfo?[NotificationMessageKey.key] as? Message { + // Message posted, message observed + return m + } else if let m = Message.makeMessage(notification) { + // Notification posted, message observed + return m + } else { + // Notification posted, unable to make a message + os_log(.fault, log: _NSRuntimeIssuesLog(), "Unable to deliver Notification to Message observer because \(String(describing: Message.self)).makeMessage() returned nil. If this is unexpected, check or provide an implementation of makeMessage() which returns a non-nil value for this notification's payload.") + return nil + } + } +#endif + + @MainActor + fileprivate func _post(message: M, subject: M.Subject? = nil) { +#if FOUNDATION_FRAMEWORK + var notification = M.makeNotification(message) + + notification.name = M.name + notification.object = subject + + var userInfo = notification.userInfo.take() ?? [:] + userInfo[NotificationMessageKey.key] = message + notification.userInfo = userInfo + + post(notification) +#else + _post(M.name, subject: subject, message: message) +#endif + } +} diff --git a/Sources/FoundationEssentials/NotificationCenter/NotificationCenter.swift b/Sources/FoundationEssentials/NotificationCenter/NotificationCenter.swift new file mode 100644 index 000000000..c682dc08d --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/NotificationCenter.swift @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(os) +internal import os.log + +extension NotificationCenter { + internal static let logger: Logger = { + Logger(subsystem: "com.apple.foundation", category: "notification-center") + }() +} +#endif + +#if !FOUNDATION_FRAMEWORK + +internal import Synchronization + +// Dictionary storage with automatic key generation +private struct AutoDictionary { + private var storage: [UInt64: Value] = [:] + private var nextKey: UInt64 = 0 + private var salvagedKeys: [UInt64] = [] + + // Effectively O(1), worst case O(n) + mutating func insert(_ value: Value) -> UInt64 { + guard storage.count <= UInt64.max else { fatalError("Exceeded maximum storage size") } + + var key = salvagedKeys.popLast() + while(key != nil) { + if let key, storage[key] == nil { + storage[key] = value + return key + } else { + key = salvagedKeys.popLast() + } + } + + while(storage[nextKey] != nil) { + if nextKey == UInt64.max { nextKey = 0 } + nextKey += 1 + } + + storage[nextKey] = value + return nextKey + } + + mutating func remove(_ key: UInt64) { + if(storage[key] != nil) { + storage[key] = nil + salvagedKeys.append(key) + } + } + + func count() -> Int { + return storage.count + } + + var values: [Value] { + return storage.compactMap(\.value) + } +} + +private let _defaultCenter = NotificationCenter() + +private struct MessageBox { + // Equivalent to storing Message in Notification.userInfo + let message: Any +} + +open class NotificationCenter: @unchecked Sendable { + private let registrar: Mutex<[String? /* Notification name */: [ObjectIdentifier? /* object */ : AutoDictionary<@Sendable (MessageBox) -> Void>]]> + internal lazy var _actorQueueManager = _NotificationCenterActorQueueManager() + + public required init() { + registrar = .init([:]) + } + + open class var `default`: NotificationCenter { + return _defaultCenter + } + + // 'M' may be a concrete NotificationCenter message or a Notification passed via swift-corelibs-foundation + @_spi(SwiftCorelibsFoundation) public func _addObserver(_ name: String?, object: Any?, using block: @escaping @Sendable (M) -> Void) -> _NotificationObserverToken { + nonisolated(unsafe) let object = object + let objectId = object.map { ObjectIdentifier($0 as AnyObject) } + + let token = registrar.withLock { _registrar in + return _registrar[name, default: [:]][objectId, default: AutoDictionary<@Sendable (MessageBox) -> Void>()].insert { box in + block(box.message as! M) + } + } + + return _NotificationObserverToken(token: token, name: name, objectId: objectId) + } + + @_spi(SwiftCorelibsFoundation) public func _removeObserver(_ token: _NotificationObserverToken) { + registrar.withLock { _registrar in + _registrar[token.name]?[token.objectId]?.remove(token.token) + + if _registrar[token.name]?[token.objectId]?.count() == 0 { + _registrar[token.name]?.removeValue(forKey: token.objectId) + if _registrar[token.name]?.isEmpty == true { + _registrar.removeValue(forKey: token.name) + } + } + } + } + + @_spi(SwiftCorelibsFoundation) public func _post(_ name: String?, subject: Any?, message: M) { + // TODO: Darwin calls observers in the order they were added, mixing wildcard and non-wildcard observers. + // It's conceivable some users rely on that ordering. + + let observers = registrar.withLock { _registrar in + var observers: [@Sendable (MessageBox) -> Void] = [] + let objectId = subject.map { ObjectIdentifier($0 as AnyObject) } + + // Observers with 'name' and 'object' + observers.append(contentsOf: _registrar[name]?[objectId]?.values ?? []) + + // Observers with wildcard name and 'object' + observers.append(contentsOf: _registrar[nil]?[objectId]?.values ?? []) + + if subject != nil { + // Observers with wildcard name and wildcard object + observers.append(contentsOf: _registrar[nil]?[nil]?.values ?? []) + + // Observers with 'name' and wildcard object + observers.append(contentsOf: _registrar[name]?[nil]?.values ?? []) + } + + return observers + } + + let messageBox = MessageBox(message: message) + observers.forEach { $0(messageBox) } + } + + // For testing purposes only! + internal func isEmpty() -> Bool { + return registrar.withLock { _registrar in + _registrar.isEmpty + } + } + + internal func _getActorQueueManager() -> _NotificationCenterActorQueueManager { + return _actorQueueManager + } +} + +extension NotificationCenter: Equatable { + public static func == (lhs: NotificationCenter, rhs: NotificationCenter) -> Bool { + return lhs === rhs + } +} + +extension NotificationCenter: CustomStringConvertible { + public var description: String { + return "" + } +} + +extension NotificationCenter { + @_spi(SwiftCorelibsFoundation) public struct _NotificationObserverToken: Equatable, Hashable, Sendable { + let token: UInt64 + public let name: String? + public let objectId: ObjectIdentifier? + } +} + +#endif diff --git a/Sources/FoundationEssentials/NotificationCenter/NotificationCenterMessage.swift b/Sources/FoundationEssentials/NotificationCenter/NotificationCenterMessage.swift new file mode 100644 index 000000000..33f49a42b --- /dev/null +++ b/Sources/FoundationEssentials/NotificationCenter/NotificationCenterMessage.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import Foundation_Private.NSNotification +internal import _ForSwiftFoundation +#endif +#if canImport(os) +internal import os.log +#endif + +@available(FoundationPreview 6.2, *) +extension NotificationCenter { + /// An optional identifier to associate a given message with a given type. + /// + /// Implement a `MessageIdentifier` to provide a typed, ergonomic experience at the call point, as described in [SE-0299](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md). + /// + /// For example, given `ExampleMessage` with a `Subject` called `ExampleSubject`: + /// + /// ```swift + /// extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + /// static var eventDidOccur: Self { .init() } + /// } + /// ``` + /// + /// This simplifies the call point for clients, as seen here: + /// + /// ```swift + /// let token = center.addObserver(of: exampleSubject, for: .eventDidOccur) { ... } + /// ``` + public protocol MessageIdentifier { + associatedtype MessageType + } + + /// A type for use when defining optional Message identifiers. + /// + /// See ``MessageIdentifier`` for an example of how to use this type when defining your own message identifiers. + public struct BaseMessageIdentifier: MessageIdentifier, Sendable { + public init() where MessageType: MainActorMessage {} + public init() where MessageType: AsyncMessage {} + } +} + +extension NotificationCenter { +#if !FOUNDATION_FRAMEWORK + internal typealias _NSNotificationObserverToken = _NotificationObserverToken +#endif + + /// A unique token representing a single observer registration in a notification center. + /// + /// You receive the `ObservationToken` type as a return value from `addObserver(of:for:using:)` and related methods. + /// + /// Retain the `ObservationToken` for as long as you need to continue observation, since observation ends when the token goes out of scope. + /// You can also explicitly stop observing by passing the token to ``removeObserver(_:)-(ObservationToken)``. + @available(FoundationPreview 6.2, *) + public struct ObservationToken: Hashable, Sendable { + private let tokenWrapper: _NSNotificationObserverTokenWrapper + internal var center: NotificationCenter? { self.tokenWrapper.center } + + internal init(center: NotificationCenter, token: _NSNotificationObserverToken) { + self.tokenWrapper = _NSNotificationObserverTokenWrapper(center: center, token: token) + } + + internal func remove() { + self.tokenWrapper.remove() + } + + fileprivate final class _NSNotificationObserverTokenWrapper: Hashable, @unchecked Sendable { + internal var token: _NSNotificationObserverToken? + fileprivate weak var center: NotificationCenter? + + init(center: NotificationCenter, token: _NSNotificationObserverToken) { + self.token = token + self.center = center + } + + func remove() { + if let value = token { + self.center?._removeObserver(value) + token = nil + } + } + + deinit { + self.remove() + } + + static func == (lhs: ObservationToken._NSNotificationObserverTokenWrapper, rhs: ObservationToken._NSNotificationObserverTokenWrapper) -> Bool { + return lhs.token == rhs.token + } + + func hash(into hasher: inout Hasher) { + hasher.combine(token) + } + } + } + + /// Stops the observation represented by the given observation token. + /// + /// - Parameter token: a unique token representing a specific observer in a specific notification center. You receive this type from prior calls to `addObserver(of:for:using:)`. + @available(FoundationPreview 6.2, *) + public func removeObserver(_ token: ObservationToken) { + guard token.center == nil || token.center == self else { +#if canImport(os) + NotificationCenter.logger.fault("Unable to remove observer. The provided token does not belong to this notification center. Expected: <\(_typeName(Self.self)) 0x\(String(UInt(bitPattern: ObjectIdentifier(token.center!)), radix: 16))>, got <\(_typeName(Self.self)) 0x\(String(UInt(bitPattern: ObjectIdentifier(self)), radix: 16))>.") +#endif + return + } + + token.remove() + } +} + +extension NotificationCenter { +#if FOUNDATION_FRAMEWORK + internal final class NotificationMessageKey: NSObject, NSCopying, Sendable { + func copy(with zone: NSZone? = nil) -> Any { return self } + + static let key = NotificationMessageKey() + } +#else + internal final class NotificationMessageKey: Sendable { + static var key: ObjectIdentifier { ObjectIdentifier(NotificationCenter.NotificationMessageKey.self) } + } +#endif +} + +extension NotificationCenter { + internal var asyncObserverQueue: _NotificationCenterActorQueueManager { + self._getActorQueueManager() as! _NotificationCenterActorQueueManager + } +} diff --git a/Tests/FoundationEssentialsTests/NotificationCenterMessageTests.swift b/Tests/FoundationEssentialsTests/NotificationCenterMessageTests.swift new file mode 100644 index 000000000..c0ee89630 --- /dev/null +++ b/Tests/FoundationEssentialsTests/NotificationCenterMessageTests.swift @@ -0,0 +1,966 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +import Foundation_Private +#else +@testable import FoundationEssentials +#endif + +import Testing + +// MARK: Test data (Subjects, Messages, MessageIdentifiers, NotificationNames) + +final class MessageTestSubject: Sendable, Equatable { + let uuid = UUID() + + static func == (lhs: MessageTestSubject, rhs: MessageTestSubject) -> Bool { + lhs.uuid == rhs.uuid + } +} +final class AsyncMessageTestSubject: Sendable, Equatable { + let uuid = UUID() + + static func == (lhs: AsyncMessageTestSubject, rhs: AsyncMessageTestSubject) -> Bool { + lhs.uuid == rhs.uuid + } +} + +struct MainActorTestMessage: NotificationCenter.MainActorMessage { + typealias Subject = MessageTestSubject + + var payloadInt: Int + var payloadString: String +} +extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + static var messagePosted: Self { .init() } +} + +struct MainActorTestNotificationMessage: NotificationCenter.MainActorMessage { + typealias Subject = MessageTestSubject + + var payloadInt: Int + var payloadString: String + +#if FOUNDATION_FRAMEWORK + static var name: Notification.Name { Notification.Name("MainActorTestMessageNotification") } + + public static func makeMessage(_ notification: Notification) -> Self? { + guard let userInfo = notification.userInfo, + let payloadInt = userInfo["payloadInt"] as? Int, + let payloadString = userInfo["payloadString"] as? String + else { + return nil + } + + return Self(payloadInt: payloadInt, payloadString: payloadString) + } + + public static func makeNotification(_ message: Self) -> Notification { + return Notification(name: MainActorTestNotificationMessage.name, userInfo: ["payloadInt": message.payloadInt, "payloadString": message.payloadString]) + } +#endif +} +extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + static var notificationMessagePosted: Self { .init() } +} + +struct AsyncTestMessage: NotificationCenter.AsyncMessage { + typealias Subject = AsyncMessageTestSubject + + var payloadInt: Int + var payloadString: String +} +extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + static var messagePosted: Self { .init() } +} + +struct AsyncTestNotificationMessage: NotificationCenter.AsyncMessage { + typealias Subject = AsyncMessageTestSubject + + var payloadInt: Int + var payloadString: String + +#if FOUNDATION_FRAMEWORK + static var name: Notification.Name { Notification.Name("AsyncTestMessageNotification") } + + static func makeNotification(_ message: Self) -> Notification { + return Notification(name: Self.name, userInfo: ["payloadInt": message.payloadInt, "payloadString": message.payloadString]) + } + + static func makeMessage(_ notification: Notification) -> Self? { + guard let userInfo = notification.userInfo, + let payloadInt = userInfo["payloadInt"] as? Int, + let payloadString = userInfo["payloadString"] as? String + else { + return nil + } + + return Self(payloadInt: payloadInt, payloadString: payloadString) + } +#endif +} +extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + static var notificationMessagePosted: Self { .init() } +} + +#if FOUNDATION_FRAMEWORK +let nonConvertingMessageNotificationName = Notification.Name("NonConvertingMessage") +struct NonConvertingMessage: NotificationCenter.MainActorMessage { + static var name: Notification.Name { nonConvertingMessageNotificationName } + typealias Subject = MessageTestSubject +} +extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier { + static var nonConvertingMessage: Self { .init() } +} +#endif + +@Suite("NotificationCenterMessage", .timeLimit(.minutes(1))) +private struct NotificationCenterMessageTests { + + // MARK: - Basic capabilities (using MainActorMessage) + + @MainActor + @Test func postWithSubjectTypeAndObserveWithSubjectType() async throws { + await confirmation("expected message to be observed") { messageObserved in + var mutableState: Int = 0 + let center = NotificationCenter() + + let token = center.addObserver(of: MessageTestSubject.self, for: .messagePosted) { message in + MainActor.assertIsolated() + mutableState += 1 + messageObserved() + } + + MainActor.assertIsolated() + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One")) + #expect(mutableState == 1) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postWithSubjectInstanceAndObserveWithSubjectInstance() async throws { + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + let testSubject = MessageTestSubject() + + let token = center.addObserver(of: testSubject, for: .messagePosted) { message in + #expect(message.payloadInt == 1) + messageObserved() + } + + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postWithSubjectInstanceAndObserveDifferentSubjectInstance() async throws { + var messageObserved = false + let center = NotificationCenter() + + let observedSubject = MessageTestSubject() + let postedSubject = MessageTestSubject() + + let token = center.addObserver(of: observedSubject, for: .messagePosted) { message in + messageObserved = true + } + + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One"), subject: postedSubject) + + center.removeObserver(token) + + #expect(messageObserved == false) + } + + @MainActor + @Test func postWithSubjectInstanceAndObserveWithSubjectType() async throws { + await confirmation("expected message to be observed", expectedCount: 2) { messageObserved in + let center = NotificationCenter() + + let token = center.addObserver(of: MessageTestSubject.self, for: .messagePosted) { message in + messageObserved() + } + + let testSubject = MessageTestSubject() + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + + let secondTestSubject = MessageTestSubject() + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One"), subject: secondTestSubject) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postWithSubjectTypeAndObserveWithSubjectInstance() async throws { + var messageObserved = false + + let center = NotificationCenter() + let testSubject = MessageTestSubject() + + let token = center.addObserver(of: testSubject, for: .messagePosted) { message in + messageObserved = true + } + + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One")) + + center.removeObserver(token) + + #expect(messageObserved == false) + } + + @MainActor + @Test func observeWithoutMessageIdentifier() async throws { + await confirmation("expected message to be observed", expectedCount: 2) { messageObserved in + let center = NotificationCenter() + + let testSubject = MessageTestSubject() + + let token = center.addObserver(of: testSubject, for: MainActorTestMessage.self) { message in + messageObserved() + } + let secondToken = center.addObserver(for: MainActorTestMessage.self) { message in + messageObserved() + } + + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + + center.removeObserver(token) + center.removeObserver(secondToken) + } + } + +#if FOUNDATION_FRAMEWORK + @Test func messageNameSynthesis() { + // Synthesized Message names are expected to mirror ABI stability + #expect(MainActorTestMessage.name.rawValue == "Unit.MainActorTestMessage") + } +#endif + + @MainActor + @Test func observationOccursOnMainActor() async { + await confirmation("expected message to be observed") { messageObserved in + actor OtherActor { + var token: NotificationCenter.ObservationToken? + let confirmation: Confirmation + + init(confirmation: Confirmation) { + self.confirmation = confirmation + } + + func registerObservation() { + token = NotificationCenter.default.addObserver(of: MessageTestSubject.self, for: .messagePosted) { _ in + MainActor.assertIsolated() + self.confirmation() + } + } + + deinit { + if let token { NotificationCenter.default.removeObserver(token) } + } + } + + let otherActor = OtherActor(confirmation: messageObserved) + await otherActor.registerObservation() + + MainActor.assertIsolated() + NotificationCenter.default.post(MainActorTestMessage(payloadInt: 1, payloadString: "One")) + } + } + + // MARK: - AsyncMessage + + @Test func asyncMessagePostWithSubjectTypeAndObserveWithSubjectType() async throws { + let center = NotificationCenter() + var token: NotificationCenter.ObservationToken? + + await confirmation("expected message to be observed") { messageObserved in + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + @Sendable func anAsyncCall() async { + messageObserved() + continuation.resume() + } + + token = center.addObserver(of: AsyncMessageTestSubject.self, for: .messagePosted) { message in + await anAsyncCall() + } + + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "One")) + } + } + + if let token { center.removeObserver(token) } + } + + @Test func asyncMessagePostWithSubjectInstanceAndObserveWithSubjectInstance() async throws { + let center = NotificationCenter() + let testSubject = AsyncMessageTestSubject() + var token: NotificationCenter.ObservationToken? + + await confirmation("expected message to be observed") { messageObserved in + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + token = center.addObserver(of: testSubject, for: .messagePosted) { message in + #expect(message.payloadInt == 1) + messageObserved() + continuation.resume() + } + + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + } + } + + if let token { center.removeObserver(token) } + } + + @Test func asyncMessageObserveWithoutMessageIdentifier() async throws { + let center = NotificationCenter() + let testSubject = AsyncMessageTestSubject() + var token: NotificationCenter.ObservationToken? + var secondToken: NotificationCenter.ObservationToken? + + final class AtomicCounter: Sendable { + private let count = LockedState(initialState: 0) + + func increment() -> Int { + count.withLock { value in + value &+= 1 + return value + } + } + } + + + await confirmation("expected message to be observed", expectedCount: 2) { messageObserved in + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + let counter = AtomicCounter() + + token = center.addObserver(of: testSubject, for: AsyncTestMessage.self) { message in + messageObserved() + if counter.increment() == 2 { continuation.resume() } + } + secondToken = center.addObserver(for: AsyncTestMessage.self) { message in + messageObserved() + if counter.increment() == 2 { continuation.resume() } + } + + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + } + } + + if let token { center.removeObserver(token) } + if let secondToken { center.removeObserver(secondToken) } + } + + @MainActor + @Test func asyncMessageObservesInSeparateIsolation() async throws { + enum TaskSignifier { + @TaskLocal static var flag: Bool = false + } + + // #isolation isn't available in the observer closure, but we can verify the expected isolation change + // by checking that a task local variable isn't preserved + await TaskSignifier.$flag.withValue(true) { + #expect(TaskSignifier.flag == true) + + let center = NotificationCenter() + let testSubject = AsyncMessageTestSubject() + var token: NotificationCenter.ObservationToken? + + await confirmation("expected message to be observed") { messageObserved in + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + token = center.addObserver(of: testSubject, for: AsyncTestMessage.self) { message in + #expect(TaskSignifier.flag == false) + messageObserved() + continuation.resume() + } + + MainActor.assertIsolated() + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + } + } + + if let token { center.removeObserver(token) } + } + } + +#if FOUNDATION_FRAMEWORK + // MARK: - Message/Notification interoperability + @MainActor + @Test func postMessageWithSubjectObserveNotificationWithObject() async throws { + await confirmation("expected notification to be observed") { notificationObserved in + let center = NotificationCenter() + let testSubject = MessageTestSubject() + + let token = center.addObserver(forName: MainActorTestNotificationMessage.name, object: testSubject, queue: nil) { notification in + guard + let object: MessageTestSubject = notification.object as? MessageTestSubject, + let userInfo = notification.userInfo + else { return } + if notification.name == MainActorTestNotificationMessage.name, + object == testSubject, + userInfo["payloadInt"] as? Int == 1, + userInfo["payloadString"] as? String == "One" + { + notificationObserved() + } + } + + center.post(MainActorTestNotificationMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postMessageWithSubjectObserveNotificationWithoutObject() async throws { + await confirmation("expected notification to be observed", expectedCount: 2) { notificationObserved in + let center = NotificationCenter() + + let token = center.addObserver(forName: MainActorTestNotificationMessage.name, object: nil, queue: nil) { notification in + guard + notification.object is MessageTestSubject, + notification.name == MainActorTestNotificationMessage.name + else { return } + + notificationObserved() + } + + let testSubject = MessageTestSubject() + center.post(MainActorTestNotificationMessage(payloadInt: 1, payloadString: "One"), subject: testSubject) + + let secondTestSubject = MessageTestSubject() + center.post(MainActorTestNotificationMessage(payloadInt: 1, payloadString: "One"), subject: secondTestSubject) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postMessageWithoutSubjectObserveNotificationWithObject() async throws { + let center = NotificationCenter() + let testSubject = MessageTestSubject() + nonisolated(unsafe) var notificationObserved = false + + let token = center.addObserver(forName: MainActorTestNotificationMessage.name, object: testSubject, queue: nil) { notification in + guard + notification.object is MessageTestSubject, + notification.name == MainActorTestNotificationMessage.name + else { return } + + notificationObserved = true + } + + center.post(MainActorTestNotificationMessage(payloadInt: 1, payloadString: "One")) + + center.removeObserver(token) + + #expect(notificationObserved == false) + } + + @MainActor + @Test func postMessageWithoutSubjectObserveNotificationWithoutObject() async throws { + await confirmation("expected notification to be observed") { notificationObserved in + let center = NotificationCenter() + + let token = center.addObserver(forName: MainActorTestNotificationMessage.name, object: nil, queue: nil) { notification in + guard + notification.name == MainActorTestNotificationMessage.name + else { return } + + notificationObserved() + } + + center.post(MainActorTestNotificationMessage(payloadInt: 1, payloadString: "One")) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postMessageWithoutNotificationConversionAndObserveNotification() async throws { + await confirmation("expected notification to be observed") { notificationObserved in + let center = NotificationCenter() + + let token = center.addObserver(forName: nonConvertingMessageNotificationName, object: nil, queue: nil) { notification in + guard + notification.name == nonConvertingMessageNotificationName, + notification.userInfo!.keys.count == 1, + notification.userInfo!.keys.contains(NotificationCenter.NotificationMessageKey.key) + else { return } + + notificationObserved() + } + + center.post(NonConvertingMessage()) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postNotificationWithObjectObserveMessageWithSubject() async throws { + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + let testSubject = MessageTestSubject() + + let token = center.addObserver(of: testSubject, for: .notificationMessagePosted) { message in + if message.payloadInt == 5, message.payloadString == "Six" { + messageObserved() + } + } + + center.post(name: MainActorTestNotificationMessage.name, object: testSubject, userInfo: ["payloadInt": 5, "payloadString": "Six"]) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postNotificationWithObjectObserveMessageWithoutSubject() async throws { + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + + let token = center.addObserver(of: MessageTestSubject.self, for: .notificationMessagePosted) { message in + messageObserved() + } + + let testSubject = MessageTestSubject() + + center.post(name: MainActorTestNotificationMessage.name, object: testSubject, userInfo: ["payloadInt": 5, "payloadString": "Six"]) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postNotificationWithoutObjectObserveMessageWithSubject() async throws { + let center = NotificationCenter() + let testSubject = MessageTestSubject() + var messageObserved = false + + let token = center.addObserver(of: testSubject, for: .notificationMessagePosted) { message in + if message.payloadInt == 5, message.payloadString == "Six" { + messageObserved = true + } + } + + center.post(name: MainActorTestNotificationMessage.name, object: nil, userInfo: ["payloadInt": 5, "payloadString": "Six"]) + + center.removeObserver(token) + + #expect(messageObserved == false) + } + + @MainActor + @Test func postNotificationWithoutObjectObserveMessageWithoutSubject() async throws { + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + + let token = center.addObserver(of: MessageTestSubject.self, for: .notificationMessagePosted) { message in + messageObserved() + } + + center.post(name: MainActorTestNotificationMessage.name, object: nil, userInfo: ["payloadInt": 5, "payloadString": "Six"]) + + center.removeObserver(token) + } + } + + @MainActor + @Test func postNotificationWithoutMessageConversionAndObserveNoMessage() async throws { + let center = NotificationCenter() + var messageObserved = false + + let token = center.addObserver(of: MessageTestSubject.self, for: .nonConvertingMessage) { message in + messageObserved = true + } + + center.post(name: nonConvertingMessageNotificationName, object: nil, userInfo: nil) + + center.removeObserver(token) + + #expect(messageObserved == false) + } +#endif + + // MARK: - Test different concrete Message types + + @MainActor + @Test func messageAsStruct() async { + struct MainActorStructMessage: NotificationCenter.MainActorMessage { + typealias Subject = MessageTestSubject + let payload: String + } + + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + let subject = MessageTestSubject() + + let token = center.addObserver(of: subject, for: MainActorStructMessage.self) { message in + if message.payload == "info" { + messageObserved() + } + } + + center.post(MainActorStructMessage(payload: "info"), subject: subject) + + center.removeObserver(token) + } + } + + @MainActor + @Test func messageAsClass() async { + class MainActorClassMessage: NotificationCenter.MainActorMessage { + typealias Subject = MessageTestSubject + let payload: String + + init(_ payload: String) { self.payload = payload } + } + + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + let subject = MessageTestSubject() + + let token = center.addObserver(of: subject, for: MainActorClassMessage.self) { message in + if message.payload == "info" { + messageObserved() + } + } + + center.post(MainActorClassMessage("info"), subject: subject) + + center.removeObserver(token) + } + } + + @MainActor + @Test func messageAsEnum() async { + enum BasicMessage: Int, NotificationCenter.MainActorMessage { + typealias Subject = MessageTestSubject + + case first = 1, second, third, fourth + } + + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + + let token = center.addObserver(for: BasicMessage.self) { message in + if message == BasicMessage.first { + messageObserved() + } + } + + center.post(BasicMessage(rawValue: 1)!) + + center.removeObserver(token) + } + } + + @MainActor + @Test func messageAsActor() async { + actor BasicMessage: NotificationCenter.MainActorMessage { + typealias Subject = MessageTestSubject + let payload: String + + init(_ payload: String) { self.payload = payload } + } + + await confirmation("expected message to be observed") { messageObserved in + let center = NotificationCenter() + + let token = center.addObserver(for: BasicMessage.self) { message in + if message.payload == "info" { + messageObserved() + } + } + + center.post(BasicMessage("info")) + + center.removeObserver(token) + } + } + + // MARK: - ObservationToken + + @MainActor + @Test func observationTokenRemoval() async throws { + let center = NotificationCenter() + var messageObserved = false + + let token = center.addObserver(of: MessageTestSubject.self, for: .messagePosted) { message in + messageObserved = true + } + + center.removeObserver(token) + + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One")) + + #expect(messageObserved == false) + } + + @MainActor + @Test func observationTokenDeinit() async throws { + let center = NotificationCenter() + var messageObserved = false + + do { + let token = center.addObserver(of: MessageTestSubject.self, for: .messagePosted) { message in + messageObserved = true + } + _ = token + } + + center.post(MainActorTestMessage(payloadInt: 1, payloadString: "One")) + + #expect(messageObserved == false) + } + +#if FOUNDATION_FRAMEWORK + @MainActor + @Test func deinitDoesNotRemoveIfAlreadyRemoved() throws { + var removeObservedCount = 0 + + class override_removeObserver: NotificationCenter, @unchecked Sendable { + var removeObserverHook: (() -> Void)? + + override func _removeObserver(_ token: UInt64) { + removeObserverHook?() + super._removeObserver(token) + } + } + + let center = override_removeObserver() + + center.removeObserverHook = { + removeObservedCount += 1 + } + + do { + let token = center.addObserver(of: MessageTestSubject.self, for: .messagePosted) { _ in } + center.removeObserver(token) + } + + #expect(removeObservedCount == 1) + } +#endif + + @MainActor + @Test func removeObserverOnlyWorksWithOriginatingCenter() throws { + var tokenObserved = false + + let center1 = NotificationCenter() + let center2 = NotificationCenter() + + let token = center1.addObserver(of: MessageTestSubject.self, for: .messagePosted) { _ in + tokenObserved = true + } + + center2.removeObserver(token) + + center1.post(MainActorTestMessage(payloadInt: 5, payloadString: "five")) + + #expect(tokenObserved) + + center1.removeObserver(token) + } + + // MARK: - ActorQueueManager + + @Test func waitForWorkResumesOnTaskCancellation() async { + await confirmation("expected task to end") { taskEnds in + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + let state: LockedState<_NotificationCenterActorQueueManager.State> = LockedState(initialState: _NotificationCenterActorQueueManager.State()) + + let managerTask = Task { + let result = await _NotificationCenterActorQueueManager.State.waitForWork(state) + #expect(result == nil) + taskEnds() + continuation.resume() + } + + // Cancel waitForWork() once state.continuation is set + Task { + while true { + let cancelTask = state.withLock { state in + return state.continuation != nil + } + if cancelTask { + managerTask.cancel() + break + } + try await Task.sleep(for: .milliseconds(50)) + } + } + } + } + } + + @Test func waitForWorkResumesWhenTaskIsAlreadyCancelled() async { + await confirmation("expected task to end") { taskEnds in + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + let task = Task { + let state: LockedState<_NotificationCenterActorQueueManager.State> = LockedState(initialState: _NotificationCenterActorQueueManager.State()) + + while Task.isCancelled == false { + do { + try await Task.sleep(for: .milliseconds(50)) + } catch is CancellationError {} + } + + let result = await _NotificationCenterActorQueueManager.State.waitForWork(state) + #expect(result == nil) + taskEnds() + continuation.resume() + } + + task.cancel() + } + } + } + + // MARK: - AsyncMessageSequence + + @Test func makeAsyncIteratorStartsObservation() async throws { + let sequence = NotificationCenter.default.messages(of: nil, for: AsyncTestMessage.self) + + NotificationCenter.default.post(AsyncTestMessage(payloadInt: 0, payloadString: "zero")) + + var iterator = sequence.makeAsyncIterator() + + NotificationCenter.default.post(AsyncTestMessage(payloadInt: 1, payloadString: "one")) + + let message: AsyncTestMessage? = try await iterator.next() + + // Observation begins on makeAsyncIterator(), so the first post() should be missed + #expect(message?.payloadInt == 1) + #expect(message?.payloadString == "one") + } + + @Test func asyncMessageSequenceStopsObservationWhenDescoped() async { + let center = NotificationCenter() + + #expect(center.isEmpty()) + + let sequence = center.messages(of: nil, for: AsyncTestMessage.self) + + do { + let iterator = sequence.makeAsyncIterator() + _ = iterator // Suppress warning about unused 'iterator' + #expect(!center.isEmpty()) + } + + #expect(center.isEmpty()) + } + + @Test func asyncMessageSequenceIteratorCopying() async throws { + let center = NotificationCenter() + + let sequence = center.messages(of: nil, for: AsyncTestMessage.self) + var iterator = sequence.makeAsyncIterator() + var iteratorCopy = iterator + + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "one")) + + var message = try await iterator.next() + #expect(message?.payloadInt == 1) + + center.post(AsyncTestMessage(payloadInt: 2, payloadString: "two")) + + message = try await iteratorCopy.next() + #expect(message?.payloadInt == 2) + + center.post(AsyncTestMessage(payloadInt: 3, payloadString: "three")) + + message = try await iterator.next() + #expect(message?.payloadInt == 3) + } + + @Test func asyncMessageSequenceBuffer() async throws { + let center = NotificationCenter() + let bufferSize = 15 + let sequence = center.messages(of: nil, for: AsyncTestMessage.self, bufferSize: bufferSize) + var iterator = sequence.makeAsyncIterator() + + // Basic buffering + for i in 1...5 { center.post(AsyncTestMessage(payloadInt: i, payloadString: "N/A")) } + for i in 1...5 { + let message = try await iterator.next() + #expect(message?.payloadInt == i) + } + + // Basic overflow handling + for i in 1...(bufferSize + 1) { center.post(AsyncTestMessage(payloadInt: i, payloadString: "N/A")) } + for i in 2...(bufferSize + 1) { + let message = try await iterator.next() + #expect(message?.payloadInt == i) + } + } + + @Test func asyncMessageSequenceDoesNotUseActorQueue() async throws { + let center = NotificationCenter() + let sequence = center.messages(of: nil, for: AsyncTestMessage.self) + var iterator = sequence.makeAsyncIterator() + + // Basic buffering + for i in 1...5 { center.post(AsyncTestMessage(payloadInt: i, payloadString: "N/A")) } + + center.asyncObserverQueue.state.withLock { _state in + #expect(_state.buffer.isEmpty) + } + + for i in 1...5 { + let message = try await iterator.next() + #expect(message?.payloadInt == i) + } + } + + @Test func asyncMessageSequenceMethods() async throws { + let center = NotificationCenter() + + // messages(of: object, for: .identifier) { ... } for Subject: AnyObject + do { + let asyncMessageTestSubject = AsyncMessageTestSubject() + var iterator = center.messages(of: asyncMessageTestSubject, for: .messagePosted).makeAsyncIterator() + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "N/A"), subject: asyncMessageTestSubject) + let message = try await iterator.next() + #expect(message?.payloadInt == 1) + } + + // messages(of: object.Type, for: .identifier) { ... } + do { + var iterator = center.messages(of: AsyncMessageTestSubject.self, for: .messagePosted).makeAsyncIterator() + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "N/A")) + let message = try await iterator.next() + #expect(message?.payloadInt == 1) + } + + // messages(of: object?, for: Message.Type) { ... } for Subject: AnyObject + do { + let asyncMessageTestSubject = AsyncMessageTestSubject() + var iterator = center.messages(of: asyncMessageTestSubject, for: AsyncTestMessage.self).makeAsyncIterator() + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "N/A"), subject: asyncMessageTestSubject) + let message = try await iterator.next() + #expect(message?.payloadInt == 1) + } + do { + var iterator = center.messages(of: nil, for: AsyncTestMessage.self).makeAsyncIterator() + center.post(AsyncTestMessage(payloadInt: 1, payloadString: "N/A")) + let message = try await iterator.next() + #expect(message?.payloadInt == 1) + } + } +} diff --git a/Tests/FoundationEssentialsTests/NotificationCenterTests.swift b/Tests/FoundationEssentialsTests/NotificationCenterTests.swift new file mode 100644 index 000000000..dcb652236 --- /dev/null +++ b/Tests/FoundationEssentialsTests/NotificationCenterTests.swift @@ -0,0 +1,229 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable @_spi(SwiftCorelibsFoundation) import FoundationEssentials +#endif + +import Testing + +fileprivate final class TestObject: Sendable {} + +@Suite("NotificationCenter", .timeLimit(.minutes(1))) +private struct NotificationCenterTests { + @Test func defaultCenter() { + let defaultCenter1 = NotificationCenter.default + let defaultCenter2 = NotificationCenter.default + #expect(defaultCenter1 == defaultCenter2) + } + + @Test func equality() { + let center1 = NotificationCenter() + let center2 = NotificationCenter() + #expect(center1 != center2) + } + +#if !FOUNDATION_FRAMEWORK + @Test func internalPostNotification() { + nonisolated(unsafe) var flag = false + + let notificationName = "test_postNotification_name" + let center = NotificationCenter() + let testObject = TestObject() + let message = MainActorTestMessage(payloadInt: 1, payloadString: "one") + + // name and object + var token = center._addObserver(notificationName, object: testObject) { (message: MainActorTestMessage) in + #expect(message.payloadInt == 1) + #expect(message.payloadString == "one") + flag = true + } + flag = false + center._post(notificationName, subject: testObject, message: message) + #expect(flag) + center._removeObserver(token) + + // nil name and object + token = center._addObserver(nil, object: testObject) { (message: MainActorTestMessage) in + #expect(message.payloadInt == 1) + #expect(message.payloadString == "one") + + flag = true + } + flag = false + center._post(notificationName, subject: testObject, message: message) + #expect(flag) + center._removeObserver(token) + + // nil name and nil object + token = center._addObserver(nil, object: nil) { (message: MainActorTestMessage) in + #expect(message.payloadInt == 1) + #expect(message.payloadString == "one") + + flag = true + } + flag = false + center._post(notificationName, subject: testObject, message: message) + #expect(flag) + center._removeObserver(token) + + // name and nil object + token = center._addObserver(notificationName, object: nil) { (message: MainActorTestMessage) in + #expect(message.payloadInt == 1) + #expect(message.payloadString == "one") + + flag = true + } + flag = false + center._post(notificationName, subject: testObject, message: message) + #expect(flag) + center._removeObserver(token) + } + + @Test func internalRemoveObserver() { + nonisolated(unsafe) var flag = false + + let notificationName = "test_postNotification_name" + let center = NotificationCenter() + let testObject = TestObject() + let message = MainActorTestMessage(payloadInt: 1, payloadString: "one") + + let token = center._addObserver(notificationName, object: testObject) { (message: MainActorTestMessage) in + #expect(message.payloadInt == 1) + #expect(message.payloadString == "one") + + flag = true + } + + flag = false + center._post(notificationName, subject: testObject, message: message) + #expect(flag) + center._removeObserver(token) + + flag = false + center._post(notificationName, subject: testObject, message: message) + #expect(!flag) + } + + @Test func internalPostNotificationForObject() { + let center = NotificationCenter() + let name = "test_postNotificationForObject_name" + let testObject = TestObject() + let testObject2 = TestObject() + let message = MainActorTestMessage(payloadInt: 1, payloadString: "one") + + nonisolated(unsafe) var flag = true + + let observer = center._addObserver(name, object: testObject) { (message: MainActorTestMessage) in + flag = false + } + + center._post(name, subject: testObject2, message: message) + #expect(flag) + + center._removeObserver(observer) + } + + @Test func internalPostNotificationForValue() { + let center = NotificationCenter() + let name = "test_postNotificationForObject_name" + let message = MainActorTestMessage(payloadInt: 1, payloadString: "one") + + nonisolated(unsafe) var literal5Observed = false + nonisolated(unsafe) var literal1024Observed = false + + let literal5Observer = center._addObserver(name, object: 5) { (message: MainActorTestMessage) in + literal5Observed = true + } + let literal1024Observer = center._addObserver(name, object: 1024) { (message: MainActorTestMessage) in + literal1024Observed = true + } + + center._post(name, subject: 5, message: message) + #expect(literal5Observed) + #expect(!literal1024Observed) + + center._removeObserver(literal5Observer) + center._removeObserver(literal1024Observer) + } + + @Test func internalPostMultipleNotifications() { + let center = NotificationCenter() + let name = "test_postMultipleNotifications_name" + let message = MainActorTestMessage(payloadInt: 1, payloadString: "one") + + nonisolated(unsafe) var observer1Called = false + + let observer1 = center._addObserver(name, object: nil) { (message: MainActorTestMessage) in + observer1Called = true + } + + nonisolated(unsafe) var observer2Called = false + let observer2 = center._addObserver(name, object: nil) { (message: MainActorTestMessage) in + observer2Called = true + } + + nonisolated(unsafe) var observer3Called = false + let observer3 = center._addObserver(name, object: nil) { (message: MainActorTestMessage) in + observer3Called = true + } + + center._removeObserver(observer2) + + center._post(name, subject: nil, message: message) + #expect(observer1Called) + #expect(!observer2Called) + #expect(observer3Called) + + center._removeObserver(observer1) + center._removeObserver(observer3) + } + + @Test func internalAddObserverForNilName() { + let center = NotificationCenter() + let name = "test_addObserverForNilName_name" + let invalidName = "test_addObserverForNilName_name_invalid" + let message = MainActorTestMessage(payloadInt: 1, payloadString: "one") + + nonisolated(unsafe) var flag1 = false + let observer1 = center._addObserver(name, object: nil) { (message: MainActorTestMessage) in + flag1 = true + } + + nonisolated(unsafe) var flag2 = true + let observer2 = center._addObserver(invalidName, object: nil) { (message: MainActorTestMessage) in + flag2 = false + } + + nonisolated(unsafe) var flag3 = false + let observer3 = center._addObserver(nil, object: nil) { (message: MainActorTestMessage) in + flag3 = true + } + + center._post(name, subject: nil, message: message) + #expect(flag1) + #expect(flag2) + #expect(flag3) + + center._removeObserver(observer1) + center._removeObserver(observer2) + center._removeObserver(observer3) + } +#endif + + @MainActor + @Test func uniqueActorQueuePerCenter() { + let center1 = NotificationCenter() + let center2 = NotificationCenter() + + #expect(center1.asyncObserverQueue !== center2.asyncObserverQueue) + } +}