diff --git a/Sources/TUIkit/Core/Lock.swift b/Sources/TUIkit/Core/Lock.swift new file mode 100644 index 0000000..3e77bcb --- /dev/null +++ b/Sources/TUIkit/Core/Lock.swift @@ -0,0 +1,60 @@ +// TUIKit - Terminal UI Kit for Swift +// Lock.swift +// +// Created by LAYERED.work +// License: MIT + +import Foundation + +#if canImport(os) +import os +#endif + +/// A cross-platform lock wrapper that uses the best available implementation. +/// +/// On Apple platforms, uses `OSAllocatedUnfairLock` for optimal performance +/// (unfair lock with no syscall in the uncontended case). On Linux, falls back +/// to `NSLock`. +/// +/// This type is `@unchecked Sendable` because the underlying lock implementations +/// are thread-safe by design. +final class Lock: @unchecked Sendable { + #if canImport(os) + private let _lock: OSAllocatedUnfairLock + + /// Creates a lock with the given initial state. + /// + /// - Parameter initialState: The initial protected state. + init(initialState: State) { + _lock = OSAllocatedUnfairLock(initialState: initialState) + } + + /// Executes the closure while holding the lock and returns the result. + /// + /// - Parameter body: The closure to execute with exclusive access to the state. + /// - Returns: The value returned by the closure. + func withLock(_ body: @Sendable (inout State) throws -> R) rethrows -> R { + try _lock.withLock(body) + } + #else + private let _lock = NSLock() + private var _state: State + + /// Creates a lock with the given initial state. + /// + /// - Parameter initialState: The initial protected state. + init(initialState: State) { + _state = initialState + } + + /// Executes the closure while holding the lock and returns the result. + /// + /// - Parameter body: The closure to execute with exclusive access to the state. + /// - Returns: The value returned by the closure. + func withLock(_ body: (inout State) throws -> R) rethrows -> R { + _lock.lock() + defer { _lock.unlock() } + return try body(&_state) + } + #endif +} diff --git a/Sources/TUIkit/Core/TupleViews.swift b/Sources/TUIkit/Core/TupleViews.swift index bcaa205..57d5345 100644 --- a/Sources/TUIkit/Core/TupleViews.swift +++ b/Sources/TUIkit/Core/TupleViews.swift @@ -17,7 +17,7 @@ /// `@ViewBuilder`. Do not instantiate directly. public struct TupleView: View { /// The packed child views. - nonisolated(unsafe) let children: (repeat each V) + let children: (repeat each V) /// Creates a tuple view from a parameter pack of child views. /// @@ -34,7 +34,7 @@ public struct TupleView: View { // MARK: - Equatable Conformance extension TupleView: @preconcurrency Equatable where repeat each V: Equatable { - public nonisolated static func == (lhs: TupleView, rhs: TupleView) -> Bool { + public static func == (lhs: TupleView, rhs: TupleView) -> Bool { func isEqual(_ left: T, _ right: T) -> Bool { left == right } var result = true repeat result = result && isEqual(each lhs.children, each rhs.children) diff --git a/Sources/TUIkit/State/State.swift b/Sources/TUIkit/State/State.swift index efa9d4a..8e838ae 100644 --- a/Sources/TUIkit/State/State.swift +++ b/Sources/TUIkit/State/State.swift @@ -22,15 +22,15 @@ import Foundation /// - Important: This is framework infrastructure. Prefer using ``State`` for reactive state /// management in your views. Direct use of `AppState` is only necessary in advanced scenarios /// where you manage state outside the view hierarchy. -public final class AppState: @unchecked Sendable { +public final class AppState: Sendable { + /// Internal state protected by a lock. + private struct StateData: Sendable { + var needsRender = false + var observers: [@Sendable () -> Void] = [] + } + /// Lock protecting all mutable state. - private let lock = NSLock() - - /// Whether state has changed since last render. - private var _needsRender = false - - /// Observers to notify on state change. - private var _observers: [@Sendable () -> Void] = [] + private let lock = Lock(initialState: StateData()) /// Creates a new app state instance. public init() {} @@ -48,11 +48,10 @@ public extension AppState { /// automatically detects environment changes via ``EnvironmentSnapshot`` /// comparison and clears the cache when needed. func setNeedsRender() { - let observers: [@Sendable () -> Void] - lock.lock() - _needsRender = true - observers = _observers - lock.unlock() + let observers = lock.withLock { state -> [@Sendable () -> Void] in + state.needsRender = true + return state.observers + } // Call observers outside the lock to avoid potential deadlocks for observer in observers { observer() @@ -65,32 +64,30 @@ public extension AppState { extension AppState { /// Whether state has changed since last render. var needsRender: Bool { - lock.lock() - defer { lock.unlock() } - return _needsRender + lock.withLock { $0.needsRender } } /// Registers an observer to be notified of state changes. /// /// - Parameter callback: The callback to invoke on state change. func observe(_ callback: @escaping @Sendable () -> Void) { - lock.lock() - _observers.append(callback) - lock.unlock() + lock.withLock { state in + state.observers.append(callback) + } } /// Clears all observers. func clearObservers() { - lock.lock() - _observers.removeAll() - lock.unlock() + lock.withLock { state in + state.observers.removeAll() + } } /// Resets the needs render flag. func didRender() { - lock.lock() - _needsRender = false - lock.unlock() + lock.withLock { state in + state.needsRender = false + } } }