From e71effde85d67420b8b010aacc4f3d4ddb77dbbd Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 12:51:14 +0900 Subject: [PATCH 1/9] Add test --- Tests/SinkSignalTests.swift | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Tests/SinkSignalTests.swift b/Tests/SinkSignalTests.swift index e8122e4..1428f0d 100644 --- a/Tests/SinkSignalTests.swift +++ b/Tests/SinkSignalTests.swift @@ -52,6 +52,35 @@ final class SinkSignalTests: XCTestCase { XCTAssertEqual(value, 1) } + func testImmediatelyDisposeSignal() { + let queue = DispatchQueue(label: "testImmediatelyDisposeSignal") + + let signal = Signal { send in + queue.async { + send(()) + } + return AnyDisposable {} + } + + queue.suspend() + + let disposable = signal.observe { + XCTFail() + } + + disposable.dispose() + + let expectation = self.expectation(description: "testImmediatelyDisposeSignal") + + queue.resume() + + queue.async { + expectation.fulfill() + } + + waitForExpectations(timeout: 1) { _ in } + } + func testDisposeOnObserving() { let sink = Sink() let signal = sink.signal From 893fba0302d186fb1581ecc72a3861c2eef5e7fe Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 12:52:12 +0900 Subject: [PATCH 2/9] No longer execute disposed producer of Signal --- VueFluxReactive/Signal.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/VueFluxReactive/Signal.swift b/VueFluxReactive/Signal.swift index 78c7681..8bf7239 100644 --- a/VueFluxReactive/Signal.swift +++ b/VueFluxReactive/Signal.swift @@ -22,7 +22,15 @@ public struct Signal { /// - Returns: A disposable to unregister given observer. @discardableResult public func observe(_ observer: @escaping (Value) -> Void) -> Disposable { - return producer(observer) + let workItem = Executor.WorkItem(observer) + let disposable = producer { value in + workItem.execute(with: value) + } + + return AnyDisposable { + workItem.cancel() + disposable.dispose() + } } /// observe the values to the given observer during during scope of specified object. From 8287062438349c7b8fd6d6085b571ba8341fbb09 Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 12:56:35 +0900 Subject: [PATCH 3/9] Add initializer of nop AnyDisposable --- VueFluxReactive/AnyDisposable.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/VueFluxReactive/AnyDisposable.swift b/VueFluxReactive/AnyDisposable.swift index 351b5e5..d2d9679 100644 --- a/VueFluxReactive/AnyDisposable.swift +++ b/VueFluxReactive/AnyDisposable.swift @@ -10,7 +10,7 @@ public final class AnyDisposable: Disposable { private let _isDisposed: AtomicBool = false private var _dispose: (() -> Void)? - /// Create with dispose function. + /// Initialize with dispose function. /// /// - Parameters: /// - dispose: A function to run when disposed. @@ -18,6 +18,11 @@ public final class AnyDisposable: Disposable { _dispose = dispose } + /// Initialize a nop disposable. + public init() { + _dispose = nil + } + /// Dispose if not already been disposed. public func dispose() { guard _isDisposed.compareAndSwapBarrier(old: false, new: true) else { return } From 7be6a4bc877b5fab2b37bc6baa5b38bd6232e584 Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 12:57:50 +0900 Subject: [PATCH 4/9] Add test --- Tests/DisposableTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/DisposableTests.swift b/Tests/DisposableTests.swift index 39cb781..435d4b0 100644 --- a/Tests/DisposableTests.swift +++ b/Tests/DisposableTests.swift @@ -21,4 +21,15 @@ final class DisposableTests: XCTestCase { XCTAssertEqual(value, 1) XCTAssertEqual(disposable.isDisposed, true) } + + func testNopDisposable() { + let disposable = AnyDisposable() + + XCTAssertEqual(disposable.isDisposed, false) + + disposable.dispose() + + XCTAssertEqual(disposable.isDisposed, true) + + } } From 2eb7754fd331669281a97c2eb805a4262a13e838 Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 13:09:20 +0900 Subject: [PATCH 5/9] Refactor --- VueFluxReactive/Signal.swift | 2 +- VueFluxReactive/SignalOperators.swift | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/VueFluxReactive/Signal.swift b/VueFluxReactive/Signal.swift index 8bf7239..74f63ff 100644 --- a/VueFluxReactive/Signal.swift +++ b/VueFluxReactive/Signal.swift @@ -23,7 +23,7 @@ public struct Signal { @discardableResult public func observe(_ observer: @escaping (Value) -> Void) -> Disposable { let workItem = Executor.WorkItem(observer) - let disposable = producer { value in + let disposable = producer { value in workItem.execute(with: value) } diff --git a/VueFluxReactive/SignalOperators.swift b/VueFluxReactive/SignalOperators.swift index e096512..395b992 100644 --- a/VueFluxReactive/SignalOperators.swift +++ b/VueFluxReactive/SignalOperators.swift @@ -8,10 +8,8 @@ public extension Signal { /// /// - Returns: A signal to be receives new values. public func map(_ transform: @escaping (Value) -> T) -> Signal { - return .init { send in - self.observe { value in - send(transform(value)) - } + return operated { value, send in + send(transform(value)) } } @@ -22,16 +20,17 @@ public extension Signal { /// /// - returns: A signal that will forward values on given executor. public func observe(on executor: Executor) -> Signal { + return operated { value, send in + executor.execute { send(value) } + } + } +} + +private extension Signal { + func operated(_ operation: @escaping (Value, @escaping (T) -> Void) -> Void) -> Signal { return .init { send in - let workItem = Executor.WorkItem(send) - - let disposable = self.observe { value in - executor.execute(workItem: workItem, with: value) - } - - return AnyDisposable { - workItem.cancel() - disposable.dispose() + self.observe { value in + operation(value, send) } } } From f1763f70f838738da29f6fdae5e674caf90ec2a4 Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 19:17:54 +0900 Subject: [PATCH 6/9] Executor.WorkItem now renamed to CancelableProcedure --- Tests/CancelableProcedureTests.swift | 85 ++++++++++++++++++++++++ Tests/ExecutorTests.swift | 98 ---------------------------- Tests/SinkSignalTests.swift | 4 +- VueFlux.xcodeproj/project.pbxproj | 12 ++-- VueFlux/CancelableProcedure.swift | 43 ++++++++++++ VueFlux/Executor.swift | 17 ----- VueFlux/ExecutorWorkItem.swift | 45 ------------- VueFlux/VueFlux.swift | 10 +-- VueFluxReactive/Signal.swift | 6 +- 9 files changed, 145 insertions(+), 175 deletions(-) create mode 100644 Tests/CancelableProcedureTests.swift create mode 100644 VueFlux/CancelableProcedure.swift delete mode 100644 VueFlux/ExecutorWorkItem.swift diff --git a/Tests/CancelableProcedureTests.swift b/Tests/CancelableProcedureTests.swift new file mode 100644 index 0000000..61cdcb8 --- /dev/null +++ b/Tests/CancelableProcedureTests.swift @@ -0,0 +1,85 @@ +import XCTest +@testable import VueFlux + +final class CancelableProcedureTests: XCTestCase { + func testVoidProcedure() { + var value = 0 + + let procedure = CancelableProcedure { + value += 1 + } + + XCTAssertEqual(value, 0) + + procedure.execute() + + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute() + + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute() + + XCTAssertEqual(value, 1) + } + + func testValueWorkItem() { + var value = 0 + + let procedure = CancelableProcedure { int in + value = int + } + + XCTAssertEqual(value, 0) + + procedure.execute(with: 1) + + XCTAssertFalse(procedure.isCanceled) + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: 2) + + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: 3) + + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 1) + } + + func testCancelProcedureAsync() { + let queue = DispatchQueue(label: "testCancelProcedureAsync") + + var value = 0 + + let expectation = self.expectation(description: "testCancelProcedureAsync") + + let procedure = CancelableProcedure { int in + value = int + } + + XCTAssertFalse(procedure.isCanceled) + + queue.suspend() + + queue.async { + procedure.execute(with: 1) + } + + procedure.cancel() + queue.resume() + + queue.async(execute: expectation.fulfill) + + waitForExpectations(timeout: 1) { _ in + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 0) + } + } +} diff --git a/Tests/ExecutorTests.swift b/Tests/ExecutorTests.swift index 4ef09db..fec33fa 100644 --- a/Tests/ExecutorTests.swift +++ b/Tests/ExecutorTests.swift @@ -84,102 +84,4 @@ final class ExecutorTests: XCTestCase { XCTAssertEqual(value, 1) } } - - func testVoidWorkItem() { - var value = 0 - - let workItem = Executor.WorkItem { - value = 1 - } - - XCTAssertEqual(value, 0) - - workItem.execute() - - XCTAssertEqual(value, 1) - } - - func testValueWorkItem() { - var value = 0 - - let workItem = Executor.WorkItem { int in - value = int - } - - XCTAssertEqual(value, 0) - - workItem.execute(with: 1) - - XCTAssertEqual(value, 1) - } - - func testVoidWorkItemWithExecutor() { - var value = 0 - - let expectation = self.expectation(description: "testVoidWorkItemWithExecutor") - - let workItem = Executor.WorkItem { - value = 1 - expectation.fulfill() - } - - let executor = Executor.queue(.globalDefault()) - - executor.execute(workItem: workItem) - - waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(value, 1) - } - } - - func testValueWorkItemWithExecutor() { - var value = 0 - - let expectation = self.expectation(description: "testValueWorkItemWithExecutor") - - let workItem = Executor.WorkItem { int in - value = int - expectation.fulfill() - } - - let executor = Executor.queue(.globalDefault()) - - executor.execute(workItem: workItem, with: 1) - - waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(value, 1) - } - } - - func testCancelWorkItem() { - let queue = DispatchQueue(label: "testCancelWorkItem") - - var value = 0 - - let expectation = self.expectation(description: "testCancelWorkItem") - - let workItem = Executor.WorkItem { int in - value = int - } - - let executor = Executor.queue(queue) - - XCTAssertFalse(workItem.isCanceled) - - queue.suspend() - - executor.execute(workItem: workItem, with: 1) - workItem.cancel() - - queue.resume() - - workItem.cancel() - - queue.async(execute: expectation.fulfill) - - waitForExpectations(timeout: 1) { _ in - XCTAssertTrue(workItem.isCanceled) - XCTAssertEqual(value, 0) - } - } } diff --git a/Tests/SinkSignalTests.swift b/Tests/SinkSignalTests.swift index 1428f0d..b41ad09 100644 --- a/Tests/SinkSignalTests.swift +++ b/Tests/SinkSignalTests.swift @@ -74,9 +74,7 @@ final class SinkSignalTests: XCTestCase { queue.resume() - queue.async { - expectation.fulfill() - } + queue.async(execute: expectation.fulfill) waitForExpectations(timeout: 1) { _ in } } diff --git a/VueFlux.xcodeproj/project.pbxproj b/VueFlux.xcodeproj/project.pbxproj index ead6fcc..f576924 100644 --- a/VueFlux.xcodeproj/project.pbxproj +++ b/VueFlux.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 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 */; }; + 6B665528200AE8A000883C12 /* CancelableProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B665527200AE8A000883C12 /* CancelableProcedure.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 */; }; @@ -40,6 +40,7 @@ 6BB30C4F2018E3CA00C52C76 /* Signal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C4E2018E3CA00C52C76 /* Signal.swift */; }; 6BB30C512018E3E700C52C76 /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C502018E3E700C52C76 /* Variable.swift */; }; 6BB30C532018E41600C52C76 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C522018E41600C52C76 /* Constant.swift */; }; + 6BB9F56B20514317008C6C77 /* CancelableProcedureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */; }; 6BC664F51FEC5ABF00BD74C8 /* AnyDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC664F41FEC5ABF00BD74C8 /* AnyDisposable.swift */; }; 6BC664F71FEC627E00BD74C8 /* SignalOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC664F61FEC627E00BD74C8 /* SignalOperators.swift */; }; 6BC665331FF032C500BD74C8 /* Executor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC665321FF032C500BD74C8 /* Executor.swift */; }; @@ -77,7 +78,7 @@ 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 = ""; }; + 6B665527200AE8A000883C12 /* CancelableProcedure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedure.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; }; @@ -102,6 +103,7 @@ 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 = ""; }; + 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedureTests.swift; 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 = ""; }; @@ -187,6 +189,7 @@ children = ( 6BE63FFE1FB3A6D8007F8B5D /* VueFluxTests.swift */, 6BE63FF91FB39EA6007F8B5D /* ExecutorTests.swift */, + 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */, 6BE63FF61FB39CDD007F8B5D /* DispatcherTests.swift */, 6BE63FF31FB39A3C007F8B5D /* DispatcherContextTests.swift */, 6BE63FF01FB39446007F8B5D /* AtomicReferenceTests.swift */, @@ -209,7 +212,7 @@ children = ( 6B9EB21C1FCB14BF009F0659 /* VueFlux.swift */, 6BC665321FF032C500BD74C8 /* Executor.swift */, - 6B665527200AE8A000883C12 /* ExecutorWorkItem.swift */, + 6B665527200AE8A000883C12 /* CancelableProcedure.swift */, 6B9EB23D1FCB1CFB009F0659 /* AtomicReference.swift */, 6B9EB2141FCB14BF009F0659 /* Internal */, 6B9EB2131FCB14BF009F0659 /* Info.plist */, @@ -444,7 +447,7 @@ 6B9EB21D1FCB14ED009F0659 /* Dispatcher.swift in Sources */, 6B14405920453C4300E0B3FF /* Storage.swift in Sources */, 6B9EB21E1FCB14ED009F0659 /* DispatcherContext.swift in Sources */, - 6B665528200AE8A000883C12 /* ExecutorWorkItem.swift in Sources */, + 6B665528200AE8A000883C12 /* CancelableProcedure.swift in Sources */, 6B9EB2241FCB14ED009F0659 /* VueFlux.swift in Sources */, 6B9EB23F1FCB1CFB009F0659 /* AtomicReference.swift in Sources */, 6B41947A2045B72300C40218 /* Lock.swift in Sources */, @@ -459,6 +462,7 @@ 6B41947D2045B7A500C40218 /* LockTests.swift in Sources */, 6B9EB2441FCB203B009F0659 /* VueFluxTests.swift in Sources */, 6B9EB2451FCB203B009F0659 /* ExecutorTests.swift in Sources */, + 6BB9F56B20514317008C6C77 /* CancelableProcedureTests.swift in Sources */, 6B9EB2571FCB2FEE009F0659 /* SinkSignalTests.swift in Sources */, 6B730BCB2045E9620059D851 /* AtomicBoolTests.swift in Sources */, 6B9EB2461FCB203B009F0659 /* DisposableTests.swift in Sources */, diff --git a/VueFlux/CancelableProcedure.swift b/VueFlux/CancelableProcedure.swift new file mode 100644 index 0000000..84bd88a --- /dev/null +++ b/VueFlux/CancelableProcedure.swift @@ -0,0 +1,43 @@ +/// Encapsulate the function and make it cancelable. +/// A function will not be executed after canceling, except already in progress execution. +public final class CancelableProcedure { + /// A Bool value indicating whether canceled. + public var isCanceled: Bool { + return _isCanceled.value + } + + private let _isCanceled: AtomicBool = false + private var _execute: ((Value) -> Void)? + + /// Initialize with an arbitrary function. + /// + /// - Parameters: + /// - 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. + /// + /// - Parameters: + /// - value: A value to be pass to specified function. + public func execute(with value: @autoclosure () -> Value) { + guard !isCanceled, let execute = _execute else { return } + execute(value()) + } + + /// Cancel the specified function. + /// Cancellation does not affect already in progress execution. + public func cancel() { + guard _isCanceled.compareAndSwapBarrier(old: false, new: true) else { return } + _execute = nil + } +} + +public extension CancelableProcedure where Value == Void { + /// Synchronously execute the specified function. + @inline(__always) + public func execute() { + self.execute(with: ()) + } +} diff --git a/VueFlux/Executor.swift b/VueFlux/Executor.swift index 64ee445..f2444a9 100644 --- a/VueFlux/Executor.swift +++ b/VueFlux/Executor.swift @@ -38,23 +38,6 @@ public struct Executor { public func execute(_ function: @escaping () -> Void) { context(function) } - - /// Execute an arbitrary work item. - /// - /// - Parameters: - /// - workItem: A work item to be execute. - /// - value: A value that pass to work item. - public func execute(workItem: WorkItem, with value: @autoclosure @escaping () -> Value) { - execute { workItem.execute(with: value()) } - } - - /// Execute an arbitrary work item. - /// - /// - Parameters: - /// - workItem: A work item to be execute. - public func execute(workItem: WorkItem) { - execute { workItem.execute() } - } } private extension Executor { diff --git a/VueFlux/ExecutorWorkItem.swift b/VueFlux/ExecutorWorkItem.swift deleted file mode 100644 index fcb67b2..0000000 --- a/VueFlux/ExecutorWorkItem.swift +++ /dev/null @@ -1,45 +0,0 @@ -public extension Executor { - /// Encapsulates the function to execute by Executor. - /// Also be able to cancel its execution. - public final class WorkItem { - /// A Bool value indicating whether canceled. - public var isCanceled: Bool { - return _isCanceled.value - } - - private let _isCanceled: AtomicBool = false - private var _execute: ((Value) -> Void)? - - /// Create with an arbitrary function. - /// - /// - Parameters: - /// - 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. - /// - /// - Parameters: - /// - value: A value to be pass to specified function. - public func execute(with value: @autoclosure () -> 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() { - guard _isCanceled.compareAndSwapBarrier(old: false, new: true) else { return } - _execute = nil - } - } -} - -public extension Executor.WorkItem where Value == Void { - /// Synchronously execute the specified function. - @inline(__always) - public func execute() { - self.execute(with: ()) - } -} diff --git a/VueFlux/VueFlux.swift b/VueFlux/VueFlux.swift index f4fb6fc..96c8732 100644 --- a/VueFlux/VueFlux.swift +++ b/VueFlux/VueFlux.swift @@ -3,7 +3,7 @@ open class Store { private let dispatcher = Dispatcher() private let sharedDispatcher = Dispatcher.shared - private let commitWorkItem: Executor.WorkItem + private let commitProcedure: CancelableProcedure private let dispatcherKey: Dispatcher.Observers.Key private let sharedDispatcherKey: Dispatcher.Observers.Key @@ -26,15 +26,15 @@ open class Store { /// - mutations: A mutations for mutates the state. /// - executor: An executor to dispatch actions on. public init(state: State, mutations: State.Mutations, executor: Executor) { - let commitWorkItem = Executor.WorkItem { action in + let commitProcedure = CancelableProcedure { action in mutations.commit(action: action, state: state) } let commit: (State.Action) -> Void = { action in - executor.execute(workItem: commitWorkItem, with: action) + executor.execute { commitProcedure.execute(with: action) } } - self.commitWorkItem = commitWorkItem + self.commitProcedure = commitProcedure actions = .init(dispatcher: dispatcher) computed = .init(state: state) @@ -44,7 +44,7 @@ open class Store { } deinit { - commitWorkItem.cancel() + commitProcedure.cancel() dispatcher.unsubscribe(for: dispatcherKey) sharedDispatcher.unsubscribe(for: sharedDispatcherKey) } diff --git a/VueFluxReactive/Signal.swift b/VueFluxReactive/Signal.swift index 74f63ff..e04548e 100644 --- a/VueFluxReactive/Signal.swift +++ b/VueFluxReactive/Signal.swift @@ -22,13 +22,13 @@ public struct Signal { /// - Returns: A disposable to unregister given observer. @discardableResult public func observe(_ observer: @escaping (Value) -> Void) -> Disposable { - let workItem = Executor.WorkItem(observer) + let observerProcedure = CancelableProcedure(observer) let disposable = producer { value in - workItem.execute(with: value) + observerProcedure.execute(with: value) } return AnyDisposable { - workItem.cancel() + observerProcedure.cancel() disposable.dispose() } } From e87d64020caa276b2404b1aed55c009c99f26e07 Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 19:39:16 +0900 Subject: [PATCH 7/9] [ci skip] Update podspec --- VueFlux.podspec | 2 +- VueFluxReactive.podspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VueFlux.podspec b/VueFlux.podspec index c7ed804..dc80369 100644 --- a/VueFlux.podspec +++ b/VueFlux.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'VueFlux' - spec.version = '1.3.1' + spec.version = '1.4.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' diff --git a/VueFluxReactive.podspec b/VueFluxReactive.podspec index 468a707..60e0baf 100644 --- a/VueFluxReactive.podspec +++ b/VueFluxReactive.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |spec| spec.name = 'VueFluxReactive' - spec.version = '1.3.1' + spec.version = '1.4.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', 'CommonInternal/**/*.swift' - spec.dependency 'VueFlux', '~> 1.3.1' + spec.dependency 'VueFlux', '~> 1.4.0' spec.requires_arc = true spec.osx.deployment_target = '10.9' spec.ios.deployment_target = '9.0' From a67113b6cf0223d6e38218c5c94fa59d26c1bf3b Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 19:52:51 +0900 Subject: [PATCH 8/9] [ci skip] Remove documents for Executor.WorkItem in README --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index 4bff6df..98470ae 100644 --- a/README.md +++ b/README.md @@ -281,26 +281,6 @@ store.computed.valueSignal } ``` -**Executor.WorkItem** -Executor can also be executed with WorkItem. -The WorkItem can call `cancel` to prevent it from execute in future. -Cancellation does not affect any execution of the function that is already in progress. - -```swift -let executor = Executor.immediate -let workItem = Executor.WorkItem { value in - print("Value is \(value)") -} - -executor.execute(workItem: workItem, with: 100) - -workItem.cancel() - -executor.execute(workItem: workItem, with: 200) - -// prints "Value is 100" -``` - ### Signal Operators VueFluxReactive restricts functional approach AMAP. However, includes minimum operators for convenience. From 4d795f49e11380eac9b6916841bc28eb210f482e Mon Sep 17 00:00:00 2001 From: Ryo Aoyama Date: Thu, 8 Mar 2018 20:11:15 +0900 Subject: [PATCH 9/9] Move CancelableProcedure to CommonInternal --- .../CancelableProcedure.swift | 14 +- Tests/CancelableProcedureTests.swift | 168 +++++++++++------- VueFlux.xcodeproj/project.pbxproj | 16 +- 3 files changed, 123 insertions(+), 75 deletions(-) rename {VueFlux => CommonInternal}/CancelableProcedure.swift (78%) diff --git a/VueFlux/CancelableProcedure.swift b/CommonInternal/CancelableProcedure.swift similarity index 78% rename from VueFlux/CancelableProcedure.swift rename to CommonInternal/CancelableProcedure.swift index 84bd88a..df6bac1 100644 --- a/VueFlux/CancelableProcedure.swift +++ b/CommonInternal/CancelableProcedure.swift @@ -1,8 +1,8 @@ /// Encapsulate the function and make it cancelable. /// A function will not be executed after canceling, except already in progress execution. -public final class CancelableProcedure { +final class CancelableProcedure { /// A Bool value indicating whether canceled. - public var isCanceled: Bool { + var isCanceled: Bool { return _isCanceled.value } @@ -13,7 +13,7 @@ public final class CancelableProcedure { /// /// - Parameters: /// - execute: A function to be executed by calling `execute(with:)` until canceled. - public init(_ execute: @escaping (Value) -> Void) { + init(_ execute: @escaping (Value) -> Void) { _execute = execute } @@ -21,23 +21,23 @@ public final class CancelableProcedure { /// /// - Parameters: /// - value: A value to be pass to specified function. - public func execute(with value: @autoclosure () -> Value) { + func execute(with value: @autoclosure () -> Value) { guard !isCanceled, let execute = _execute else { return } execute(value()) } /// Cancel the specified function. /// Cancellation does not affect already in progress execution. - public func cancel() { + func cancel() { guard _isCanceled.compareAndSwapBarrier(old: false, new: true) else { return } _execute = nil } } -public extension CancelableProcedure where Value == Void { +extension CancelableProcedure where Value == Void { /// Synchronously execute the specified function. @inline(__always) - public func execute() { + func execute() { self.execute(with: ()) } } diff --git a/Tests/CancelableProcedureTests.swift b/Tests/CancelableProcedureTests.swift index 61cdcb8..610b227 100644 --- a/Tests/CancelableProcedureTests.swift +++ b/Tests/CancelableProcedureTests.swift @@ -1,85 +1,131 @@ import XCTest @testable import VueFlux +@testable import VueFluxReactive + +private protocol CancelableProcedureProtocol { + associatedtype Value + + var isCanceled: Bool { get } + + init(_ execute: @escaping (Value) -> Void) + func execute(with value: @autoclosure () -> Value) + func cancel() +} + +extension VueFlux.CancelableProcedure: CancelableProcedureProtocol {} +extension VueFluxReactive.CancelableProcedure: CancelableProcedureProtocol {} final class CancelableProcedureTests: XCTestCase { func testVoidProcedure() { - var value = 0 - - let procedure = CancelableProcedure { - value += 1 + func runTest(for type: CancelableProcedure.Type) where CancelableProcedure.Value == Void { + + var value = 0 + + let procedure = CancelableProcedure { + value += 1 + } + + XCTAssertEqual(value, 0) + + procedure.execute(with: ()) + + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: ()) + + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: ()) + + XCTAssertEqual(value, 1) } - XCTAssertEqual(value, 0) - - procedure.execute() - - XCTAssertEqual(value, 1) - - procedure.cancel() - procedure.execute() - - XCTAssertEqual(value, 1) - - procedure.cancel() - procedure.execute() - - XCTAssertEqual(value, 1) + runTest(for: VueFlux.CancelableProcedure.self) + runTest(for: VueFluxReactive.CancelableProcedure.self) } func testValueWorkItem() { - var value = 0 - - let procedure = CancelableProcedure { int in - value = int + func runTest(for type: CancelableProcedure.Type) where CancelableProcedure.Value == Int { + var value = 0 + + let procedure = CancelableProcedure { int in + value = int + } + + XCTAssertEqual(value, 0) + + procedure.execute(with: 1) + + XCTAssertFalse(procedure.isCanceled) + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: 2) + + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 1) + + procedure.cancel() + procedure.execute(with: 3) + + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 1) } - XCTAssertEqual(value, 0) - - procedure.execute(with: 1) - - XCTAssertFalse(procedure.isCanceled) - XCTAssertEqual(value, 1) - - procedure.cancel() - procedure.execute(with: 2) - - XCTAssertTrue(procedure.isCanceled) - XCTAssertEqual(value, 1) - - procedure.cancel() - procedure.execute(with: 3) - - XCTAssertTrue(procedure.isCanceled) - XCTAssertEqual(value, 1) + runTest(for: VueFlux.CancelableProcedure.self) + runTest(for: VueFluxReactive.CancelableProcedure.self) } func testCancelProcedureAsync() { - let queue = DispatchQueue(label: "testCancelProcedureAsync") + func runTest(for type: CancelableProcedure.Type) where CancelableProcedure.Value == Int { + let queue = DispatchQueue(label: "testCancelProcedureAsync") + + var value = 0 + + let expectation = self.expectation(description: "testCancelProcedureAsync") + + let procedure = CancelableProcedure { int in + value = int + } + + XCTAssertFalse(procedure.isCanceled) + + queue.suspend() + + queue.async { + procedure.execute(with: 1) + } + + procedure.cancel() + queue.resume() + + queue.async(execute: expectation.fulfill) + + waitForExpectations(timeout: 1) { _ in + XCTAssertTrue(procedure.isCanceled) + XCTAssertEqual(value, 0) + } + } + runTest(for: VueFlux.CancelableProcedure.self) + runTest(for: VueFluxReactive.CancelableProcedure.self) + } + + func testExecuteVoidProcedure() { var value = 0 - - let expectation = self.expectation(description: "testCancelProcedureAsync") - - let procedure = CancelableProcedure { int in - value = int + let vueFluxProcedure = VueFlux.CancelableProcedure { + value = 1 } - XCTAssertFalse(procedure.isCanceled) - - queue.suspend() - - queue.async { - procedure.execute(with: 1) + let vueFluxReactiveProcedure = VueFluxReactive.CancelableProcedure { + value = 2 } - - procedure.cancel() - queue.resume() - queue.async(execute: expectation.fulfill) + vueFluxProcedure.execute() + vueFluxReactiveProcedure.execute() - waitForExpectations(timeout: 1) { _ in - XCTAssertTrue(procedure.isCanceled) - XCTAssertEqual(value, 0) - } + XCTAssertEqual(value, 2) } } diff --git a/VueFlux.xcodeproj/project.pbxproj b/VueFlux.xcodeproj/project.pbxproj index f576924..4d1f9b9 100644 --- a/VueFlux.xcodeproj/project.pbxproj +++ b/VueFlux.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 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 /* CancelableProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B665527200AE8A000883C12 /* CancelableProcedure.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 */; }; @@ -41,6 +40,8 @@ 6BB30C512018E3E700C52C76 /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C502018E3E700C52C76 /* Variable.swift */; }; 6BB30C532018E41600C52C76 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB30C522018E41600C52C76 /* Constant.swift */; }; 6BB9F56B20514317008C6C77 /* CancelableProcedureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */; }; + 6BB9F56D20514DCA008C6C77 /* CancelableProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */; }; + 6BB9F56E20514DCA008C6C77 /* CancelableProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */; }; 6BC664F51FEC5ABF00BD74C8 /* AnyDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC664F41FEC5ABF00BD74C8 /* AnyDisposable.swift */; }; 6BC664F71FEC627E00BD74C8 /* SignalOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC664F61FEC627E00BD74C8 /* SignalOperators.swift */; }; 6BC665331FF032C500BD74C8 /* Executor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC665321FF032C500BD74C8 /* Executor.swift */; }; @@ -78,7 +79,6 @@ 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 /* CancelableProcedure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedure.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; }; @@ -104,6 +104,7 @@ 6BB30C522018E41600C52C76 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; 6BB5BAF92044353A0009F779 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedureTests.swift; sourceTree = ""; }; + 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableProcedure.swift; 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 = ""; }; @@ -155,8 +156,9 @@ isa = PBXGroup; children = ( 6B14405820453C3D00E0B3FF /* Storage.swift */, - 6B4194792045B72300C40218 /* Lock.swift */, + 6BB9F56C20514DCA008C6C77 /* CancelableProcedure.swift */, 6B730BC72045E9280059D851 /* AtomicBool.swift */, + 6B4194792045B72300C40218 /* Lock.swift */, ); path = CommonInternal; sourceTree = ""; @@ -189,7 +191,6 @@ children = ( 6BE63FFE1FB3A6D8007F8B5D /* VueFluxTests.swift */, 6BE63FF91FB39EA6007F8B5D /* ExecutorTests.swift */, - 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */, 6BE63FF61FB39CDD007F8B5D /* DispatcherTests.swift */, 6BE63FF31FB39A3C007F8B5D /* DispatcherContextTests.swift */, 6BE63FF01FB39446007F8B5D /* AtomicReferenceTests.swift */, @@ -200,8 +201,9 @@ 6BA27BCB1FB8CD3100809472 /* DisposableTests.swift */, 6BA27BCE1FB8CE1A00809472 /* DisposableScopeTests.swift */, 6BA27BD31FB8CFE600809472 /* StorageTests.swift */, - 6B41947C2045B7A500C40218 /* LockTests.swift */, + 6BB9F56A20514317008C6C77 /* CancelableProcedureTests.swift */, 6B730BCA2045E9620059D851 /* AtomicBoolTests.swift */, + 6B41947C2045B7A500C40218 /* LockTests.swift */, 6B9E329F1FA63B07000B24D4 /* Info.plist */, ); path = Tests; @@ -212,7 +214,6 @@ children = ( 6B9EB21C1FCB14BF009F0659 /* VueFlux.swift */, 6BC665321FF032C500BD74C8 /* Executor.swift */, - 6B665527200AE8A000883C12 /* CancelableProcedure.swift */, 6B9EB23D1FCB1CFB009F0659 /* AtomicReference.swift */, 6B9EB2141FCB14BF009F0659 /* Internal */, 6B9EB2131FCB14BF009F0659 /* Info.plist */, @@ -444,10 +445,10 @@ buildActionMask = 2147483647; files = ( 6B730BC82045E9280059D851 /* AtomicBool.swift in Sources */, + 6BB9F56E20514DCA008C6C77 /* CancelableProcedure.swift in Sources */, 6B9EB21D1FCB14ED009F0659 /* Dispatcher.swift in Sources */, 6B14405920453C4300E0B3FF /* Storage.swift in Sources */, 6B9EB21E1FCB14ED009F0659 /* DispatcherContext.swift in Sources */, - 6B665528200AE8A000883C12 /* CancelableProcedure.swift in Sources */, 6B9EB2241FCB14ED009F0659 /* VueFlux.swift in Sources */, 6B9EB23F1FCB1CFB009F0659 /* AtomicReference.swift in Sources */, 6B41947A2045B72300C40218 /* Lock.swift in Sources */, @@ -486,6 +487,7 @@ 6BB30C4F2018E3CA00C52C76 /* Signal.swift in Sources */, 6BC664F71FEC627E00BD74C8 /* SignalOperators.swift in Sources */, 6BB30C4D2018E3AC00C52C76 /* Sink.swift in Sources */, + 6BB9F56D20514DCA008C6C77 /* CancelableProcedure.swift in Sources */, 6BB30C512018E3E700C52C76 /* Variable.swift in Sources */, 6BC664F51FEC5ABF00BD74C8 /* AnyDisposable.swift in Sources */, 6B730BC92045E9480059D851 /* AtomicBool.swift in Sources */,