├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .swiftformat ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources └── AsyncCollections │ ├── AsyncSemaphore.swift │ ├── CompactMap.swift │ ├── Filter.swift │ ├── FlatMap.swift │ ├── ForEach.swift │ ├── Map.swift │ └── TaskQueue.swift └── Tests └── AsyncCollectionTests ├── AsyncSemaphoreTests.swift ├── CompactMapTests.swift ├── FilterTests.swift ├── FlatMapTests.swift ├── ForEachTests.swift ├── MapTests.swift └── TaskQueueTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "**.swift" 9 | - "**.yml" 10 | pull_request: 11 | branches: 12 | - main 13 | paths: 14 | - "**.swift" 15 | - "**.yml" 16 | workflow_dispatch: 17 | 18 | jobs: 19 | macOS: 20 | runs-on: macOS-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: SPM tests 25 | run: swift test --enable-code-coverage 26 | - name: Convert coverage files 27 | run: | 28 | xcrun llvm-cov export -format "lcov" \ 29 | .build/debug/async-collectionsPackageTests.xctest/Contents/MacOs/async-collectionsPackageTests \ 30 | -ignore-filename-regex="\/Tests\/" \ 31 | -ignore-filename-regex="\/Benchmarks\/" \ 32 | -instr-profile=.build/debug/codecov/default.profdata > info.lcov 33 | - name: Upload to codecov.io 34 | uses: codecov/codecov-action@v3 35 | with: 36 | file: info.lcov 37 | linux: 38 | runs-on: ubuntu-latest 39 | strategy: 40 | matrix: 41 | image: 42 | - swift:5.8-jammy 43 | - swift:5.9-jammy 44 | - swift:5.10-jammy 45 | - swiftlang/swift:nightly-6.0-jammy 46 | - swiftlang/swift:nightly-main-jammy 47 | 48 | container: ${{ matrix.image }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | - name: Test 53 | run: | 54 | swift test --enable-code-coverage 55 | - name: Convert coverage files 56 | run: | 57 | llvm-cov export -format="lcov" \ 58 | .build/debug/async-collectionsPackageTests.xctest \ 59 | -ignore-filename-regex="\/Tests\/" \ 60 | -ignore-filename-regex="\/Benchmarks\/" \ 61 | -instr-profile .build/debug/codecov/default.profdata > info.lcov 62 | - name: Upload to codecov.io 63 | uses: codecov/codecov-action@v3 64 | with: 65 | file: info.lcov 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | Package.resolved -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # Minimum swiftformat version 2 | --minversion 0.47.4 3 | 4 | # Swift version 5 | --swiftversion 5.1 6 | 7 | # file options 8 | --exclude .build 9 | --exclude Sources/Soto/Services 10 | 11 | # rules 12 | --disable redundantReturn, redundantBackticks, trailingCommas, extensionAccessControl 13 | 14 | # format options 15 | --ifdef no-indent 16 | --nospaceoperators ...,..< 17 | --patternlet inline 18 | --self insert 19 | --stripunusedargs unnamed-only 20 | 21 | #--maxwidth 150 22 | --wraparguments before-first 23 | --wrapparameters before-first 24 | --wrapcollections before-first 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Adam Fowler 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "async-collections", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], 9 | products: [ 10 | .library(name: "AsyncCollections", targets: ["AsyncCollections"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0"), 14 | .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .target(name: "AsyncCollections", dependencies: [ 18 | .product(name: "Atomics", package: "swift-atomics"), 19 | .product(name: "Collections", package: "swift-collections"), 20 | ]), 21 | .testTarget(name: "AsyncCollectionTests", dependencies: ["AsyncCollections"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let swiftSettings: [SwiftSetting] = [ 7 | .enableExperimentalFeature("StrictConcurrency=complete") 8 | ] 9 | 10 | let package = Package( 11 | name: "async-collections", 12 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], 13 | products: [ 14 | .library(name: "AsyncCollections", targets: ["AsyncCollections"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0"), 18 | .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "AsyncCollections", 23 | dependencies: [ 24 | .product(name: "Atomics", package: "swift-atomics"), 25 | .product(name: "Collections", package: "swift-collections"), 26 | ], 27 | swiftSettings: swiftSettings 28 | ), 29 | .testTarget( 30 | name: "AsyncCollectionTests", 31 | dependencies: ["AsyncCollections"], 32 | swiftSettings: swiftSettings 33 | ), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AsyncCollections 2 | 3 | Functions for running async processes on Swift Collections 4 | 5 | ## ForEach 6 | 7 | Run an async function on every element of a Sequence. 8 | ```swift 9 | await array.asyncForEach { 10 | await asyncProcess($0) 11 | } 12 | ``` 13 | The async closures are run serially ie the closure run on the current element of a sequence has to finish before we run the closure on the next element. 14 | 15 | To run the closures concurrently use 16 | ```swift 17 | try await array.concurrentForEach { 18 | try await asyncProcess($0) 19 | } 20 | ``` 21 | You can manage the number of tasks running at any one time with the `maxConcurrentTasks` parameter 22 | ```swift 23 | try await array.concurrentForEach(maxConcurrentTasks: 4) { 24 | try await asyncProcess($0) 25 | } 26 | ``` 27 | 28 | ## Map 29 | 30 | Return an array transformed by an async function. 31 | ```swift 32 | let result = await array.asyncMap { 33 | return await asyncTransform($0) 34 | } 35 | ``` 36 | 37 | Similar to `asyncForEach` there are versions of `asyncMap` that runs the transforms concurrently. 38 | 39 | ```swift 40 | let result = await array.concurrentMap(maxConcurrentTasks: 8) { 41 | return await asyncTransform($0) 42 | } 43 | ``` 44 | 45 | ## Compact Map 46 | 47 | Return a non-optional array transformed by an async function returning optional results. 48 | ```swift 49 | let result: [MyType] = await array.asyncCompactMap { value -> MyType? in 50 | return await asyncTransform(value) 51 | } 52 | ``` 53 | 54 | Similar to `asyncForEach` there are versions of `asyncCompactMap` that runs the transforms concurrently. 55 | 56 | ```swift 57 | let result: [MyType] = await array.concurrentCompactMap(maxConcurrentTasks: 8) { value -> MyType? in 58 | return await asyncTransform(value) 59 | ``` 60 | 61 | ## FlatMap 62 | 63 | Return a concatenated array transformed by an async function that returns a sequence. 64 | ```swift 65 | let result: [MySequence.Element] = await array.asyncMap { value -> MySequence in 66 | return await asyncTransform($0) 67 | } 68 | ``` 69 | 70 | Similar to `asyncForEach` there are versions of `asyncFlatMap` that runs the transforms concurrently. 71 | 72 | ```swift 73 | let result: [MySequence.Element] = await array.concurrentMap(maxConcurrentTasks: 8) { value -> MySequence in 74 | return await asyncTransform($0) 75 | } 76 | ``` 77 | 78 | ## Filter 79 | 80 | Return a filtered array transformed by an async function. 81 | ```swift 82 | let result = await array.asyncFilter { 83 | return await asyncTransform($0) 84 | } 85 | ``` 86 | 87 | Similar to `asyncForEach` there are versions of `asyncFilter` that runs the transforms concurrently. 88 | 89 | ```swift 90 | let result = await array.concurrentFilter(maxConcurrentTasks: 8) { 91 | return await asyncTransform($0) 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/AsyncSemaphore.swift: -------------------------------------------------------------------------------- 1 | import Atomics 2 | import Collections 3 | import Foundation 4 | 5 | /// A Semaphore implementation that can be used with Swift Concurrency. 6 | /// 7 | /// Waiting on this semaphore will not stall the underlying thread 8 | /// 9 | /// Much of this is inspired by the implementation from Gwendal Roué found 10 | /// here https://github.com/groue/Semaphore. It manages to avoid the recursive 11 | /// lock by decrementing the semaphore counter inside the withTaskCancellationHandler 12 | /// function. 13 | public final class AsyncSemaphore: @unchecked Sendable { 14 | static let idGenerator = ManagedAtomic(0) 15 | struct Suspension: Sendable { 16 | let continuation: UnsafeContinuation 17 | let id: Int 18 | 19 | init(_ continuation: UnsafeContinuation, id: Int) { 20 | self.continuation = continuation 21 | self.id = id 22 | } 23 | } 24 | 25 | /// Semaphore value 26 | private var value: Int 27 | /// queue of suspensions waiting on semaphore 28 | private var suspended: Deque 29 | /// lock. Can only access `suspended` and `missedSignals` inside lock 30 | private let _lock: NSLock 31 | 32 | /// Initialize AsyncSemaphore 33 | public init(value: Int = 0) { 34 | self.value = .init(value) 35 | self.suspended = [] 36 | self._lock = .init() 37 | } 38 | 39 | // Lock functionality has been moved to its own functions to avoid warning about using 40 | // lock in an asynchronous context 41 | func lock() { self._lock.lock() } 42 | func unlock() { self._lock.unlock() } 43 | 44 | /// Signal (increments) semaphore 45 | /// - Returns: Returns if a task was awaken 46 | @discardableResult public func signal() -> Bool { 47 | self.lock() 48 | self.value += 1 49 | if self.value <= 0 { 50 | // if value after signal is <= 0 then there should be a suspended 51 | // task in the suspended array. 52 | if let suspension = suspended.popFirst() { 53 | self.unlock() 54 | suspension.continuation.resume() 55 | } else { 56 | self.unlock() 57 | fatalError("Cannot have a negative semaphore value without values in the suspension array") 58 | } 59 | return true 60 | } else { 61 | self.unlock() 62 | } 63 | return false 64 | } 65 | 66 | /// Wait for or decrement a semaphore 67 | public func wait() async throws { 68 | let id = Self.idGenerator.loadThenWrappingIncrement(by: 1, ordering: .relaxed) 69 | try await withTaskCancellationHandler { 70 | self.lock() 71 | self.value -= 1 72 | if self.value >= 0 { 73 | self.unlock() 74 | return 75 | } 76 | try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation) in 77 | if Task.isCancelled { 78 | self.value += 1 79 | self.unlock() 80 | // if the state is cancelled, send cancellation error to continuation 81 | cont.resume(throwing: CancellationError()) 82 | } else { 83 | // set state to suspended and add to suspended array 84 | self.suspended.append(.init(cont, id: id)) 85 | self.unlock() 86 | } 87 | } 88 | } onCancel: { 89 | self.lock() 90 | if let index = self.suspended.firstIndex(where: { $0.id == id }) { 91 | // if we find the suspension in the suspended array the remove and resume 92 | // continuation with a cancellation error 93 | self.value += 1 94 | let suspension = self.suspended.remove(at: index) 95 | self.unlock() 96 | suspension.continuation.resume(throwing: CancellationError()) 97 | } else { 98 | self.unlock() 99 | } 100 | } 101 | } 102 | } 103 | 104 | extension AsyncSemaphore { 105 | // used in tests 106 | func getValue() -> Int { 107 | self.lock() 108 | let value = self.value 109 | self.unlock() 110 | return value 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/CompactMap.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Sequence where Element: Sendable { 3 | /// Returns an array containing the non-`nil` results of calling the given 4 | /// transformation with each element of this sequence. 5 | /// 6 | /// Use this method to receive an array of non-optional values when your 7 | /// transformation produces an optional value. 8 | /// 9 | /// The closure calls are made serially. The next call is only made once the previous call 10 | /// has finished. Returns once the closure has run on all the elements of the Sequence 11 | /// or when the closure throws an error. 12 | /// - Parameter transform: An async mapping closure. transform accepts an 13 | /// element of this sequence as its parameter and returns a transformed value of 14 | /// the same or of a different type. 15 | /// - Returns: An array containing the transformed elements of this sequence. 16 | public func asyncCompactMap(_ transform: @Sendable (Element) async throws -> T?) async rethrows -> [T] { 17 | var result = ContiguousArray() 18 | 19 | var iterator = self.makeIterator() 20 | 21 | // Add remaining elements, if any. 22 | while let next = iterator.next() { 23 | if let element = try await transform(next) { 24 | result.append(element) 25 | } 26 | } 27 | return Array(result) 28 | } 29 | 30 | /// Returns an array containing, in order, the non-`nil` results of calling the given 31 | /// transformation with each element of this sequence. 32 | /// 33 | /// Use this method to receive an array of non-optional values when your 34 | /// transformation produces an optional value. 35 | /// 36 | /// This differs from `asyncCompactMap` in that it uses a `TaskGroup` to run the transform 37 | /// closure for all the elements of the Sequence. This allows all the transform closures 38 | /// to run concurrently instead of serially. Returns only when the closure has been run 39 | /// on all the elements of the Sequence. 40 | /// - Parameters: 41 | /// - priority: Task priority for tasks in TaskGroup 42 | /// - transform: An async closure that takes an element of the 43 | /// sequence as its argument and returns a Boolean value indicating 44 | /// whether the element should be included in the returned array. 45 | /// - Returns: An array of the elements that `transform` allowed. 46 | public func concurrentCompactMap(priority: TaskPriority? = nil, _ transform: @Sendable @escaping (Element) async throws -> T?) async rethrows -> [T] { 47 | let result: ContiguousArray<(Int, T)> = try await withThrowingTaskGroup( 48 | of: (Int, T)?.self 49 | ) { group in 50 | for (index, element) in self.enumerated() { 51 | group.addTask(priority: priority) { 52 | if let transformed = try await transform(element) { 53 | return (index, transformed) 54 | } else { 55 | return nil 56 | } 57 | } 58 | } 59 | // Code for collating results copied from Sequence.map in Swift codebase 60 | var result = ContiguousArray<(Int, T)>() 61 | 62 | // Add all the elements. 63 | while let next = try await group.next() { 64 | if let enumerated = next { 65 | result.append(enumerated) 66 | } 67 | } 68 | return result 69 | } 70 | 71 | return result.sorted(by: { $0.0 < $1.0 }).map(\.1) 72 | } 73 | 74 | /// Returns an array containing, in order, the non-`nil` results of calling the given 75 | /// transformation with each element of this sequence. 76 | /// 77 | /// Use this method to receive an array of non-optional values when your 78 | /// transformation produces an optional value. 79 | /// 80 | /// This differs from `asyncCompactMap` in that it uses a `TaskGroup` to run the transform 81 | /// closure for all the elements of the Sequence. This allows all the transform closures 82 | /// to run concurrently instead of serially. Returns only when the closure has been run 83 | /// on all the elements of the Sequence. 84 | /// - Parameters: 85 | /// - maxConcurrentTasks: Maximum number of tasks to running at the same time 86 | /// - priority: Task priority for tasks in TaskGroup 87 | /// - transform: An async closure that takes an element of the 88 | /// sequence as its argument and returns a Boolean value indicating 89 | /// whether the element should be included in the returned array. 90 | /// - Returns: An array of the elements that `transform` allowed. 91 | public func concurrentCompactMap(maxConcurrentTasks: Int, priority: TaskPriority? = nil, _ transform: @Sendable @escaping (Element) async throws -> T?) async rethrows -> [T] { 92 | let result: ContiguousArray<(Int, T)> = try await withThrowingTaskGroup( 93 | of: (Int, T)?.self 94 | ) { group in 95 | var result = ContiguousArray<(Int, T)>() 96 | 97 | for (index, element) in self.enumerated() { 98 | if index >= maxConcurrentTasks { 99 | if let enumerated = try await group.next() ?? nil { 100 | result.append(enumerated) 101 | } 102 | } 103 | group.addTask(priority: priority) { 104 | if let transformed = try await transform(element) { 105 | return (index, transformed) 106 | } else { 107 | return nil 108 | } 109 | } 110 | } 111 | 112 | // Add remaining elements, if any. 113 | while let next = try await group.next() { 114 | if let enumerated = next { 115 | result.append(enumerated) 116 | } 117 | } 118 | return result 119 | } 120 | 121 | return result.sorted(by: { $0.0 < $1.0 }).map(\.1) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/Filter.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Sequence where Element: Sendable { 3 | /// Returns an array containing, in order, the elements of the sequence 4 | /// that satisfy the given predicate. 5 | /// 6 | /// - Parameter isIncluded: An async closure that takes an element of the 7 | /// sequence as its argument and returns a Boolean value indicating 8 | /// whether the element should be included in the returned array. 9 | /// - Returns: An array of the elements that `isIncluded` allowed. 10 | public func asyncFilter(_ isIncluded: @Sendable (Element) async throws -> Bool) async rethrows -> [Element] { 11 | var result = ContiguousArray() 12 | 13 | var iterator = self.makeIterator() 14 | 15 | while let element = iterator.next() { 16 | if try await isIncluded(element) { 17 | result.append(element) 18 | } 19 | } 20 | 21 | return Array(result) 22 | } 23 | 24 | /// Returns an array containing, in order, the elements of the sequence 25 | /// that satisfy the given predicate. 26 | /// 27 | /// This differs from `asyncFilter` in that it uses a `TaskGroup` to run the transform 28 | /// closure for all the elements of the Sequence. This allows all the transform closures 29 | /// to run concurrently instead of serially. Returns only when the closure has been run 30 | /// on all the elements of the Sequence. 31 | /// - Parameters: 32 | /// - priority: Task priority for tasks in TaskGroup 33 | /// - isIncluded: An async closure that takes an element of the 34 | /// sequence as its argument and returns a Boolean value indicating 35 | /// whether the element should be included in the returned array. 36 | /// - Returns: An array of the elements that `isIncluded` allowed. 37 | public func concurrentFilter(priority: TaskPriority? = nil, _ isIncluded: @Sendable @escaping (Element) async throws -> Bool) async rethrows -> [Element] { 38 | let result: ContiguousArray<(Int, Element)> = try await withThrowingTaskGroup( 39 | of: (Int, Element)?.self 40 | ) { group in 41 | for (index, element) in self.enumerated() { 42 | group.addTask(priority: priority) { 43 | if try await isIncluded(element) { 44 | return (index, element) 45 | } else { 46 | return nil 47 | } 48 | } 49 | } 50 | // Code for collating results copied from Sequence.map in Swift codebase 51 | var result = ContiguousArray<(Int, Element)>() 52 | 53 | // Add all the elements. 54 | while let next = try await group.next() { 55 | if let enumerated = next { 56 | result.append(enumerated) 57 | } 58 | } 59 | return result 60 | } 61 | 62 | return result.sorted(by: { $0.0 < $1.0 }).map(\.1) 63 | } 64 | 65 | /// Returns an array containing, in order, the elements of the sequence 66 | /// that satisfy the given predicate. 67 | /// 68 | /// This differs from `asyncFilter` in that it uses a `TaskGroup` to run the transform 69 | /// closure for all the elements of the Sequence. This allows all the transform closures 70 | /// to run concurrently instead of serially. Returns only when the closure has been run 71 | /// on all the elements of the Sequence. 72 | /// - Parameters: 73 | /// - maxConcurrentTasks: Maximum number of tasks to running at the same time 74 | /// - priority: Task priority for tasks in TaskGroup 75 | /// - isIncluded: An async closure that takes an element of the 76 | /// sequence as its argument and returns a Boolean value indicating 77 | /// whether the element should be included in the returned array. 78 | /// - Returns: An array of the elements that `isIncluded` allowed. 79 | public func concurrentFilter(maxConcurrentTasks: Int, priority: TaskPriority? = nil, _ isIncluded: @Sendable @escaping (Element) async throws -> Bool) async rethrows -> [Element] { 80 | let result: ContiguousArray<(Int, Element)> = try await withThrowingTaskGroup( 81 | of: (Int, Element)?.self 82 | ) { group in 83 | var result = ContiguousArray<(Int, Element)>() 84 | 85 | for (index, element) in self.enumerated() { 86 | if index >= maxConcurrentTasks { 87 | if let enumerated = try await group.next() ?? nil { 88 | result.append(enumerated) 89 | } 90 | } 91 | group.addTask(priority: priority) { 92 | if try await isIncluded(element) { 93 | return (index, element) 94 | } else { 95 | return nil 96 | } 97 | } 98 | } 99 | 100 | // Add remaining elements, if any. 101 | while let next = try await group.next() { 102 | if let enumerated = next { 103 | result.append(enumerated) 104 | } 105 | } 106 | return result 107 | } 108 | 109 | return result.sorted(by: { $0.0 < $1.0 }).map(\.1) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/FlatMap.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Sequence where Element: Sendable { 3 | /// Returns an array containing the concatenated results of calling the 4 | /// given transformation with each element of this sequence. 5 | /// 6 | /// Use this method to receive a single-level collection when your 7 | /// transformation produces a sequence or collection for each element. 8 | /// 9 | /// The closure calls are made serially. The next call is only made once the previous call 10 | /// has finished. Returns once the closure has run on all the elements of the Sequence 11 | /// or when the closure throws an error. 12 | /// - Parameter transform: An async mapping closure. transform accepts an 13 | /// element of this sequence as its parameter and returns a transformed value of 14 | /// the same or of a different type. 15 | /// - Returns: An array containing the transformed elements of this sequence. 16 | public func asyncFlatMap(_ transform: @Sendable (Element) async throws -> T) async rethrows -> [T.Element] { 17 | var result: [T.Element] = [] 18 | for element in self { 19 | result.append(contentsOf: try await transform(element)) 20 | } 21 | return result 22 | } 23 | 24 | /// Returns an array containing the concatenated results of calling the 25 | /// given transformation with each element of this sequence. 26 | /// 27 | /// Use this method to receive a single-level collection when your 28 | /// transformation produces a sequence or collection for each element. 29 | /// 30 | /// This differs from `asyncFlatMap` in that it uses a `TaskGroup` to run the transform 31 | /// closure for all the elements of the Sequence. This allows all the transform closures 32 | /// to run concurrently instead of serially. Returns only when the closure has been run 33 | /// on all the elements of the Sequence. 34 | /// - Parameters: 35 | /// - priority: Task priority for tasks in TaskGroup 36 | /// - transform: An async mapping closure. transform accepts an 37 | /// element of this sequence as its parameter and returns a transformed value of 38 | /// the same or of a different type. 39 | /// - Returns: An array containing the transformed elements of this sequence. 40 | public func concurrentFlatMap(priority: TaskPriority? = nil, _ transform: @Sendable @escaping (Element) async throws -> T) async rethrows -> [T.Element] where T: Sendable { 41 | let result: ContiguousArray<(Int, T)> = try await withThrowingTaskGroup(of: (Int, T).self) { group in 42 | for (index, element) in self.enumerated() { 43 | group.addTask(priority: priority) { 44 | let result = try await transform(element) 45 | return (index, result) 46 | } 47 | } 48 | // Code for collating results copied from Sequence.map in Swift codebase 49 | let initialCapacity = underestimatedCount 50 | var result = ContiguousArray<(Int, T)>() 51 | result.reserveCapacity(initialCapacity) 52 | 53 | // Add elements up to the initial capacity without checking for regrowth. 54 | for _ in 0..(maxConcurrentTasks: Int, priority: TaskPriority? = nil, _ transform: @Sendable @escaping (Element) async throws -> T) async rethrows -> [T.Element] where T: Sendable { 91 | let result: ContiguousArray<(Int, T)> = try await withThrowingTaskGroup(of: (Int, T).self) { group in 92 | // Code for collating results copied from Sequence.map in Swift codebase 93 | let initialCapacity = underestimatedCount 94 | var results = ContiguousArray<(Int, T)>() 95 | results.reserveCapacity(initialCapacity) 96 | 97 | for (index, element) in self.enumerated() { 98 | if index >= maxConcurrentTasks { 99 | if let result = try await group.next() { 100 | results.append(result) 101 | } 102 | } 103 | group.addTask(priority: priority) { 104 | let result = try await transform(element) 105 | return (index, result) 106 | } 107 | } 108 | 109 | // Add remaining elements, if any. 110 | while let enumerated = try await group.next() { 111 | results.append(enumerated) 112 | } 113 | return results 114 | } 115 | 116 | // construct final array and fill in elements 117 | return [T](unsafeUninitializedCapacity: result.count) { buffer, count in 118 | for value in result { 119 | (buffer.baseAddress! + value.0).initialize(to: value.1) 120 | } 121 | count = result.count 122 | }.flatMap { $0 } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/ForEach.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Sequence where Element: Sendable { 3 | /// Run async closure for each member of a Sequence 4 | /// 5 | /// The closure calls are made serially. The next call is only made once the previous call 6 | /// has finished. Returns once the closure has run on all the elements of the Sequence 7 | /// or when the closure throws an error 8 | /// - Parameter body: Closure to be called for each element 9 | public func asyncForEach(_ body: @Sendable (Element) async throws -> Void) async rethrows { 10 | for element in self { 11 | try await body(element) 12 | } 13 | } 14 | 15 | /// Run async closure for each member of a sequence 16 | /// 17 | /// This differs from `asyncForEach` in that it uses a `TaskGroup` to run closure 18 | /// for all the elements of the Sequence. So all the closures can run concurrently. Returns 19 | /// only when the closure has been run on all the elements of the Sequence. 20 | /// - Parameters: 21 | /// - priority: Task priority for tasks in TaskGroup 22 | /// - body: Closure to be called for each element 23 | public func concurrentForEach(priority: TaskPriority? = nil, _ body: @Sendable @escaping (Element) async throws -> Void) async rethrows { 24 | try await withThrowingTaskGroup(of: Void.self) { group in 25 | self.forEach { element in 26 | group.addTask(priority: priority) { 27 | try await body(element) 28 | } 29 | } 30 | try await group.waitForAll() 31 | } 32 | } 33 | 34 | /// Run async closure for each member of a sequence 35 | /// 36 | /// This differs from `asyncForEach` in that it uses a `TaskGroup` to run closure 37 | /// for all the elements of the Sequence. So all the closures can run concurrently. Returns 38 | /// only when the closure has been run on all the elements of the Sequence. 39 | /// - Parameters: 40 | /// - priority: Task priority for tasks in TaskGroup 41 | /// - body: Closure to be called for each element 42 | public func concurrentForEach(maxConcurrentTasks: Int, priority: TaskPriority? = nil, _ body: @Sendable @escaping (Element) async throws -> Void) async rethrows { 43 | try await withThrowingTaskGroup(of: Void.self) { group in 44 | var count = 0 45 | for element in self { 46 | count += 1 47 | if count > maxConcurrentTasks { 48 | try await group.next() 49 | } 50 | group.addTask(priority: priority) { 51 | try await body(element) 52 | } 53 | } 54 | try await group.waitForAll() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/Map.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Sequence where Element: Sendable { 3 | /// Returns an array containing the results of mapping the given async closure over 4 | /// the sequence’s elements. 5 | /// 6 | /// The closure calls are made serially. The next call is only made once the previous call 7 | /// has finished. Returns once the closure has run on all the elements of the Sequence 8 | /// or when the closure throws an error. 9 | /// - Parameter transform: An async mapping closure. transform accepts an 10 | /// element of this sequence as its parameter and returns a transformed value of 11 | /// the same or of a different type. 12 | /// - Returns: An array containing the transformed elements of this sequence. 13 | public func asyncMap(_ transform: @Sendable (Element) async throws -> T) async rethrows -> [T] { 14 | let initialCapacity = underestimatedCount 15 | var result = ContiguousArray() 16 | result.reserveCapacity(initialCapacity) 17 | 18 | var iterator = self.makeIterator() 19 | 20 | // Add elements up to the initial capacity without checking for regrowth. 21 | for _ in 0..(priority: TaskPriority? = nil, _ transform: @Sendable @escaping (Element) async throws -> T) async rethrows -> [T] { 45 | let result: ContiguousArray<(Int, T)> = try await withThrowingTaskGroup(of: (Int, T).self) { group in 46 | for (index, element) in self.enumerated() { 47 | group.addTask(priority: priority) { 48 | let result = try await transform(element) 49 | return (index, result) 50 | } 51 | } 52 | // Code for collating results copied from Sequence.map in Swift codebase 53 | let initialCapacity = underestimatedCount 54 | var result = ContiguousArray<(Int, T)>() 55 | result.reserveCapacity(initialCapacity) 56 | 57 | // Add elements up to the initial capacity without checking for regrowth. 58 | for _ in 0..(maxConcurrentTasks: Int, priority: TaskPriority? = nil, _ transform: @Sendable @escaping (Element) async throws -> T) async rethrows -> [T] { 91 | let result: ContiguousArray<(Int, T)> = try await withThrowingTaskGroup(of: (Int, T).self) { group in 92 | // Code for collating results copied from Sequence.map in Swift codebase 93 | let initialCapacity = underestimatedCount 94 | var results = ContiguousArray<(Int, T)>() 95 | results.reserveCapacity(initialCapacity) 96 | 97 | for (index, element) in self.enumerated() { 98 | if index >= maxConcurrentTasks { 99 | if let result = try await group.next() { 100 | results.append(result) 101 | } 102 | } 103 | group.addTask(priority: priority) { 104 | let result = try await transform(element) 105 | return (index, result) 106 | } 107 | } 108 | 109 | // Add remaining elements, if any. 110 | while let enumerated = try await group.next() { 111 | results.append(enumerated) 112 | } 113 | return results 114 | } 115 | // construct final array and fill in elements 116 | return [T](unsafeUninitializedCapacity: result.count) { buffer, count in 117 | for value in result { 118 | (buffer.baseAddress! + value.0).initialize(to: value.1) 119 | } 120 | count = result.count 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/AsyncCollections/TaskQueue.swift: -------------------------------------------------------------------------------- 1 | import Collections 2 | 3 | /// Queue for managing the running of Tasks ensuring only so many concurrent tasks are 4 | /// running at any one point in time. 5 | /// 6 | /// TaskQueue can be used in conjunction with `concurrentMap` and `concurrentForEach` 7 | /// to run concurrent tasks across a Sequence while limiting the number of concurrent 8 | /// tasks running at one point in time e.g. 9 | /// ``` 10 | /// let queue = TaskQueue(maxConcurrent: 8) 11 | /// let result = await array.concurrentMap { element in 12 | /// await queue.add { element in 13 | /// await asyncOperation(element) 14 | /// } 15 | /// } 16 | /// ``` 17 | @available(*, deprecated, message: "Use concurrentMap(maxConcurrentTasks:priority:transform:)") 18 | public actor TaskQueue { 19 | /// Task closure 20 | public typealias TaskFunc = @Sendable () async throws -> Result 21 | 22 | /// Task details stored in queue, body of operation and continuation 23 | /// to resume when task completes 24 | struct TaskDetails { 25 | let body: TaskFunc 26 | let continuation: UnsafeContinuation 27 | } 28 | 29 | /// task queue 30 | var queue: Deque 31 | /// number of tasks in progress 32 | var numInProgress: Int 33 | /// maximum concurrent tasks that can run at any one time 34 | let maxConcurrentTasks: Int 35 | /// priority of tasks 36 | let priority: TaskPriority? 37 | 38 | /// Create task queue 39 | /// - Parameters: 40 | /// - maxConcurrent: Maximum number of concurrent tasks queue allows 41 | /// - priority: priority of queued tasks 42 | public init(maxConcurrentTasks: Int, priority: TaskPriority? = nil) { 43 | self.queue = .init() 44 | self.numInProgress = 0 45 | self.maxConcurrentTasks = maxConcurrentTasks 46 | self.priority = priority 47 | } 48 | 49 | /// Add task to queue 50 | /// 51 | /// - Parameter body: Body of task function 52 | /// - Returns: Result of task 53 | public func add(_ body: @escaping TaskFunc) async throws -> Result { 54 | return try await withUnsafeThrowingContinuation { cont in 55 | if numInProgress < maxConcurrentTasks { 56 | numInProgress += 1 57 | Task(priority: priority) { 58 | await self.performTask(.init(body: body, continuation: cont)) 59 | } 60 | } else { 61 | queue.append(.init(body: body, continuation: cont)) 62 | } 63 | } 64 | } 65 | 66 | /// perform task and resume continuation 67 | func performTask(_ task: TaskDetails) async { 68 | do { 69 | let result = try await performTask(task.body) 70 | task.continuation.resume(returning: result) 71 | } catch { 72 | task.continuation.resume(throwing: error) 73 | } 74 | } 75 | 76 | /// perform task 77 | func performTask(_ function: TaskFunc) async rethrows -> Result { 78 | let result = try await function() 79 | // once task is complete if there are tasks on the queue then 80 | // initiate next task from queue. 81 | if let t = queue.popFirst(), !Task.isCancelled { 82 | Task(priority: self.priority) { 83 | await self.performTask(t) 84 | } 85 | } else { 86 | assert(self.numInProgress > 0) 87 | self.numInProgress -= 1 88 | } 89 | return result 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/AsyncSemaphoreTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AsyncCollections 2 | import XCTest 3 | 4 | final class AsyncSemaphoreTests: XCTestCase { 5 | func testSignalWait() async throws { 6 | let semaphore = AsyncSemaphore() 7 | let rt = semaphore.signal() 8 | XCTAssertEqual(rt, false) 9 | try await semaphore.wait() 10 | } 11 | 12 | func testNoWaitingTask() async throws { 13 | let semaphore = AsyncSemaphore(value: 1) 14 | let rt = semaphore.signal() 15 | XCTAssertEqual(rt, false) 16 | let rt2 = semaphore.signal() 17 | XCTAssertEqual(rt2, false) 18 | try await semaphore.wait() 19 | } 20 | 21 | func testWaitSignal() async throws { 22 | await withThrowingTaskGroup(of: Void.self) { group in 23 | let semaphore = AsyncSemaphore() 24 | group.addTask { 25 | try await semaphore.wait() 26 | } 27 | group.addTask { 28 | semaphore.signal() 29 | } 30 | } 31 | } 32 | 33 | func testWaitDelayedSignal() async throws { 34 | await withThrowingTaskGroup(of: Void.self) { group in 35 | let semaphore = AsyncSemaphore() 36 | let semaphore2 = AsyncSemaphore() 37 | group.addTask { 38 | semaphore2.signal() 39 | try await semaphore.wait() 40 | } 41 | group.addTask { 42 | try await semaphore2.wait() 43 | try await Task.sleep(nanoseconds: 10_000) 44 | let rt = semaphore.signal() 45 | XCTAssertEqual(rt, true) 46 | } 47 | } 48 | } 49 | 50 | func testDoubleWaitSignal() async throws { 51 | await withThrowingTaskGroup(of: Void.self) { group in 52 | let semaphore = AsyncSemaphore() 53 | let semaphore2 = AsyncSemaphore() 54 | group.addTask { 55 | semaphore2.signal() 56 | try await semaphore.wait() 57 | } 58 | group.addTask { 59 | semaphore2.signal() 60 | try await semaphore.wait() 61 | } 62 | group.addTask { 63 | try await semaphore2.wait() 64 | try await semaphore2.wait() 65 | try await Task.sleep(nanoseconds: 10_000) 66 | let rt = semaphore.signal() 67 | XCTAssertEqual(rt, true) 68 | let rt2 = semaphore.signal() 69 | XCTAssertEqual(rt2, true) 70 | } 71 | } 72 | } 73 | 74 | func testManySignalWait() async throws { 75 | await withThrowingTaskGroup(of: Void.self) { group in 76 | let semaphore = AsyncSemaphore() 77 | group.addTask { 78 | semaphore.signal() 79 | try await semaphore.wait() 80 | semaphore.signal() 81 | try await semaphore.wait() 82 | semaphore.signal() 83 | try await semaphore.wait() 84 | } 85 | group.addTask { 86 | semaphore.signal() 87 | semaphore.signal() 88 | semaphore.signal() 89 | try await semaphore.wait() 90 | try await semaphore.wait() 91 | try await semaphore.wait() 92 | } 93 | group.addTask { 94 | semaphore.signal() 95 | semaphore.signal() 96 | try await semaphore.wait() 97 | try await semaphore.wait() 98 | semaphore.signal() 99 | try await semaphore.wait() 100 | } 101 | } 102 | } 103 | 104 | func testCancellationWhileSuspended() async throws { 105 | let semaphore = AsyncSemaphore() 106 | let task = Task { 107 | do { 108 | try await semaphore.wait() 109 | } catch is CancellationError { 110 | XCTAssertEqual(semaphore.getValue(), 0) 111 | } catch { 112 | XCTFail("Wrong Error") 113 | } 114 | } 115 | try await Task.sleep(nanoseconds: 10000) 116 | task.cancel() 117 | } 118 | 119 | func testCancellationBeforeWait() async throws { 120 | let semaphore = AsyncSemaphore() 121 | let task = Task { 122 | do { 123 | do { 124 | try await Task.sleep(nanoseconds: 100_000) 125 | } catch {} 126 | try await semaphore.wait() 127 | } catch is CancellationError { 128 | XCTAssertEqual(semaphore.getValue(), 0) 129 | } catch { 130 | XCTFail("Wrong Error") 131 | } 132 | } 133 | task.cancel() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/CompactMapTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncCollections 2 | import XCTest 3 | 4 | final class CompactCompactMapTests: XCTestCase { 5 | func testAsyncCompactMap() async throws { 6 | let array = Array(0..<80).map { (element: $0, isIncluded: Bool.random()) } 7 | let result = try await array.asyncCompactMap { value -> Int? in 8 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 9 | return value.isIncluded ? value.element : nil 10 | } 11 | 12 | XCTAssertEqual(result, array.filter(\.isIncluded).map(\.element)) 13 | } 14 | 15 | func testConcurrentCompactMap() async throws { 16 | let array = Array(0..<800).map { (element: $0, isIncluded: Bool.random()) } 17 | let result = try await array.concurrentCompactMap { value -> Int? in 18 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 19 | return value.isIncluded ? value.element : nil 20 | } 21 | 22 | XCTAssertEqual(result, array.filter(\.isIncluded).map(\.element)) 23 | } 24 | 25 | func testConcurrentCompactMapWithString() async throws { 26 | let array = Array(0..<800).map { (element: $0, isIncluded: Bool.random()) } 27 | let result = try await array.concurrentCompactMap { value -> String? in 28 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 29 | return value.isIncluded ? String(value.element) : nil 30 | } 31 | 32 | XCTAssertEqual(result, array.filter(\.isIncluded).map(\.element.description)) 33 | } 34 | 35 | func testAsyncCompactMapNonOptionalClosure() async throws { 36 | let array = Array(0..<80) 37 | let result = try await array.asyncCompactMap { value -> Int in 38 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 39 | return value 40 | } 41 | 42 | XCTAssertEqual(result, array) 43 | } 44 | 45 | func testConcurrentCompactMapNonOptionalClosure() async throws { 46 | let array = Array(0..<800) 47 | let result = try await array.concurrentCompactMap { value -> Int in 48 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 49 | return value 50 | } 51 | 52 | XCTAssertEqual(result, array) 53 | } 54 | 55 | func testConcurrentCompactMapWithStringNonOptionalClosure() async throws { 56 | let array = Array(0..<800) 57 | let result = try await array.concurrentCompactMap { value -> String in 58 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 59 | return String(value) 60 | } 61 | 62 | XCTAssertEqual(result, array.map(\.description)) 63 | } 64 | 65 | func testAsyncCompactMapConcurrency() async throws { 66 | let count = Count(0) 67 | let maxCount = Count(0) 68 | 69 | let array = Array(0..<80).map { (element: $0, isIncluded: Bool.random()) } 70 | let result = try await array.asyncCompactMap { value -> Int? in 71 | let c = await count.add(1) 72 | await maxCount.max(c) 73 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 74 | await count.add(-1) 75 | return value.isIncluded ? value.element : nil 76 | } 77 | 78 | XCTAssertEqual(result, array.filter(\.isIncluded).map(\.element)) 79 | let maxValue = await maxCount.value 80 | XCTAssertEqual(maxValue, 1) 81 | } 82 | 83 | func testConcurrentCompactMapConcurrency() async throws { 84 | let count = Count(0) 85 | let maxCount = Count(0) 86 | 87 | let array = Array(0..<800).map { (element: $0, isIncluded: Bool.random()) } 88 | let result = try await array.concurrentCompactMap { value -> Int? in 89 | let c = await count.add(1) 90 | await maxCount.max(c) 91 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 92 | await count.add(-1) 93 | return value.isIncluded ? value.element : nil 94 | } 95 | 96 | XCTAssertEqual(result, array.filter(\.isIncluded).map(\.element)) 97 | let maxValue = await maxCount.value 98 | XCTAssertGreaterThan(maxValue, 1) 99 | } 100 | 101 | func testConcurrentCompactMapConcurrencyWithMaxTasks() async throws { 102 | let count = Count(0) 103 | let maxCount = Count(0) 104 | 105 | let array = Array(0..<800).map { (element: $0, isIncluded: Bool.random()) } 106 | let result = try await array.concurrentCompactMap(maxConcurrentTasks: 4) { value -> Int? in 107 | let c = await count.add(1) 108 | await maxCount.max(c) 109 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 110 | await count.add(-1) 111 | return value.isIncluded ? value.element : nil 112 | } 113 | 114 | XCTAssertEqual(result, array.filter(\.isIncluded).map(\.element)) 115 | let maxValue = await maxCount.value 116 | XCTAssertLessThanOrEqual(maxValue, 4) 117 | XCTAssertGreaterThan(maxValue, 1) 118 | } 119 | 120 | func testConcurrentCompactMapConcurrencyWithMaxTasksNonOptionalClosure() async throws { 121 | let count = Count(0) 122 | let maxCount = Count(0) 123 | 124 | let array = Array(0..<800) 125 | let result = try await array.concurrentCompactMap(maxConcurrentTasks: 4) { value -> Int in 126 | let c = await count.add(1) 127 | await maxCount.max(c) 128 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 129 | await count.add(-1) 130 | return value 131 | } 132 | 133 | XCTAssertEqual(result, array) 134 | let maxValue = await maxCount.value 135 | XCTAssertLessThanOrEqual(maxValue, 4) 136 | XCTAssertGreaterThan(maxValue, 1) 137 | } 138 | 139 | func testAsyncCompactMapErrorThrowing() async throws { 140 | struct TaskError: Error {} 141 | 142 | do { 143 | _ = try await(1...8).asyncCompactMap { element -> Int? in 144 | if element == 4 { 145 | throw TaskError() 146 | } 147 | return element 148 | } 149 | XCTFail("Should have failed") 150 | } catch is TaskError { 151 | } catch { 152 | XCTFail("Error: \(error)") 153 | } 154 | } 155 | 156 | func testConcurrentCompactMapErrorThrowing() async throws { 157 | struct TaskError: Error {} 158 | 159 | do { 160 | _ = try await(1...8).concurrentCompactMap { element -> Int? in 161 | if element == 4 { 162 | throw TaskError() 163 | } 164 | return element 165 | } 166 | XCTFail("Should have failed") 167 | } catch is TaskError { 168 | } catch { 169 | XCTFail("Error: \(error)") 170 | } 171 | } 172 | 173 | func testAsyncCompactMapCancellation() async throws { 174 | let count = Count(1) 175 | 176 | let array = Array((1...8).reversed()) 177 | let task = Task { 178 | _ = try await array.asyncCompactMap { value -> Int? in 179 | try await Task.sleep(nanoseconds: numericCast(value) * 1000 * 100) 180 | await count.mul(value) 181 | return Bool.random() ? value : nil 182 | } 183 | } 184 | try await Task.sleep(nanoseconds: 15 * 1000 * 100) 185 | task.cancel() 186 | let value = await count.value 187 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 188 | } 189 | 190 | func testConcurrentCompactMapCancellation() async throws { 191 | let count = Count(1) 192 | 193 | let array = Array((1...8).reversed()) 194 | let task = Task { 195 | _ = try await array.concurrentCompactMap { value -> Int? in 196 | try await Task.sleep(nanoseconds: numericCast(value) * 1000 * 100) 197 | await count.mul(value) 198 | return Bool.random() ? value : nil 199 | } 200 | } 201 | try await Task.sleep(nanoseconds: 1 * 1000 * 100) 202 | task.cancel() 203 | let value = await count.value 204 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/FilterTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncCollections 2 | import XCTest 3 | 4 | final class FilterTests: XCTestCase { 5 | func testAsyncFilter() async throws { 6 | let array = Array(0..<80).map { (element: $0, isIncluded: Bool.random()) } 7 | let result = try await array.asyncFilter { value -> Bool in 8 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 9 | return value.isIncluded 10 | } 11 | 12 | XCTAssertTrue(result.allSatisfy(\.isIncluded)) 13 | XCTAssertEqual(result.map(\.element), array.filter(\.isIncluded).map(\.element)) 14 | } 15 | 16 | func testConcurrentFilter() async throws { 17 | let array = Array(0..<80).map { (element: $0, isIncluded: Bool.random()) } 18 | let result = try await array.concurrentFilter { value -> Bool in 19 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 20 | return value.isIncluded 21 | } 22 | 23 | XCTAssertTrue(result.allSatisfy(\.isIncluded)) 24 | XCTAssertEqual(result.map(\.element), array.filter(\.isIncluded).map(\.element)) 25 | } 26 | 27 | func testConcurrentFilterWithString() async throws { 28 | let array = Array(0..<80).map { (element: "\($0)", isIncluded: Bool.random()) } 29 | let result = try await array.concurrentFilter { value -> Bool in 30 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 31 | return value.isIncluded 32 | } 33 | 34 | XCTAssertTrue(result.allSatisfy(\.isIncluded)) 35 | XCTAssertEqual(result.map(\.element), array.filter(\.isIncluded).map(\.element)) 36 | } 37 | 38 | func testAsyncFilterConcurrency() async throws { 39 | let count = Count(0) 40 | let maxCount = Count(0) 41 | 42 | let array = Array(0..<80).map { (element: "\($0)", isIncluded: Bool.random()) } 43 | let result = try await array.asyncFilter { value -> Bool in 44 | let c = await count.add(1) 45 | await maxCount.max(c) 46 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 47 | await count.add(-1) 48 | return value.isIncluded 49 | } 50 | 51 | XCTAssertTrue(result.allSatisfy(\.isIncluded)) 52 | XCTAssertEqual(result.map(\.element), array.filter(\.isIncluded).map(\.element)) 53 | let maxValue = await maxCount.value 54 | XCTAssertEqual(maxValue, 1) 55 | } 56 | 57 | func testConcurrentFilterConcurrency() async throws { 58 | let count = Count(0) 59 | let maxCount = Count(0) 60 | 61 | let array = Array(0..<80).map { (element: "\($0)", isIncluded: Bool.random()) } 62 | let result = try await array.concurrentFilter { value -> Bool in 63 | let c = await count.add(1) 64 | await maxCount.max(c) 65 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 66 | await count.add(-1) 67 | return value.isIncluded 68 | } 69 | 70 | XCTAssertTrue(result.allSatisfy(\.isIncluded)) 71 | XCTAssertEqual(result.map(\.element), array.filter(\.isIncluded).map(\.element)) 72 | let maxValue = await maxCount.value 73 | XCTAssertGreaterThan(maxValue, 1) 74 | } 75 | 76 | func testConcurrentFilterConcurrencyWithMaxTasks() async throws { 77 | let count = Count(0) 78 | let maxCount = Count(0) 79 | 80 | let array = Array(0..<80).map { (element: "\($0)", isIncluded: Bool.random()) } 81 | let result = try await array.concurrentFilter(maxConcurrentTasks: 4) { value -> Bool in 82 | let c = await count.add(1) 83 | await maxCount.max(c) 84 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 85 | await count.add(-1) 86 | return value.isIncluded 87 | } 88 | 89 | XCTAssertTrue(result.allSatisfy(\.isIncluded)) 90 | XCTAssertEqual(result.map(\.element), array.filter(\.isIncluded).map(\.element)) 91 | let maxValue = await maxCount.value 92 | XCTAssertLessThanOrEqual(maxValue, 4) 93 | XCTAssertGreaterThan(maxValue, 1) 94 | } 95 | 96 | func testAsyncFilterErrorThrowing() async throws { 97 | struct TaskError: Error {} 98 | 99 | do { 100 | _ = try await(1...8).asyncFilter { element -> Bool in 101 | if element == 4 { 102 | throw TaskError() 103 | } 104 | return Bool.random() 105 | } 106 | XCTFail("Should have failed") 107 | } catch is TaskError { 108 | } catch { 109 | XCTFail("Error: \(error)") 110 | } 111 | } 112 | 113 | func testConcurrentFilterErrorThrowing() async throws { 114 | struct TaskError: Error {} 115 | 116 | do { 117 | _ = try await(1...8).concurrentFilter { element -> Bool in 118 | if element == 4 { 119 | throw TaskError() 120 | } 121 | return Bool.random() 122 | } 123 | XCTFail("Should have failed") 124 | } catch is TaskError { 125 | } catch { 126 | XCTFail("Error: \(error)") 127 | } 128 | } 129 | 130 | func testAsyncFilterCancellation() async throws { 131 | let count = Count(1) 132 | 133 | let array = Array((1...8).reversed()) 134 | let task = Task { 135 | _ = try await array.asyncFilter { value -> Bool in 136 | try await Task.sleep(nanoseconds: numericCast(value) * 1000 * 100) 137 | await count.mul(value) 138 | return Bool.random() 139 | } 140 | } 141 | try await Task.sleep(nanoseconds: 15 * 1000 * 100) 142 | task.cancel() 143 | let value = await count.value 144 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 145 | } 146 | 147 | func testConcurrentFilterCancellation() async throws { 148 | let count = Count(1) 149 | 150 | let array = Array((1...8).reversed()) 151 | let task = Task { 152 | _ = try await array.concurrentFilter { value -> Bool in 153 | try await Task.sleep(nanoseconds: numericCast(value) * 1000 * 100) 154 | await count.mul(value) 155 | return Bool.random() 156 | } 157 | } 158 | try await Task.sleep(nanoseconds: 1 * 1000 * 100) 159 | task.cancel() 160 | let value = await count.value 161 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/FlatMapTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncCollections 2 | import XCTest 3 | 4 | final class FlatMapTests: XCTestCase { 5 | func testAsyncFlatMap() async throws { 6 | let array = MereSequence(0..<80).map { _ in MereSequence(0..<8) } 7 | let result = try await array.asyncFlatMap { value -> MereSequence in 8 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 9 | return value 10 | } 11 | 12 | XCTAssertEqual(result, array.flatMap { $0 }) 13 | } 14 | 15 | func testConcurrentFlatMap() async throws { 16 | let array = MereSequence(0..<800).map { _ in MereSequence(0..<8) } 17 | let result = try await array.concurrentFlatMap { value -> MereSequence in 18 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 19 | return value 20 | } 21 | 22 | XCTAssertEqual(result, array.flatMap { $0 }) 23 | } 24 | 25 | func testConcurrentFlatMapWithString() async throws { 26 | let array = MereSequence(0..<800).map { _ in MereSequence(0..<8) } 27 | let result = try await array.concurrentFlatMap { value -> [String] in 28 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 29 | return value.map(String.init) 30 | } 31 | 32 | XCTAssertEqual(result, array.flatMap { $0.map(String.init) }) 33 | } 34 | 35 | func testAsyncFlatMapConcurrency() async throws { 36 | let count = Count(0) 37 | let maxCount = Count(0) 38 | 39 | let array = Array(0..<80).map { _ in Array(0..<8) } 40 | let result = try await array.asyncFlatMap { value -> Array in 41 | let c = await count.add(1) 42 | await maxCount.max(c) 43 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 44 | await count.add(-1) 45 | return value 46 | } 47 | 48 | XCTAssertEqual(result, array.flatMap { $0 }) 49 | let maxValue = await maxCount.value 50 | XCTAssertEqual(maxValue, 1) 51 | } 52 | 53 | func testConcurrentFlatMapConcurrency() async throws { 54 | let count = Count(0) 55 | let maxCount = Count(0) 56 | 57 | let array = Array(0..<800).map { _ in Array(0..<8) } 58 | let result = try await array.concurrentFlatMap { value -> [Int] in 59 | let c = await count.add(1) 60 | await maxCount.max(c) 61 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 62 | await count.add(-1) 63 | return value 64 | } 65 | 66 | XCTAssertEqual(result, array.flatMap { $0 }) 67 | let maxValue = await maxCount.value 68 | XCTAssertGreaterThan(maxValue, 1) 69 | } 70 | 71 | func testConcurrentFlatMapConcurrencyWithMaxTasks() async throws { 72 | let count = Count(0) 73 | let maxCount = Count(0) 74 | 75 | let array = MereSequence(0..<800).map { _ in MereSequence(0..<8) } 76 | let result = try await array.concurrentFlatMap(maxConcurrentTasks: 4) { 77 | value -> MereSequence in 78 | let c = await count.add(1) 79 | await maxCount.max(c) 80 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 81 | await count.add(-1) 82 | return value 83 | } 84 | 85 | XCTAssertEqual(result, array.flatMap { $0 }) 86 | let maxValue = await maxCount.value 87 | XCTAssertLessThanOrEqual(maxValue, 4) 88 | XCTAssertGreaterThan(maxValue, 1) 89 | } 90 | 91 | func testConcurrentFlatMapConcurrencyWithMaxTasksWithArray() async throws { 92 | let count = Count(0) 93 | let maxCount = Count(0) 94 | 95 | let array = Array(0..<800).map { _ in Array(0..<8) } 96 | let result = try await array.concurrentFlatMap(maxConcurrentTasks: 4) { value -> [Int] in 97 | let c = await count.add(1) 98 | await maxCount.max(c) 99 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 100 | await count.add(-1) 101 | return value 102 | } 103 | 104 | XCTAssertEqual(result, array.flatMap { $0 }) 105 | let maxValue = await maxCount.value 106 | XCTAssertLessThanOrEqual(maxValue, 4) 107 | XCTAssertGreaterThan(maxValue, 1) 108 | } 109 | 110 | func testAsyncFlatMapErrorThrowing() async throws { 111 | struct TaskError: Error {} 112 | 113 | do { 114 | _ = try await (1...8).asyncFlatMap { element -> [Int] in 115 | if element == 4 { 116 | throw TaskError() 117 | } 118 | return [element] 119 | } 120 | XCTFail("Should have failed") 121 | } catch is TaskError { 122 | } catch { 123 | XCTFail("Error: \(error)") 124 | } 125 | } 126 | 127 | func testConcurrentFlatMapErrorThrowing() async throws { 128 | struct TaskError: Error {} 129 | 130 | do { 131 | _ = try await (1...8).concurrentFlatMap { element -> MereSequence in 132 | if element == 4 { 133 | throw TaskError() 134 | } 135 | return MereSequence([element]) 136 | } 137 | XCTFail("Should have failed") 138 | } catch is TaskError { 139 | } catch { 140 | XCTFail("Error: \(error)") 141 | } 142 | } 143 | 144 | func testAsyncFlatMapCancellation() async throws { 145 | let count = Count(1) 146 | 147 | let array = MereSequence((1...8).reversed()).map { MereSequence([$0]) } 148 | let task = Task { 149 | _ = try await array.asyncFlatMap { value -> MereSequence in 150 | let first = value.first(where: { _ in true })! 151 | try await Task.sleep(nanoseconds: numericCast(first) * 1000 * 100) 152 | await count.mul(first) 153 | return value 154 | } 155 | } 156 | try await Task.sleep(nanoseconds: 15 * 1000 * 100) 157 | task.cancel() 158 | let value = await count.value 159 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 160 | } 161 | 162 | func testConcurrentFlatMapCancellation() async throws { 163 | let count = Count(1) 164 | 165 | let array = MereSequence((1...8).reversed()).map { MereSequence([$0]) } 166 | let task = Task { 167 | _ = try await array.concurrentFlatMap { value -> MereSequence in 168 | let first = value.first(where: { _ in true })! 169 | try await Task.sleep(nanoseconds: numericCast(first) * 1000 * 100) 170 | await count.mul(first) 171 | return value 172 | } 173 | } 174 | try await Task.sleep(nanoseconds: 1 * 1000 * 100) 175 | task.cancel() 176 | let value = await count.value 177 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 178 | } 179 | } 180 | 181 | /// A base `Sequence` type. 182 | private struct MereSequence: Sequence { 183 | /// Use an unordered underlying sequence, no need to assume order. 184 | let underlying: Set 185 | 186 | /// Don't just return `self.underlying.count` to truly test the property as "underestimated". 187 | /// This will test the parts where there is a `for-loop`, followed by a `while let` and 188 | /// the `while let` is supposed to accumulate any remaining elements that are 189 | /// after `underestimatedCount` in the sequence's order. 190 | var underestimatedCount: Int { 191 | self.underlying.count / 2 192 | } 193 | 194 | struct Iterator: IteratorProtocol { 195 | var base: Set.Iterator 196 | 197 | mutating func next() -> Element? { 198 | self.base.next() 199 | } 200 | } 201 | 202 | func makeIterator() -> Iterator { 203 | Iterator(base: self.underlying.makeIterator()) 204 | } 205 | 206 | init(_ sequence: some Sequence) { 207 | self.underlying = .init(sequence) 208 | } 209 | } 210 | 211 | extension MereSequence: Sendable where Element: Sendable { } 212 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/ForEachTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncCollections 2 | import XCTest 3 | 4 | final class ForEachTests: XCTestCase { 5 | func testAsyncForEach() async { 6 | let count = Count(1) 7 | 8 | let primes = [2, 3, 5, 7, 11, 13] 9 | await primes.asyncForEach { await count.mul($0) } 10 | 11 | let value = await count.value 12 | XCTAssertEqual(value, 2 * 3 * 5 * 7 * 11 * 13) 13 | } 14 | 15 | func testConcurrentForEach() async { 16 | let count = Count(1) 17 | 18 | let primes = [2, 3, 5, 7, 11, 13] 19 | await primes.concurrentForEach { await count.mul($0) } 20 | 21 | let value = await count.value 22 | XCTAssertEqual(value, 2 * 3 * 5 * 7 * 11 * 13) 23 | } 24 | 25 | func testConcurrentAsyncForEach() async throws { 26 | let count = Count(0) 27 | let maxCount = Count(0) 28 | 29 | try await(0..<8).asyncForEach { _ in 30 | let value = await count.add(1) 31 | await maxCount.max(value) 32 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<1000 * 1000)) 33 | await count.add(-1) 34 | } 35 | 36 | let maxValue = await maxCount.value 37 | XCTAssertEqual(maxValue, 1) 38 | } 39 | 40 | func testConcurrentForEachConcurrency() async throws { 41 | let count = Count(0) 42 | let maxCount = Count(0) 43 | 44 | try await(0..<8).concurrentForEach { _ in 45 | let value = await count.add(1) 46 | await maxCount.max(value) 47 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<1000 * 1000)) 48 | await count.add(-1) 49 | } 50 | 51 | let maxValue = await maxCount.value 52 | XCTAssertGreaterThan(maxValue, 1) 53 | } 54 | 55 | func testConcurrentForEachConcurrencyWithMaxTasks() async throws { 56 | let count = Count(0) 57 | let maxCount = Count(0) 58 | 59 | try await(0..<80).concurrentForEach(maxConcurrentTasks: 8) { _ in 60 | let value = await count.add(1) 61 | await maxCount.max(value) 62 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<1000 * 1000)) 63 | await count.add(-1) 64 | } 65 | 66 | let maxValue = await maxCount.value 67 | XCTAssertGreaterThan(maxValue, 1) 68 | XCTAssertLessThanOrEqual(maxValue, 8) 69 | } 70 | 71 | func testAsyncForEachIrregularDuration() async throws { 72 | let count = Count(1) 73 | 74 | let primes = [2, 3, 5, 7, 11, 13] 75 | try await primes.asyncForEach { 76 | await count.mul($0) 77 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<1000 * 1000)) 78 | } 79 | 80 | let value = await count.value 81 | XCTAssertEqual(value, 2 * 3 * 5 * 7 * 11 * 13) 82 | } 83 | 84 | func testConcurrentForEachIrregularDuration() async throws { 85 | let count = Count(1) 86 | 87 | let primes = [2, 3, 5, 7, 11, 13] 88 | try await primes.concurrentForEach { 89 | await count.mul($0) 90 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<1000 * 1000)) 91 | } 92 | 93 | let value = await count.value 94 | XCTAssertEqual(value, 2 * 3 * 5 * 7 * 11 * 13) 95 | } 96 | 97 | func testAsyncForEachErrorThrowing() async throws { 98 | struct TaskError: Error {} 99 | let count = Count(1) 100 | 101 | do { 102 | try await(1...8).asyncForEach { 103 | await count.mul($0) 104 | if $0 == 4 { 105 | throw TaskError() 106 | } 107 | } 108 | XCTFail("Should have failed") 109 | } catch is TaskError { 110 | let value = await count.value 111 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 112 | } catch { 113 | XCTFail("Error: \(error)") 114 | } 115 | } 116 | 117 | func testConcurrentForEachErrorThrowing() async throws { 118 | struct TaskError: Error {} 119 | let count = Count(1) 120 | 121 | let task = Task { 122 | try await(0..<8).concurrentForEach { 123 | await count.mul($0) 124 | if $0 == 4 { 125 | throw TaskError() 126 | } 127 | } 128 | } 129 | switch await task.result { 130 | case .failure(let error): 131 | guard error is TaskError else { 132 | XCTFail("Error: \(error)") 133 | return 134 | } 135 | case .success: 136 | XCTFail("Should have failed") 137 | } 138 | } 139 | 140 | func testAsyncForEachCancellation() async throws { 141 | let count = Count(1) 142 | 143 | let primes = Array((1...8).reversed()) 144 | let task = Task { 145 | try await primes.asyncForEach { 146 | try await Task.sleep(nanoseconds: numericCast($0) * 1000 * 100) 147 | await count.mul($0) 148 | } 149 | } 150 | try await Task.sleep(nanoseconds: 15 * 1000 * 100) 151 | task.cancel() 152 | 153 | let value = await count.value 154 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 155 | } 156 | 157 | func testConcurrentForEachCancellation() async throws { 158 | let count = Count(1) 159 | 160 | let primes = Array((1...8).reversed()) 161 | let task = Task { 162 | try await primes.asyncForEach { 163 | try await Task.sleep(nanoseconds: numericCast($0) * 1000 * 100) 164 | await count.mul($0) 165 | } 166 | } 167 | try await Task.sleep(nanoseconds: 10 * 1000 * 100) 168 | task.cancel() 169 | 170 | let value = await count.value 171 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 172 | } 173 | } 174 | 175 | actor Count { 176 | var value: Int 177 | 178 | init(_ value: Int = 0) { 179 | self.value = value 180 | } 181 | 182 | func set(_ rhs: Int) { 183 | self.value = rhs 184 | } 185 | 186 | @discardableResult func add(_ rhs: Int) -> Int { 187 | self.value += rhs 188 | return self.value 189 | } 190 | 191 | @discardableResult func mul(_ rhs: Int) -> Int { 192 | self.value *= rhs 193 | return self.value 194 | } 195 | 196 | @discardableResult func min(_ rhs: Int) -> Int { 197 | self.value = Swift.min(self.value, rhs) 198 | return self.value 199 | } 200 | 201 | @discardableResult func max(_ rhs: Int) -> Int { 202 | self.value = Swift.max(self.value, rhs) 203 | return self.value 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/MapTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncCollections 2 | import XCTest 3 | 4 | final class MapTests: XCTestCase { 5 | func testAsyncMap() async throws { 6 | let array = Array(0..<80) 7 | let result = try await array.asyncMap { value -> Int in 8 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 9 | return value 10 | } 11 | 12 | XCTAssertEqual(result, array) 13 | } 14 | 15 | func testConcurrentMap() async throws { 16 | let array = Array(0..<800) 17 | let result = try await array.concurrentMap { value -> Int in 18 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 19 | return value 20 | } 21 | 22 | XCTAssertEqual(result, array) 23 | } 24 | 25 | func testConcurrentMapWithString() async throws { 26 | let array = Array(0..<800) 27 | let result = try await array.concurrentMap { value -> String in 28 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 29 | return String(value) 30 | } 31 | 32 | XCTAssertEqual(result, array.map { String($0) }) 33 | } 34 | 35 | func testAsyncMapConcurrency() async throws { 36 | let count = Count(0) 37 | let maxCount = Count(0) 38 | 39 | let array = Array(0..<80) 40 | let result = try await array.asyncMap { value -> Int in 41 | let c = await count.add(1) 42 | await maxCount.max(c) 43 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 44 | await count.add(-1) 45 | return value 46 | } 47 | 48 | XCTAssertEqual(result, array) 49 | let maxValue = await maxCount.value 50 | XCTAssertEqual(maxValue, 1) 51 | } 52 | 53 | func testConcurrentMapConcurrency() async throws { 54 | let count = Count(0) 55 | let maxCount = Count(0) 56 | 57 | let array = Array(0..<800) 58 | let result = try await array.concurrentMap { value -> Int in 59 | let c = await count.add(1) 60 | await maxCount.max(c) 61 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 62 | await count.add(-1) 63 | return value 64 | } 65 | 66 | XCTAssertEqual(result, array) 67 | let maxValue = await maxCount.value 68 | XCTAssertGreaterThan(maxValue, 1) 69 | } 70 | 71 | func testConcurrentMapConcurrencyWithMaxTasks() async throws { 72 | let count = Count(0) 73 | let maxCount = Count(0) 74 | 75 | let array = Array(0..<800) 76 | let result = try await array.concurrentMap(maxConcurrentTasks: 4) { value -> Int in 77 | let c = await count.add(1) 78 | await maxCount.max(c) 79 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100_000)) 80 | await count.add(-1) 81 | return value 82 | } 83 | 84 | XCTAssertEqual(result, array) 85 | let maxValue = await maxCount.value 86 | XCTAssertLessThanOrEqual(maxValue, 4) 87 | XCTAssertGreaterThan(maxValue, 1) 88 | } 89 | 90 | func testAsyncMapErrorThrowing() async throws { 91 | struct TaskError: Error {} 92 | 93 | do { 94 | _ = try await(1...8).asyncMap { element -> Int in 95 | if element == 4 { 96 | throw TaskError() 97 | } 98 | return element 99 | } 100 | XCTFail("Should have failed") 101 | } catch is TaskError { 102 | } catch { 103 | XCTFail("Error: \(error)") 104 | } 105 | } 106 | 107 | func testConcurrentMapErrorThrowing() async throws { 108 | struct TaskError: Error {} 109 | 110 | do { 111 | _ = try await(1...8).concurrentMap { element -> Int in 112 | if element == 4 { 113 | throw TaskError() 114 | } 115 | return element 116 | } 117 | XCTFail("Should have failed") 118 | } catch is TaskError { 119 | } catch { 120 | XCTFail("Error: \(error)") 121 | } 122 | } 123 | 124 | func testAsyncMapCancellation() async throws { 125 | let count = Count(1) 126 | 127 | let array = Array((1...8).reversed()) 128 | let task = Task { 129 | _ = try await array.asyncMap { value -> Int in 130 | try await Task.sleep(nanoseconds: numericCast(value) * 1000 * 100) 131 | await count.mul(value) 132 | return value 133 | } 134 | } 135 | try await Task.sleep(nanoseconds: 15 * 1000 * 100) 136 | task.cancel() 137 | let value = await count.value 138 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 139 | } 140 | 141 | func testConcurrentMapCancellation() async throws { 142 | let count = Count(1) 143 | 144 | let array = Array((1...8).reversed()) 145 | let task = Task { 146 | _ = try await array.concurrentMap { value -> Int in 147 | try await Task.sleep(nanoseconds: numericCast(value) * 1000 * 100) 148 | await count.mul(value) 149 | return value 150 | } 151 | } 152 | try await Task.sleep(nanoseconds: 1 * 1000 * 100) 153 | task.cancel() 154 | let value = await count.value 155 | XCTAssertNotEqual(value, 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Tests/AsyncCollectionTests/TaskQueueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AsyncCollections 3 | 4 | final class TaskQueueTests: XCTestCase { 5 | func testTaskQueue() async throws { 6 | let queue = TaskQueue(maxConcurrentTasks: 32) 7 | let array = Array((0..<8000)) 8 | let result = try await array.concurrentMap { value -> Int in 9 | try await queue.add { 10 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100000)) 11 | return value 12 | } 13 | } 14 | 15 | XCTAssertEqual(result, array) 16 | } 17 | 18 | func testMaxConcurrent() async throws { 19 | let count = Count(0) 20 | let maxCount = Count(0) 21 | 22 | let queue = TaskQueue(maxConcurrentTasks: 8) 23 | let array = Array((0..<800)) 24 | let result = try await array.concurrentMap { value -> Int in 25 | try await queue.add { 26 | let c = await count.add(1) 27 | await maxCount.max(c) 28 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100000)) 29 | await count.add(-1) 30 | return value 31 | } 32 | } 33 | 34 | XCTAssertEqual(result, array) 35 | let maxValue = await maxCount.value 36 | XCTAssertGreaterThan(maxValue, 1) 37 | XCTAssertLessThanOrEqual(maxValue, 8) 38 | } 39 | 40 | func testCancellation() async throws { 41 | let count = Count(0) 42 | 43 | let queue = TaskQueue(maxConcurrentTasks: 16) 44 | let array = Array((1...200).reversed()) 45 | let task = Task { 46 | _ = try await array.concurrentMap { value -> Int in 47 | try await queue.add { 48 | try await Task.sleep(nanoseconds: UInt64.random(in: 1000..<100000)) 49 | await count.add(value) 50 | return value 51 | } 52 | } 53 | } 54 | try await Task.sleep(nanoseconds: 1000 * 1000) 55 | task.cancel() 56 | let value = await count.value 57 | XCTAssertNotEqual(value, array.reduce(0, +)) 58 | } 59 | } 60 | --------------------------------------------------------------------------------