Skip to content

Commit 9d58420

Browse files
authored
Merge pull request #83 from glessard/devel
improve `recover`
2 parents 5fea189 + 6d67bd9 commit 9d58420

File tree

3 files changed

+145
-111
lines changed

3 files changed

+145
-111
lines changed

Source/deferred/deferred-extras.swift

Lines changed: 93 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ extension Deferred
405405

406406
extension Deferred
407407
{
408-
/// Flatten a `Deferred<Deferred<Success>, Failure>` to a `Deferred<Success, Failure>`
408+
/// Flatten a `Deferred<Deferred<Success, Failure>, Failure>` to a `Deferred<Success, Failure>`
409409
///
410410
/// In the right conditions, acts like a fast path for a flatMap with no transform.
411411
///
@@ -462,7 +462,7 @@ extension Deferred
462462
}
463463
}
464464

465-
/// Flatten a `Deferred<Deferred<Success>, Never>` to a `Deferred<Success, Failure>`
465+
/// Flatten a `Deferred<Deferred<Success, Failure>, Never>` to a `Deferred<Success, Failure>`
466466
///
467467
/// In the right conditions, acts like a fast path for a flatMap with no transform.
468468
///
@@ -516,7 +516,7 @@ extension Deferred
516516

517517
}
518518

519-
extension Deferred where Failure == Error
519+
extension Deferred
520520
{
521521
/// Enqueue a transform to be computed asynchronously if and when `self` becomes resolved with an error.
522522
///
@@ -526,7 +526,7 @@ extension Deferred where Failure == Error
526526
/// - parameter error: the Error to be transformed for the new `Deferred`
527527

528528
public func recover(queue: DispatchQueue? = nil,
529-
transform: @escaping (_ error: Error) throws -> Deferred) -> Deferred
529+
transform: @escaping (_ error: Failure) -> Deferred) -> Deferred
530530
{
531531
return Deferred(queue: queue ?? self.queue) {
532532
resolver in
@@ -535,24 +535,19 @@ extension Deferred where Failure == Error
535535
guard resolver.needsResolution else { return }
536536
switch result
537537
{
538-
case let .success(value):
539-
resolver.resolve(value: value)
538+
case .success:
539+
resolver.resolve(result)
540540

541541
case let .failure(error):
542-
do {
543-
let transformed = try transform(error)
544-
if let transformed = transformed.peek()
545-
{
546-
resolver.resolve(transformed)
547-
}
548-
else
549-
{
550-
transformed.notify(queue: queue, handler: resolver.resolve)
551-
resolver.retainSource(transformed)
552-
}
542+
let transformed = transform(error)
543+
if let transformed = transformed.peek()
544+
{
545+
resolver.resolve(transformed)
553546
}
554-
catch {
555-
resolver.resolve(error: error)
547+
else
548+
{
549+
transformed.notify(queue: queue, handler: resolver.resolve)
550+
resolver.retainSource(transformed)
556551
}
557552
}
558553
}
@@ -568,7 +563,7 @@ extension Deferred where Failure == Error
568563
/// - parameter error: the Error to be transformed for the new `Deferred`
569564

570565
public func recover(qos: DispatchQoS,
571-
transform: @escaping (_ error: Error) throws -> Deferred) -> Deferred
566+
transform: @escaping (_ error: Failure) -> Deferred) -> Deferred
572567
{
573568
let queue = DispatchQueue(label: "deferred-recover", qos: qos)
574569
return recover(queue: queue, transform: transform)
@@ -582,25 +577,86 @@ extension Deferred where Failure == Error
582577
/// - parameter qos: the QoS at which the computation (and notifications) should be performed; defaults to the current QoS class.
583578
/// - parameter task: the computation to be performed
584579

585-
public static func RetryTask(_ attempts: Int, qos: DispatchQoS = .current,
586-
task: @escaping () throws -> Success) -> Deferred
580+
public static func Retrying(_ attempts: Int, qos: DispatchQoS = .current,
581+
task: @escaping () -> Deferred) -> Deferred
587582
{
588-
let queue = DispatchQueue(label: "deferred", qos: qos)
589-
return Deferred.RetryTask(attempts, queue: queue, task: task)
583+
let queue = DispatchQueue(label: "retrying", qos: qos)
584+
return Deferred.Retrying(attempts, queue: queue, task: task)
590585
}
591586

592587
/// Initialize a `Deferred` with a computation task to be performed in the background
593588
///
594589
/// If at first it does not succeed, it will try `attempts` times in total before being resolved with an `Error`.
595590
///
596-
/// - parameter attempts: a maximum number of times to attempt `task`
591+
/// - parameter attempts: a maximum number of times to attempt `task` (must be greater than zero)
597592
/// - parameter queue: the `DispatchQueue` on which the computation (and notifications) will be executed
598593
/// - parameter task: the computation to be performed
599594

600-
public static func RetryTask(_ attempts: Int, queue: DispatchQueue,
601-
task: @escaping () throws -> Success) -> Deferred
595+
public static func Retrying(_ attempts: Int, queue: DispatchQueue,
596+
task: @escaping () -> Deferred) -> Deferred
602597
{
603-
return Deferred.Retrying(attempts, queue: queue, task: { Deferred(queue: queue, task: task) })
598+
if attempts < 1
599+
{
600+
let message = "number of attempts must be greater than 0 in \(#function)"
601+
guard let error = Invalidation.invalid(message) as? Failure else { fatalError(message) }
602+
return Deferred(error: error)
603+
}
604+
605+
return (1..<attempts).reduce(task()) {
606+
(deferred, _) in
607+
deferred.recover(transform: { _ in task() })
608+
}
609+
}
610+
}
611+
612+
extension Deferred where Failure == Error
613+
{
614+
/// Enqueue a transform to be computed asynchronously if and when `self` becomes resolved with an error.
615+
///
616+
/// - parameter queue: the `DispatchQueue` to attach to the new `Deferred`; defaults to `self`'s queue.
617+
/// - parameter transform: the transform to be performed
618+
/// - returns: a `Deferred` reference representing the return value of the transform
619+
/// - parameter error: the Error to be transformed for the new `Deferred`
620+
621+
public func recover(queue: DispatchQueue? = nil,
622+
transform: @escaping (_ error: Error) throws -> Success) -> Deferred
623+
{
624+
return Deferred(queue: queue ?? self.queue) {
625+
resolver in
626+
self.notify(queue: queue) {
627+
result in
628+
guard resolver.needsResolution else { return }
629+
switch result
630+
{
631+
case .success:
632+
resolver.resolve(result)
633+
634+
case let .failure(error):
635+
do {
636+
let value = try transform(error)
637+
resolver.resolve(value: value)
638+
}
639+
catch {
640+
resolver.resolve(error: error)
641+
}
642+
}
643+
}
644+
resolver.retainSource(self)
645+
}
646+
}
647+
648+
/// Enqueue a transform to be computed asynchronously if and when `self` becomes resolved with an error.
649+
///
650+
/// - parameter qos: the QoS at which to execute the transform and the new `Deferred`'s notifications
651+
/// - parameter transform: the transform to be performed
652+
/// - returns: a `Deferred` reference representing the return value of the transform
653+
/// - parameter error: the Error to be transformed for the new `Deferred`
654+
655+
public func recover(qos: DispatchQoS,
656+
transform: @escaping (_ error: Error) throws -> Success) -> Deferred
657+
{
658+
let queue = DispatchQueue(label: "deferred-recover", qos: qos)
659+
return recover(queue: queue, transform: transform)
604660
}
605661

606662
/// Initialize a `Deferred` with a computation task to be performed in the background
@@ -612,9 +668,9 @@ extension Deferred where Failure == Error
612668
/// - parameter task: the computation to be performed
613669

614670
public static func Retrying(_ attempts: Int, qos: DispatchQoS = .current,
615-
task: @escaping () throws -> Deferred) -> Deferred
671+
task: @escaping () throws -> Success) -> Deferred
616672
{
617-
let queue = DispatchQueue(label: "retrying", qos: qos)
673+
let queue = DispatchQueue(label: "deferred", qos: qos)
618674
return Deferred.Retrying(attempts, queue: queue, task: task)
619675
}
620676

@@ -627,19 +683,15 @@ extension Deferred where Failure == Error
627683
/// - parameter task: the computation to be performed
628684

629685
public static func Retrying(_ attempts: Int, queue: DispatchQueue,
630-
task: @escaping () throws -> Deferred) -> Deferred
686+
task: @escaping () throws -> Success) -> Deferred
631687
{
632-
let error = Invalidation.invalid("task was not allowed a single attempt in \(#function)")
633-
let deferred = Deferred(queue: queue, error: error)
634-
635-
if attempts < 1 { return deferred }
636-
637-
return Deferred.Retrying(attempts, deferred, task: task)
638-
}
688+
if attempts < 1
689+
{
690+
let message = "number of attempts must be greater than 0 in \(#function)"
691+
return Deferred(queue: queue, error: Invalidation.invalid(message))
692+
}
639693

640-
private static func Retrying(_ attempts: Int, _ deferred: Deferred, task: @escaping () throws -> Deferred) -> Deferred
641-
{
642-
return (0..<attempts).reduce(deferred) {
694+
return (1..<attempts).reduce(Deferred(queue: queue, task: task)) {
643695
(deferred, _) in
644696
deferred.recover(transform: { _ in try task() })
645697
}

Tests/deferredTests/DeferredExtrasTests.swift

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -110,111 +110,93 @@ class DeferredExtrasTests: XCTestCase
110110
XCTAssertEqual(d4.result, value)
111111
}
112112

113-
func testRecover()
113+
func testRecover1()
114114
{
115115
let value = nzRandom()
116116
let error = nzRandom()
117117
let goodOperand = Deferred<Int, Error>(value: value)
118-
let badOperand = Deferred<Double, Error>(error: TestError(error))
118+
let badOperand = Deferred<Double, TestError>(error: TestError(error))
119119

120120
// good operand, transform short-circuited
121-
let d1 = goodOperand.recover(qos: .default) { e in XCTFail(); return Deferred(error: TestError(error)) }
121+
let d1 = goodOperand.recover(qos: .default) { _ -> Deferred<Int, Error> in fatalError(#function) }
122122
XCTAssertEqual(d1.value, value)
123123
XCTAssertEqual(d1.error, nil)
124124

125-
// bad operand, transform throws (type 1)
126-
let d2 = badOperand.recover { error in Deferred { throw TestError(value) } }
125+
// bad operand, transform errors
126+
let d2 = badOperand.recover { error in Deferred(error: TestError(error.error)) }
127127
XCTAssertEqual(d2.value, nil)
128-
XCTAssertEqual(d2.error, TestError(value))
129-
130-
// bad operand, transform throws (type 2)
131-
let d5 = badOperand.recover { _ in throw TestError(value) }
132-
XCTAssertEqual(d5.value, nil)
133-
XCTAssertEqual(d5.error, TestError(value))
128+
XCTAssertEqual(d2.error, TestError(error))
134129

135-
// bad operand, transform executes
136-
let d3 = badOperand.recover { error in Deferred(value: Double(value)) }
137-
XCTAssertEqual(d3.value, Double(value))
130+
// bad operand, transform executes later
131+
let d3 = badOperand.recover { error in Deferred(value: Double(error.error)).delay(.milliseconds(10)) }
132+
XCTAssertEqual(d3.value, Double(error))
138133
XCTAssertEqual(d3.error, nil)
139-
140-
// test early return from notification block
141-
let reason = "reason"
142-
let d4 = goodOperand.delay(.milliseconds(50))
143-
let r4 = d4.recover { e in Deferred(value: value) }
144-
r4.cancel(reason)
145-
XCTAssertEqual(r4.value, nil)
146-
XCTAssertEqual(r4.error as? Cancellation, .canceled(reason))
147134
}
148135

149136
func testRetrying1()
150137
{
151138
let retries = 5
152-
let queue = DispatchQueue(label: "test")
153-
154-
let r1 = Deferred<Void, Error>.Retrying(0, queue: queue, task: { Deferred<Void, Error>(task: {XCTFail()}) })
155-
XCTAssertNotNil(r1.error as? Invalidation)
156-
157139
var counter = 0
158-
let r2 = Deferred<Int, Error>.Retrying(retries, queue: queue) {
159-
() -> Deferred<Int, Error> in
140+
let retried = Deferred<Int, TestError>.Retrying(retries, qos: .utility) {
141+
() -> Deferred<Int, TestError> in
160142
counter += 1
161143
if counter < retries { return Deferred(error: TestError(counter)) }
162144
return Deferred(value: counter)
163145
}
164-
XCTAssertEqual(r2.value, retries)
146+
XCTAssertEqual(retried.value, retries)
147+
148+
let errored = Deferred<Int, Error>.Retrying(0, task: { () -> Deferred<Int, Error> in fatalError() })
149+
XCTAssertEqual(errored.value, nil)
150+
XCTAssertNotNil(errored.error as? Invalidation)
165151
}
166152

167-
func testRetrying2()
153+
func testRecover2()
168154
{
169-
let retries = 5
155+
let value = nzRandom()
156+
let error = nzRandom()
157+
let goodOperand = Deferred<Int, Error>(value: value)
158+
let badOperand = Deferred<Double, Error>(error: TestError(error))
170159

171-
let r1 = Deferred<Void, Error>.Retrying(0, task: { Deferred<Void, Error>(task: {XCTFail()}) })
172-
XCTAssertNotNil(r1.error as? Invalidation)
160+
// good operand, transform short-circuited
161+
let d1 = goodOperand.recover(qos: .default) { _ throws -> Int in fatalError(#function) }
162+
XCTAssertEqual(d1.value, value)
163+
XCTAssertEqual(d1.error, nil)
173164

174-
var counter = 0
175-
let r2 = Deferred<Int, Error>.Retrying(retries) {
176-
() -> Deferred<Int, Error> in
177-
counter += 1
178-
if counter < retries { return Deferred(error: TestError(counter)) }
179-
return Deferred(value: counter)
180-
}
181-
XCTAssertEqual(r2.value, retries)
165+
// bad operand, transform errors
166+
let d2 = badOperand.recover { try ($0 as? TestError).map { throw TestError($0.error) } ?? 0.0 }
167+
XCTAssertEqual(d2.value, nil)
168+
XCTAssertEqual(d2.error, TestError(error))
182169

183-
let r3 = Deferred<Int, Error>.Retrying(retries, qos: .background) {
184-
() -> Deferred<Int, Error> in
185-
counter += 1
186-
return Deferred(error: TestError(counter))
187-
}
188-
XCTAssertEqual(r3.error, TestError(2*retries))
170+
// bad operand, transform executes
171+
let d3 = badOperand.recover { ($0 as? TestError).map { Double($0.error) } ?? 0.0 }
172+
XCTAssertEqual(d3.value, Double(error))
173+
XCTAssertEqual(d3.error, nil)
189174
}
190175

191-
func testRetryTask()
176+
func testRetrying2()
192177
{
193178
let retries = 5
194179
let queue = DispatchQueue(label: "test", qos: .background)
195180

196-
var counter = 0
197-
let r1 = Deferred.RetryTask(retries, queue: queue) {
198-
() throws -> Int in
199-
counter += 1
200-
throw TestError(counter)
181+
var counter = retries+retries-1
182+
func transform() throws -> Int
183+
{
184+
counter -= 1
185+
guard counter <= 0 else { throw TestError(counter) }
186+
return counter
201187
}
188+
189+
let r1 = Deferred.Retrying(retries, queue: queue, task: transform)
202190
XCTAssertEqual(r1.value, nil)
203-
XCTAssertEqual(r1.error, TestError(retries))
204-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
205-
XCTAssertEqual(r1.qos, .background)
206-
#endif
191+
XCTAssertEqual(r1.error, TestError(retries-1))
207192

208-
let r2 = Deferred.RetryTask(retries, qos: .utility) {
209-
() throws -> Int in
210-
counter += 1
211-
throw TestError(counter)
212-
}
213-
XCTAssertEqual(r2.value, nil)
214-
XCTAssertEqual(r2.error, TestError(2*retries))
215-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
216-
XCTAssertEqual(r2.qos, .utility)
217-
#endif
193+
let r2 = Deferred.Retrying(retries, qos: .utility, task: transform)
194+
XCTAssertEqual(r2.value, 0)
195+
XCTAssertEqual(r2.error, nil)
196+
197+
let r3 = Deferred.Retrying(0, task: { Double.nan })
198+
XCTAssertEqual(r3.value, nil)
199+
XCTAssertNotNil(r3.error as? Invalidation)
218200
}
219201

220202
func testFlatMap()

Tests/deferredTests/XCTestManifests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ extension DeferredExtrasTests {
7171
("testOnValueAndOnError", testOnValueAndOnError),
7272
("testOptional", testOptional),
7373
("testQoS", testQoS),
74-
("testRecover", testRecover),
74+
("testRecover1", testRecover1),
75+
("testRecover2", testRecover2),
7576
("testRetrying1", testRetrying1),
7677
("testRetrying2", testRetrying2),
77-
("testRetryTask", testRetryTask),
7878
("testSplit", testSplit),
7979
("testTryFlatMap", testTryFlatMap),
8080
("testTryMap", testTryMap),

0 commit comments

Comments
 (0)