Skip to content

Commit 9438564

Browse files
committed
Add try() (retryable feature)
1 parent bdf910d commit 9438564

File tree

3 files changed

+146
-15
lines changed

3 files changed

+146
-15
lines changed

SwiftTask/SwiftTask.swift

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public class Task<Progress, Value, Error>
8686
internal typealias Machine = StateMachine<TaskState, TaskEvent>
8787

8888
internal var machine: Machine!
89+
90+
// store initial parameters for cloning task when using `try()`
91+
internal let _weakified: Bool
92+
internal var _initClosure: _InitClosure? // will be nil on fulfilled/rejected
8993

9094
/// progress value
9195
public internal(set) var progress: Progress?
@@ -122,11 +126,14 @@ public class Task<Progress, Value, Error>
122126
///
123127
public init(weakified: Bool, initClosure: InitClosure)
124128
{
125-
self.setup(weakified) { (progress, fulfill, _reject: ErrorInfo -> Void, configure) in
129+
self._weakified = weakified
130+
self._initClosure = { (progress, fulfill, _reject: _RejectHandler, configure) in
126131
// NOTE: don't expose rejectHandler with ErrorInfo (isCancelled) for public init
127132
initClosure(progress: progress, fulfill: fulfill, reject: { (error: Error?) in _reject(ErrorInfo(error: error, isCancelled: false)) }, configure: configure)
128133
return
129134
}
135+
136+
self.setup(weakified, self._initClosure!)
130137
}
131138

132139
/// creates task without weakifying progress/fulfill/reject handlers
@@ -162,13 +169,21 @@ public class Task<Progress, Value, Error>
162169
})
163170
}
164171

165-
internal init(_initClosure: _InitClosure)
172+
/// NOTE: _initClosure has _RejectHandler as argument
173+
internal init(weakified: Bool = false, _initClosure: _InitClosure)
166174
{
167-
self.setup(false, _initClosure)
175+
self._weakified = weakified
176+
self._initClosure = _initClosure
177+
178+
self.setup(weakified, _initClosure)
168179
}
169180

170181
internal func setup(weakified: Bool, _initClosure: _InitClosure)
171182
{
183+
#if DEBUG
184+
println("[init] \(self)")
185+
#endif
186+
172187
let configuration = Configuration()
173188

174189
weak var weakSelf = self
@@ -192,32 +207,36 @@ public class Task<Progress, Value, Error>
192207
return
193208
}
194209

210+
// NOTE: use order = 90 (< default = 100) to prepare setting value before handling progress/fulfill/reject
195211
$0.addEventHandler(.Progress, order: 90) { context in
196212
if let progressTuple = context.userInfo as? ProgressTuple {
197-
if let self_ = weakSelf {
198-
self_.progress = progressTuple.newProgress
199-
}
213+
weakSelf?.progress = progressTuple.newProgress
200214
}
201215
}
202-
203216
$0.addEventHandler(.Fulfill, order: 90) { context in
204217
if let value = context.userInfo as? Value {
205-
if let self_ = weakSelf {
206-
self_.value = value
207-
}
218+
weakSelf?.value = value
208219
}
209220
configuration.clear()
210221
}
211222
$0.addEventHandler(.Reject, order: 90) { context in
212223
if let errorInfo = context.userInfo as? ErrorInfo {
213-
if let self_ = weakSelf {
214-
self_.errorInfo = errorInfo
215-
}
224+
weakSelf?.errorInfo = errorInfo
216225
configuration.cancel?() // NOTE: call configured cancellation on reject as well
217226
}
218227
configuration.clear()
219228
}
220229

230+
// clear `_initClosure` after fulfilled/rejected to prevent retain cycle
231+
$0.addEventHandler(.Fulfill, order: 255) { context in
232+
weakSelf?._initClosure = nil
233+
return
234+
}
235+
$0.addEventHandler(.Reject, order: 255) { context in
236+
weakSelf?._initClosure = nil
237+
return
238+
}
239+
221240
}
222241

223242
var progressHandler: ProgressHandler
@@ -268,12 +287,36 @@ public class Task<Progress, Value, Error>
268287

269288
deinit
270289
{
271-
// println("deinit: \(self)")
290+
// #if DEBUG
291+
// println("[deinit] \(self)")
292+
// #endif
272293

273294
// cancel in case machine is still running
274295
self._cancel(error: nil)
275296
}
276297

298+
/// Returns new task that is retryable for `tryCount-1` times.
299+
/// `task.try(n)` is conceptually equal to `task.failure(clonedTask1).failure(clonedTask2)...` with n-1 failure-able.
300+
public func try(tryCount: Int) -> Task
301+
{
302+
if tryCount < 2 { return self }
303+
304+
let weakified = self._weakified
305+
let initClosure = self._initClosure
306+
307+
if initClosure == nil { return self }
308+
309+
var nextTask: Task? = self
310+
311+
for i in 1...tryCount-1 {
312+
nextTask = nextTask!.failure { _ -> Task in
313+
return Task(weakified: weakified, _initClosure: initClosure!) // create a clone-task when rejected
314+
}
315+
}
316+
317+
return nextTask!
318+
}
319+
277320
public func progress(progressClosure: ProgressTuple -> Void) -> Task
278321
{
279322
self.machine.addEventHandler(.Progress) { [weak self] context in
@@ -631,6 +674,20 @@ extension Task
631674
}
632675
}
633676

677+
//--------------------------------------------------
678+
// MARK: - Custom Operators
679+
// + - * / % = < > ! & | ^ ~ .
680+
//--------------------------------------------------
681+
682+
infix operator ~ { associativity left }
683+
684+
/// abbreviation for `try()`
685+
/// e.g. (task ~ 3).then { ... }
686+
public func ~ <P, V, E>(task: Task<P, V, E>, tryCount: Int) -> Task<P, V, E>
687+
{
688+
return task.try(tryCount)
689+
}
690+
634691
//--------------------------------------------------
635692
// MARK: - Utility
636693
//--------------------------------------------------

SwiftTaskTests/RetainCycleTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ class Player
1313
{
1414
var completionHandler: (Void -> Void)?
1515

16+
init()
17+
{
18+
println("[init] \(self)")
19+
}
20+
1621
deinit
1722
{
18-
println("deinit: Player")
23+
println("[deinit] \(self)")
1924
}
2025

2126
func doSomething(completion: (Void -> Void)? = nil)

SwiftTaskTests/SwiftTaskTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,75 @@ class SwiftTaskTests: _TestCase
672672
self.wait()
673673
}
674674

675+
//--------------------------------------------------
676+
// MARK: - Try
677+
//--------------------------------------------------
678+
679+
func testTry_success()
680+
{
681+
// NOTE: async only
682+
if !self.isAsync { return }
683+
684+
var expect = self.expectationWithDescription(__FUNCTION__)
685+
var tryCount = 3
686+
var completeCount = 0
687+
688+
Task<Float, String, ErrorString> { progress, fulfill, reject, configure in
689+
690+
self.perform {
691+
completeCount++
692+
693+
if completeCount < tryCount {
694+
reject("ERROR \(completeCount)")
695+
}
696+
else {
697+
fulfill("OK")
698+
}
699+
}
700+
701+
}.try(tryCount).failure { errorInfo -> String in
702+
703+
XCTFail("Should never reach here because `task.try(\(tryCount))` will be fulfilled on try[\(tryCount)] even though try[1...\(tryCount-1)] will be rejected.")
704+
705+
return "DUMMY"
706+
707+
}.success { value -> Void in
708+
709+
XCTAssertEqual(value, "OK")
710+
expect.fulfill()
711+
712+
}
713+
714+
self.wait()
715+
}
716+
717+
func testTry_failure()
718+
{
719+
var expect = self.expectationWithDescription(__FUNCTION__)
720+
var tryCount = 3
721+
var completeCount = 0
722+
723+
let t = Task<Float, String, ErrorString> { progress, fulfill, reject, configure in
724+
725+
self.perform {
726+
completeCount++
727+
reject("ERROR \(completeCount)")
728+
}
729+
730+
}.try(tryCount).failure { error, isCancelled -> String in
731+
732+
XCTAssertEqual(error!, "ERROR \(completeCount)")
733+
XCTAssertFalse(isCancelled)
734+
735+
expect.fulfill()
736+
737+
return "DUMMY"
738+
739+
}
740+
741+
self.wait()
742+
}
743+
675744
//--------------------------------------------------
676745
// MARK: - All
677746
//--------------------------------------------------

0 commit comments

Comments
 (0)