Last active
October 6, 2025 11:03
-
-
Save glukianets/74012f690a6621a43fb9496e27d2635e to your computer and use it in GitHub Desktop.
An idiomatic swift wrapper for Darwin's notification.h api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import notify | |
| import Darwin | |
| import Dispatch | |
| import Synchronization | |
| public actor DarwinNotifications<State> where State: RawRepresentable & Sendable, State.RawValue == UInt64 { | |
| private let name: String | |
| private var token: Token? | |
| public nonisolated let unownedExecutor: UnownedSerialExecutor | |
| private let queue: DispatchSerialQueue | |
| private var continuations: [CheckedContinuation<Element?, Never>] | |
| private let ordinal: Atomic<Int> = .init(-1) | |
| public init(name: String) throws(Error) { | |
| self.name = name | |
| self.queue = .init(label: "Darwin Notification Queue for \(name)") | |
| self.unownedExecutor = queue.asUnownedSerialExecutor() | |
| self.continuations = [] | |
| do { | |
| try self.queue.sync { | |
| try self.assumeIsolated { (self: isolated DarwinNotifications) in | |
| try self.resubscribe() | |
| } | |
| } | |
| } catch let error as Error { | |
| throw error | |
| } catch { | |
| fatalError("Unexpected error: \(error)") | |
| } | |
| } | |
| isolated deinit { | |
| try! self.unsubscribe() // best effort to handle errors | |
| } | |
| public static func post(_ name: String) throws where State == NoState { | |
| try name.withCString { name in | |
| let status = notify_post(name) | |
| if let error = Error(rawValue: status) { | |
| throw error | |
| } | |
| } | |
| } | |
| public func cancel() throws(Error) { | |
| try self.unsubscribe() | |
| } | |
| private func receive(_ token: Int32) { | |
| guard self.token != nil else { return } | |
| assert(self.token?.rawValue == token) | |
| let (_, newValue) = self.ordinal.add(1, ordering: .sequentiallyConsistent) | |
| self.resumeAll(with: self.element(ordinal: newValue)) | |
| } | |
| private func resumeAll(with element: Element?) { | |
| var continuations: [CheckedContinuation<Element?, Never>] = [] | |
| swap(&continuations, &self.continuations) | |
| for continuation in continuations { | |
| continuation.resume(returning: element) | |
| } | |
| } | |
| fileprivate func next(after: Int) async -> Element? { | |
| let current = self.ordinal.load(ordering: .sequentiallyConsistent) | |
| guard current <= after else { return self.element(ordinal: current) } | |
| guard self.token != nil else { return nil } | |
| return await withCheckedContinuation(isolation: self) { continuation in | |
| self.continuations.append(continuation) | |
| } | |
| } | |
| private func element(ordinal: Int) -> Element { | |
| Element( | |
| name: self.name, | |
| ordinal: ordinal, | |
| state: Result { () throws(Error) -> State? in try self.globalState } | |
| ) | |
| } | |
| private func unsubscribe() throws(Error) { | |
| guard let token else { return } | |
| defer { self.token = nil; self.resumeAll(with: nil) } | |
| if let error = Error(rawValue: notify_cancel(token.rawValue)) { | |
| throw error | |
| } | |
| } | |
| private func resubscribe() throws(Error) { | |
| do { | |
| self.token = try name.withCString { name in | |
| var rawToken: Int32 = NOTIFY_TOKEN_INVALID | |
| let rawError = notify_register_dispatch(name, &rawToken, self.queue, { [unowned self] value in | |
| self.receive(value) | |
| }) | |
| if let error = Error(rawValue: rawError) { throw error } | |
| guard let token = Token(rawValue: rawToken) else { throw Error.failed } | |
| return token | |
| } | |
| } catch let error as Error { | |
| throw error | |
| } catch { | |
| fatalError("Unexpected error: \(error)") | |
| } | |
| } | |
| } | |
| extension DarwinNotifications { | |
| public var globalState: State? { | |
| get throws(Error) { | |
| guard let token else { throw Error.invalidToken } | |
| var rawState: UInt64 = 0 | |
| let rawError = notify_get_state(token.rawValue, &rawState) | |
| if let error = Error(rawValue: rawError) { throw error } | |
| return .init(rawValue: rawState) | |
| } | |
| } | |
| public func setGlobalState(_ newValue: State) throws(Error) { | |
| guard let token else { throw Error.invalidToken } | |
| let rawError = notify_set_state(token.rawValue, newValue.rawValue) | |
| if let error = Error(rawValue: rawError) { throw error } | |
| } | |
| } | |
| extension DarwinNotifications { | |
| public struct Error: RawRepresentable & Sendable & Hashable & Swift.Error { | |
| public static var invalidName: Self { Self(NOTIFY_STATUS_INVALID_NAME) } | |
| public static var invalidToken: Self { Self(NOTIFY_STATUS_INVALID_TOKEN) } | |
| public static var invalidPort: Self { Self(NOTIFY_STATUS_INVALID_PORT) } | |
| public static var invalidFile: Self { Self(NOTIFY_STATUS_INVALID_FILE) } | |
| public static var invalidSignal: Self { Self(NOTIFY_STATUS_INVALID_SIGNAL) } | |
| public static var invalidRequest: Self { Self(NOTIFY_STATUS_INVALID_REQUEST) } | |
| public static var notAuthorized: Self { Self(NOTIFY_STATUS_NOT_AUTHORIZED) } | |
| public static var optDisable: Self { Self(NOTIFY_STATUS_OPT_DISABLE) } | |
| public static var serverNotFound: Self { Self(NOTIFY_STATUS_SERVER_NOT_FOUND) } | |
| public static var nullInput: Self { Self(NOTIFY_STATUS_NULL_INPUT) } | |
| public static var failed: Self { Self(NOTIFY_STATUS_FAILED) } | |
| public let rawValue: UInt32 | |
| public init?(rawValue: UInt32) { | |
| guard rawValue != NOTIFY_STATUS_OK else { return nil } | |
| self.rawValue = rawValue | |
| } | |
| private init(_ value: Int32) { | |
| precondition(value >= 0) | |
| self.init(rawValue: UInt32(value))! | |
| } | |
| } | |
| fileprivate struct Token: Sendable { | |
| public let rawValue: Int32 | |
| internal init?(rawValue: Int32) { | |
| guard rawValue != NOTIFY_TOKEN_INVALID else { return nil } | |
| self.rawValue = rawValue | |
| } | |
| } | |
| } | |
| extension DarwinNotifications: AsyncSequence { | |
| public struct Element: Sendable { | |
| public let name: String | |
| public let ordinal: Int | |
| public let state: Result<State?, Error> | |
| fileprivate init(name: String, ordinal: Int, state: Result<State?, Error>) { | |
| self.name = name | |
| self.ordinal = ordinal | |
| self.state = state | |
| } | |
| } | |
| public struct AsyncIterator: AsyncIteratorProtocol { | |
| public typealias Element = DarwinNotifications.Element | |
| public typealias Failure = Never | |
| private let subscription: DarwinNotifications | |
| private var ordinal: Int | |
| fileprivate init(subscription: DarwinNotifications, ordinal: Int) { | |
| self.subscription = subscription | |
| self.ordinal = ordinal | |
| } | |
| public mutating func next() async throws -> Self.Element? { | |
| guard let result = await self.subscription.next(after: self.ordinal) else { return nil } | |
| self.ordinal = result.ordinal | |
| return result | |
| } | |
| } | |
| nonisolated public func makeAsyncIterator() -> AsyncIterator { | |
| .init(subscription: self, ordinal: self.ordinal.load(ordering: .sequentiallyConsistent)) | |
| } | |
| } | |
| public struct NoState: RawRepresentable & Sendable { | |
| public typealias RawValue = UInt64 | |
| public var rawValue: UInt64 { | |
| fatalError("unreachable") | |
| } | |
| public init?(rawValue: UInt64) { | |
| nil | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment