diff --git a/CommonInternal/AtomicBool.swift b/CommonInternal/AtomicBool.swift new file mode 100644 index 0000000..2836e9b --- /dev/null +++ b/CommonInternal/AtomicBool.swift @@ -0,0 +1,28 @@ +import Foundation + +final class AtomicBool: ExpressibleByBooleanLiteral { + private let rawValue = UnsafeMutablePointer.allocate(capacity: 1) + + var value: Bool { + return rawValue.pointee == true.int32Value + } + + init(booleanLiteral value: Bool) { + rawValue.initialize(to: value.int32Value) + } + + deinit { + rawValue.deinitialize() + rawValue.deallocate(capacity: 1) + } + + func compareAndSwapBarrier(old: Bool, new: Bool) -> Bool { + return OSAtomicCompareAndSwap32Barrier(old.int32Value, new.int32Value, rawValue) + } +} + +private extension Bool { + var int32Value: Int32 { + return self ? 1 : 0 + } +} diff --git a/CommonInternal/Lock.swift b/CommonInternal/Lock.swift new file mode 100644 index 0000000..62d1dfc --- /dev/null +++ b/CommonInternal/Lock.swift @@ -0,0 +1,90 @@ +import Foundation + +/// Coordinates the operation of multiple threads of execution. +struct Lock { + @available(iOS 10.0, *) + @available(macOS 10.12, *) + @available(tvOS 10.0, *) + @available(watchOS 3.0, *) + private final class OSUnfairLock: NSLocking { + private let _lock = os_unfair_lock_t.allocate(capacity: 1) + + init() { + _lock.initialize(to: os_unfair_lock()) + } + + deinit { + _lock.deinitialize() + _lock.deallocate(capacity: 1) + } + + func lock() { + os_unfair_lock_lock(_lock) + } + + func unlock() { + os_unfair_lock_unlock(_lock) + } + } + + private final class PosixThreadMutex: NSLocking { + private let _lock = UnsafeMutablePointer.allocate(capacity: 1) + + init(recursive: Bool = false) { + _lock.initialize(to: pthread_mutex_t()) + + if recursive { + let attributes = UnsafeMutablePointer.allocate(capacity: 1) + attributes.initialize(to: pthread_mutexattr_t()) + pthread_mutexattr_init(attributes) + pthread_mutexattr_settype(attributes, Int32(PTHREAD_MUTEX_RECURSIVE)) + pthread_mutex_init(_lock, attributes) + + pthread_mutexattr_destroy(attributes) + attributes.deinitialize() + attributes.deallocate(capacity: 1) + } else { + pthread_mutex_init(_lock, nil) + } + } + + deinit { + pthread_mutex_destroy(_lock) + _lock.deinitialize() + _lock.deallocate(capacity: 1) + } + + func lock() { + pthread_mutex_lock(_lock) + } + + func unlock() { + pthread_mutex_unlock(_lock) + } + } + + private let inner: NSLocking + + /// Attempts to acquire a lock, blocking a thread’s execution until the lock can be acquired. + func lock() { + inner.lock() + } + + /// Relinquishes a previously acquired lock. + func unlock() { + inner.unlock() + } + + /// Create a lock. + /// + /// - Parameters: + /// - recursive: A Bool value indicating whether locking is recursive. + /// - usePosixThreadMutexForced: Force to use Posix thread mutex. + init(recursive: Bool, usePosixThreadMutexForced: Bool = false) { + if #available(*, iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0), !usePosixThreadMutexForced, !recursive { + inner = OSUnfairLock() + } else { + inner = PosixThreadMutex(recursive: recursive) + } + } +} diff --git a/VueFlux/Storage.swift b/CommonInternal/Storage.swift similarity index 80% rename from VueFlux/Storage.swift rename to CommonInternal/Storage.swift index 274702d..f001229 100644 --- a/VueFlux/Storage.swift +++ b/CommonInternal/Storage.swift @@ -1,11 +1,11 @@ /// Collection of values of type `Element` that to be able to remove value by key. -public struct Storage { +struct Storage { private var elements = ContiguousArray() private var keyRawValues = ContiguousArray() private var nextKey = Key.first /// Create the new, empty storage. - public init() {} + init() {} /// Add a new element. /// @@ -14,7 +14,7 @@ public struct Storage { /// /// - Returns: A key for remove given element. @discardableResult - public mutating func add(_ element: Element) -> Key { + mutating func add(_ element: Element) -> Key { let key = nextKey nextKey = key.next @@ -31,7 +31,7 @@ public struct Storage { /// /// - Returns: A removed element. @discardableResult - public mutating func remove(for key: Key) -> Element? { + mutating func remove(for key: Key) -> Element? { guard let index = indices.first(where: { keyRawValues[$0] == key.rawValue }) else { return nil } keyRawValues.remove(at: index) @@ -40,26 +40,26 @@ public struct Storage { } extension Storage: RandomAccessCollection { - public var startIndex: Int { + var startIndex: Int { return elements.startIndex } - public var endIndex: Int { + var endIndex: Int { return elements.endIndex } - public subscript(index: Int) -> Element { + subscript(index: Int) -> Element { return elements[index] } - public func makeIterator() -> IndexingIterator> { + func makeIterator() -> IndexingIterator> { return elements.makeIterator() } } -public extension Storage { +extension Storage { /// An unique key for remove element. - public struct Key { + struct Key { fileprivate typealias RawValue = UInt64 fileprivate let rawValue: RawValue diff --git a/Configurations/Universal.xcconfig b/Configurations/Common.xcconfig similarity index 100% rename from Configurations/Universal.xcconfig rename to Configurations/Common.xcconfig diff --git a/Tests/AtomicBoolTests.swift b/Tests/AtomicBoolTests.swift new file mode 100644 index 0000000..f9e3024 --- /dev/null +++ b/Tests/AtomicBoolTests.swift @@ -0,0 +1,35 @@ +import XCTest +@testable import VueFlux +@testable import VueFluxReactive + +protocol AtomicBoolProtocol: ExpressibleByBooleanLiteral { + var value: Bool { get } + + func compareAndSwapBarrier(old: Bool, new: Bool) -> Bool +} + +extension VueFlux.AtomicBool: AtomicBoolProtocol {} +extension VueFluxReactive.AtomicBool: AtomicBoolProtocol {} + +final class AtomicBoolTests: XCTestCase { + func testAtomicBool() { + func runTest(for type: AtomicBool.Type) { + let atomicBool: AtomicBool = true + + XCTAssertTrue(atomicBool.value) + + let result1 = atomicBool.compareAndSwapBarrier(old: true, new: false) + + XCTAssertFalse(atomicBool.value) + XCTAssertTrue(result1) + + let result2 = atomicBool.compareAndSwapBarrier(old: true, new: false) + + XCTAssertFalse(atomicBool.value) + XCTAssertFalse(result2) + } + + runTest(for: VueFlux.AtomicBool.self) + runTest(for: VueFluxReactive.AtomicBool.self) + } +} diff --git a/Tests/AtomicReferenceTests.swift b/Tests/AtomicReferenceTests.swift index 9268114..c03976b 100644 --- a/Tests/AtomicReferenceTests.swift +++ b/Tests/AtomicReferenceTests.swift @@ -124,25 +124,4 @@ final class AtomicReferenceTests: XCTestCase { _ = group.wait(timeout: .now() + 10) XCTAssertEqual(atomicReference.value, 0) } - - func testPosixThreadMutex() { - let atomicReference = AtomicReference(0, usePosixThreadMutexForced: true, recursive: false) - - let queue = DispatchQueue(label: "testPosixThreadMutex", attributes: .concurrent) - let group = DispatchGroup() - - for _ in (1...100) { - queue.async(group: group) { - atomicReference.modify { value in - value += 1 - value -= 1 - value += 2 - value -= 2 - } - } - } - - _ = group.wait(timeout: .now() + 10) - XCTAssertEqual(atomicReference.value, 0) - } } diff --git a/Tests/ExecutorTests.swift b/Tests/ExecutorTests.swift index c561087..4ef09db 100644 --- a/Tests/ExecutorTests.swift +++ b/Tests/ExecutorTests.swift @@ -173,6 +173,8 @@ final class ExecutorTests: XCTestCase { queue.resume() + workItem.cancel() + queue.async(execute: expectation.fulfill) waitForExpectations(timeout: 1) { _ in diff --git a/Tests/LockTests.swift b/Tests/LockTests.swift new file mode 100644 index 0000000..a271161 --- /dev/null +++ b/Tests/LockTests.swift @@ -0,0 +1,63 @@ +import XCTest +@testable import VueFlux +@testable import VueFluxReactive + +protocol LockProtocol { + func lock() + func unlock() +} + +extension VueFlux.Lock: LockProtocol {} +extension VueFluxReactive.Lock: LockProtocol {} + +final class LockTests: XCTestCase { + func testLock() { + func runTest(synchronized: @escaping (() -> Void) -> Void) { + let queue = DispatchQueue(label: "testLock", attributes: .concurrent) + let group = DispatchGroup() + + var value = 0 + for _ in (1...100) { + queue.async(group: group) { + synchronized { + value += 1 + value -= 1 + value += 2 + value -= 2 + } + } + } + + _ = group.wait(timeout: .now() + 10) + XCTAssertEqual(value, 0) + } + + func runNonRecursiveTest(for lock: Lock) { + runTest { criticalSection in + lock.lock() + criticalSection() + lock.unlock() + } + } + + func runRecursiveTest(for lock: Lock) { + runTest { criticalSection in + lock.lock() + lock.lock() + criticalSection() + lock.unlock() + lock.unlock() + } + } + + runNonRecursiveTest(for: VueFlux.Lock(recursive: false)) + runNonRecursiveTest(for: VueFluxReactive.Lock(recursive: false)) + runNonRecursiveTest(for: VueFlux.Lock(recursive: false, usePosixThreadMutexForced: true)) + runNonRecursiveTest(for: VueFluxReactive.Lock(recursive: false, usePosixThreadMutexForced: true)) + + runRecursiveTest(for: VueFlux.Lock(recursive: true)) + runRecursiveTest(for: VueFluxReactive.Lock(recursive: true)) + runRecursiveTest(for: VueFlux.Lock(recursive: true, usePosixThreadMutexForced: true)) + runRecursiveTest(for: VueFluxReactive.Lock(recursive: true, usePosixThreadMutexForced: true)) + } +} diff --git a/Tests/StorageTests.swift b/Tests/StorageTests.swift index da0b4f9..434930a 100644 --- a/Tests/StorageTests.swift +++ b/Tests/StorageTests.swift @@ -1,47 +1,64 @@ import XCTest @testable import VueFlux +@testable import VueFluxReactive + +private protocol StorageProtocol: RandomAccessCollection { + associatedtype Key + + mutating func add(_ element: Element) -> Key + mutating func remove(for key: Key) -> Element? +} + +extension VueFlux.Storage: StorageProtocol {} +extension VueFluxReactive.Storage: StorageProtocol {} final class StorageTests: XCTestCase { func testStorage() { - var storage = Storage() - - let additionValue1 = 1 - let additionValue2 = 2 - - let key1 = storage.add(additionValue1) - let key2 = storage.add(additionValue2) - - XCTAssertEqual(storage[0], additionValue1) - XCTAssertEqual(storage[1], additionValue2) - - var targetValue1 = 0 - for additionValue in storage { - targetValue1 += additionValue - } - - XCTAssertEqual(targetValue1, additionValue1 + additionValue2) - - let removed1 = storage.remove(for: key1) - XCTAssertEqual(removed1, additionValue1) - - var targetValue2 = 0 - for additionValue in storage { - targetValue2 += additionValue - } - - XCTAssertEqual(targetValue2, additionValue2) - - let removed2 = storage.remove(for: key2) - XCTAssertEqual(removed2, additionValue2) - - var targetValue3 = 0 - for additionValue in storage { - targetValue3 += additionValue + func runTest(for storage: inout Storage) where Storage.Element == Int, Storage.Index == Int { + let additionValue1 = 1 + let additionValue2 = 2 + + let key1 = storage.add(additionValue1) + let key2 = storage.add(additionValue2) + + XCTAssertEqual(storage[0], additionValue1) + XCTAssertEqual(storage[1], additionValue2) + + var targetValue1 = 0 + for additionValue in storage { + targetValue1 += additionValue + } + + XCTAssertEqual(targetValue1, additionValue1 + additionValue2) + + let removed1 = storage.remove(for: key1) + XCTAssertEqual(removed1, additionValue1) + + var targetValue2 = 0 + for additionValue in storage { + targetValue2 += additionValue + } + + XCTAssertEqual(targetValue2, additionValue2) + + let removed2 = storage.remove(for: key2) + XCTAssertEqual(removed2, additionValue2) + + var targetValue3 = 0 + for additionValue in storage { + targetValue3 += additionValue + } + + let removed3 = storage.remove(for: key2) + XCTAssertNil(removed3) + + XCTAssertEqual(targetValue3, 0) } - let removed3 = storage.remove(for: key2) - XCTAssertNil(removed3) + var vueFluxStorage = VueFlux.Storage() + var vueFluxReactiveStorage = VueFluxReactive.Storage() - XCTAssertEqual(targetValue3, 0) + runTest(for: &vueFluxStorage) + runTest(for: &vueFluxReactiveStorage) } } diff --git a/VueFlux.podspec b/VueFlux.podspec index c0eee8f..43a943a 100644 --- a/VueFlux.podspec +++ b/VueFlux.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |spec| spec.name = 'VueFlux' - spec.version = '1.2.0' + spec.version = '1.3.0' spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' } spec.homepage = 'https://github.com/ra1028/VueFlux' spec.summary = 'Unidirectional State Management for Swift - Inspired by Vuex and Flux' spec.source = { :git => 'https://github.com/ra1028/VueFlux.git', :tag => spec.version.to_s } spec.license = { :type => 'MIT', :file => 'LICENSE' } - spec.source_files = 'VueFlux/**/*.swift' + spec.source_files = 'VueFlux/**/*.swift', 'CommonInternal/**/*.swift' spec.requires_arc = true spec.osx.deployment_target = '10.9' spec.ios.deployment_target = '9.0' diff --git a/VueFlux.xcodeproj/project.pbxproj b/VueFlux.xcodeproj/project.pbxproj index fb5b534..ead6fcc 100644 --- a/VueFlux.xcodeproj/project.pbxproj +++ b/VueFlux.xcodeproj/project.pbxproj @@ -9,12 +9,19 @@ /* Begin PBXBuildFile section */ 6B008F47202490C400F93B32 /* BinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B008F46202490C400F93B32 /* BinderTests.swift */; }; 6B052E141FCB4EE2008B3731 /* Binder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B052E131FCB4EE2008B3731 /* Binder.swift */; }; + 6B14405920453C4300E0B3FF /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B14405820453C3D00E0B3FF /* Storage.swift */; }; + 6B14405A20453C4400E0B3FF /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B14405820453C3D00E0B3FF /* Storage.swift */; }; + 6B41947A2045B72300C40218 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4194792045B72300C40218 /* Lock.swift */; }; + 6B41947B2045B72800C40218 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4194792045B72300C40218 /* Lock.swift */; }; + 6B41947D2045B7A500C40218 /* LockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B41947C2045B7A500C40218 /* LockTests.swift */; }; 6B665528200AE8A000883C12 /* ExecutorWorkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */; }; + 6B730BC82045E9280059D851 /* AtomicBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B730BC72045E9280059D851 /* AtomicBool.swift */; }; + 6B730BC92045E9480059D851 /* AtomicBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B730BC72045E9280059D851 /* AtomicBool.swift */; }; + 6B730BCB2045E9620059D851 /* AtomicBoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */; }; 6B9E328B1FA639DF000B24D4 /* VueFlux.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B9E32811FA639DF000B24D4 /* VueFlux.framework */; }; 6B9EB21D1FCB14ED009F0659 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9EB2151FCB14BF009F0659 /* Dispatcher.swift */; }; 6B9EB21E1FCB14ED009F0659 /* DispatcherContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9EB2161FCB14BF009F0659 /* DispatcherContext.swift */; }; 6B9EB2241FCB14ED009F0659 /* VueFlux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9EB21C1FCB14BF009F0659 /* VueFlux.swift */; }; - 6B9EB23E1FCB1CFB009F0659 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9EB23C1FCB1CFB009F0659 /* Storage.swift */; }; 6B9EB23F1FCB1CFB009F0659 /* AtomicReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9EB23D1FCB1CFB009F0659 /* AtomicReference.swift */; }; 6B9EB2431FCB1FF8009F0659 /* VueFluxReactive.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B9EB2361FCB157C009F0659 /* VueFluxReactive.framework */; }; 6B9EB2441FCB203B009F0659 /* VueFluxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE63FFE1FB3A6D8007F8B5D /* VueFluxTests.swift */; }; @@ -67,8 +74,12 @@ /* Begin PBXFileReference section */ 6B008F46202490C400F93B32 /* BinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinderTests.swift; sourceTree = ""; }; 6B052E131FCB4EE2008B3731 /* Binder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binder.swift; sourceTree = ""; }; - 6B120DC81FD5B52D001FEAB4 /* Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Universal.xcconfig; sourceTree = ""; }; + 6B14405820453C3D00E0B3FF /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + 6B4194792045B72300C40218 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; + 6B41947C2045B7A500C40218 /* LockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockTests.swift; sourceTree = ""; }; 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutorWorkItem.swift; sourceTree = ""; }; + 6B730BC72045E9280059D851 /* AtomicBool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicBool.swift; sourceTree = ""; }; + 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicBoolTests.swift; sourceTree = ""; }; 6B9E32811FA639DF000B24D4 /* VueFlux.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VueFlux.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6B9E328A1FA639DF000B24D4 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 6B9E329F1FA63B07000B24D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -80,7 +91,6 @@ 6B9EB2391FCB1611009F0659 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6B9EB23A1FCB1C87009F0659 /* Disposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disposable.swift; sourceTree = ""; }; 6B9EB23B1FCB1C88009F0659 /* DisposableScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposableScope.swift; sourceTree = ""; }; - 6B9EB23C1FCB1CFB009F0659 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 6B9EB23D1FCB1CFB009F0659 /* AtomicReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicReference.swift; sourceTree = ""; }; 6B9EB2561FCB2FEE009F0659 /* SinkSignalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinkSignalTests.swift; sourceTree = ""; }; 6B9EB25C1FCB3D81009F0659 /* VariableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = ""; }; @@ -91,6 +101,7 @@ 6BB30C4E2018E3CA00C52C76 /* Signal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signal.swift; sourceTree = ""; }; 6BB30C502018E3E700C52C76 /* Variable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = ""; }; 6BB30C522018E41600C52C76 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; + 6BB5BAF92044353A0009F779 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; 6BC664F41FEC5ABF00BD74C8 /* AnyDisposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDisposable.swift; sourceTree = ""; }; 6BC664F61FEC627E00BD74C8 /* SignalOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalOperators.swift; sourceTree = ""; }; 6BC665321FF032C500BD74C8 /* Executor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Executor.swift; sourceTree = ""; }; @@ -133,16 +144,27 @@ 6B120DC71FD5B4CB001FEAB4 /* Configurations */ = { isa = PBXGroup; children = ( - 6B120DC81FD5B52D001FEAB4 /* Universal.xcconfig */, + 6BB5BAF92044353A0009F779 /* Common.xcconfig */, ); path = Configurations; sourceTree = ""; }; + 6B14405720453C3D00E0B3FF /* CommonInternal */ = { + isa = PBXGroup; + children = ( + 6B14405820453C3D00E0B3FF /* Storage.swift */, + 6B4194792045B72300C40218 /* Lock.swift */, + 6B730BC72045E9280059D851 /* AtomicBool.swift */, + ); + path = CommonInternal; + sourceTree = ""; + }; 6B9E32771FA639DF000B24D4 = { isa = PBXGroup; children = ( 6B9EB2121FCB14BF009F0659 /* VueFlux */, 6B9EB2381FCB1611009F0659 /* VueFluxReactive */, + 6B14405720453C3D00E0B3FF /* CommonInternal */, 6B9E329E1FA63B07000B24D4 /* Tests */, 6B120DC71FD5B4CB001FEAB4 /* Configurations */, 6B9E32821FA639DF000B24D4 /* Products */, @@ -167,7 +189,6 @@ 6BE63FF91FB39EA6007F8B5D /* ExecutorTests.swift */, 6BE63FF61FB39CDD007F8B5D /* DispatcherTests.swift */, 6BE63FF31FB39A3C007F8B5D /* DispatcherContextTests.swift */, - 6BA27BD31FB8CFE600809472 /* StorageTests.swift */, 6BE63FF01FB39446007F8B5D /* AtomicReferenceTests.swift */, 6B9EB2561FCB2FEE009F0659 /* SinkSignalTests.swift */, 6B9EB25C1FCB3D81009F0659 /* VariableTests.swift */, @@ -175,6 +196,9 @@ 6B008F46202490C400F93B32 /* BinderTests.swift */, 6BA27BCB1FB8CD3100809472 /* DisposableTests.swift */, 6BA27BCE1FB8CE1A00809472 /* DisposableScopeTests.swift */, + 6BA27BD31FB8CFE600809472 /* StorageTests.swift */, + 6B41947C2045B7A500C40218 /* LockTests.swift */, + 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */, 6B9E329F1FA63B07000B24D4 /* Info.plist */, ); path = Tests; @@ -186,7 +210,6 @@ 6B9EB21C1FCB14BF009F0659 /* VueFlux.swift */, 6BC665321FF032C500BD74C8 /* Executor.swift */, 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */, - 6B9EB23C1FCB1CFB009F0659 /* Storage.swift */, 6B9EB23D1FCB1CFB009F0659 /* AtomicReference.swift */, 6B9EB2141FCB14BF009F0659 /* Internal */, 6B9EB2131FCB14BF009F0659 /* Info.plist */, @@ -417,13 +440,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6B730BC82045E9280059D851 /* AtomicBool.swift in Sources */, 6B9EB21D1FCB14ED009F0659 /* Dispatcher.swift in Sources */, + 6B14405920453C4300E0B3FF /* Storage.swift in Sources */, 6B9EB21E1FCB14ED009F0659 /* DispatcherContext.swift in Sources */, 6B665528200AE8A000883C12 /* ExecutorWorkItem.swift in Sources */, 6B9EB2241FCB14ED009F0659 /* VueFlux.swift in Sources */, 6B9EB23F1FCB1CFB009F0659 /* AtomicReference.swift in Sources */, + 6B41947A2045B72300C40218 /* Lock.swift in Sources */, 6BC665331FF032C500BD74C8 /* Executor.swift in Sources */, - 6B9EB23E1FCB1CFB009F0659 /* Storage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -431,9 +456,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6B41947D2045B7A500C40218 /* LockTests.swift in Sources */, 6B9EB2441FCB203B009F0659 /* VueFluxTests.swift in Sources */, 6B9EB2451FCB203B009F0659 /* ExecutorTests.swift in Sources */, 6B9EB2571FCB2FEE009F0659 /* SinkSignalTests.swift in Sources */, + 6B730BCB2045E9620059D851 /* AtomicBoolTests.swift in Sources */, 6B9EB2461FCB203B009F0659 /* DisposableTests.swift in Sources */, 6BEB90131FCF2340001B9BF6 /* ConstantTests.swift in Sources */, 6B008F47202490C400F93B32 /* BinderTests.swift in Sources */, @@ -451,14 +478,17 @@ buildActionMask = 2147483647; files = ( 6B9EB24C1FCB2078009F0659 /* Disposable.swift in Sources */, + 6B14405A20453C4400E0B3FF /* Storage.swift in Sources */, 6BB30C4F2018E3CA00C52C76 /* Signal.swift in Sources */, 6BC664F71FEC627E00BD74C8 /* SignalOperators.swift in Sources */, 6BB30C4D2018E3AC00C52C76 /* Sink.swift in Sources */, 6BB30C512018E3E700C52C76 /* Variable.swift in Sources */, 6BC664F51FEC5ABF00BD74C8 /* AnyDisposable.swift in Sources */, + 6B730BC92045E9480059D851 /* AtomicBool.swift in Sources */, 6B052E141FCB4EE2008B3731 /* Binder.swift in Sources */, 6BEB901A1FCF2DB2001B9BF6 /* AssociatedDisposableScope.swift in Sources */, 6B9EB24D1FCB2078009F0659 /* DisposableScope.swift in Sources */, + 6B41947B2045B72800C40218 /* Lock.swift in Sources */, 6BB30C532018E41600C52C76 /* Constant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -610,7 +640,7 @@ }; 6B9E32961FA639DF000B24D4 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6B120DC81FD5B52D001FEAB4 /* Universal.xcconfig */; + baseConfigurationReference = 6BB5BAF92044353A0009F779 /* Common.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = "$(inherited)"; CODE_SIGN_IDENTITY = ""; @@ -641,7 +671,7 @@ }; 6B9E32971FA639DF000B24D4 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6B120DC81FD5B52D001FEAB4 /* Universal.xcconfig */; + baseConfigurationReference = 6BB5BAF92044353A0009F779 /* Common.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = "$(inherited)"; CODE_SIGN_IDENTITY = ""; @@ -651,6 +681,7 @@ DYLIB_COMPATIBILITY_VERSION = "$(inherited)"; DYLIB_CURRENT_VERSION = "$(inherited)"; DYLIB_INSTALL_NAME_BASE = "$(inherited)"; + ENABLE_TESTABILITY = NO; INFOPLIST_FILE = "$(SRCROOT)/VueFlux/Info.plist"; INSTALL_PATH = "$(inherited)"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -722,7 +753,7 @@ }; 6B9EB2341FCB157C009F0659 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6B120DC81FD5B52D001FEAB4 /* Universal.xcconfig */; + baseConfigurationReference = 6BB5BAF92044353A0009F779 /* Common.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = "$(inherited)"; CODE_SIGN_IDENTITY = ""; @@ -753,7 +784,7 @@ }; 6B9EB2351FCB157C009F0659 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6B120DC81FD5B52D001FEAB4 /* Universal.xcconfig */; + baseConfigurationReference = 6BB5BAF92044353A0009F779 /* Common.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = "$(inherited)"; CODE_SIGN_IDENTITY = ""; @@ -763,6 +794,7 @@ DYLIB_COMPATIBILITY_VERSION = "$(inherited)"; DYLIB_CURRENT_VERSION = "$(inherited)"; DYLIB_INSTALL_NAME_BASE = "$(inherited)"; + ENABLE_TESTABILITY = NO; INFOPLIST_FILE = "$(SRCROOT)/VueFlux/Info.plist"; INSTALL_PATH = "$(inherited)"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; diff --git a/VueFlux/AtomicReference.swift b/VueFlux/AtomicReference.swift index c2739bc..1ba97e2 100644 --- a/VueFlux/AtomicReference.swift +++ b/VueFlux/AtomicReference.swift @@ -1,5 +1,3 @@ -import Foundation - /// A value reference that may be updated atomically. public final class AtomicReference { /// Atomically value getter and setter. @@ -13,23 +11,13 @@ public final class AtomicReference { /// - Parameters: /// - value: Initial value. /// - recursive: A Bool value indicating whether locking is recursive. - public convenience init(_ value: Value, recursive: Bool = false) { - self.init(value, usePosixThreadMutexForced: false, recursive: recursive) - } - - /// For testability. - init(_ value: Value, usePosixThreadMutexForced: Bool, recursive: Bool) { + public init(_ value: Value, recursive: Bool = false) { _value = value - - if #available(*, iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0), !usePosixThreadMutexForced, !recursive { - lock = OSUnfairLock() - } else { - lock = PosixThreadMutex(recursive: recursive) - } + lock = .init(recursive: recursive) } private var _value: Value - private let lock: NSLocking + private let lock: Lock /// Atomically perform an arbitrary function using the current value. /// @@ -72,66 +60,3 @@ public final class AtomicReference { } } } - -private extension AtomicReference { - @available(iOS 10.0, *) - @available(macOS 10.12, *) - @available(tvOS 10.0, *) - @available(watchOS 3.0, *) - final class OSUnfairLock: NSLocking { - private let _lock = os_unfair_lock_t.allocate(capacity: 1) - - init() { - _lock.initialize(to: os_unfair_lock()) - } - - deinit { - _lock.deinitialize() - _lock.deallocate(capacity: 1) - } - - func lock() { - os_unfair_lock_lock(_lock) - } - - func unlock() { - os_unfair_lock_unlock(_lock) - } - } - - final class PosixThreadMutex: NSLocking { - private let _lock = UnsafeMutablePointer.allocate(capacity: 1) - - init(recursive: Bool = false) { - _lock.initialize(to: pthread_mutex_t()) - - if recursive { - let attributes = UnsafeMutablePointer.allocate(capacity: 1) - attributes.initialize(to: pthread_mutexattr_t()) - pthread_mutexattr_init(attributes) - pthread_mutexattr_settype(attributes, Int32(PTHREAD_MUTEX_RECURSIVE)) - pthread_mutex_init(_lock, attributes) - - pthread_mutexattr_destroy(attributes) - attributes.deinitialize() - attributes.deallocate(capacity: 1) - } else { - pthread_mutex_init(_lock, nil) - } - } - - deinit { - pthread_mutex_destroy(_lock) - _lock.deinitialize() - _lock.deallocate(capacity: 1) - } - - func lock() { - pthread_mutex_lock(_lock) - } - - func unlock() { - pthread_mutex_unlock(_lock) - } - } -} diff --git a/VueFlux/ExecutorWorkItem.swift b/VueFlux/ExecutorWorkItem.swift index e42fffa..fcb67b2 100644 --- a/VueFlux/ExecutorWorkItem.swift +++ b/VueFlux/ExecutorWorkItem.swift @@ -1,26 +1,21 @@ public extension Executor { /// Encapsulates the function to execute by Executor. /// Also be able to cancel its execution. - public struct WorkItem { - private enum State { - case active(function: (Value) -> Void) - case canceled - } - - private let state: AtomicReference - + public final class WorkItem { /// A Bool value indicating whether canceled. public var isCanceled: Bool { - guard case .canceled = state.value else { return false } - return true + return _isCanceled.value } + private let _isCanceled: AtomicBool = false + private var _execute: ((Value) -> Void)? + /// Create with an arbitrary function. /// /// - Parameters: - /// - function: A function to be executed by calling `execute(with:)` until canceled. - public init(_ function: @escaping (Value) -> Void) { - state = .init(.active(function: function)) + /// - execute: A function to be executed by calling `execute(with:)` until canceled. + public init(_ execute: @escaping (Value) -> Void) { + _execute = execute } /// Synchronously execute the specified function. @@ -28,14 +23,15 @@ public extension Executor { /// - Parameters: /// - value: A value to be pass to specified function. public func execute(with value: @autoclosure () -> Value) { - guard case let .active(function) = state.value else { return } - function(value()) + guard !isCanceled, let execute = _execute else { return } + execute(value()) } /// Cancel the specified function. /// Cancellation does not affect any execution of the function that is already in progress. public func cancel() { - state.value = .canceled + guard _isCanceled.compareAndSwapBarrier(old: false, new: true) else { return } + _execute = nil } } } diff --git a/VueFluxReactive.podspec b/VueFluxReactive.podspec index 9539464..8ecabbc 100644 --- a/VueFluxReactive.podspec +++ b/VueFluxReactive.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |spec| spec.name = 'VueFluxReactive' - spec.version = '1.2.0' + spec.version = '1.3.0' spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' } spec.homepage = 'https://github.com/ra1028/VueFlux' spec.summary = 'Reactive system for VueFlux architecture in Swift' spec.source = { :git => 'https://github.com/ra1028/VueFlux.git', :tag => spec.version.to_s } spec.license = { :type => 'MIT', :file => 'LICENSE' } - spec.source_files = 'VueFluxReactive/**/*.swift' + spec.source_files = 'VueFluxReactive/**/*.swift', 'CommonInternal/**/*.swift' spec.dependency 'VueFlux', '~> 1.2.0' spec.requires_arc = true spec.osx.deployment_target = '10.9' diff --git a/VueFluxReactive/AnyDisposable.swift b/VueFluxReactive/AnyDisposable.swift index 8c67c80..351b5e5 100644 --- a/VueFluxReactive/AnyDisposable.swift +++ b/VueFluxReactive/AnyDisposable.swift @@ -1,31 +1,27 @@ import VueFlux /// Disposable that consist of any function. -public struct AnyDisposable: Disposable { - private enum State { - case active(dispose: () -> Void) - case disposed - } - - private let state: AtomicReference - +public final class AnyDisposable: Disposable { /// A Bool value indicating whether disposed. public var isDisposed: Bool { - guard case .disposed = state.value else { return false } - return true + return _isDisposed.value } + private let _isDisposed: AtomicBool = false + private var _dispose: (() -> Void)? + /// Create with dispose function. /// /// - Parameters: /// - dispose: A function to run when disposed. public init(_ dispose: @escaping () -> Void) { - state = .init(.active(dispose: dispose)) + _dispose = dispose } /// Dispose if not already been disposed. public func dispose() { - guard case let .active(dispose) = state.swap(.disposed) else { return } - dispose() + guard _isDisposed.compareAndSwapBarrier(old: false, new: true) else { return } + _dispose?() + _dispose = nil } } diff --git a/VueFluxReactive/DisposableScope.swift b/VueFluxReactive/DisposableScope.swift index 6e1d868..e641b4a 100644 --- a/VueFluxReactive/DisposableScope.swift +++ b/VueFluxReactive/DisposableScope.swift @@ -3,18 +3,13 @@ import VueFlux /// A container that automatically dispose all added disposables when deinitialized. /// Itself also behaves as Disposable. public final class DisposableScope: Disposable { - private enum State { - case active(disposables: ContiguousArray) - case disposed - } - /// A Bool value indicating whether disposed. public var isDisposed: Bool { - guard case .disposed = state.value else { return false } - return true + return _isDisposed.value } - private let state: AtomicReference + private let _isDisposed: AtomicBool = false + private var disposables: ContiguousArray? deinit { dispose() @@ -25,7 +20,7 @@ public final class DisposableScope: Disposable { /// - Parameters: /// - disposables: Sequence of something conformed to Disposable. public init(_ disposebles: Sequence) where Sequence.Element == Disposable { - state = .init(.active(disposables: .init(disposebles))) + self.disposables = .init(.init(disposebles)) } /// Initialize a new, empty DisposableScope. @@ -33,26 +28,19 @@ public final class DisposableScope: Disposable { self.init([]) } - /// Add a new Disposable to a scope. + /// Add a new Disposable to a scope if not already disposed. /// /// - Parameters: /// - disposable: A disposable to be add to scope. public func add(disposable: Disposable) { - guard !disposable.isDisposed else { return } - - state.modify { state in - guard case var .active(disposables) = state else { - disposable.dispose() - return - } - disposables.append(disposable) - state = .active(disposables: disposables) - } + guard !isDisposed, !disposable.isDisposed else { return disposable.dispose() } + disposables?.append(disposable) } /// Dispose all disposables if not already been disposed. public func dispose() { - guard case let .active(disposables) = state.swap(.disposed) else { return } + guard _isDisposed.compareAndSwapBarrier(old: false, new: true), let disposables = disposables else { return } + self.disposables = nil for disposable in disposables { disposable.dispose() diff --git a/VueFluxReactive/Sink.swift b/VueFluxReactive/Sink.swift index bc45dc5..64f14ae 100644 --- a/VueFluxReactive/Sink.swift +++ b/VueFluxReactive/Sink.swift @@ -18,7 +18,7 @@ public final class Sink { } private let observers = AtomicReference(Storage<(Value) -> Void>()) - private let sendLock = AtomicReference(()) + private let sendLock = Lock(recursive: false) /// Initialize a sink. public init() {} @@ -28,10 +28,11 @@ public final class Sink { /// - Parameters: /// - value: A value to send to the signal. public func send(value: Value) { - sendLock.synchronized { - for observer in observers.value { - observer(value) - } + sendLock.lock() + defer { sendLock.unlock() } + + for observer in observers.value { + observer(value) } } }