From 16a2e60158e2178a43308638450d46d27e19bbfe Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Fri, 8 May 2015 10:26:44 +0900 Subject: [PATCH 1/6] Add removeProgress() & removeThen() --- SwiftTask.xcodeproj/project.pbxproj | 6 ++ SwiftTask/SwiftTask.swift | 76 +++++++++++++--- SwiftTask/_StateMachine.swift | 61 +++++++++++-- SwiftTaskTests/RemoveHandlerTests.swift | 114 ++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 SwiftTaskTests/RemoveHandlerTests.swift diff --git a/SwiftTask.xcodeproj/project.pbxproj b/SwiftTask.xcodeproj/project.pbxproj index 4a596da..b61bd40 100644 --- a/SwiftTask.xcodeproj/project.pbxproj +++ b/SwiftTask.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 1F20250219ADA8FD00DE0495 /* BasicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F20250119ADA8FD00DE0495 /* BasicTests.swift */; }; + 1F218D5B1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */; }; + 1F218D5C1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */; }; 1F46DEDA199EDF1000F97868 /* SwiftTask.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F46DED9199EDF1000F97868 /* SwiftTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1F46DEFB199EDF8100F97868 /* SwiftTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFA199EDF8100F97868 /* SwiftTask.swift */; }; 1F46DEFD199EE2C200F97868 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFC199EE2C200F97868 /* _TestCase.swift */; }; @@ -37,6 +39,7 @@ /* Begin PBXFileReference section */ 1F20250119ADA8FD00DE0495 /* BasicTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicTests.swift; sourceTree = ""; }; + 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveHandlerTests.swift; sourceTree = ""; }; 1F46DED4199EDF1000F97868 /* SwiftTask.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftTask.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1F46DED8199EDF1000F97868 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1F46DED9199EDF1000F97868 /* SwiftTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftTask.h; sourceTree = ""; }; @@ -144,6 +147,7 @@ 1F46DEE3199EDF1000F97868 /* SwiftTaskTests.swift */, 48511C5A19C17563002FE03C /* RetainCycleTests.swift */, 485C31F01A1D619A00040DA3 /* TypeInferenceTests.swift */, + 1F218D5A1AFC3FFD00C849FF /* RemoveHandlerTests.swift */, 1F5FA35619A374E600975FB9 /* AlamofireTests.swift */, 1F46DEE1199EDF1000F97868 /* Supporting Files */, ); @@ -345,6 +349,7 @@ files = ( 1F20250219ADA8FD00DE0495 /* BasicTests.swift in Sources */, 1FF52EB41A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */, + 1F218D5B1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */, 1F6A8CA319A4E4F200369A5D /* SwiftTaskTests.swift in Sources */, 1F4C76A41AD8CF40004E47C1 /* AlamofireTests.swift in Sources */, 485C31F11A1D619A00040DA3 /* TypeInferenceTests.swift in Sources */, @@ -359,6 +364,7 @@ files = ( 4822F0DE19D00B2300F5F572 /* SwiftTaskTests.swift in Sources */, 4822F0DD19D00B2300F5F572 /* BasicTests.swift in Sources */, + 1F218D5C1AFC3FFD00C849FF /* RemoveHandlerTests.swift in Sources */, 1FF52EB51A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */, 1F4C76A51AD8CF41004E47C1 /* AlamofireTests.swift in Sources */, 485C31F21A1D619A00040DA3 /* TypeInferenceTests.swift in Sources */, diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index d99f26d..703ba8d 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -355,7 +355,13 @@ public class Task: Printable /// public func progress(progressClosure: ProgressTuple -> Void) -> Task { - self._machine.addProgressTupleHandler(progressClosure) + var token: HandlerToken? = nil + return self.progress(&token, progressClosure) + } + + public func progress(inout token: HandlerToken?, _ progressClosure: ProgressTuple -> Void) -> Task + { + self._machine.addProgressTupleHandler(&token, progressClosure) return self } @@ -367,7 +373,13 @@ public class Task: Printable /// public func then(thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task { - return self.then { (value: Value?, errorInfo: ErrorInfo?) -> Task in + var token: HandlerToken? = nil + return self.then(&token, thenClosure) + } + + public func then(inout token: HandlerToken?, _ thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task + { + return self.then(&token) { (value: Value?, errorInfo: ErrorInfo?) -> Task in return Task(value: thenClosure(value, errorInfo)) } } @@ -378,6 +390,12 @@ public class Task: Printable /// - e.g. task.then { value, errorInfo -> NextTaskType in ... } /// public func then(thenClosure: (Value?, ErrorInfo?) -> Task) -> Task + { + var token: HandlerToken? = nil + return self.then(&token, thenClosure) + } + + public func then(inout token: HandlerToken?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in @@ -389,8 +407,8 @@ public class Task: Printable // This is especially important for ReactKit's `deinitSignal` behavior. // let selfMachine = self._machine - - self._then { + + self._then(&token) { let innerTask = thenClosure(selfMachine.value, selfMachine.errorInfo) _bindInnerTask(innerTask, newMachine, progress, fulfill, _reject, configure) } @@ -399,13 +417,13 @@ public class Task: Printable } /// invokes `completionHandler` "now" or "in the future" - private func _then(completionHandler: Void -> Void) + private func _then(inout token: HandlerToken?, _ completionHandler: Void -> Void) { switch self.state { case .Fulfilled, .Rejected, .Cancelled: completionHandler() default: - self._machine.addCompletionHandler(completionHandler) + self._machine.addCompletionHandler(&token, completionHandler) } } @@ -416,7 +434,13 @@ public class Task: Printable /// public func success(successClosure: Value -> Value2) -> Task { - return self.success { (value: Value) -> Task in + var token: HandlerToken? = nil + return self.success(&token, successClosure) + } + + public func success(inout token: HandlerToken?, _ successClosure: Value -> Value2) -> Task + { + return self.success(&token) { (value: Value) -> Task in return Task(value: successClosure(value)) } } @@ -427,13 +451,19 @@ public class Task: Printable /// - e.g. task.success { value -> NextTaskType in ... } /// public func success(successClosure: Value -> Task) -> Task + { + var token: HandlerToken? = nil + return self.success(&token, successClosure) + } + + public func success(inout token: HandlerToken?, _ successClosure: Value -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine // NOTE: using `self._then()` + `selfMachine` instead of `self.then()` will reduce Task allocation - self._then { + self._then(&token) { if let value = selfMachine.value { let innerTask = successClosure(value) _bindInnerTask(innerTask, newMachine, progress, fulfill, _reject, configure) @@ -454,7 +484,13 @@ public class Task: Printable /// public func failure(failureClosure: ErrorInfo -> Value) -> Task { - return self.failure { (errorInfo: ErrorInfo) -> Task in + var token: HandlerToken? = nil + return self.failure(&token, failureClosure) + } + + public func failure(inout token: HandlerToken?, _ failureClosure: ErrorInfo -> Value) -> Task + { + return self.failure(&token) { (errorInfo: ErrorInfo) -> Task in return Task(value: failureClosure(errorInfo)) } } @@ -466,12 +502,18 @@ public class Task: Printable /// - e.g. task.failure { error, isCancelled -> NextTaskType in ... } /// public func failure(failureClosure: ErrorInfo -> Task) -> Task + { + var token: HandlerToken? = nil + return self.failure(&token, failureClosure) + } + + public func failure(inout token: HandlerToken?, _ failureClosure: ErrorInfo -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine - self._then { + self._then(&token) { if let value = selfMachine.value { fulfill(value) } @@ -503,6 +545,20 @@ public class Task: Printable { return self._machine.handleCancel(error: error) } + + // MARK: Remove Handlers + + public func removeProgress(handlerToken: HandlerToken) + { + return self._machine.removeProgressTupleHandler(handlerToken) + } + + /// NOTE: `task.removeThen(token)` will force `let task2 = task.then(&token)` to deinit immediately and tries cancellation if it is still running. + public func removeThen(handlerToken: HandlerToken) + { + return self._machine.removeCompletionHandler(handlerToken) + } + } // MARK: - Helper diff --git a/SwiftTask/_StateMachine.swift b/SwiftTask/_StateMachine.swift index 7f204da..2ddab94 100644 --- a/SwiftTask/_StateMachine.swift +++ b/SwiftTask/_StateMachine.swift @@ -30,8 +30,8 @@ internal class _StateMachine /// and will be set to `nil` afterward internal var initResumeClosure: (Void -> Void)? - internal private(set) lazy var progressTupleHandlers: [ProgressTupleHandler] = [] - internal private(set) lazy var completionHandlers: [Void -> Void] = [] + internal private(set) lazy var progressTupleHandlers = _Handlers() + internal private(set) lazy var completionHandlers = _Handlers Void>() internal let configuration = TaskConfiguration() @@ -41,20 +41,30 @@ internal class _StateMachine self.state = paused ? .Paused : .Running } - internal func addProgressTupleHandler(progressTupleHandler: ProgressTupleHandler) + internal func addProgressTupleHandler(inout token: HandlerToken?, _ progressTupleHandler: ProgressTupleHandler) { if self.state == .Running || self.state == .Paused { - self.progressTupleHandlers.append(progressTupleHandler) + token = self.progressTupleHandlers.append(progressTupleHandler) } } - internal func addCompletionHandler(completionHandler: Void -> Void) + internal func removeProgressTupleHandler(handlerToken: HandlerToken) + { + self.progressTupleHandlers.remove(handlerToken) + } + + internal func addCompletionHandler(inout token: HandlerToken?, _ completionHandler: Void -> Void) { if self.state == .Running || self.state == .Paused { - self.completionHandlers.append(completionHandler) + token = self.completionHandlers.append(completionHandler) } } + internal func removeCompletionHandler(handlerToken: HandlerToken) + { + self.completionHandlers.remove(handlerToken) + } + internal func handleProgress(progress: Progress) { if self.state == .Running { @@ -185,4 +195,43 @@ internal class _StateMachine self.initResumeClosure = nil self.progress = nil } +} + +//-------------------------------------------------- +// MARK: - Utility +//-------------------------------------------------- + +public struct HandlerToken +{ + internal let key: Int +} + +internal struct _Handlers: SequenceType +{ + private var currentKey: Int = 0 + private var elements = [Int : T]() + + internal mutating func append(value: T) -> HandlerToken + { + self.currentKey = self.currentKey &+ 1 + + self.elements[self.currentKey] = value + + return HandlerToken(key: self.currentKey) + } + + internal mutating func remove(token: HandlerToken) + { + self.elements.removeValueForKey(token.key) + } + + internal mutating func removeAll(keepCapacity: Bool = false) + { + self.elements.removeAll(keepCapacity: keepCapacity) + } + + internal func generate() -> GeneratorOf + { + return GeneratorOf(self.elements.values.generate()) + } } \ No newline at end of file diff --git a/SwiftTaskTests/RemoveHandlerTests.swift b/SwiftTaskTests/RemoveHandlerTests.swift new file mode 100644 index 0000000..76fe072 --- /dev/null +++ b/SwiftTaskTests/RemoveHandlerTests.swift @@ -0,0 +1,114 @@ +// +// RemoveHandlerTests.swift +// SwiftTask +// +// Created by Yasuhiro Inami on 2015/05/28. +// Copyright (c) 2015年 Yasuhiro Inami. All rights reserved. +// + +import SwiftTask +import Async +import XCTest + +class RemoveHandlerTests: _TestCase +{ + func testRemoveProgress() + { + typealias Task = SwiftTask.Task + + var expect = self.expectationWithDescription(__FUNCTION__) + + var progressToken: HandlerToken? = nil + + // define task + let task = Task { progress, fulfill, reject, configure in + + progress(0.0) + + Async.main(after: 0.1) { + progress(1.0) + + fulfill("OK") + } + + } + + task.progress { oldProgress, newProgress in + + println("progress1 = \(newProgress)") + + }.progress(&progressToken) { oldProgress, newProgress in + + println("progress2 = \(newProgress)") + XCTFail("Should never reach here because this progress-handler will be removed soon.") + + }.then { value, errorInfo -> Void in + + XCTAssertTrue(value == "OK") + XCTAssertTrue(errorInfo == nil) + expect.fulfill() + + } + + XCTAssertTrue(progressToken != nil, "Async `task` will return non-nil `progressToken`.") + + // remove progress-handler + task.removeProgress(progressToken!) + + self.wait() + } + + func testRemoveThen() + { + typealias Task = SwiftTask.Task + + var expect = self.expectationWithDescription(__FUNCTION__) + var progressCount = 0 + + // define task + let task = Task { progress, fulfill, reject, configure in + + progress(0.0) + + Async.main(after: 0.1) { + progress(1.0) + fulfill("OK") + } + + } + + var thenToken: HandlerToken? = nil + + // NOTE: reference to `task2` is required in order to call `task2.removeThen(thenToken)` + let task2 = task.success { value -> String in + + XCTAssertEqual(value, "OK") + return "Now OK" + + } + + task2.then(&thenToken) { value, errorInfo -> String in + + println("Should never reach here") + + XCTFail("Should never reach here because this then-handler will be removed soon.") + + return "Never reaches here" + + }.then { value, errorInfo -> Void in + + XCTAssertTrue(value == nil) + XCTAssertTrue(errorInfo != nil) + XCTAssertTrue(errorInfo!.error == nil) + XCTAssertTrue(errorInfo!.isCancelled, "`task2.removeThen(token)` will force `let task3 = task2.then(&token)` to deinit immediately and tries cancellation if it is still running.") + + expect.fulfill() + + } + + // remove then-handler + task2.removeThen(thenToken!) + + self.wait() + } +} \ No newline at end of file From 8a22ed56e62a5a4a9442df7d35a4251049e93706 Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 10 May 2015 00:34:30 +0900 Subject: [PATCH 2/6] Remove removeProgress() & removeThen(), add Cancellable instead. --- SwiftTask.xcodeproj/project.pbxproj | 6 ++ SwiftTask/Cancellable.swift | 45 +++++++++++ SwiftTask/SwiftTask.swift | 102 +++++++++++++----------- SwiftTask/_StateMachine.swift | 24 +++--- SwiftTaskTests/RemoveHandlerTests.swift | 50 +++++------- SwiftTaskTests/SwiftTaskTests.swift | 8 +- 6 files changed, 146 insertions(+), 89 deletions(-) create mode 100644 SwiftTask/Cancellable.swift diff --git a/SwiftTask.xcodeproj/project.pbxproj b/SwiftTask.xcodeproj/project.pbxproj index b61bd40..55d2778 100644 --- a/SwiftTask.xcodeproj/project.pbxproj +++ b/SwiftTask.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 1FCF71141AD8CD2F007079C2 /* Async.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCF71131AD8CD2F007079C2 /* Async.framework */; }; 1FCF71161AD8CD38007079C2 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCF71151AD8CD38007079C2 /* Alamofire.framework */; }; 1FCF71181AD8CD3C007079C2 /* Async.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCF71171AD8CD3C007079C2 /* Async.framework */; }; + 1FD7197B1AFE387C00BC38C4 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */; }; + 1FD7197C1AFE387C00BC38C4 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */; }; 1FF52EB41A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF52EB31A4C395A00B4BA28 /* _InterruptableTask.swift */; }; 1FF52EB51A4C395A00B4BA28 /* _InterruptableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF52EB31A4C395A00B4BA28 /* _InterruptableTask.swift */; }; 4822F0DC19D00B2300F5F572 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFC199EE2C200F97868 /* _TestCase.swift */; }; @@ -53,6 +55,7 @@ 1FCF71131AD8CD2F007079C2 /* Async.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Async.framework; path = "../Carthage/Checkouts/Async/build/Debug-iphoneos/Async.framework"; sourceTree = ""; }; 1FCF71151AD8CD38007079C2 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = ../Carthage/Checkouts/Alamofire/build/Debug/Alamofire.framework; sourceTree = ""; }; 1FCF71171AD8CD3C007079C2 /* Async.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Async.framework; path = ../Carthage/Checkouts/Async/build/Debug/Async.framework; sourceTree = ""; }; + 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = ""; }; 1FF52EB31A4C395A00B4BA28 /* _InterruptableTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _InterruptableTask.swift; sourceTree = ""; }; 4822F0D019D00ABF00F5F572 /* SwiftTask-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftTask-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 48511C5A19C17563002FE03C /* RetainCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetainCycleTests.swift; sourceTree = ""; }; @@ -124,6 +127,7 @@ children = ( 1F46DED9199EDF1000F97868 /* SwiftTask.h */, 1F46DEFA199EDF8100F97868 /* SwiftTask.swift */, + 1FD7197A1AFE387C00BC38C4 /* Cancellable.swift */, 48B58D7A1A6F255E0068E18C /* _StateMachine.swift */, 1F46DED7199EDF1000F97868 /* Supporting Files */, ); @@ -339,6 +343,7 @@ buildActionMask = 2147483647; files = ( 1F46DEFB199EDF8100F97868 /* SwiftTask.swift in Sources */, + 1FD7197B1AFE387C00BC38C4 /* Cancellable.swift in Sources */, 48B58D7B1A6F255E0068E18C /* _StateMachine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -378,6 +383,7 @@ buildActionMask = 2147483647; files = ( 48CD5A3C19AEEBDF0042B9F1 /* SwiftTask.swift in Sources */, + 1FD7197C1AFE387C00BC38C4 /* Cancellable.swift in Sources */, 48B58D7C1A6F255E0068E18C /* _StateMachine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SwiftTask/Cancellable.swift b/SwiftTask/Cancellable.swift new file mode 100644 index 0000000..94df34e --- /dev/null +++ b/SwiftTask/Cancellable.swift @@ -0,0 +1,45 @@ +// +// Cancellable.swift +// SwiftTask +// +// Created by Yasuhiro Inami on 2015/05/09. +// Copyright (c) 2015年 Yasuhiro Inami. All rights reserved. +// + +import Foundation + +public protocol Cancellable +{ + typealias Error + + func cancel(error: Error) -> Bool +} + +public class Canceller: Cancellable +{ + private var cancelHandler: (Void -> Void)? + + public required init(cancelHandler: Void -> Void) + { + self.cancelHandler = cancelHandler + } + + public func cancel(error: Void) -> Bool + { + if let cancelHandler = self.cancelHandler { + self.cancelHandler = nil + cancelHandler() + return true + } + + return false + } +} + +public class AutoCanceller: Canceller +{ + deinit + { + self.cancel() + } +} \ No newline at end of file diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index 703ba8d..497f419 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -57,7 +57,7 @@ public class TaskConfiguration } } -public class Task: Printable +public class Task: Cancellable, Printable { public typealias ProgressTuple = (oldProgress: Progress?, newProgress: Progress) public typealias ErrorInfo = (error: Error?, isCancelled: Bool) @@ -213,7 +213,8 @@ public class Task: Printable internal func setup(#weakified: Bool, paused: Bool, _initClosure: _InitClosure) { // #if DEBUG -// println("[init] \(self.name)") +// let addr = String(format: "%p", unsafeAddressOf(self)) +// NSLog("[init] \(self.name) \(addr)") // #endif self._initClosure = _initClosure @@ -287,7 +288,8 @@ public class Task: Printable deinit { // #if DEBUG -// println("[deinit] \(self.name)") +// let addr = String(format: "%p", unsafeAddressOf(self)) +// NSLog("[deinit] \(self.name) \(addr)") // #endif // cancel in case machine is still running @@ -355,14 +357,19 @@ public class Task: Printable /// public func progress(progressClosure: ProgressTuple -> Void) -> Task { - var token: HandlerToken? = nil - return self.progress(&token, progressClosure) + var dummyCanceller: Canceller? = nil + return self.progress(&dummyCanceller, progressClosure) } - public func progress(inout token: HandlerToken?, _ progressClosure: ProgressTuple -> Void) -> Task + public func progress(inout canceller: C?, _ progressClosure: ProgressTuple -> Void) -> Task { + var token: _HandlerToken? = nil self._machine.addProgressTupleHandler(&token, progressClosure) + canceller = C { [weak self] in + self?._machine.removeProgressTupleHandler(token) + } + return self } @@ -373,13 +380,13 @@ public class Task: Printable /// public func then(thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task { - var token: HandlerToken? = nil - return self.then(&token, thenClosure) + var dummyCanceller: Canceller? = nil + return self.then(&dummyCanceller, thenClosure) } - public func then(inout token: HandlerToken?, _ thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task + public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Value2) -> Task { - return self.then(&token) { (value: Value?, errorInfo: ErrorInfo?) -> Task in + return self.then(&canceller) { (value: Value?, errorInfo: ErrorInfo?) -> Task in return Task(value: thenClosure(value, errorInfo)) } } @@ -391,13 +398,13 @@ public class Task: Printable /// public func then(thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { - var token: HandlerToken? = nil - return self.then(&token, thenClosure) + var dummyCanceller: Canceller? = nil + return self.then(&dummyCanceller, thenClosure) } - public func then(inout token: HandlerToken?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task + public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { - return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in + return Task { [unowned self, weak canceller] newMachine, progress, fulfill, _reject, configure in // // NOTE: @@ -408,7 +415,7 @@ public class Task: Printable // let selfMachine = self._machine - self._then(&token) { + self._then(&canceller) { let innerTask = thenClosure(selfMachine.value, selfMachine.errorInfo) _bindInnerTask(innerTask, newMachine, progress, fulfill, _reject, configure) } @@ -417,13 +424,18 @@ public class Task: Printable } /// invokes `completionHandler` "now" or "in the future" - private func _then(inout token: HandlerToken?, _ completionHandler: Void -> Void) + private func _then(inout canceller: C?, _ completionHandler: Void -> Void) { switch self.state { case .Fulfilled, .Rejected, .Cancelled: completionHandler() default: + var token: _HandlerToken? = nil self._machine.addCompletionHandler(&token, completionHandler) + + canceller = C { [weak self] in + self?._machine.removeCompletionHandler(token) + } } } @@ -434,13 +446,13 @@ public class Task: Printable /// public func success(successClosure: Value -> Value2) -> Task { - var token: HandlerToken? = nil - return self.success(&token, successClosure) + var dummyCanceller: Canceller? = nil + return self.success(&dummyCanceller, successClosure) } - public func success(inout token: HandlerToken?, _ successClosure: Value -> Value2) -> Task + public func success(inout canceller: C?, _ successClosure: Value -> Value2) -> Task { - return self.success(&token) { (value: Value) -> Task in + return self.success(&canceller) { (value: Value) -> Task in return Task(value: successClosure(value)) } } @@ -452,18 +464,18 @@ public class Task: Printable /// public func success(successClosure: Value -> Task) -> Task { - var token: HandlerToken? = nil - return self.success(&token, successClosure) + var dummyCanceller: Canceller? = nil + return self.success(&dummyCanceller, successClosure) } - public func success(inout token: HandlerToken?, _ successClosure: Value -> Task) -> Task + public func success(inout canceller: C?, _ successClosure: Value -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine // NOTE: using `self._then()` + `selfMachine` instead of `self.then()` will reduce Task allocation - self._then(&token) { + self._then(&canceller) { if let value = selfMachine.value { let innerTask = successClosure(value) _bindInnerTask(innerTask, newMachine, progress, fulfill, _reject, configure) @@ -484,13 +496,13 @@ public class Task: Printable /// public func failure(failureClosure: ErrorInfo -> Value) -> Task { - var token: HandlerToken? = nil - return self.failure(&token, failureClosure) + var dummyCanceller: Canceller? = nil + return self.failure(&dummyCanceller, failureClosure) } - public func failure(inout token: HandlerToken?, _ failureClosure: ErrorInfo -> Value) -> Task + public func failure(inout canceller: C?, _ failureClosure: ErrorInfo -> Value) -> Task { - return self.failure(&token) { (errorInfo: ErrorInfo) -> Task in + return self.failure(&canceller) { (errorInfo: ErrorInfo) -> Task in return Task(value: failureClosure(errorInfo)) } } @@ -503,17 +515,17 @@ public class Task: Printable /// public func failure(failureClosure: ErrorInfo -> Task) -> Task { - var token: HandlerToken? = nil - return self.failure(&token, failureClosure) + var dummyCanceller: Canceller? = nil + return self.failure(&dummyCanceller, failureClosure) } - public func failure(inout token: HandlerToken?, _ failureClosure: ErrorInfo -> Task) -> Task + public func failure(inout canceller: C?, _ failureClosure: ErrorInfo -> Task) -> Task { return Task { [unowned self] newMachine, progress, fulfill, _reject, configure in let selfMachine = self._machine - self._then(&token) { + self._then(&canceller) { if let value = selfMachine.value { fulfill(value) } @@ -536,27 +548,25 @@ public class Task: Printable return self._machine.handleResume() } - public func cancel(error: Error? = nil) -> Bool + // + // NOTE: + // To conform to `Cancellable`, this method is needed in replace of: + // - `public func cancel(error: Error? = nil) -> Bool` + // - `public func cancel(_ error: Error? = nil) -> Bool` (segfault in Swift 1.2) + // + public func cancel() -> Bool { - return self._cancel(error: error) + return self.cancel(nil) } - internal func _cancel(error: Error? = nil) -> Bool + public func cancel(error: Error?) -> Bool { - return self._machine.handleCancel(error: error) - } - - // MARK: Remove Handlers - - public func removeProgress(handlerToken: HandlerToken) - { - return self._machine.removeProgressTupleHandler(handlerToken) + return self._cancel(error: error) } - /// NOTE: `task.removeThen(token)` will force `let task2 = task.then(&token)` to deinit immediately and tries cancellation if it is still running. - public func removeThen(handlerToken: HandlerToken) + internal func _cancel(error: Error? = nil) -> Bool { - return self._machine.removeCompletionHandler(handlerToken) + return self._machine.handleCancel(error: error) } } diff --git a/SwiftTask/_StateMachine.swift b/SwiftTask/_StateMachine.swift index 2ddab94..58b0d3b 100644 --- a/SwiftTask/_StateMachine.swift +++ b/SwiftTask/_StateMachine.swift @@ -41,28 +41,32 @@ internal class _StateMachine self.state = paused ? .Paused : .Running } - internal func addProgressTupleHandler(inout token: HandlerToken?, _ progressTupleHandler: ProgressTupleHandler) + internal func addProgressTupleHandler(inout token: _HandlerToken?, _ progressTupleHandler: ProgressTupleHandler) { if self.state == .Running || self.state == .Paused { token = self.progressTupleHandlers.append(progressTupleHandler) } } - internal func removeProgressTupleHandler(handlerToken: HandlerToken) + internal func removeProgressTupleHandler(handlerToken: _HandlerToken?) { - self.progressTupleHandlers.remove(handlerToken) + if let handlerToken = handlerToken { + self.progressTupleHandlers.remove(handlerToken) + } } - internal func addCompletionHandler(inout token: HandlerToken?, _ completionHandler: Void -> Void) + internal func addCompletionHandler(inout token: _HandlerToken?, _ completionHandler: Void -> Void) { if self.state == .Running || self.state == .Paused { token = self.completionHandlers.append(completionHandler) } } - internal func removeCompletionHandler(handlerToken: HandlerToken) + internal func removeCompletionHandler(handlerToken: _HandlerToken?) { - self.completionHandlers.remove(handlerToken) + if let handlerToken = handlerToken { + self.completionHandlers.remove(handlerToken) + } } internal func handleProgress(progress: Progress) @@ -201,7 +205,7 @@ internal class _StateMachine // MARK: - Utility //-------------------------------------------------- -public struct HandlerToken +internal struct _HandlerToken { internal let key: Int } @@ -211,16 +215,16 @@ internal struct _Handlers: SequenceType private var currentKey: Int = 0 private var elements = [Int : T]() - internal mutating func append(value: T) -> HandlerToken + internal mutating func append(value: T) -> _HandlerToken { self.currentKey = self.currentKey &+ 1 self.elements[self.currentKey] = value - return HandlerToken(key: self.currentKey) + return _HandlerToken(key: self.currentKey) } - internal mutating func remove(token: HandlerToken) + internal mutating func remove(token: _HandlerToken) { self.elements.removeValueForKey(token.key) } diff --git a/SwiftTaskTests/RemoveHandlerTests.swift b/SwiftTaskTests/RemoveHandlerTests.swift index 76fe072..7345b6a 100644 --- a/SwiftTaskTests/RemoveHandlerTests.swift +++ b/SwiftTaskTests/RemoveHandlerTests.swift @@ -18,26 +18,22 @@ class RemoveHandlerTests: _TestCase var expect = self.expectationWithDescription(__FUNCTION__) - var progressToken: HandlerToken? = nil + var latestProgressValue: Float? + var canceller: AutoCanceller? = nil // define task - let task = Task { progress, fulfill, reject, configure in - + Task { progress, fulfill, reject, configure in progress(0.0) - Async.main(after: 0.1) { progress(1.0) - fulfill("OK") } - - } - - task.progress { oldProgress, newProgress in + }.progress { oldProgress, newProgress in println("progress1 = \(newProgress)") + latestProgressValue = newProgress - }.progress(&progressToken) { oldProgress, newProgress in + }.progress(&canceller) { oldProgress, newProgress in println("progress2 = \(newProgress)") XCTFail("Should never reach here because this progress-handler will be removed soon.") @@ -50,44 +46,37 @@ class RemoveHandlerTests: _TestCase } - XCTAssertTrue(progressToken != nil, "Async `task` will return non-nil `progressToken`.") + XCTAssertTrue(canceller != nil, "Async `task` will return non-nil `progressToken`.") // remove progress-handler - task.removeProgress(progressToken!) + canceller = nil self.wait() + + XCTAssertTrue(latestProgressValue == 1.0) } func testRemoveThen() { typealias Task = SwiftTask.Task - + var expect = self.expectationWithDescription(__FUNCTION__) - var progressCount = 0 + var canceller: AutoCanceller? = nil // define task - let task = Task { progress, fulfill, reject, configure in - - progress(0.0) + Task { progress, fulfill, reject, configure in Async.main(after: 0.1) { - progress(1.0) fulfill("OK") } + return - } - - var thenToken: HandlerToken? = nil - - // NOTE: reference to `task2` is required in order to call `task2.removeThen(thenToken)` - let task2 = task.success { value -> String in + }.success { value -> String in XCTAssertEqual(value, "OK") return "Now OK" - - } - task2.then(&thenToken) { value, errorInfo -> String in + }.then(&canceller) { value, errorInfo -> String in println("Should never reach here") @@ -97,17 +86,20 @@ class RemoveHandlerTests: _TestCase }.then { value, errorInfo -> Void in + println("value = \(value)") + println("errorInfo = \(errorInfo)") + XCTAssertTrue(value == nil) XCTAssertTrue(errorInfo != nil) XCTAssertTrue(errorInfo!.error == nil) - XCTAssertTrue(errorInfo!.isCancelled, "`task2.removeThen(token)` will force `let task3 = task2.then(&token)` to deinit immediately and tries cancellation if it is still running.") + XCTAssertTrue(errorInfo!.isCancelled, "Deallocation of `canceller` will force `task2` (where `task2 = task.then(&canceller)`) to deinit immediately and tries cancellation if it is still running.") expect.fulfill() } // remove then-handler - task2.removeThen(thenToken!) + canceller = nil self.wait() } diff --git a/SwiftTaskTests/SwiftTaskTests.swift b/SwiftTaskTests/SwiftTaskTests.swift index 8968447..77b1eac 100644 --- a/SwiftTaskTests/SwiftTaskTests.swift +++ b/SwiftTaskTests/SwiftTaskTests.swift @@ -598,7 +598,7 @@ class SwiftTaskTests: _TestCase // cancel at time between 1st & 2nd delay (t=0.3) Async.main(after: 0.3) { - task.cancel(error: "I get bored.") + task.cancel("I get bored.") XCTAssertEqual(task.state, TaskState.Cancelled) @@ -635,7 +635,7 @@ class SwiftTaskTests: _TestCase // cancel task3 at time between task1 fulfilled & before task2 completed (t=0.6) Async.main(after: 0.6) { - task3.cancel(error: "I get bored.") + task3.cancel("I get bored.") XCTAssertEqual(task3.state, TaskState.Cancelled) @@ -1175,7 +1175,7 @@ class SwiftTaskTests: _TestCase // cancel before fulfilled Async.main(after: 0.01) { - groupedTask.cancel(error: "Cancel") + groupedTask.cancel("Cancel") return } @@ -1390,7 +1390,7 @@ class SwiftTaskTests: _TestCase // cancel before fulfilled self.perform { - groupedTask.cancel(error: "Cancel") + groupedTask.cancel("Cancel") return } From 907008acf07ecb882dd6ae911bdb511c6c594752 Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 10 May 2015 01:11:02 +0900 Subject: [PATCH 3/6] Return boolean flag for add/remove handlers. --- SwiftTask/_StateMachine.swift | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/SwiftTask/_StateMachine.swift b/SwiftTask/_StateMachine.swift index 58b0d3b..ae7000f 100644 --- a/SwiftTask/_StateMachine.swift +++ b/SwiftTask/_StateMachine.swift @@ -41,32 +41,40 @@ internal class _StateMachine self.state = paused ? .Paused : .Running } - internal func addProgressTupleHandler(inout token: _HandlerToken?, _ progressTupleHandler: ProgressTupleHandler) + internal func addProgressTupleHandler(inout token: _HandlerToken?, _ progressTupleHandler: ProgressTupleHandler) -> Bool { if self.state == .Running || self.state == .Paused { token = self.progressTupleHandlers.append(progressTupleHandler) + return token != nil } + return false } - internal func removeProgressTupleHandler(handlerToken: _HandlerToken?) + internal func removeProgressTupleHandler(handlerToken: _HandlerToken?) -> Bool { if let handlerToken = handlerToken { - self.progressTupleHandlers.remove(handlerToken) + let removedHandler = self.progressTupleHandlers.remove(handlerToken) + return removedHandler != nil } + return false } - internal func addCompletionHandler(inout token: _HandlerToken?, _ completionHandler: Void -> Void) + internal func addCompletionHandler(inout token: _HandlerToken?, _ completionHandler: Void -> Void) -> Bool { if self.state == .Running || self.state == .Paused { token = self.completionHandlers.append(completionHandler) + return token != nil } + return false } - internal func removeCompletionHandler(handlerToken: _HandlerToken?) + internal func removeCompletionHandler(handlerToken: _HandlerToken?) -> Bool { if let handlerToken = handlerToken { - self.completionHandlers.remove(handlerToken) + let removedHandler = self.completionHandlers.remove(handlerToken) + return removedHandler != nil } + return false } internal func handleProgress(progress: Progress) @@ -224,9 +232,9 @@ internal struct _Handlers: SequenceType return _HandlerToken(key: self.currentKey) } - internal mutating func remove(token: _HandlerToken) + internal mutating func remove(token: _HandlerToken) -> T? { - self.elements.removeValueForKey(token.key) + return self.elements.removeValueForKey(token.key) } internal mutating func removeAll(keepCapacity: Bool = false) From 406e7d09f712ce92629ad6596054355d7e4474b6 Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 10 May 2015 17:48:27 +0900 Subject: [PATCH 4/6] Add comment. --- SwiftTask/SwiftTask.swift | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index 497f419..cc81168 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -374,7 +374,8 @@ public class Task: Cancellable, Printable } /// - /// then (fulfilled & rejected) + closure returning value + /// then (fulfilled & rejected) + closure returning **value** + /// (a.k.a. `map` in functional programming term) /// /// - e.g. task.then { value, errorInfo -> NextValueType in ... } /// @@ -392,7 +393,8 @@ public class Task: Cancellable, Printable } /// - /// then (fulfilled & rejected) + closure returning task + /// then (fulfilled & rejected) + closure returning **task** + /// (a.k.a. `flatMap` in functional programming term) /// /// - e.g. task.then { value, errorInfo -> NextTaskType in ... } /// @@ -402,6 +404,12 @@ public class Task: Cancellable, Printable return self.then(&dummyCanceller, thenClosure) } + // + // NOTE: then-canceller is a shorthand of `task.cancel(nil)`, i.e. these two are the same: + // + // - `let canceller = Canceller(); task1.then(&canceller) {...}; canceller.cancel();` + // - `let task2 = task1.then {...}; task2.cancel();` + // public func then(inout canceller: C?, _ thenClosure: (Value?, ErrorInfo?) -> Task) -> Task { return Task { [unowned self, weak canceller] newMachine, progress, fulfill, _reject, configure in @@ -440,7 +448,7 @@ public class Task: Cancellable, Printable } /// - /// success (fulfilled) + closure returning value + /// success (fulfilled) + closure returning **value** /// /// - e.g. task.success { value -> NextValueType in ... } /// @@ -458,7 +466,7 @@ public class Task: Cancellable, Printable } /// - /// success (fulfilled) + closure returning task + /// success (fulfilled) + closure returning **task** /// /// - e.g. task.success { value -> NextTaskType in ... } /// @@ -489,7 +497,7 @@ public class Task: Cancellable, Printable } /// - /// failure (rejected) + closure returning value + /// failure (rejected or cancelled) + closure returning **value** /// /// - e.g. task.failure { errorInfo -> NextValueType in ... } /// - e.g. task.failure { error, isCancelled -> NextValueType in ... } @@ -508,7 +516,7 @@ public class Task: Cancellable, Printable } /// - /// failure (rejected) + closure returning task + /// failure (rejected or cancelled) + closure returning **task** /// /// - e.g. task.failure { errorInfo -> NextTaskType in ... } /// - e.g. task.failure { error, isCancelled -> NextTaskType in ... } From 47ffe6a4fdfdae6c58cb926e4aafbc5d2639f87a Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 10 May 2015 20:50:02 +0900 Subject: [PATCH 5/6] Improve _Handlers by keeping element order. --- SwiftTask/_StateMachine.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/SwiftTask/_StateMachine.swift b/SwiftTask/_StateMachine.swift index ae7000f..ba1da0e 100644 --- a/SwiftTask/_StateMachine.swift +++ b/SwiftTask/_StateMachine.swift @@ -220,21 +220,28 @@ internal struct _HandlerToken internal struct _Handlers: SequenceType { + internal typealias KeyValue = (key: Int, value: T) + private var currentKey: Int = 0 - private var elements = [Int : T]() + private var elements = [KeyValue]() internal mutating func append(value: T) -> _HandlerToken { self.currentKey = self.currentKey &+ 1 - self.elements[self.currentKey] = value + self.elements += [(key: self.currentKey, value: value)] return _HandlerToken(key: self.currentKey) } internal mutating func remove(token: _HandlerToken) -> T? { - return self.elements.removeValueForKey(token.key) + for var i = 0; i < self.elements.count; i++ { + if self.elements[i].key == token.key { + return self.elements.removeAtIndex(i).value + } + } + return nil } internal mutating func removeAll(keepCapacity: Bool = false) @@ -244,6 +251,6 @@ internal struct _Handlers: SequenceType internal func generate() -> GeneratorOf { - return GeneratorOf(self.elements.values.generate()) + return GeneratorOf(self.elements.map { $0.value }.generate()) } } \ No newline at end of file From 18c4f7a09e39c6975c7b1dd72895bbf184a0282d Mon Sep 17 00:00:00 2001 From: Yasuhiro Inami Date: Sun, 10 May 2015 23:59:02 +0900 Subject: [PATCH 6/6] Fix Cancellable for ver 3.x API compatibility. --- SwiftTask/Cancellable.swift | 15 +++++++++++++-- SwiftTask/SwiftTask.swift | 4 ++-- SwiftTaskTests/SwiftTaskTests.swift | 8 ++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/SwiftTask/Cancellable.swift b/SwiftTask/Cancellable.swift index 94df34e..a707f16 100644 --- a/SwiftTask/Cancellable.swift +++ b/SwiftTask/Cancellable.swift @@ -12,7 +12,13 @@ public protocol Cancellable { typealias Error - func cancel(error: Error) -> Bool + // + // NOTE: + // Single `func cancel(error: Error) -> Bool` is preferred (as first implemented in 8a22ed5), + // but two overloaded methods are required for SwiftTask ver 3.x API compatibility. + // + func cancel() -> Bool + func cancel(#error: Error) -> Bool } public class Canceller: Cancellable @@ -24,7 +30,12 @@ public class Canceller: Cancellable self.cancelHandler = cancelHandler } - public func cancel(error: Void) -> Bool + public func cancel() -> Bool + { + return self.cancel(error: ()) + } + + public func cancel(#error: Void) -> Bool { if let cancelHandler = self.cancelHandler { self.cancelHandler = nil diff --git a/SwiftTask/SwiftTask.swift b/SwiftTask/SwiftTask.swift index cc81168..503151a 100644 --- a/SwiftTask/SwiftTask.swift +++ b/SwiftTask/SwiftTask.swift @@ -564,10 +564,10 @@ public class Task: Cancellable, Printable // public func cancel() -> Bool { - return self.cancel(nil) + return self.cancel(error: nil) } - public func cancel(error: Error?) -> Bool + public func cancel(#error: Error?) -> Bool { return self._cancel(error: error) } diff --git a/SwiftTaskTests/SwiftTaskTests.swift b/SwiftTaskTests/SwiftTaskTests.swift index 77b1eac..8968447 100644 --- a/SwiftTaskTests/SwiftTaskTests.swift +++ b/SwiftTaskTests/SwiftTaskTests.swift @@ -598,7 +598,7 @@ class SwiftTaskTests: _TestCase // cancel at time between 1st & 2nd delay (t=0.3) Async.main(after: 0.3) { - task.cancel("I get bored.") + task.cancel(error: "I get bored.") XCTAssertEqual(task.state, TaskState.Cancelled) @@ -635,7 +635,7 @@ class SwiftTaskTests: _TestCase // cancel task3 at time between task1 fulfilled & before task2 completed (t=0.6) Async.main(after: 0.6) { - task3.cancel("I get bored.") + task3.cancel(error: "I get bored.") XCTAssertEqual(task3.state, TaskState.Cancelled) @@ -1175,7 +1175,7 @@ class SwiftTaskTests: _TestCase // cancel before fulfilled Async.main(after: 0.01) { - groupedTask.cancel("Cancel") + groupedTask.cancel(error: "Cancel") return } @@ -1390,7 +1390,7 @@ class SwiftTaskTests: _TestCase // cancel before fulfilled self.perform { - groupedTask.cancel("Cancel") + groupedTask.cancel(error: "Cancel") return }