diff --git a/Sources/Error.swift b/Sources/Error.swift index 7229e6f49..9e995f027 100644 --- a/Sources/Error.swift +++ b/Sources/Error.swift @@ -32,6 +32,9 @@ public enum PMKError: Error { Also used if all values of this collection failed the test passed to `firstValue(where:)`. */ case emptySequence + + /// no winner in `race(fulfilled:)` + case noWinner } extension PMKError: CustomDebugStringConvertible { @@ -51,6 +54,8 @@ extension PMKError: CustomDebugStringConvertible { return "The asynchronous sequence was cancelled" case .emptySequence: return "The first or last element was requested for an empty sequence" + case .noWinner: + return "All thenables passed to race(fulfilled:) were rejected" } } } diff --git a/Sources/race.swift b/Sources/race.swift index 2b817de26..76ae96d06 100644 --- a/Sources/race.swift +++ b/Sources/race.swift @@ -1,3 +1,5 @@ +import Dispatch + @inline(__always) private func _race(_ thenables: [U]) -> Promise { let rp = Promise(.pending) @@ -55,3 +57,46 @@ public func race(_ guarantees: Guarantee...) -> Guarantee { } return rg } + +/** + Waits for one promise to fulfill + + race(fulfilled: [promise1, promise2, promise3]).then { winner in + //… + } + + - Returns: The promise that was fulfilled first. + - Warning: Skips all rejected promises. + - Remark: If the provided array is empty, the returned promise is rejected with `PMKError.badInput`. If there are no fulfilled promises, the returned promise is rejected with `PMKError.noWinner`. +*/ +public func race(fulfilled thenables: [U]) -> Promise { + var countdown = thenables.count + guard countdown > 0 else { + return Promise(error: PMKError.badInput) + } + + let rp = Promise(.pending) + + let barrier = DispatchQueue(label: "org.promisekit.barrier.race", attributes: .concurrent) + + for promise in thenables { + promise.pipe { result in + barrier.sync(flags: .barrier) { + switch result { + case .rejected: + guard rp.isPending else { return } + countdown -= 1 + if countdown == 0 { + rp.box.seal(.rejected(PMKError.noWinner)) + } + case .fulfilled(let value): + guard rp.isPending else { return } + countdown = 0 + rp.box.seal(.fulfilled(value)) + } + } + } + } + + return rp +} diff --git a/Tests/CorePromise/RaceTests.swift b/Tests/CorePromise/RaceTests.swift index c3676a11e..45d7ef0b7 100644 --- a/Tests/CorePromise/RaceTests.swift +++ b/Tests/CorePromise/RaceTests.swift @@ -48,4 +48,44 @@ class RaceTests: XCTestCase { } wait(for: [ex], timeout: 10) } + + func testFulfilled() { + enum Error: Swift.Error { case test1, test2, test3 } + let ex = expectation(description: "") + let promises: [Promise] = [after(seconds: 1).map { _ in throw Error.test1 }, after(seconds: 2).map { _ in throw Error.test2 }, after(seconds: 5).map { 1 }, after(seconds: 4).map { 2 }, after(seconds: 3).map { _ in throw Error.test3 }] + race(fulfilled: promises).done { + XCTAssertEqual($0, 2) + ex.fulfill() + }.catch { _ in + XCTFail() + ex.fulfill() + } + wait(for: [ex], timeout: 10) + } + + func testFulfilledEmptyArray() { + let ex = expectation(description: "") + let empty = [Promise]() + race(fulfilled: empty).catch { + guard case PMKError.badInput = $0 else { return XCTFail() } + ex.fulfill() + } + wait(for: [ex], timeout: 10) + } + + func testFulfilledWithNoWinner() { + enum Error: Swift.Error { case test1, test2 } + let ex = expectation(description: "") + let promises: [Promise] = [after(seconds: 1).map { _ in throw Error.test1 }, after(seconds: 2).map { _ in throw Error.test2 }] + race(fulfilled: promises).done { _ in + XCTFail() + ex.fulfill() + }.catch { + guard let pmkError = $0 as? PMKError else { return XCTFail() } + guard case .noWinner = pmkError else { return XCTFail() } + guard pmkError.debugDescription == "All thenables passed to race(fulfilled:) were rejected" else { return XCTFail() } + ex.fulfill() + } + wait(for: [ex], timeout: 10) + } } diff --git a/Tests/CorePromise/XCTestManifests.swift b/Tests/CorePromise/XCTestManifests.swift index 8baedbb41..09ce7005c 100644 --- a/Tests/CorePromise/XCTestManifests.swift +++ b/Tests/CorePromise/XCTestManifests.swift @@ -161,6 +161,9 @@ extension RaceTests { ("test2", test2), ("test2Array", test2Array), ("testEmptyArray", testEmptyArray), + ("testFulfilled", testFulfilled), + ("testFulfilledEmptyArray", testFulfilledEmptyArray), + ("testFulfilledWithNoWinner", testFulfilledWithNoWinner), ] }