├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources └── FunAsync │ ├── Async.swift │ ├── AsyncAll.swift │ ├── AsyncAllSettled.swift │ ├── AsyncFirst.swift │ ├── AsyncFirstSuccess.swift │ ├── AsyncOr.swift │ ├── AsyncSequence+Conversion.swift │ ├── AsyncStream.swift │ ├── Conversion.swift │ ├── WithRetry.swift │ └── WithTimeout.swift └── Tests └── FunAsyncTests ├── AsyncAllSettledTests.swift ├── AsyncAllTests.swift ├── AsyncFirstSuccessTests.swift ├── AsyncFirstTests.swift ├── AsyncOrTests.swift ├── AsyncStreamTests.swift ├── Fixtures.swift ├── WithRetryTests.swift └── WithTimeoutTests.swift /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions 2 | # https://github.com/actions/virtual-environments/blob/master/images/macos 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - ci/** 10 | pull_request: 11 | 12 | jobs: 13 | xcodebuild-macOS: 14 | runs-on: macos-11 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Select Xcode version 18 | run: sudo xcode-select -s '/Applications/Xcode_13.0.app' 19 | - name: Run tests 20 | run: make test-macOS 21 | 22 | xcodebuild-iOS: 23 | runs-on: macos-11 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Select Xcode version 27 | run: sudo xcode-select -s '/Applications/Xcode_13.0.app' 28 | - name: Run tests 29 | run: make test-iOS 30 | 31 | xcodebuild-tvOS: 32 | runs-on: macos-11 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Select Xcode version 36 | run: sudo xcode-select -s '/Applications/Xcode_13.0.app' 37 | - name: Run tests 38 | run: make test-tvOS 39 | 40 | xcodebuild-watchOS: 41 | runs-on: macos-11 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Select Xcode version 45 | run: sudo xcode-select -s '/Applications/Xcode_13.0.app' 46 | - name: Run tests 47 | run: make test-watchOS 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yasuhiro Inami 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test-macOS: 2 | xcodebuild test -scheme FunAsync -destination 'platform=OS X' | xcpretty 3 | 4 | test-iOS: 5 | xcodebuild test -scheme FunAsync -destination 'platform=iOS Simulator,name=iPhone 13 Pro' | xcpretty 6 | 7 | test-watchOS: 8 | xcodebuild test -scheme FunAsync -destination 'platform=watchOS Simulator,name=Apple Watch Series 7 - 45mm' | xcpretty 9 | 10 | test-tvOS: 11 | xcodebuild test -scheme FunAsync -destination 'platform=tvOS Simulator,name=Apple TV 4K' | xcpretty 12 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "FunAsync", 7 | platforms: [.macOS(.v10_15), .iOS(.v13), .watchOS(.v6), .tvOS(.v13)], 8 | products: [ 9 | .library( 10 | name: "FunAsync", 11 | targets: ["FunAsync"]), 12 | ], 13 | dependencies: [ 14 | ], 15 | targets: [ 16 | .target( 17 | name: "FunAsync", 18 | dependencies: []), 19 | .testTarget( 20 | name: "FunAsyncTests", 21 | dependencies: ["FunAsync"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⏳ FunAsync 2 | 3 | Collection of Swift 5.5 `async`/`await` utility functions. 4 | 5 | - **Throw <-> Result conversion** 6 | - `asyncThrowsToAsyncResult` 7 | - `asyncResultToAsyncThrows` 8 | - **More Concurrency helpers** 9 | - `asyncFirst` (`Promise.race`) 10 | - `asyncFirstSuccess` (`Promise.any`) 11 | - `asyncAll` (`Promise.all`) 12 | - `asyncAllSettled` (`Promise.allSettled`) 13 | - `asyncOr` (sequential execution until first success) 14 | - `asyncStream` (from asyncs to `AsyncStream`) 15 | - `withRetry` (with customizability e.g. exponential backoff) 16 | - `withTimeout` (racing with time) 17 | 18 | ## License 19 | 20 | [MIT](LICENSE) 21 | -------------------------------------------------------------------------------- /Sources/FunAsync/Async.swift: -------------------------------------------------------------------------------- 1 | /// Async monad that wraps `() async -> T`. 2 | public struct Async 3 | { 4 | public let run: () async -> T 5 | 6 | public init(run: @escaping () async -> T) 7 | { 8 | self.run = run 9 | } 10 | 11 | public init(_ value: T) 12 | { 13 | self.init { value } 14 | } 15 | 16 | public func map(_ f: @escaping (T) -> U) -> Async 17 | { 18 | .init { 19 | f(await run()) 20 | } 21 | } 22 | 23 | public func zipWith(_ u: Async) -> Async<(T, U)> 24 | { 25 | .init { 26 | await (self.run(), u.run()) 27 | } 28 | } 29 | 30 | public func flatMap(_ f: @escaping (T) -> Async) -> Async 31 | { 32 | .init { 33 | await f(await run()).run() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncAll.swift: -------------------------------------------------------------------------------- 1 | // MARK: - asyncAll (non-throwing) 2 | 3 | /// Runs multiple `fs` concurrently and returns all values. 4 | /// This method is equivalent to `Promise.all` in JavaScript. 5 | public func asyncAll(_ fs: [() async -> B]) -> () async -> [B] 6 | { 7 | precondition(!fs.isEmpty, "asyncAll error: async array is empty.") 8 | 9 | return { 10 | let fs_: [(()) async -> B] = fs.map { f in { _ in await f() } } 11 | return await asyncAll(fs_)(()) 12 | } 13 | } 14 | 15 | /// Runs multiple `fs` concurrently and returns all values. 16 | /// This method is equivalent to `Promise.all` in JavaScript. 17 | public func asyncAll(_ fs: [(A) async -> B]) -> (A) async -> [B] 18 | { 19 | precondition(!fs.isEmpty, "asyncAll error: async array is empty.") 20 | 21 | return { a in 22 | await withTaskGroup(of: (Int, B).self) { group in 23 | for (i, f) in fs.enumerated() { 24 | group.addTask { 25 | (i, await f(a)) 26 | } 27 | } 28 | 29 | return await group 30 | .reduce(into: [], { $0.append($1) }) 31 | .sorted(by: { $0.0 < $1.0 }) 32 | .map { $1 } 33 | } 34 | } 35 | } 36 | 37 | // MARK: - asyncAll (throwing) 38 | 39 | /// Runs multiple `fs` concurrently and returns all values, where first error may be thrown. 40 | /// This method is equivalent to `Promise.all` in JavaScript. 41 | public func asyncAll(_ fs: [() async throws -> B]) -> () async throws -> [B] 42 | { 43 | precondition(!fs.isEmpty, "asyncAll error: async array is empty.") 44 | 45 | return { 46 | let fs_: [(()) async throws -> B] = fs.map { f in { _ in try await f() } } 47 | return try await asyncAll(fs_)(()) 48 | } 49 | } 50 | 51 | /// Runs multiple `fs` concurrently and returns all values, where first error may be thrown. 52 | /// This method is equivalent to `Promise.all` in JavaScript. 53 | public func asyncAll(_ fs: [(A) async throws -> B]) -> (A) async throws -> [B] 54 | { 55 | precondition(!fs.isEmpty, "asyncAll error: async array is empty.") 56 | 57 | return { a in 58 | try await withThrowingTaskGroup(of: (Int, B).self) { group in 59 | for (i, f) in fs.enumerated() { 60 | group.addTask { 61 | (i, try await f(a)) 62 | } 63 | } 64 | 65 | return try await group 66 | .reduce(into: [], { $0.append($1) }) 67 | .sorted(by: { $0.0 < $1.0 }) 68 | .map { $1 } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncAllSettled.swift: -------------------------------------------------------------------------------- 1 | /// Runs multiple `fs` concurrently and returns all results (values and errors). 2 | /// This method is equivalent to `Promise.allSettled` in JavaScript. 3 | public func asyncAllSettled(_ fs: [() async throws -> B]) -> () async -> [Result] 4 | { 5 | precondition(!fs.isEmpty, "asyncAllSettled error: async array is empty.") 6 | 7 | return { 8 | let fs_: [(()) async throws -> B] = fs.map { f in { _ in try await f() } } 9 | return await asyncAllSettled(fs_)(()) 10 | } 11 | } 12 | 13 | /// Runs multiple `fs` concurrently and returns all results (values and errors). 14 | /// This method is equivalent to `Promise.allSettled` in JavaScript. 15 | public func asyncAllSettled(_ fs: [(A) async throws -> B]) -> (A) async -> [Result] 16 | { 17 | precondition(!fs.isEmpty, "asyncAllSettled error: async array is empty.") 18 | 19 | return { a in 20 | await withTaskGroup(of: (Int, Result).self) { group in 21 | for (i, f) in fs.enumerated() { 22 | group.addTask { 23 | do { 24 | return (i, .success(try await f(a))) 25 | } 26 | catch { 27 | return (i, .failure(error)) 28 | } 29 | } 30 | } 31 | 32 | return await group 33 | .reduce(into: [], { $0.append($1) }) 34 | .sorted(by: { $0.0 < $1.0 }) 35 | .map { $1 } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncFirst.swift: -------------------------------------------------------------------------------- 1 | // MARK: - asyncFirst (non-throwing) 2 | 3 | /// Runs multiple `fs` concurrently and returns the first arrival. 4 | /// This method is equivalent to `Promise.race` or `Promise.any` (without errors) in JavaScript. 5 | public func asyncFirst(_ fs: [() async -> B]) -> () async -> B 6 | { 7 | precondition(!fs.isEmpty, "asyncFirst error: async array is empty.") 8 | 9 | return { 10 | let fs_: [(()) async -> B] = fs.map { f in { _ in await f() } } 11 | return await asyncFirst(fs_)(()) 12 | } 13 | } 14 | 15 | /// Runs multiple `fs` concurrently and returns the first arrival. 16 | /// This method is equivalent to `Promise.race` or `Promise.any` (without errors) in JavaScript. 17 | public func asyncFirst(_ fs: [(A) async -> B]) -> (A) async -> B 18 | { 19 | precondition(!fs.isEmpty, "asyncFirst error: async array is empty.") 20 | 21 | return { a in 22 | await withTaskGroup(of: B.self) { group in 23 | for f in fs { 24 | group.addTask { 25 | await f(a) 26 | } 27 | } 28 | 29 | let first = await group.next()! 30 | 31 | // Cancel others when first result is arrived (either success or error). 32 | group.cancelAll() 33 | 34 | return first 35 | } 36 | } 37 | } 38 | 39 | // MARK: - asyncFirst (throwing) 40 | 41 | /// Runs multiple `fs` concurrently and returns the first arrival, which can be either success or error. 42 | /// This method is equivalent to `Promise.race` in JavaScript. 43 | public func asyncFirst(_ fs: [() async throws -> B]) -> () async throws -> B 44 | { 45 | precondition(!fs.isEmpty, "asyncFirst error: async array is empty.") 46 | 47 | return { 48 | let fs_: [(()) async throws -> B] = fs.map { f in { _ in try await f() } } 49 | return try await asyncFirst(fs_)(()) 50 | } 51 | } 52 | 53 | /// Runs multiple `fs` concurrently and returns the first arrival, which can be either success or error. 54 | /// This method is equivalent to `Promise.race` in JavaScript. 55 | public func asyncFirst(_ fs: [(A) async throws -> B]) -> (A) async throws -> B 56 | { 57 | precondition(!fs.isEmpty, "asyncFirst error: async array is empty.") 58 | 59 | return { a in 60 | try await withThrowingTaskGroup(of: B.self) { group -> B in 61 | for f in fs { 62 | group.addTask { 63 | try await f(a) 64 | } 65 | } 66 | 67 | let first = try await group.next()! 68 | group.cancelAll() 69 | return first 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncFirstSuccess.swift: -------------------------------------------------------------------------------- 1 | /// Runs multiple `fs` concurrently and returns the first successful value, ignoring the error arrivals. 2 | /// This method is equivalent to `Promise.any` in JavaScript. 3 | public func asyncFirstSuccess(_ fs: [() async throws -> B]) -> () async -> B? 4 | { 5 | precondition(!fs.isEmpty, "asyncFirstSuccess error: async array is empty.") 6 | 7 | return { 8 | let fs_: [(()) async throws -> B] = fs.map { f in { _ in try await f() } } 9 | return await asyncFirstSuccess(fs_)(()) 10 | } 11 | } 12 | 13 | /// Runs multiple `fs` concurrently and returns the first successful value, ignoring the error arrivals. 14 | /// This method is equivalent to `Promise.any` in JavaScript. 15 | public func asyncFirstSuccess(_ fs: [(A) async throws -> B]) -> (A) async -> B? 16 | { 17 | precondition(!fs.isEmpty, "asyncFirstSuccess error: async array is empty.") 18 | 19 | return { a in 20 | await withTaskGroup(of: Result.self) { group -> B? in 21 | for f in fs { 22 | group.addTask { 23 | return await asyncThrowsToAsyncResult { 24 | try await f(a) 25 | }(()) 26 | } 27 | } 28 | 29 | for await case let .success(value) in group { 30 | group.cancelAll() // Cancel others when first successful value is arrived. 31 | return value 32 | } 33 | 34 | return nil 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncOr.swift: -------------------------------------------------------------------------------- 1 | /// Runs `fs[0]`, and if it fails will run `fs[1]`, and so on, until first one succeeds. 2 | /// - Throws: `fs.last` error. 3 | public func asyncOr(_ fs: [() async throws -> B]) -> () async throws -> B 4 | { 5 | precondition(!fs.isEmpty, "asyncOr error: async array is empty.") 6 | 7 | return { 8 | let fs_: [(()) async throws -> B] = fs.map { f in { _ in try await f() } } 9 | return try await asyncOr(fs_)(()) 10 | } 11 | } 12 | 13 | /// Runs `fs[0]`, and if it fails will run `fs[1]`, and so on, until first one succeeds. 14 | /// - Throws: `fs.last` error. 15 | public func asyncOr(_ fs: [(A) async throws -> B]) -> (A) async throws -> B 16 | { 17 | precondition(!fs.isEmpty, "asyncOr error: async array is empty.") 18 | 19 | return { a in 20 | var lastError: Error? 21 | 22 | for f in fs { 23 | do { 24 | return try await f(a) 25 | } 26 | catch { 27 | lastError = error 28 | } 29 | } 30 | 31 | throw lastError! 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncSequence+Conversion.swift: -------------------------------------------------------------------------------- 1 | extension AsyncSequence 2 | { 3 | /// `AsyncSequence` to `AsyncThrowingStream`. 4 | public func toAsyncThrowingStream() -> AsyncThrowingStream 5 | { 6 | AsyncThrowingStream { continuation in 7 | let task = Task { 8 | do { 9 | for try await value in self { 10 | continuation.yield(value) 11 | } 12 | continuation.finish(throwing: nil) 13 | } 14 | catch { 15 | continuation.finish(throwing: error) 16 | } 17 | } 18 | 19 | continuation.onTermination = { @Sendable _ in 20 | task.cancel() 21 | } 22 | } 23 | } 24 | 25 | /// `AsyncSequence` to `toAsyncStream`, discarding error. 26 | public func toAsyncStream() -> AsyncStream 27 | { 28 | AsyncStream { continuation in 29 | let task = Task { 30 | do { 31 | for try await value in self { 32 | continuation.yield(value) 33 | } 34 | continuation.finish() 35 | } 36 | catch { 37 | continuation.finish() 38 | } 39 | } 40 | 41 | continuation.onTermination = { @Sendable _ in 42 | task.cancel() 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/FunAsync/AsyncStream.swift: -------------------------------------------------------------------------------- 1 | // MARK: - asyncStream (non-throwing) 2 | 3 | /// Runs multiple `fs` concurrently and returns its `AsyncStream`. 4 | public func asyncStream(_ fs: [() async -> B]) -> () -> AsyncStream 5 | { 6 | precondition(!fs.isEmpty, "asyncAll error: async array is empty.") 7 | 8 | return { 9 | let fs_: [(()) async -> B] = fs.map { f in { _ in await f() } } 10 | return asyncStream(fs_)(()) 11 | } 12 | } 13 | 14 | /// Runs multiple `fs` concurrently and returns its `AsyncStream`. 15 | public func asyncStream(_ fs: [(A) async -> B]) -> (A) -> AsyncStream 16 | { 17 | precondition(!fs.isEmpty, "asyncStream error: async array is empty.") 18 | 19 | return { a in 20 | AsyncStream { continuation in 21 | let task = Task { 22 | await withTaskGroup(of: B.self) { group in 23 | for f in fs { 24 | group.addTask { 25 | await f(a) 26 | } 27 | } 28 | 29 | for await value in group { 30 | continuation.yield(value) 31 | } 32 | continuation.finish() 33 | } 34 | } 35 | 36 | continuation.onTermination = { @Sendable _ in 37 | task.cancel() 38 | } 39 | } 40 | } 41 | } 42 | 43 | // MARK: - asyncStream (throwing) 44 | 45 | /// Runs multiple `fs` concurrently and returns its `AsyncThrowingStream`. 46 | public func asyncStream(_ fs: [() async throws -> B]) -> () -> AsyncThrowingStream 47 | { 48 | precondition(!fs.isEmpty, "asyncAll error: async array is empty.") 49 | 50 | return { 51 | let fs_: [(()) async throws -> B] = fs.map { f in { _ in try await f() } } 52 | return asyncStream(fs_)(()) 53 | } 54 | } 55 | 56 | /// Runs multiple `fs` concurrently and returns its `AsyncThrowingStream`. 57 | public func asyncStream(_ fs: [(A) async throws -> B]) -> (A) -> AsyncThrowingStream 58 | { 59 | precondition(!fs.isEmpty, "asyncStream error: async array is empty.") 60 | 61 | return { a in 62 | AsyncThrowingStream { continuation in 63 | let task = Task { 64 | await withThrowingTaskGroup(of: B.self) { group in 65 | for f in fs { 66 | group.addTask { 67 | try await f(a) 68 | } 69 | } 70 | 71 | do { 72 | for try await value in group { 73 | continuation.yield(value) 74 | } 75 | continuation.finish() 76 | } 77 | catch { 78 | continuation.finish(throwing: error) 79 | } 80 | } 81 | } 82 | 83 | continuation.onTermination = { @Sendable _ in 84 | task.cancel() 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/FunAsync/Conversion.swift: -------------------------------------------------------------------------------- 1 | // MARK: - `async throws` to `async Result` 2 | 3 | /// Converts from `async throws` to `async Result` in JavaScript. 4 | public func asyncThrowsToAsyncResult(_ f: @escaping (A) async throws -> B) 5 | -> (A) async -> Result 6 | { 7 | { a in 8 | do { 9 | return .success(try await f(a)) 10 | } catch { 11 | return .failure(error) 12 | } 13 | } 14 | } 15 | 16 | // MARK: - `async Result` to `async throws` 17 | 18 | /// Converts from `async Result` to `async throws` in JavaScript. 19 | public func asyncResultToAsyncThrows(_ f: @escaping (A) async -> Result) 20 | -> (A) async throws -> B 21 | { 22 | { a in 23 | switch await f(a) { 24 | case let .success(success): 25 | return success 26 | case let .failure(error): 27 | throw error 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/FunAsync/WithRetry.swift: -------------------------------------------------------------------------------- 1 | /// Retries `run` when failed, until `when` throws an error. 2 | public func withRetry( 3 | when: @escaping (Error, _ failedCount: UInt64) async throws -> Void, 4 | run: @escaping () async throws -> B 5 | ) -> () async throws -> B 6 | { 7 | { 8 | try await withRetry(when: when, run: { _ in try await run() })(()) 9 | } 10 | } 11 | 12 | /// Retries `run` when failed, until `when` throws an error. 13 | public func withRetry( 14 | when: @escaping (Error, _ failedCount: UInt64) async throws -> Void, 15 | run: @escaping (A) async throws -> B 16 | ) -> (A) async throws -> B 17 | { 18 | { a in 19 | var failedCount: UInt64 = 0 20 | 21 | while true { 22 | do { 23 | return try await run(a) 24 | } 25 | catch { 26 | failedCount += 1 27 | try await when(error, failedCount) 28 | } 29 | } 30 | } 31 | } 32 | 33 | // MARK: - withRetry + exponential backoff 34 | 35 | /// Retries `run` when failed, until failed count reaches`count`. 36 | /// 37 | /// Additionally, retry with customizable `delay` is also supported for exponential backoff, 38 | /// e.g. `delay = { UInt64(pow(2, (Double($1) - 1))) * initialExponentialBackoffNanoseconds }` 39 | public func withRetry( 40 | maxCount: UInt64, 41 | delay: @escaping (Error, _ failedCount: UInt64) -> UInt64 = { _, _ in 0 }, 42 | run: @escaping () async throws -> B 43 | ) -> () async throws -> B 44 | { 45 | { 46 | try await withRetry( 47 | maxCount: maxCount, 48 | delay: delay, 49 | run: { () in 50 | try await run() 51 | } 52 | )(()) 53 | } 54 | } 55 | 56 | /// Retries `run` when failed, until failed count reaches`count`. 57 | /// 58 | /// Additionally, retry with customizable `delay` is also supported for exponential backoff, 59 | /// e.g. `delay = { UInt64(pow(2, (Double($1) - 1))) * initialExponentialBackoffNanoseconds }` 60 | public func withRetry( 61 | maxCount: UInt64, 62 | delay: @escaping (Error, _ failedCount: UInt64) -> UInt64 = { _, _ in 0 }, 63 | run: @escaping (A) async throws -> B 64 | ) -> (A) async throws -> B 65 | { 66 | { a in 67 | try await withRetry( 68 | when: { error, failedCount in 69 | if failedCount <= maxCount { 70 | let delayValue = delay(error, failedCount) 71 | if delayValue > 0 { 72 | try await Task.sleep(nanoseconds: delayValue) 73 | } 74 | return () 75 | } else { 76 | throw error 77 | } 78 | }, 79 | run: run 80 | )(a) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/FunAsync/WithTimeout.swift: -------------------------------------------------------------------------------- 1 | /// Adds timeout that races with `run`. 2 | public func withTimeout( 3 | nanoseconds: UInt64, 4 | run: @escaping () async throws -> B 5 | ) -> () async throws -> B 6 | { 7 | { 8 | try await withTimeout(nanoseconds: nanoseconds, run: { () in 9 | try await run() 10 | })(()) 11 | } 12 | } 13 | 14 | /// Adds timeout that races with `run`. 15 | public func withTimeout( 16 | nanoseconds: UInt64, 17 | run: @escaping (A) async throws -> B 18 | ) -> (A) async throws -> B 19 | { 20 | { a in 21 | let timeout: (A) async throws -> B? = { _ in 22 | try await Task.sleep(nanoseconds: nanoseconds) 23 | return .none 24 | } 25 | 26 | let race = asyncFirst([run, timeout]) 27 | 28 | if let value = try await race(a) { 29 | return value 30 | } 31 | else { 32 | throw TimeoutCancellationError() 33 | } 34 | } 35 | } 36 | 37 | // MARK: - TimeoutCancellationError 38 | 39 | public struct TimeoutCancellationError : Error {} 40 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/AsyncAllSettledTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class AsyncAllSettledTests: XCTestCase 5 | { 6 | func testAsyncAllSettled_success() async throws 7 | { 8 | let results = await asyncAllSettled([ 9 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 10 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 11 | ])() 12 | .map { $0.mapError(EqError.init) } 13 | 14 | XCTAssertEqual(results, [.success(1), .success(2)]) 15 | } 16 | 17 | func testAsyncAllSettled_success2() async throws 18 | { 19 | let results = await asyncAllSettled([ 20 | { try await makeAsync("1", sleep: sleepUnit * 2, result: .success(1)) }, 21 | { try await makeAsync("2", sleep: sleepUnit, result: .success(2)) } 22 | ])() 23 | .map { $0.mapError(EqError.init) } 24 | 25 | XCTAssertEqual(results, [.success(1), .success(2)], 26 | "Should not affect order") 27 | } 28 | 29 | func testAsyncAllSettled_fail() async throws 30 | { 31 | let results = await asyncAllSettled([ 32 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 33 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 34 | ])() 35 | .map { $0.mapError(EqError.init) } 36 | 37 | XCTAssertEqual(results, [.failure(EqError(MyError())), .success(2)]) 38 | } 39 | 40 | func testAsyncAllSettled_fail2() async throws 41 | { 42 | let results = await asyncAllSettled([ 43 | { try await makeAsync("1", sleep: sleepUnit * 2, result: .failure(MyError())) }, 44 | { try await makeAsync("2", sleep: sleepUnit, result: .success(2)) } 45 | ])() 46 | .map { $0.mapError(EqError.init) } 47 | 48 | XCTAssertEqual(results, [.failure(EqError(MyError())), .success(2)], 49 | "Should not affect order") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/AsyncAllTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class AsyncAllTests: XCTestCase 5 | { 6 | func testNonThrowingAsyncAll() async throws 7 | { 8 | // NOTE: Using `try?` to ignore throwing-function part. 9 | let values = await asyncAll([ 10 | { try? await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 11 | { try? await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 12 | ])() 13 | 14 | XCTAssertEqual(values, [1, 2]) 15 | } 16 | 17 | func testNonThrowingAsyncAll2() async throws 18 | { 19 | // NOTE: Using `try?` to ignore throwing-function part. 20 | let values = await asyncAll([ 21 | { try? await makeAsync("1", sleep: sleepUnit * 2, result: .success(1)) }, 22 | { try? await makeAsync("2", sleep: sleepUnit, result: .success(2)) } 23 | ])() 24 | 25 | XCTAssertEqual(values, [1, 2], 26 | "Should not affect order") 27 | } 28 | 29 | func testAsyncAll_success() async throws 30 | { 31 | let values = try await asyncAll([ 32 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 33 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 34 | ])() 35 | 36 | XCTAssertEqual(values, [1, 2]) 37 | } 38 | 39 | func testAsyncAll_success2() async throws 40 | { 41 | let values = try await asyncAll([ 42 | { try await makeAsync("1", sleep: sleepUnit * 2, result: .success(1)) }, 43 | { try await makeAsync("2", sleep: sleepUnit, result: .success(2)) } 44 | ])() 45 | 46 | XCTAssertEqual(values, [1, 2], 47 | "Should not affect order") 48 | } 49 | 50 | func testAsyncAll_fail() async throws 51 | { 52 | do { 53 | _ = try await asyncAll([ 54 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 55 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 56 | ])() 57 | 58 | XCTFail("Should never reach here.") 59 | } 60 | catch let error as MyError { 61 | XCTAssertEqual(error, MyError(), "First arrival should be MyError.") 62 | } 63 | catch { 64 | XCTFail("Should never reach here.") 65 | } 66 | } 67 | 68 | func testAsyncAll_sibling_cancelling() async throws 69 | { 70 | actor Box { 71 | var isSiblingCancelled = false 72 | 73 | func toggle() { 74 | isSiblingCancelled.toggle() 75 | } 76 | } 77 | 78 | let box = Box() 79 | 80 | do { 81 | _ = try await asyncAll([ 82 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 83 | { 84 | try await withTaskCancellationHandler { 85 | try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) 86 | } onCancel: { 87 | Task.init { await box.toggle() } 88 | } 89 | } 90 | ])() 91 | 92 | XCTFail() 93 | } 94 | catch let error as MyError { 95 | XCTAssertEqual(error, MyError(), "First arrival should be MyError.") 96 | } 97 | catch { 98 | XCTFail("Should never reach here.") 99 | } 100 | 101 | let isSiblingCancelled = await box.isSiblingCancelled 102 | XCTAssertTrue(isSiblingCancelled, "Should cancel other running asyncs") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/AsyncFirstSuccessTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class AsyncFirstSuccessTests: XCTestCase 5 | { 6 | func testAsyncFirstSuccess() async throws 7 | { 8 | let firstSuccess = await asyncFirstSuccess([ 9 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 10 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 11 | ])() 12 | 13 | XCTAssertEqual(firstSuccess, 1) 14 | } 15 | 16 | func testAsyncFirstSuccess_firstFail_secondSuccess() async throws 17 | { 18 | let firstSuccess = await asyncFirstSuccess([ 19 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 20 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 21 | ])() 22 | 23 | XCTAssertEqual(firstSuccess, 2) 24 | } 25 | 26 | func testAsyncFirstSuccess_allFail() async throws 27 | { 28 | let firstSuccess = await asyncFirstSuccess([ 29 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) as Int }, 30 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .failure(MyError())) as Int } 31 | ])() 32 | 33 | XCTAssertNil(firstSuccess, "All asyncs have failed.") 34 | } 35 | 36 | func testAsyncFirstSuccess_sibling_cancelling() async throws 37 | { 38 | actor Box { 39 | var isSiblingCancelled = false 40 | 41 | func toggle() { 42 | isSiblingCancelled.toggle() 43 | } 44 | } 45 | 46 | let box = Box() 47 | 48 | _ = await asyncFirstSuccess([ 49 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 50 | { 51 | try await withTaskCancellationHandler { 52 | try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) 53 | } onCancel: { 54 | Task.init { await box.toggle() } 55 | } 56 | } 57 | ])() 58 | 59 | let isSiblingCancelled = await box.isSiblingCancelled 60 | XCTAssertTrue(isSiblingCancelled, "Race should cancel other running asyncs") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/AsyncFirstTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class AsyncFirstTests: XCTestCase 5 | { 6 | func testNonThrowingAsyncFirst() async throws 7 | { 8 | // NOTE: Using `try?` to ignore throwing-function part. 9 | let first = await asyncFirst([ 10 | { try? await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 11 | { try? await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 12 | ])() 13 | 14 | XCTAssertEqual(first, 1) 15 | } 16 | 17 | func testAsyncFirst_success() async throws 18 | { 19 | let first = try await asyncFirst([ 20 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 21 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 22 | ])() 23 | 24 | XCTAssertEqual(first, 1) 25 | } 26 | 27 | func testAsyncFirst_fail() async throws 28 | { 29 | do { 30 | _ = try await asyncFirst([ 31 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 32 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 33 | ])() 34 | 35 | XCTFail("Should never reach here.") 36 | } 37 | catch { 38 | XCTAssertNotNil(error as? MyError, "First arrival should be MyError.") 39 | } 40 | } 41 | 42 | func testAsyncFirst_sibling_cancelling() async throws 43 | { 44 | actor Box { 45 | var isSiblingCancelled = false 46 | 47 | func toggle() { 48 | isSiblingCancelled.toggle() 49 | } 50 | } 51 | 52 | let box = Box() 53 | 54 | do { 55 | _ = try await asyncFirst([ 56 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 57 | { 58 | try await withTaskCancellationHandler { 59 | try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) 60 | } onCancel: { 61 | Task.init { await box.toggle() } 62 | } 63 | } 64 | ])() 65 | 66 | XCTFail() 67 | } 68 | catch { 69 | XCTAssertNotNil(error as? MyError) 70 | } 71 | 72 | let isSiblingCancelled = await box.isSiblingCancelled 73 | XCTAssertTrue(isSiblingCancelled, "Race should cancel other running asyncs") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/AsyncOrTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class AsyncOrTests: XCTestCase 5 | { 6 | func testAsyncOr_1st_success() async throws 7 | { 8 | do { 9 | let first = try await asyncOr([ 10 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 11 | { try await makeAsync("2", sleep: sleepUnit, result: .success(2)) } 12 | ])() 13 | 14 | XCTAssertEqual(first, 1) 15 | } 16 | catch { 17 | XCTFail() 18 | } 19 | } 20 | 21 | func testAsyncOr_2nd_success() async throws 22 | { 23 | do { 24 | let first = try await asyncOr([ 25 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 26 | { try await makeAsync("2", sleep: sleepUnit, result: .success(2)) } 27 | ])() 28 | 29 | XCTAssertEqual(first, 2) 30 | } 31 | catch { 32 | XCTFail() 33 | } 34 | } 35 | 36 | func testAsyncOr_both_fail() async throws 37 | { 38 | let lastError = MyError(message: "Err2") 39 | 40 | do { 41 | let _: Int = try await asyncOr([ 42 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError(message: "Err1"))) }, 43 | { try await makeAsync("2", sleep: sleepUnit, result: .failure(lastError)) } 44 | ])() 45 | 46 | XCTFail() 47 | } 48 | catch let error as MyError { 49 | XCTAssertEqual(error, lastError) 50 | } 51 | catch { 52 | XCTFail() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/AsyncStreamTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class AsyncStreamTests: XCTestCase 5 | { 6 | func testAsyncStream() async throws 7 | { 8 | // NOTE: Using `try?` to ignore throwing-function part. 9 | let values = asyncStream([ 10 | { try? await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 11 | { try? await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 12 | ])() 13 | 14 | var results: [Int?] = [] 15 | 16 | for await value in values { 17 | results.append(value) 18 | } 19 | 20 | XCTAssertEqual(results, [1, 2]) 21 | } 22 | 23 | func testAsyncStream_success() async throws 24 | { 25 | let values = asyncStream([ 26 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 27 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 28 | ])() 29 | 30 | var results: [Int?] = [] 31 | 32 | do { 33 | for try await value in values { 34 | results.append(value) 35 | } 36 | } 37 | catch { 38 | XCTFail("Should never reach here") 39 | } 40 | 41 | XCTAssertEqual(results, [1, 2]) 42 | } 43 | 44 | func testAsyncStream_fail_1st() async throws 45 | { 46 | let values = asyncStream([ 47 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 48 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) } 49 | ])() 50 | 51 | var results: [Int?] = [] 52 | 53 | do { 54 | for try await value in values { 55 | results.append(value) 56 | } 57 | XCTFail("Should never reach here") 58 | } 59 | catch let error as MyError { 60 | XCTAssertEqual(error, MyError(), "First arrival should be MyError.") 61 | XCTAssertEqual(results, [], "No values should arrive.") 62 | } 63 | catch { 64 | XCTFail("Should never reach here.") 65 | } 66 | } 67 | 68 | func testAsyncStream_fail_2nd() async throws 69 | { 70 | let values = asyncStream([ 71 | { try await makeAsync("1", sleep: sleepUnit, result: .success(1)) }, 72 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .failure(MyError())) } 73 | ])() 74 | 75 | var results: [Int?] = [] 76 | 77 | do { 78 | for try await value in values { 79 | results.append(value) 80 | } 81 | XCTFail("Should never reach here") 82 | } 83 | catch let error as MyError { 84 | XCTAssertEqual(error, MyError(), "First arrival should be MyError.") 85 | XCTAssertEqual(results, [1], "1st value should arrive.") 86 | } 87 | catch { 88 | XCTFail("Should never reach here.") 89 | } 90 | } 91 | 92 | func testAsyncStream_race_bothComplete() async throws 93 | { 94 | /// asyncStream should have `Task.select` semantics (https://github.com/apple/swift-async-algorithms/issues/109) 95 | /// where the losing async tasks aren't cancelled on the first winner completing 96 | let values = asyncStream([ 97 | { try await makeAsync("1", sleep: sleepUnit, result: .success(2)) }, 98 | { try await makeAsync("2", sleep: sleepUnit * 2, result: .success(3)) }, 99 | ])() 100 | 101 | let expectBothValues = XCTestExpectation() 102 | expectBothValues.assertForOverFulfill = true 103 | expectBothValues.expectedFulfillmentCount = 2 104 | for try await _ in values { 105 | expectBothValues.fulfill() 106 | } 107 | 108 | wait(for: [expectBothValues], timeout: 1) 109 | } 110 | 111 | func testAsyncStream_sibling_cancelling() async throws 112 | { 113 | actor Box { 114 | var isSiblingCancelled = false 115 | 116 | func toggle() { 117 | isSiblingCancelled.toggle() 118 | } 119 | } 120 | 121 | let box = Box() 122 | 123 | let values = asyncStream([ 124 | { try await makeAsync("1", sleep: sleepUnit, result: .failure(MyError())) }, 125 | { 126 | try await withTaskCancellationHandler { 127 | try await makeAsync("2", sleep: sleepUnit * 2, result: .success(2)) 128 | } onCancel: { 129 | Task.init { await box.toggle() } 130 | } 131 | } 132 | ])() 133 | 134 | var results: [Int?] = [] 135 | 136 | do { 137 | for try await value in values { 138 | results.append(value) 139 | } 140 | XCTFail("Should never reach here") 141 | } 142 | catch let error as MyError { 143 | XCTAssertEqual(error, MyError(), "First arrival should be MyError.") 144 | XCTAssertEqual(results, [], "No values should arrive.") 145 | } 146 | catch { 147 | XCTFail("Should never reach here.") 148 | } 149 | 150 | let isSiblingCancelled = await box.isSiblingCancelled 151 | XCTAssertTrue(isSiblingCancelled, "Should cancel other running asyncs") 152 | } 153 | 154 | func testAsyncStream_wrapper_cancelling() async throws 155 | { 156 | actor Box { 157 | var isSiblingCancelled = false 158 | 159 | func toggle() { 160 | isSiblingCancelled.toggle() 161 | } 162 | } 163 | 164 | let box1 = Box() 165 | let box2 = Box() 166 | 167 | let wrapperTask = Task { 168 | let stream = asyncStream([ 169 | { 170 | try await withTaskCancellationHandler { 171 | try await makeAsync("1", sleep: sleepUnit, result: .success(1)) 172 | } onCancel: { 173 | Task.init { await box1.toggle() } 174 | } 175 | }, 176 | { 177 | try await withTaskCancellationHandler { 178 | try await makeAsync("2", sleep: sleepUnit * 3, result: .success(2)) 179 | } onCancel: { 180 | Task.init { await box2.toggle() } 181 | } 182 | } 183 | ])() 184 | 185 | return try await stream.reduce(into: ()) { _, _ in } 186 | } 187 | 188 | // Add short tick before cancellation. 189 | try await Task.sleep(nanoseconds: sleepUnit * 2) 190 | 191 | // Then cancel. 192 | wrapperTask.cancel() 193 | 194 | // Wait for completion. 195 | try await wrapperTask.value 196 | 197 | let isSiblingCancelled1 = await box1.isSiblingCancelled 198 | let isSiblingCancelled2 = await box2.isSiblingCancelled 199 | 200 | XCTAssertFalse(isSiblingCancelled1, "Async 1 should be completed before cancellation.") 201 | XCTAssertTrue(isSiblingCancelled2, "Should cancel other running asyncs") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/Fixtures.swift: -------------------------------------------------------------------------------- 1 | func makeAsync(_ id: String, sleep: UInt64, result: Result) async throws -> T { 2 | return try await withTaskCancellationHandler { 3 | debugLog("async \(id) start") 4 | 5 | let loopCount: UInt64 = sleep / shortSleep 6 | 7 | // NOTE: for-loop for quick cancellation without too much sleep. 8 | for _ in 0 ... loopCount { 9 | try await Task.sleep(nanoseconds: shortSleep) 10 | 11 | do { 12 | try Task.checkCancellation() 13 | } catch { 14 | debugLog("async \(id) cancelled") 15 | throw error 16 | } 17 | } 18 | 19 | switch result { 20 | case let .success(value): 21 | debugLog("async \(id) succeeded") 22 | return value 23 | case let .failure(error): 24 | debugLog("async \(id) failed") 25 | throw error 26 | } 27 | 28 | } onCancel: { 29 | debugLog("async \(id) onCancel") 30 | } 31 | } 32 | 33 | private let shortSleep: UInt64 = 10_000_000 // sleep per loop 34 | 35 | /// Recommended minimum sleep interval = 100 ms. 36 | let sleepUnit: UInt64 = shortSleep * 10 37 | 38 | struct MyError: Error, Equatable 39 | { 40 | var message: String = "" 41 | } 42 | 43 | struct EqError: Error, Equatable 44 | { 45 | let error: String 46 | 47 | init(_ error: Error) 48 | { 49 | self.error = error.localizedDescription 50 | } 51 | } 52 | 53 | private func debugLog(_ msg: Any) { 54 | print(msg) 55 | } 56 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/WithRetryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class WithRetryTests: XCTestCase 5 | { 6 | func testRetry() async throws 7 | { 8 | let maxCount: UInt64 = 5 9 | var count = 0 10 | 11 | let value = try await withRetry(maxCount: maxCount) { () async throws -> Int in 12 | count += 1 13 | if count < maxCount { 14 | return try await makeAsync("\(count)", sleep: sleepUnit, result: .failure(MyError())) as Int 15 | } 16 | else { 17 | return try await makeAsync("\(count)", sleep: sleepUnit, result: .success(count)) 18 | } 19 | }() 20 | 21 | XCTAssertEqual(value, count) 22 | } 23 | 24 | func testRetry_exponentialBackoff() async throws 25 | { 26 | let initialExponentialBackoffNanoseconds: UInt64 = 1 // = 1_000_000_000 27 | let maxCount: UInt64 = 5 28 | var count = 0 29 | 30 | let value = try await withRetry( 31 | maxCount: maxCount, 32 | delay: { UInt64(pow(2, (Double($1) - 1))) * initialExponentialBackoffNanoseconds }, 33 | run: { () async throws -> Int in 34 | count += 1 35 | if count < maxCount { 36 | return try await makeAsync("\(count)", sleep: sleepUnit, result: .failure(MyError())) as Int 37 | } 38 | else { 39 | return try await makeAsync("\(count)", sleep: sleepUnit, result: .success(count)) 40 | } 41 | } 42 | )() 43 | 44 | XCTAssertEqual(value, count) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/FunAsyncTests/WithTimeoutTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FunAsync 3 | 4 | final class WithTimeoutTests: XCTestCase 5 | { 6 | func testWithTimeout() async throws 7 | { 8 | do { 9 | // 50ms timeout, 100ms task. 10 | let _ = try await withTimeout(nanoseconds: sleepUnit / 2) { 11 | try? await makeAsync("1", sleep: sleepUnit, result: .success(1)) 12 | }() 13 | 14 | XCTFail("Should never reach here") 15 | } 16 | catch { 17 | XCTAssertTrue(error is TimeoutCancellationError) 18 | } 19 | } 20 | 21 | func testWithTimeout_noTimeout() async throws 22 | { 23 | do { 24 | // 200ms timeout, 100ms task. 25 | let value = try await withTimeout(nanoseconds: sleepUnit * 2) { 26 | try? await makeAsync("1", sleep: sleepUnit, result: .success(1)) 27 | }() 28 | 29 | XCTAssertEqual(value, 1) 30 | } 31 | catch { 32 | XCTFail("Should never reach here") 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------