Refactor: Add cross-platform Lock wrapper for optimal performance

Create Lock<State> that uses OSAllocatedUnfairLock on Apple platforms (faster unfair lock) and NSLock on Linux. Also fix TupleView Equatable conformance for Swift 6 concurrency.
This commit is contained in:
phranck
2026-02-07 03:01:37 +01:00
parent 31c1c91fe0
commit 366b74e7fc
3 changed files with 84 additions and 27 deletions
+60
View File
@@ -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<State: Sendable>: @unchecked Sendable {
#if canImport(os)
private let _lock: OSAllocatedUnfairLock<State>
/// 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<R: Sendable>(_ 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<R>(_ body: (inout State) throws -> R) rethrows -> R {
_lock.lock()
defer { _lock.unlock() }
return try body(&_state)
}
#endif
}
+2 -2
View File
@@ -17,7 +17,7 @@
/// `@ViewBuilder`. Do not instantiate directly.
public struct TupleView<each V: View>: 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<each V: View>: 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<T: Equatable>(_ left: T, _ right: T) -> Bool { left == right }
var result = true
repeat result = result && isEqual(each lhs.children, each rhs.children)
+22 -25
View File
@@ -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
}
}
}