├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── AsyncTimeSequences.xcscheme ├── FormatScript.sh ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── AsyncTimeSequences │ ├── AsyncScheduler │ │ ├── AsyncScheduler.swift │ │ ├── AsyncSchedulerHandlerElement.swift │ │ └── MinimumPriorityQueue.swift │ ├── Debounce │ │ └── AsyncDebounceSequence.swift │ ├── Delay │ │ └── AsyncDelaySequence.swift │ ├── Extensions │ │ └── Task+Sleep.swift │ ├── MeasureInterval │ │ └── AsyncMeasureIntervalSequence.swift │ ├── Throttle │ │ └── AsyncThrottleSequence.swift │ └── Timeout │ │ └── AsyncTimeoutSequence.swift └── AsyncTimeSequencesSupport │ ├── ControlledDataSequence.swift │ ├── DataStructures │ ├── Dequeue.swift │ └── DoublyLinkedList.swift │ ├── Extensions │ └── Array+ElementAtIndex.swift │ └── TestAsyncScheduler.swift ├── SwiftFormatConfiguration.json └── Tests └── AsyncTimeSequencesTests ├── AsyncDebounceSequence+Tests.swift ├── AsyncDelaySequence+Tests.swift ├── AsyncMeasureIntervalSequence+Tests.swift ├── AsyncSchedulerTests.swift ├── AsyncThrottleSequence+Tests.swift ├── AsyncTimeoutSequence+Tests.swift ├── MockSequences ├── InfiniteDataSequence.swift └── SampleDataSequence.swift └── Support └── SafeActorArrayWrapper.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/AsyncTimeSequences.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /FormatScript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | swift-format format --in-place --recursive Sources --configuration SwiftFormatConfiguration.json 4 | 5 | swift-format format --in-place --recursive Tests --configuration SwiftFormatConfiguration.json 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Henryforce 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 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: "AsyncTimeSequences", 8 | platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "AsyncTimeSequences", 13 | targets: ["AsyncTimeSequences"] 14 | ), 15 | .library( 16 | name: "AsyncTimeSequencesSupport", 17 | targets: ["AsyncTimeSequencesSupport"] 18 | ), 19 | ], 20 | dependencies: [ 21 | // Dependencies declare other packages that this package depends on. 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "AsyncTimeSequences", 28 | dependencies: [], 29 | path: "Sources/AsyncTimeSequences" 30 | ), 31 | .target( 32 | name: "AsyncTimeSequencesSupport", 33 | dependencies: [ 34 | "AsyncTimeSequences", 35 | ], 36 | path: "Sources/AsyncTimeSequencesSupport" 37 | ), 38 | .testTarget( 39 | name: "AsyncTimeSequencesTests", 40 | dependencies: [ 41 | "AsyncTimeSequences", 42 | "AsyncTimeSequencesSupport" 43 | ], 44 | path: "Tests" 45 | ), 46 | ] 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AsyncTimeSequences 2 | 3 | ![badge-platforms][] [![badge-spm][]][spm] 4 | 5 | This is a convenient package to add missing time async sequences such as debounce, throttle, delay, timeout and measure interval. 6 | 7 | These sequences work with any AsyncSequence (such as AsyncStream), and as they conform to the AsyncSequence Protocol the possibilities are endless. 8 | 9 | This library relies on an AsynScheduler-conforming class that guarantees async execution order. Due to the nature of the Swift Async architecture, the execution of Tasks is not deterministic and to guarantee the order of time operations it becomes necessary to add further handling. This is critical for many time operators such as Debounce and Delay. The AsyncScheduler class provided in this library promises async task execution following FIFO and cancellation. 10 | 11 | ## Compatibility 12 | 13 | This package is supported on Xcode 13.2+ targeting iOS 13+, MacOS 10.15+, WatchOS 6+ and TvOS 13+. 14 | 15 | ## How to use 16 | 17 | For all examples, please first consider this sample sequence (remember that you can use any AsyncSequence): 18 | 19 | ```swift 20 | let asyncSequence = AsyncStream { (continuation:AsyncStream.Continuation) in 21 | Task { 22 | let items = [1,2,3,4,5,6] 23 | for item in items { 24 | continuation.yield(item) 25 | try? await Task.sleep(nanoseconds: 1000000) 26 | } 27 | continuation.finish() 28 | } 29 | } 30 | ``` 31 | 32 | All the AsyncTimeSequences will need an AsyncScheduler object. For convenience, there is one already bundled with this package. You should be good with the main one provided. But you are free to add your custom one if required (by conforming to the AsyncScheduler protocol). 33 | 34 | ### Timeout 35 | 36 | ```swift 37 | asyncSequence.timeout(for: 2, scheduler: MainAsyncScheduler.default) 38 | ``` 39 | 40 | ### Delay 41 | 42 | ```swift 43 | asyncSequence.delay(for: 3, scheduler: MainAsyncScheduler.default) 44 | ``` 45 | 46 | ### Debounce 47 | 48 | ```swift 49 | asyncSequence.debounce(for: 3, scheduler: MainAsyncScheduler.default) 50 | ``` 51 | 52 | ### Throttle 53 | 54 | ```swift 55 | asyncSequence.throttle(for: 3, scheduler: MainAsyncScheduler.default, latest: true) 56 | ``` 57 | 58 | ### MeasureInterval 59 | 60 | ```swift 61 | asyncSequence.measureInterval(using: MainAsyncScheduler.default) 62 | ``` 63 | 64 | ## How to test 65 | 66 | Properly testing these time sequences requires some setup. Ideally, it is recommended to inject the scheduler, which will execute the time handling of your sequences, into your logic object. 67 | 68 | By injecting the scheduler, you can for example inject a test scheduler to manipulate the time operators. 69 | 70 | It is recommended to use the TestAsyncScheduler included in the AsyncTimeSequencesSupport sub-package. It has some really convenient functions to manipulate time: 71 | 72 | ```swift 73 | let scheduler = AsyncTestScheduler() 74 | scheduler.advance(by: 3.0) // Advances the time virtually and executes scheduled jobs immediately without actually waiting the time interval specified 75 | ``` 76 | 77 | An example on how to inject the scheduler if you have a view model: 78 | 79 | ```swift 80 | final class MyViewModel { 81 | 82 | private let scheduler: AsyncScheduler 83 | 84 | init( 85 | scheduler: AsyncScheduler = MainAsyncScheduler.default // Allow injection while providing a default scheduler 86 | ) { 87 | self.scheduler = scheduler 88 | } 89 | 90 | func debounceSequence(_ sequence: T) { 91 | let debounceSequence = sequence.debounce(for: 3.0, scheduler: scheduler) 92 | 93 | Task { 94 | for await value in debounceSequence { 95 | // do something that produces an output which can be evaluated and asserted during testing... 96 | } 97 | } 98 | } 99 | 100 | } 101 | ``` 102 | 103 | ```swift 104 | import AsyncTimeSequences 105 | import AsyncTimeSequencesSupport 106 | 107 | ... 108 | 109 | func testAsyncDebounceSequence() async { 110 | // Given 111 | let scheduler = TestAsyncScheduler() 112 | let viewModel = MyViewModel(scheduler: scheduler) 113 | let items = [1,5,10,15,20] 114 | let expectedItems = [20] 115 | let baseDelay = 3.0 116 | var receivedItems = [Int]() 117 | 118 | // When 119 | let sequence = ControlledDataSequence(items: items) 120 | viewModel.debounceSequence(sequence) 121 | 122 | // The ControlledDataSequence can send elements one by one, or up to n elements per one call of 123 | // the `waitForItemsToBeSent` method 124 | await sequence.iterator.waitForItemsToBeSent(items.count) 125 | // If we don't wait for jobs to get scheduled, advancing the scheduler does virtually nothing... 126 | await scheduler.advance(by: baseDelay) 127 | 128 | // your code to process the view model output... 129 | } 130 | ``` 131 | 132 | If you need further code examples, you can take a look at the tests for this package library. They rely heavily on the AsyncTestScheduler and the ControlledDataSequence classes, which are included in the AsyncTimeSequencesSupport sub-package. 133 | 134 | ## Installation 135 | 136 | ### Swift Package Manager 137 | 138 | In Xcode, select File --> Swift Packages --> Add Package Dependency and then add the following url: 139 | 140 | ```swift 141 | https://github.com/Henryforce/AsyncTimeSequences 142 | ``` 143 | 144 | There are two package included: 145 | 146 | - AsyncTimeSequences - async time sequences extensions 147 | - AsyncTimeSequencesSupport - async-time-sequences support classes for testing. (Recommended to include only in your test targets) 148 | 149 | [badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg 150 | 151 | [badge-spm]: https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg 152 | 153 | [spm]: https://github.com/apple/swift-package-manager 154 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/AsyncScheduler/AsyncScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncScheduler.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 5/1/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias AsyncSchedulerHandler = () async -> Void 11 | 12 | public protocol AsyncScheduler: Actor { 13 | var now: TimeInterval { get } 14 | 15 | @discardableResult 16 | func schedule(after: TimeInterval, handler: @escaping AsyncSchedulerHandler) -> Task 17 | } 18 | 19 | public actor MainAsyncScheduler: AsyncScheduler { 20 | public static let `default` = MainAsyncScheduler() 21 | 22 | lazy var queue = MinimumPriorityQueue() 23 | lazy var idCounter: UInt = 0 24 | lazy var completedElementIds = Set() 25 | lazy var cancelledElementIds = Set() 26 | 27 | public var now: TimeInterval { 28 | Date().timeIntervalSince1970 29 | } 30 | 31 | /// Schedule async-closures to be executed in order based on the timeinterval provided. 32 | /// 33 | /// - parameter after: TimeInterval to wait until execution 34 | /// - parameter handler: async closure to be executed when 'after' time elapses 35 | /// 36 | /// - Returns: reference to a Task which supports cancellation 37 | /// 38 | /// - Complexity: O(log n) where n is the number of elements currently scheduled 39 | @discardableResult 40 | public func schedule( 41 | after: TimeInterval, 42 | handler: @escaping AsyncSchedulerHandler 43 | ) -> Task { 44 | let currentId = idCounter 45 | let element = AsyncSchedulerHandlerElement( 46 | handler: handler, 47 | id: currentId, 48 | time: now + after 49 | ) 50 | 51 | queue.enqueue(element) 52 | 53 | increaseCounterId() 54 | 55 | return Task { 56 | try? await Task.sleep(nanoseconds: UInt64(after * 1_000_000_000)) 57 | await complete(currentId: currentId, cancelled: Task.isCancelled) 58 | } 59 | } 60 | 61 | /// Based on the timeIntervalSince1970 from Date, the smallest intervals will need 62 | /// to complete before other elements' handlers can be executed. Due to the nature 63 | /// of Tasks, there could be some situations where some tasks scheduled to finish 64 | /// before others finish first. This could potentially have unwanted behaviors on 65 | /// objects scheduling events. To address this matter, a minimum priority queue 66 | /// is critical to always keep the first element that should be completed in the 67 | /// top of the queue. Once its task completes, a Set will keep track of all 68 | /// completed ID tasks that are yet to be executed. If the current top element of 69 | /// the queue has already completed, its closure will execute. This will repeat 70 | /// until all completed top elements of the queue are executed. 71 | /// The obvious drawback of this handling, is that a small delay could be 72 | /// introduced to some scheduled async-closures. Ideally, this would be in the 73 | /// order of micro/nanoseconds depending of the system load. 74 | /// 75 | /// - parameter currentId: integer variable denoting handler/task id 76 | /// - parameter cancelled: boolean flag required to determine whether or not to execute the handler 77 | /// 78 | /// - Complexity: O(log n) where n is the number of elements currently scheduled 79 | private func complete(currentId: UInt, cancelled: Bool) async { 80 | completedElementIds.insert(currentId) 81 | if cancelled { 82 | cancelledElementIds.insert(currentId) 83 | } 84 | 85 | while let minElement = queue.peek, completedElementIds.contains(minElement.id) { 86 | queue.removeFirst() 87 | completedElementIds.remove(minElement.id) 88 | // If the current minimum element id is not cancelled, proceed to 89 | // complete its handler. Otherwise, skip and remove it from the set 90 | guard !cancelledElementIds.contains(minElement.id) else { 91 | cancelledElementIds.remove(minElement.id) 92 | continue 93 | } 94 | await minElement.handler() 95 | } 96 | } 97 | 98 | private func increaseCounterId() { 99 | if idCounter == UInt.max { 100 | idCounter = .zero 101 | } else { 102 | idCounter += 1 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/AsyncScheduler/AsyncSchedulerHandlerElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSchedulerHandlerElement.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 17/4/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AsyncSchedulerHandlerElement { 11 | let handler: AsyncSchedulerHandler 12 | let id: UInt 13 | let time: TimeInterval 14 | } 15 | 16 | extension AsyncSchedulerHandlerElement: Comparable { 17 | static func < (lhs: AsyncSchedulerHandlerElement, rhs: AsyncSchedulerHandlerElement) -> Bool { 18 | if lhs.time == rhs.time { 19 | return lhs.id <= rhs.id 20 | } 21 | return lhs.time < rhs.time 22 | } 23 | 24 | static func == (lhs: AsyncSchedulerHandlerElement, rhs: AsyncSchedulerHandlerElement) -> Bool { 25 | return lhs.time == rhs.time && lhs.id == rhs.id 26 | } 27 | } 28 | 29 | extension AsyncSchedulerHandlerElement { 30 | var unsafeMutablePointer: UnsafeMutablePointer { 31 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 32 | pointer.initialize(to: self) 33 | return pointer 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/AsyncScheduler/MinimumPriorityQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MinimumPriorityQueue.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 12/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | final class MinimumPriorityQueue { 11 | 12 | lazy var heap: CFBinaryHeap = { 13 | var callbacks = CFBinaryHeapCallBacks() 14 | callbacks.compare = { lPtr, rPtr, _ -> CFComparisonResult in 15 | guard let lhs = lPtr?.load(as: AsyncSchedulerHandlerElement.self), 16 | let rhs = rPtr?.load(as: AsyncSchedulerHandlerElement.self) 17 | else { return CFComparisonResult.compareEqualTo } 18 | 19 | if lhs == rhs { 20 | return CFComparisonResult.compareEqualTo 21 | } 22 | return lhs < rhs ? CFComparisonResult.compareLessThan : CFComparisonResult.compareGreaterThan 23 | } 24 | return CFBinaryHeapCreate(nil, 0, &callbacks, nil) 25 | }() 26 | 27 | var isEmpty: Bool { 28 | CFBinaryHeapGetCount(heap) <= 0 29 | } 30 | 31 | var peek: AsyncSchedulerHandlerElement? { 32 | guard !isEmpty else { return nil } 33 | return CFBinaryHeapGetMinimum(heap).load(as: AsyncSchedulerHandlerElement.self) 34 | } 35 | 36 | @discardableResult 37 | func enqueue(_ element: AsyncSchedulerHandlerElement) -> Bool { 38 | CFBinaryHeapAddValue(heap, element.unsafeMutablePointer) 39 | return true 40 | } 41 | 42 | func dequeue() -> AsyncSchedulerHandlerElement? { 43 | guard let firstElement = peek else { return nil } 44 | removeFirst() 45 | return firstElement 46 | } 47 | 48 | func removeFirst() { 49 | CFBinaryHeapRemoveMinimumValue(heap) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/Debounce/AsyncDebounceSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncDebounceSequence.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/11/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | public struct AsyncDebounceSequence { 12 | @usableFromInline 13 | let base: Base 14 | 15 | @usableFromInline 16 | let interval: TimeInterval 17 | 18 | @usableFromInline 19 | let scheduler: AsyncScheduler 20 | 21 | @usableFromInline 22 | init(_ base: Base, interval: TimeInterval, scheduler: AsyncScheduler) { 23 | self.base = base 24 | self.interval = interval 25 | self.scheduler = scheduler 26 | } 27 | } 28 | 29 | extension AsyncSequence { 30 | @inlinable 31 | public __consuming func debounce( 32 | for interval: TimeInterval, 33 | scheduler: AsyncScheduler 34 | ) -> AsyncDebounceSequence { 35 | return AsyncDebounceSequence(self, interval: interval, scheduler: scheduler) 36 | } 37 | } 38 | 39 | extension AsyncDebounceSequence: AsyncSequence { 40 | 41 | public typealias Element = Base.Element 42 | /// The type of iterator that produces elements of the sequence. 43 | public typealias AsyncIterator = AsyncStream.Iterator 44 | 45 | actor DebounceActor { 46 | let continuation: AsyncStream.Continuation 47 | let interval: TimeInterval 48 | let scheduler: AsyncScheduler 49 | 50 | var counter: UInt = .zero 51 | var scheduledCount: UInt = .zero 52 | var finishedContinuation: CheckedContinuation? 53 | 54 | init( 55 | continuation: AsyncStream.Continuation, 56 | interval: TimeInterval, 57 | scheduler: AsyncScheduler 58 | ) { 59 | self.continuation = continuation 60 | self.interval = interval 61 | self.scheduler = scheduler 62 | } 63 | 64 | func putNext(_ element: Base.Element) async { 65 | let localCounter = updateCounter() 66 | scheduledCount += 1 67 | await scheduler.schedule( 68 | after: interval, 69 | handler: { [weak self] in 70 | await self?.yield(element, savedCounter: localCounter) 71 | }) 72 | } 73 | 74 | func finish() async { 75 | if scheduledCount == .zero { 76 | continuation.finish() 77 | } else { 78 | // Keep the owner of the actor waiting for the end to avoid having this actor released from memory 79 | await withCheckedContinuation({ (continuation: CheckedContinuation) in 80 | finishedContinuation = continuation 81 | }) 82 | } 83 | } 84 | 85 | private func updateCounter() -> UInt { 86 | counter += 1 87 | if counter == .max { 88 | counter = .zero 89 | } 90 | return counter 91 | } 92 | 93 | private func yield(_ element: Base.Element, savedCounter: UInt) { 94 | scheduledCount -= 1 95 | 96 | guard savedCounter == counter else { return } 97 | 98 | continuation.yield(element) 99 | 100 | // If finished has been triggered and there are no more items in the queue, finish now 101 | if let finishedContinuation = finishedContinuation { 102 | continuation.finish() 103 | finishedContinuation.resume() 104 | self.finishedContinuation = nil 105 | } 106 | } 107 | } 108 | 109 | @usableFromInline 110 | struct Debounce { 111 | private var baseIterator: Base.AsyncIterator 112 | private let actor: DebounceActor 113 | 114 | @usableFromInline 115 | init( 116 | baseIterator: Base.AsyncIterator, 117 | continuation: AsyncStream.Continuation, 118 | interval: TimeInterval, 119 | scheduler: AsyncScheduler 120 | ) { 121 | self.baseIterator = baseIterator 122 | self.actor = DebounceActor( 123 | continuation: continuation, 124 | interval: interval, 125 | scheduler: scheduler 126 | ) 127 | } 128 | 129 | @usableFromInline 130 | mutating func start() async { 131 | while let element = try? await baseIterator.next() { 132 | await actor.putNext(element) 133 | } 134 | await actor.finish() 135 | } 136 | } 137 | 138 | @inlinable 139 | public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { 140 | return AsyncStream { (continuation: AsyncStream.Continuation) in 141 | Task { 142 | var debounce = Debounce( 143 | baseIterator: base.makeAsyncIterator(), 144 | continuation: continuation, 145 | interval: interval, 146 | scheduler: scheduler 147 | ) 148 | await debounce.start() 149 | } 150 | }.makeAsyncIterator() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/Delay/AsyncDelaySequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncDelaySequence.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/11/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | public struct AsyncDelaySequence { 12 | @usableFromInline 13 | let base: Base 14 | 15 | @usableFromInline 16 | let interval: TimeInterval 17 | 18 | @usableFromInline 19 | let scheduler: AsyncScheduler 20 | 21 | @usableFromInline 22 | init(_ base: Base, interval: TimeInterval, scheduler: AsyncScheduler) { 23 | self.base = base 24 | self.interval = interval 25 | self.scheduler = scheduler 26 | } 27 | } 28 | 29 | extension AsyncSequence { 30 | @inlinable 31 | public __consuming func delay( 32 | for interval: TimeInterval, 33 | scheduler: AsyncScheduler 34 | ) -> AsyncDelaySequence { 35 | return AsyncDelaySequence(self, interval: interval, scheduler: scheduler) 36 | } 37 | } 38 | 39 | extension AsyncDelaySequence: AsyncSequence { 40 | 41 | public typealias Element = Base.Element 42 | /// The type of iterator that produces elements of the sequence. 43 | public typealias AsyncIterator = AsyncStream.Iterator 44 | 45 | actor DelayActor { 46 | let continuation: AsyncStream.Continuation 47 | let interval: TimeInterval 48 | let scheduler: AsyncScheduler 49 | 50 | var counter = 0 51 | var finishedContinuation: CheckedContinuation? 52 | 53 | init( 54 | continuation: AsyncStream.Continuation, 55 | interval: TimeInterval, 56 | scheduler: AsyncScheduler 57 | ) { 58 | self.continuation = continuation 59 | self.interval = interval 60 | self.scheduler = scheduler 61 | } 62 | 63 | func putNext(_ element: Base.Element) async { 64 | counter += 1 65 | 66 | await scheduler.schedule( 67 | after: interval, 68 | handler: { [weak self] in 69 | await self?.yield(element) 70 | }) 71 | } 72 | 73 | func finish() async { 74 | // If there are no elements waiting to be yielded, finish now 75 | if counter == .zero { 76 | continuation.finish() 77 | } else { 78 | // Keep the owner of the actor waiting for the end to avoid having this actor released from memory 79 | await withCheckedContinuation({ (continuation: CheckedContinuation) in 80 | finishedContinuation = continuation 81 | }) 82 | } 83 | } 84 | 85 | private func yield(_ element: Base.Element) { 86 | counter -= 1 87 | 88 | continuation.yield(element) 89 | 90 | // If finished has been triggered and there are no more items waiting to be delayed, finish now 91 | if let finishedContinuation = finishedContinuation, counter == .zero { 92 | continuation.finish() 93 | finishedContinuation.resume() 94 | self.finishedContinuation = nil 95 | } 96 | } 97 | } 98 | 99 | @usableFromInline 100 | struct Delay { 101 | private var baseIterator: Base.AsyncIterator 102 | private let actor: DelayActor 103 | 104 | @usableFromInline 105 | init( 106 | baseIterator: Base.AsyncIterator, 107 | continuation: AsyncStream.Continuation, 108 | interval: TimeInterval, 109 | scheduler: AsyncScheduler 110 | ) { 111 | self.baseIterator = baseIterator 112 | self.actor = DelayActor( 113 | continuation: continuation, 114 | interval: interval, 115 | scheduler: scheduler 116 | ) 117 | } 118 | 119 | @usableFromInline 120 | mutating func start() async { 121 | while let element = try? await baseIterator.next() { 122 | await actor.putNext(element) 123 | } 124 | await actor.finish() 125 | } 126 | } 127 | 128 | @inlinable 129 | public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { 130 | return AsyncStream { (continuation: AsyncStream.Continuation) in 131 | Task { 132 | var delay = Delay( 133 | baseIterator: base.makeAsyncIterator(), 134 | continuation: continuation, 135 | interval: interval, 136 | scheduler: scheduler 137 | ) 138 | await delay.start() 139 | } 140 | }.makeAsyncIterator() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/Extensions/Task+Sleep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task+Sleep.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 13/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Task where Success == Never, Failure == Never { 11 | static public func sleep(seconds: UInt64) async throws { 12 | try await sleep(nanoseconds: seconds * 1_000_000_000) 13 | } 14 | 15 | static public func sleep(milliseconds: UInt64) async throws { 16 | try await sleep(nanoseconds: milliseconds * 1_000_000) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/MeasureInterval/AsyncMeasureIntervalSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncMeasureIntervalSequences.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 27/12/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | public struct AsyncMeasureIntervalSequence { 12 | @usableFromInline 13 | let base: Base 14 | 15 | @usableFromInline 16 | let scheduler: AsyncScheduler 17 | 18 | @usableFromInline 19 | init(_ base: Base, using scheduler: AsyncScheduler) { 20 | self.base = base 21 | self.scheduler = scheduler 22 | } 23 | } 24 | 25 | extension AsyncSequence { 26 | @inlinable 27 | public __consuming func measureInterval( 28 | using scheduler: AsyncScheduler 29 | ) -> AsyncMeasureIntervalSequence { 30 | return AsyncMeasureIntervalSequence(self, using: scheduler) 31 | } 32 | } 33 | 34 | extension AsyncMeasureIntervalSequence: AsyncSequence { 35 | 36 | public typealias Element = TimeInterval 37 | /// The type of iterator that produces elements of the sequence. 38 | public typealias AsyncIterator = AsyncStream.Iterator 39 | 40 | actor MeasureIntervalActor { 41 | let continuation: AsyncStream.Continuation 42 | let scheduler: AsyncScheduler 43 | 44 | var lastTime: TimeInterval? 45 | 46 | init( 47 | continuation: AsyncStream.Continuation, 48 | scheduler: AsyncScheduler 49 | ) { 50 | self.continuation = continuation 51 | self.scheduler = scheduler 52 | } 53 | 54 | func putNext() async { 55 | let now = await scheduler.now 56 | 57 | if let lastTime = lastTime { 58 | let distance = lastTime.distance(to: now) 59 | yield(distance) 60 | } 61 | 62 | self.lastTime = now 63 | } 64 | 65 | func finish() { 66 | continuation.finish() 67 | } 68 | 69 | private func yield(_ element: TimeInterval) { 70 | continuation.yield(element) 71 | } 72 | } 73 | 74 | @usableFromInline 75 | struct MeasureInterval { 76 | private var baseIterator: Base.AsyncIterator 77 | private let actor: MeasureIntervalActor 78 | 79 | @usableFromInline 80 | init( 81 | baseIterator: Base.AsyncIterator, 82 | continuation: AsyncStream.Continuation, 83 | scheduler: AsyncScheduler 84 | ) { 85 | self.baseIterator = baseIterator 86 | self.actor = MeasureIntervalActor( 87 | continuation: continuation, 88 | scheduler: scheduler 89 | ) 90 | } 91 | 92 | @usableFromInline 93 | mutating func start() async { 94 | while (try? await baseIterator.next() != nil) ?? false { 95 | await actor.putNext() 96 | } 97 | await actor.finish() 98 | } 99 | } 100 | 101 | @inlinable 102 | public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { 103 | return AsyncStream { (continuation: AsyncStream.Continuation) in 104 | Task { 105 | var measureInterval = MeasureInterval( 106 | baseIterator: base.makeAsyncIterator(), 107 | continuation: continuation, 108 | scheduler: scheduler 109 | ) 110 | await measureInterval.start() 111 | } 112 | }.makeAsyncIterator() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/Throttle/AsyncThrottleSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncThrottleSequence.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/11/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension AsyncSequence { 12 | @inlinable 13 | public __consuming func throttle( 14 | for interval: TimeInterval, 15 | scheduler: AsyncScheduler, 16 | latest: Bool 17 | ) -> AsyncThrottleSequence { 18 | return AsyncThrottleSequence(self, interval: interval, scheduler: scheduler, latest: latest) 19 | } 20 | } 21 | 22 | public struct AsyncThrottleSequence { 23 | @usableFromInline 24 | let base: Base 25 | 26 | @usableFromInline 27 | let interval: TimeInterval 28 | 29 | @usableFromInline 30 | let scheduler: AsyncScheduler 31 | 32 | @usableFromInline 33 | let latest: Bool 34 | 35 | @usableFromInline 36 | init(_ base: Base, interval: TimeInterval, scheduler: AsyncScheduler, latest: Bool) { 37 | self.base = base 38 | self.interval = interval 39 | self.scheduler = scheduler 40 | self.latest = latest 41 | } 42 | } 43 | 44 | extension AsyncThrottleSequence: AsyncSequence { 45 | 46 | public typealias Element = Base.Element 47 | /// The type of iterator that produces elements of the sequence. 48 | public typealias AsyncIterator = AsyncStream.Iterator 49 | 50 | actor ThrottleActor { 51 | let continuation: AsyncStream.Continuation 52 | let interval: TimeInterval 53 | let scheduler: AsyncScheduler 54 | let latest: Bool 55 | 56 | var savedElement: Base.Element? 57 | var readyToSendFirst = false 58 | var started = false 59 | var finishedContinuation: CheckedContinuation? 60 | 61 | init( 62 | continuation: AsyncStream.Continuation, 63 | interval: TimeInterval, 64 | scheduler: AsyncScheduler, 65 | latest: Bool 66 | ) { 67 | self.continuation = continuation 68 | self.interval = interval 69 | self.scheduler = scheduler 70 | self.latest = latest 71 | } 72 | 73 | func putNext(_ element: Base.Element) async { 74 | savedElement = element 75 | 76 | if !started { 77 | await start() 78 | } 79 | 80 | if !latest, readyToSendFirst { 81 | readyToSendFirst = false 82 | yield() 83 | } 84 | } 85 | 86 | func finish() async { 87 | if savedElement == nil, !started { 88 | continuation.finish() 89 | } else { 90 | // Keep the owner of the actor waiting for the end to avoid having this actor released from memory 91 | await withCheckedContinuation({ (continuation: CheckedContinuation) in 92 | finishedContinuation = continuation 93 | }) 94 | } 95 | } 96 | 97 | func start() async { 98 | guard !started else { return } 99 | started = true 100 | await runTimer() 101 | } 102 | 103 | private func yield() { 104 | if let element = savedElement { 105 | continuation.yield(element) 106 | savedElement = nil 107 | } 108 | 109 | // If finished has been triggered and there are no more items in the queue, finish now 110 | if let finishedContinuation = finishedContinuation { 111 | continuation.finish() 112 | finishedContinuation.resume() 113 | self.finishedContinuation = nil 114 | } 115 | } 116 | 117 | private func runTimer() async { 118 | guard finishedContinuation == nil else { return } 119 | readyToSendFirst = true 120 | await scheduler.schedule(after: interval) { [weak self] in 121 | await self?.closureFromTimer() 122 | } 123 | } 124 | 125 | private func closureFromTimer() async { 126 | if latest { 127 | yield() 128 | } 129 | await runTimer() 130 | } 131 | } 132 | 133 | @usableFromInline 134 | struct Throttle { 135 | private var baseIterator: Base.AsyncIterator 136 | private let actor: ThrottleActor 137 | 138 | @usableFromInline 139 | init( 140 | baseIterator: Base.AsyncIterator, 141 | continuation: AsyncStream.Continuation, 142 | interval: TimeInterval, 143 | scheduler: AsyncScheduler, 144 | latest: Bool 145 | ) { 146 | self.baseIterator = baseIterator 147 | self.actor = ThrottleActor( 148 | continuation: continuation, 149 | interval: interval, 150 | scheduler: scheduler, 151 | latest: latest 152 | ) 153 | } 154 | 155 | @usableFromInline 156 | mutating func start() async { 157 | while let element = try? await baseIterator.next() { 158 | await actor.putNext(element) 159 | } 160 | await actor.finish() 161 | } 162 | } 163 | 164 | @inlinable 165 | public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { 166 | return AsyncStream { (continuation: AsyncStream.Continuation) in 167 | Task { 168 | var throttle = Throttle( 169 | baseIterator: base.makeAsyncIterator(), 170 | continuation: continuation, 171 | interval: interval, 172 | scheduler: scheduler, 173 | latest: latest 174 | ) 175 | await throttle.start() 176 | } 177 | }.makeAsyncIterator() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequences/Timeout/AsyncTimeoutSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncTimeoutSequence.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 22/11/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | public struct AsyncTimeoutSequence { 12 | @usableFromInline 13 | let base: Base 14 | 15 | @usableFromInline 16 | let interval: TimeInterval 17 | 18 | @usableFromInline 19 | let scheduler: AsyncScheduler 20 | 21 | @usableFromInline 22 | init(_ base: Base, interval: TimeInterval, scheduler: AsyncScheduler) { 23 | self.base = base 24 | self.interval = interval 25 | self.scheduler = scheduler 26 | } 27 | } 28 | 29 | extension AsyncSequence { 30 | @inlinable 31 | public __consuming func timeout( 32 | for interval: TimeInterval, 33 | scheduler: AsyncScheduler 34 | ) -> AsyncTimeoutSequence { 35 | return AsyncTimeoutSequence(self, interval: interval, scheduler: scheduler) 36 | } 37 | } 38 | 39 | public enum AsyncTimeSequenceError: Error { 40 | case timeout 41 | } 42 | 43 | // TODO: handle continuation.finish being called multiple times 44 | extension AsyncTimeoutSequence: AsyncSequence { 45 | 46 | public typealias Element = Base.Element 47 | /// The type of iterator that produces elements of the sequence. 48 | public typealias AsyncIterator = AsyncThrowingStream.Iterator 49 | 50 | actor TimeoutActor { 51 | let continuation: AsyncThrowingStream.Continuation 52 | let interval: TimeInterval 53 | let scheduler: AsyncScheduler 54 | 55 | var counter: UInt = .zero 56 | 57 | init( 58 | continuation: AsyncThrowingStream.Continuation, 59 | interval: TimeInterval, 60 | scheduler: AsyncScheduler 61 | ) { 62 | self.continuation = continuation 63 | self.interval = interval 64 | self.scheduler = scheduler 65 | } 66 | 67 | func start() async { 68 | await startTimeout() 69 | } 70 | 71 | func putNext(_ element: Base.Element) async { 72 | yield(element) 73 | await startTimeout() 74 | } 75 | 76 | func finish() { 77 | continuation.finish(throwing: nil) 78 | } 79 | 80 | private func yield(_ element: Base.Element) { 81 | continuation.yield(element) 82 | } 83 | 84 | private func yield(error: Error, savedCounter: UInt) { 85 | guard counter == savedCounter else { return } 86 | continuation.finish(throwing: error) 87 | } 88 | 89 | private func startTimeout() async { 90 | let localCounter = updateCounter() 91 | await scheduler.schedule( 92 | after: interval, 93 | handler: { [weak self] in 94 | await self?.yield(error: AsyncTimeSequenceError.timeout, savedCounter: localCounter) 95 | }) 96 | } 97 | 98 | private func updateCounter() -> UInt { 99 | counter += 1 100 | if counter == .max { 101 | counter = .zero 102 | } 103 | return counter 104 | } 105 | } 106 | 107 | @usableFromInline 108 | struct Timeout { 109 | private var baseIterator: Base.AsyncIterator 110 | private let actor: TimeoutActor 111 | 112 | @usableFromInline 113 | init( 114 | baseIterator: Base.AsyncIterator, 115 | continuation: AsyncThrowingStream.Continuation, 116 | interval: TimeInterval, 117 | scheduler: AsyncScheduler 118 | ) { 119 | self.baseIterator = baseIterator 120 | self.actor = TimeoutActor( 121 | continuation: continuation, 122 | interval: interval, 123 | scheduler: scheduler 124 | ) 125 | } 126 | 127 | @usableFromInline 128 | mutating func start() async { 129 | await actor.start() 130 | while let element = try? await baseIterator.next() { 131 | await actor.putNext(element) 132 | } 133 | await actor.finish() 134 | } 135 | } 136 | 137 | @inlinable 138 | public __consuming func makeAsyncIterator() -> AsyncThrowingStream.Iterator { 139 | return AsyncThrowingStream { 140 | (continuation: AsyncThrowingStream.Continuation) in 141 | Task { 142 | var timeout = Timeout( 143 | baseIterator: base.makeAsyncIterator(), 144 | continuation: continuation, 145 | interval: interval, 146 | scheduler: scheduler 147 | ) 148 | await timeout.start() 149 | } 150 | }.makeAsyncIterator() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequencesSupport/ControlledDataSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControlledDataSequence.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This is a really convenient sequence designed to ease the testing of async sequences. 11 | /// It provides access to the ControlledDataIterator, which is critical for testing. 12 | public struct ControlledDataSequence: AsyncSequence { 13 | public typealias Element = T 14 | 15 | public let iterator: ControlledDataIterator 16 | 17 | public init(items: [T]) { 18 | self.iterator = ControlledDataIterator(items: items) 19 | } 20 | 21 | public func makeAsyncIterator() -> ControlledDataIterator { 22 | iterator 23 | } 24 | } 25 | 26 | /// This class extends AsyncIteratorProtocol in order to provide an object that returns 27 | /// elements on next(). The critical function in this class is waitForItemsToBeSent(count), 28 | /// which allows the owner of this iterator to wait until n elements have been dispatched via next(). 29 | public final class ControlledDataIterator: AsyncIteratorProtocol { 30 | private let dataActor: ControlledDataActor 31 | 32 | init(items: [T]) { 33 | self.dataActor = ControlledDataActor(items: items) 34 | } 35 | 36 | public func next() async throws -> T? { 37 | return await dataActor.next() 38 | } 39 | 40 | public func waitForItemsToBeSent(_ count: Int) async { 41 | await dataActor.waitForItemsToBeSent(count) 42 | } 43 | } 44 | 45 | actor ControlledDataActor { 46 | let items: [T] 47 | var allowedItemsToBeSentCount = Int.zero 48 | var index = Int.zero 49 | var savedNextContinuation: CheckedContinuation? 50 | var waitContinuation: CheckedContinuation? 51 | 52 | init( 53 | items: [T] 54 | ) { 55 | self.items = items 56 | } 57 | 58 | /// This function returns the next value in the given initializer items 59 | /// If there are no allowed items to be sent, return a checked continuation that needs to 60 | /// be resumed by an awaitForItemsToBeSent. 61 | func next() async -> T? { 62 | if allowedItemsToBeSentCount > .zero { 63 | return nextElement() 64 | } else if let waitContinuation = waitContinuation { 65 | waitContinuation.resume(returning: ()) 66 | self.waitContinuation = nil 67 | } 68 | return await withCheckedContinuation({ (continuation: CheckedContinuation) in 69 | savedNextContinuation = continuation 70 | }) 71 | } 72 | 73 | /// Wait for n items to be sent 74 | /// This function sets up a continuation to be resumed upon the count reaching zero, 75 | /// meaning that all the requested items have been returned via next(). 76 | /// If a call to next() has already been made at this point, unwrap its continuation 77 | /// to return the next valid item. 78 | func waitForItemsToBeSent(_ count: Int) async { 79 | await withCheckedContinuation({ (continuation: CheckedContinuation) in 80 | allowedItemsToBeSentCount = count 81 | waitContinuation = continuation 82 | 83 | if let savedNextContinuation = savedNextContinuation { 84 | savedNextContinuation.resume(returning: nextElement()) 85 | self.savedNextContinuation = nil 86 | } 87 | }) 88 | } 89 | 90 | /// Return next element if any 91 | /// This function should only be called if the allowedItemsToBeSentCount value is 92 | /// greater than zero. 93 | private func nextElement() -> T? { 94 | defer { 95 | index += 1 96 | allowedItemsToBeSentCount -= 1 97 | } 98 | let element = items.element(at: index) 99 | return element 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequencesSupport/DataStructures/Dequeue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dequeue.swift 3 | // AsyncTimeSequencesSupport 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 13/1/22. 6 | // 7 | 8 | import Foundation 9 | 10 | final class Dequeue { 11 | 12 | var head: DoublyLinkedList? 13 | var tail: DoublyLinkedList? 14 | var _count = 0 15 | 16 | public init() { 17 | head = DoublyLinkedList() 18 | tail = DoublyLinkedList() 19 | 20 | head?.next = tail 21 | tail?.previous = head 22 | } 23 | 24 | public func enqueue(_ value: T) { 25 | _count += 1 26 | add(value) 27 | } 28 | 29 | @discardableResult 30 | public func dequeue() -> T? { 31 | guard count > 0, let first = head?.next else { return nil } 32 | _count -= 1 33 | disconnect(first) 34 | return first.value 35 | } 36 | 37 | public func peek() -> T? { 38 | guard let first = head?.next else { return nil } 39 | return first.value 40 | } 41 | 42 | private func add(_ value: T) { 43 | let list = DoublyLinkedList() 44 | list.value = value 45 | 46 | tail?.previous?.next = list 47 | list.previous = tail?.previous 48 | list.next = tail 49 | tail?.previous = list 50 | } 51 | 52 | private func disconnect(_ list: DoublyLinkedList) { 53 | list.previous?.next = list.next 54 | list.next?.previous = list.previous 55 | } 56 | } 57 | 58 | extension Dequeue { 59 | public var count: Int { _count } 60 | public var isEmpty: Bool { _count == .zero } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequencesSupport/DataStructures/DoublyLinkedList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoublyLinkedList.swift 3 | // AsyncTimeSequencesSupport 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 13/1/22. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DoublyLinkedList { 11 | weak var previous: DoublyLinkedList? 12 | var next: DoublyLinkedList? 13 | var value: T! 14 | 15 | init( 16 | value: T? = nil, 17 | previous: DoublyLinkedList? = nil, 18 | next: DoublyLinkedList? = nil 19 | ) { 20 | self.value = value 21 | self.previous = previous 22 | self.next = next 23 | } 24 | } 25 | 26 | // Useful extension for debugging 27 | extension DoublyLinkedList { 28 | func toArray(until item: DoublyLinkedList?) -> [T] { 29 | var array = [T]() 30 | var next: DoublyLinkedList? = self.next 31 | while next !== item { 32 | guard let value = next?.value else { break } 33 | array.append(value) 34 | next = next?.next 35 | } 36 | return array 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequencesSupport/Extensions/Array+ElementAtIndex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+ElementAtIndex.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | func element(at index: Int) -> Element? { 12 | guard !isEmpty, index >= 0, index < count else { return nil } 13 | return self[index] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AsyncTimeSequencesSupport/TestAsyncScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAsyncScheduler.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 5/1/22. 6 | // 7 | 8 | import AsyncTimeSequences 9 | import Foundation 10 | 11 | /// This class conforms to AsyncScheduler and provides convenient functions to easily test async time sequences. 12 | public actor TestAsyncScheduler: AsyncScheduler { 13 | 14 | struct QueueItem { 15 | let interval: TimeInterval 16 | let handler: AsyncSchedulerHandler 17 | } 18 | 19 | private let queue = Dequeue() 20 | private var savedContinuation: CheckedContinuation? 21 | private var savedContinuationCount: Int = .zero 22 | 23 | public var now: TimeInterval = Date().timeIntervalSince1970 24 | 25 | public init() {} 26 | 27 | /// Schedule an async handler to be executed after a specific interval. 28 | /// All the async handler will be enqueued until advance() is called for processing. 29 | /// This function will also check if n jobs have been scheduled and try to resume a 30 | /// saved continuation set during waitForScheduledJobs(). 31 | public func schedule( 32 | after interval: TimeInterval, 33 | handler: @escaping AsyncSchedulerHandler 34 | ) -> Task { 35 | let item = QueueItem(interval: interval + now, handler: handler) 36 | queue.enqueue(item) 37 | 38 | checkForSavedContinuation() 39 | 40 | // TODO: decide whether or not to support cancellation with this Task 41 | return Task {} 42 | } 43 | 44 | /// Advances local time interval and executes all enqueued async handlers that are 45 | /// contained within the advanced interval. 46 | public func advance(by interval: TimeInterval) async { 47 | let threshold = now + interval 48 | now = threshold 49 | 50 | while let peekedItem = queue.peek(), peekedItem.interval <= now, 51 | let item = queue.dequeue() 52 | { 53 | await item.handler() 54 | } 55 | } 56 | 57 | /// This function waits until n jobs are scheduled via the schedule() function. 58 | /// If there are already n jobs scheduled, this function will return immediately. 59 | public func waitForScheduledJobs(count: Int) async { 60 | await withCheckedContinuation({ (continuation: CheckedContinuation) in 61 | if queue.count >= count { 62 | continuation.resume() 63 | return 64 | } 65 | savedContinuation = continuation 66 | }) 67 | } 68 | 69 | private func checkForSavedContinuation() { 70 | guard let savedContinuation = savedContinuation, 71 | queue.count >= savedContinuationCount 72 | else { return } 73 | savedContinuationCount = .zero 74 | savedContinuation.resume() 75 | self.savedContinuation = nil 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SwiftFormatConfiguration.json: -------------------------------------------------------------------------------- 1 | { 2 | "blankLineBetweenMembers" : { 3 | "ignoreSingleLineProperties" : true 4 | }, 5 | "indentation" : { 6 | "spaces" : 2 7 | }, 8 | "indentConditionalCompilationBlocks" : true, 9 | "lineBreakBeforeControlFlowKeywords" : false, 10 | "lineBreakBeforeEachArgument" : false, 11 | "lineLength" : 100, 12 | "maximumBlankLines" : 1, 13 | "respectsExistingLineBreaks" : true, 14 | "rules" : { 15 | "AllPublicDeclarationsHaveDocumentation" : true, 16 | "AlwaysUseLowerCamelCase" : true, 17 | "AmbiguousTrailingClosureOverload" : true, 18 | "BeginDocumentationCommentWithOneLineSummary" : true, 19 | "BlankLineBetweenMembers" : true, 20 | "CaseIndentLevelEqualsSwitch" : true, 21 | "DoNotUseSemicolons" : true, 22 | "DontRepeatTypeInStaticProperties" : true, 23 | "FullyIndirectEnum" : true, 24 | "GroupNumericLiterals" : true, 25 | "IdentifiersMustBeASCII" : true, 26 | "MultiLineTrailingCommas" : true, 27 | "NeverForceUnwrap" : true, 28 | "NeverUseForceTry" : true, 29 | "NeverUseImplicitlyUnwrappedOptionals" : true, 30 | "NoAccessLevelOnExtensionDeclaration" : true, 31 | "NoBlockComments" : true, 32 | "NoCasesWithOnlyFallthrough" : true, 33 | "NoEmptyTrailingClosureParentheses" : true, 34 | "NoLabelsInCasePatterns" : true, 35 | "NoLeadingUnderscores" : true, 36 | "NoParensAroundConditions" : true, 37 | "NoVoidReturnOnFunctionSignature" : true, 38 | "OneCasePerLine" : true, 39 | "OneVariableDeclarationPerLine" : true, 40 | "OnlyOneTrailingClosureArgument" : true, 41 | "OrderedImports" : true, 42 | "ReturnVoidInsteadOfEmptyTuple" : true, 43 | "UseEnumForNamespacing" : true, 44 | "UseLetInEveryBoundCaseVariable" : true, 45 | "UseShorthandTypeNames" : true, 46 | "UseSingleLinePropertyGetter" : true, 47 | "UseSynthesizedInitializer" : true, 48 | "UseTripleSlashForDocumentationComments" : true, 49 | "ValidateDocumentationComments" : true 50 | }, 51 | "tabWidth" : 8, 52 | "version" : 1 53 | } 54 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/AsyncDebounceSequence+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncDebounceSequence+Tests.swift 3 | // AsyncTimeSequencesTests 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 13/12/21. 6 | // 7 | 8 | import AsyncTimeSequencesSupport 9 | import Foundation 10 | import XCTest 11 | 12 | @testable import AsyncTimeSequences 13 | 14 | final class AsyncDebounceSequence_Tests: XCTestCase { 15 | 16 | func testAsyncDebounceSequence() async { 17 | // Given 18 | let scheduler = TestAsyncScheduler() 19 | let items = [1, 5, 10, 15, 20] 20 | let expectedItems = [20] 21 | let baseDelay = 5.0 22 | var receivedItems = [Int]() 23 | 24 | // When 25 | let sequence = ControlledDataSequence(items: items) 26 | var iterator = 27 | sequence 28 | .debounce(for: baseDelay, scheduler: scheduler) 29 | .makeAsyncIterator() 30 | 31 | // If we don't wait for jobs to get scheduled, advancing the scheduler does virtually nothing... 32 | await sequence.iterator.waitForItemsToBeSent(items.count) 33 | await scheduler.advance(by: baseDelay) 34 | 35 | while receivedItems.count < expectedItems.count, let value = await iterator.next() { 36 | receivedItems.append(value) 37 | } 38 | 39 | // Then 40 | XCTAssertEqual(receivedItems, expectedItems) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/AsyncDelaySequence+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncDelaySequence+Tests.swift 3 | // AsyncTimeSequencesTests 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/11/21. 6 | // 7 | 8 | import AsyncTimeSequencesSupport 9 | import XCTest 10 | 11 | @testable import AsyncTimeSequences 12 | 13 | final class AsyncDelaySequence_Tests: XCTestCase { 14 | 15 | func testAsyncDelaySequence() async { 16 | // Given 17 | let scheduler = TestAsyncScheduler() 18 | let items = [1, 5, 10, 15, 20] 19 | let expectedItems = [1, 5, 10, 15, 20] 20 | let baseDelay = 5.0 21 | var receivedItems = [Int]() 22 | 23 | // When 24 | let sequence = ControlledDataSequence(items: items) 25 | var iterator = 26 | sequence 27 | .delay( 28 | for: baseDelay, 29 | scheduler: scheduler 30 | ).makeAsyncIterator() 31 | 32 | // If we don't wait for jobs to get scheduled, advancing the scheduler does virtually nothing... 33 | await sequence.iterator.waitForItemsToBeSent(items.count) 34 | await scheduler.advance(by: baseDelay) 35 | 36 | while receivedItems.count < expectedItems.count, let value = await iterator.next() { 37 | receivedItems.append(value) 38 | } 39 | 40 | // Then 41 | XCTAssertEqual(receivedItems, expectedItems) 42 | } 43 | 44 | func testAsyncDelaySequenceWithPartialTimeAdvances() async { 45 | // Given 46 | let scheduler = TestAsyncScheduler() 47 | let items = [1, 5, 10, 15, 20] 48 | let expectedFirstItems = [1, 5, 10] 49 | let expectedSecondItems = [15, 20] 50 | let baseDelay = 5.0 51 | var receivedFirstItems = [Int]() 52 | var receivedSecondItems = [Int]() 53 | 54 | // When 55 | let sequence = ControlledDataSequence(items: items) 56 | var iterator = 57 | sequence 58 | .delay( 59 | for: baseDelay, 60 | scheduler: scheduler 61 | ).makeAsyncIterator() 62 | 63 | // If we don't wait for jobs to get scheduled, advancing the scheduler does virtually nothing... 64 | await sequence.iterator.waitForItemsToBeSent(expectedFirstItems.count) 65 | await scheduler.advance(by: baseDelay) 66 | 67 | while receivedFirstItems.count < expectedFirstItems.count, let value = await iterator.next() { 68 | receivedFirstItems.append(value) 69 | } 70 | 71 | await sequence.iterator.waitForItemsToBeSent(expectedSecondItems.count) 72 | await scheduler.advance(by: baseDelay) 73 | 74 | while receivedSecondItems.count < expectedSecondItems.count, let value = await iterator.next() { 75 | receivedSecondItems.append(value) 76 | } 77 | 78 | // Then 79 | XCTAssertEqual(receivedFirstItems, expectedFirstItems) 80 | XCTAssertEqual(receivedSecondItems, expectedSecondItems) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/AsyncMeasureIntervalSequence+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncMeasureIntervalSequence+Tests.swift 3 | // AsyncTimeSequencesTests 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 27/12/21. 6 | // 7 | 8 | import AsyncTimeSequencesSupport 9 | import Foundation 10 | import XCTest 11 | 12 | @testable import AsyncTimeSequences 13 | 14 | final class AsyncMeasureIntervalSequence_Tests: XCTestCase { 15 | 16 | func testAsyncMeasureIntervalSequence() async { 17 | // Given 18 | let scheduler = TestAsyncScheduler() 19 | let items = [1, 5, 10, 15, 20] 20 | let expectedItems: [TimeInterval] = [ 21 | 3, 22 | 8, 23 | 12, 24 | 1000, 25 | ] 26 | var receivedItems = [TimeInterval]() 27 | 28 | // When 29 | let sequence = ControlledDataSequence( 30 | items: items 31 | ) 32 | var iterator = 33 | sequence 34 | .measureInterval(using: scheduler) 35 | .makeAsyncIterator() 36 | 37 | await sequence.iterator.waitForItemsToBeSent(1) 38 | 39 | for item in expectedItems { 40 | await scheduler.advance(by: item) 41 | await sequence.iterator.waitForItemsToBeSent(1) 42 | } 43 | 44 | while receivedItems.count < expectedItems.count, let value = await iterator.next() { 45 | receivedItems.append(value) 46 | } 47 | 48 | // Then 49 | XCTAssertEqual(receivedItems, expectedItems) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/AsyncSchedulerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSchedulerTests.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 12/2/22. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import AsyncTimeSequences 11 | 12 | final class AsyncSchedulerTests: XCTestCase { 13 | 14 | func testMainAsyncSchedulerSortsScheduledClosures() async { 15 | // Given 16 | let scheduler = MainAsyncScheduler() 17 | 18 | // When 19 | let (expectedResult, result) = await subTest(with: scheduler) 20 | let isQueueEmpty = await scheduler.isQueueEmpty() 21 | let allItemsCompleted = await scheduler.areAllScheduledItemsCompleted() 22 | 23 | // Then 24 | XCTAssertEqual(expectedResult, result) 25 | XCTAssertTrue(isQueueEmpty) 26 | XCTAssertTrue(allItemsCompleted) 27 | } 28 | 29 | /// NOTE: This is a slow test, which depends on time waiting in the order of milliseconds 30 | func testMainAsyncSchedulerReturnsACancellableTask() async { 31 | // Given 32 | let scheduler = MainAsyncScheduler() 33 | let firstExpectation = XCTestExpectation(description: "First Expectation") 34 | let secondExpectation = XCTestExpectation(description: "Second Expectation") 35 | var setCounter = Set() 36 | 37 | // When 38 | // schedule after 100us 39 | await scheduler.schedule(after: 0.0001) { 40 | setCounter.insert(1) 41 | firstExpectation.fulfill() 42 | } 43 | // schedule after 15 ms 44 | let cancellableTask = await scheduler.schedule(after: 0.030) { 45 | setCounter.insert(2) 46 | XCTFail("This should be triggered once cancelled") 47 | } 48 | // Without cancelling the task, it would execute and fail 49 | cancellableTask.cancel() 50 | // schedule after 100us 51 | await scheduler.schedule(after: 0.0001) { 52 | setCounter.insert(3) 53 | secondExpectation.fulfill() 54 | } 55 | 56 | await fulfillment(of: [firstExpectation, secondExpectation], timeout: 1.0) 57 | 58 | // The cancelled task is scheduled after 30 ms, hence waiting for 50ms 59 | try? await Task.sleep(milliseconds: 50) 60 | 61 | let isQueueEmpty = await scheduler.isQueueEmpty() 62 | let allItemsCompleted = await scheduler.areAllScheduledItemsCompleted() 63 | 64 | // Then 65 | XCTAssertFalse(setCounter.contains(2)) 66 | XCTAssertEqual(setCounter, Set([1, 3])) 67 | XCTAssertTrue(isQueueEmpty) 68 | XCTAssertTrue(allItemsCompleted) 69 | } 70 | 71 | // Uncomment to see the flaky behavior of a scheduler without time-based ordering 72 | // func testFakeAsyncScheduler() async { 73 | // // Given 74 | // let scheduler = FlakyAsyncScheduler() 75 | // 76 | // // When 77 | // let (expectedResult, result) = await subTest(with: scheduler) 78 | // 79 | // // Then 80 | // XCTAssertEqual(expectedResult, result) 81 | // } 82 | // 83 | // actor FlakyAsyncScheduler: AsyncScheduler { 84 | // var now: TimeInterval { 85 | // Date().timeIntervalSince1970 86 | // } 87 | // func schedule(after: TimeInterval, handler: @escaping AsyncSchedulerHandler) { 88 | // Task { 89 | // try? await Task.sleep(nanoseconds: UInt64(after * 1000000000)) 90 | // await handler() 91 | // } 92 | // } 93 | // } 94 | 95 | /// This test aims to identify the case when many elements are scheduled almost immediately 96 | /// and the time interval is in the ordered of microseconds. Given the Task behavior plus 97 | /// the use of Task.sleep() will result in a random execution order of scheduled closures 98 | /// if not properly handled. 99 | private func subTest(with scheduler: AsyncScheduler) async -> ([Int], [Int]) { 100 | // Given 101 | let timeInterval: TimeInterval = 0.0001 // 100us 102 | let safeActorArray = SafeActorArrayWrapper() 103 | var expectedResult = [Int]() 104 | let maxElements = 100 105 | 106 | // When 107 | for index in 0.. Bool { queue.isEmpty } 128 | func areAllScheduledItemsCompleted() async -> Bool { completedElementIds.isEmpty } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/AsyncThrottleSequence+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncThrottleSequence+Tests.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 14/12/21. 6 | // 7 | 8 | import AsyncTimeSequencesSupport 9 | import XCTest 10 | 11 | @testable import AsyncTimeSequences 12 | 13 | final class AsyncThrottleSequenceTests: XCTestCase { 14 | 15 | func testAsyncThrottleSequenceWithLatest() async { 16 | // Given 17 | let scheduler = TestAsyncScheduler() 18 | let items = [1, 5, 10, 15, 20] 19 | let expectedItems = [20] 20 | let baseDelay = 5.0 21 | var receivedItems = [Int]() 22 | let sequence = ControlledDataSequence( 23 | items: items 24 | ) 25 | var iterator = 26 | sequence 27 | .throttle( 28 | for: baseDelay, 29 | scheduler: scheduler, 30 | latest: true 31 | ).makeAsyncIterator() 32 | 33 | // When 34 | await sequence.iterator.waitForItemsToBeSent(5) // Wait for all items to be dispatched 35 | await scheduler.advance(by: baseDelay) // Wait for all scheduled to be executed 36 | 37 | while receivedItems.count < expectedItems.count, let value = await iterator.next() { 38 | receivedItems.append(value) 39 | } 40 | 41 | // Then 42 | XCTAssertEqual(receivedItems, expectedItems) 43 | } 44 | 45 | func testAsyncThrottleSequenceWithLatestWithTwoTimeAdvances() async { 46 | // Given 47 | let scheduler = TestAsyncScheduler() 48 | let items = [1, 5, 10, 15, 20] 49 | let expectedItems = [10, 20] 50 | let baseDelay = 5.0 51 | var receivedItems = [Int]() 52 | let sequence = ControlledDataSequence( 53 | items: items 54 | ) 55 | var iterator = 56 | sequence 57 | .throttle( 58 | for: baseDelay, 59 | scheduler: scheduler, 60 | latest: true 61 | ).makeAsyncIterator() 62 | 63 | // When 64 | await sequence.iterator.waitForItemsToBeSent(3) // Wait for 3 items to be dispatched 65 | await scheduler.advance(by: baseDelay) // Wait for all scheduled to be executed 66 | 67 | await sequence.iterator.waitForItemsToBeSent(2) 68 | await scheduler.advance(by: baseDelay) // Wait for all scheduled to be executed 69 | 70 | while receivedItems.count < expectedItems.count, let value = await iterator.next() { 71 | receivedItems.append(value) 72 | } 73 | 74 | // Then 75 | XCTAssertEqual(receivedItems, expectedItems) 76 | } 77 | 78 | func testAsyncThrottleSequenceWithoutLatestWithTwoTimeAdvances() async { 79 | // Given 80 | let scheduler = TestAsyncScheduler() 81 | let items = [1, 5, 10, 15, 20] 82 | let expectedItems = [1, 15] 83 | let baseDelay = 5.0 84 | var receivedItems = [Int]() 85 | let sequence = ControlledDataSequence( 86 | items: items 87 | ) 88 | var iterator = 89 | sequence 90 | .throttle( 91 | for: baseDelay, 92 | scheduler: scheduler, 93 | latest: false 94 | ).makeAsyncIterator() 95 | 96 | // When 97 | await sequence.iterator.waitForItemsToBeSent(3) 98 | await scheduler.advance(by: baseDelay) // Wait for all scheduled to be executed 99 | 100 | await sequence.iterator.waitForItemsToBeSent(2) 101 | await scheduler.advance(by: baseDelay) // Wait for all scheduled to be executed 102 | 103 | while receivedItems.count < expectedItems.count, let value = await iterator.next() { 104 | receivedItems.append(value) 105 | } 106 | 107 | // Then 108 | XCTAssertEqual(receivedItems, expectedItems) 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/AsyncTimeoutSequence+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncDelaySequence+Tests.swift 3 | // AsyncTimeSequencesTests 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 22/11/21. 6 | // 7 | 8 | import AsyncTimeSequencesSupport 9 | import XCTest 10 | 11 | @testable import AsyncTimeSequences 12 | 13 | final class AsyncDelaySequenceTests: XCTestCase { 14 | 15 | func testAsyncTimeoutSequenceThrowsErrorOnTimeout() async { 16 | // Given 17 | let scheduler = TestAsyncScheduler() 18 | let items = [1, 5, 10, 15, 20] 19 | let baseDelay = 5.0 20 | var expectedItems = [Int]() 21 | 22 | // When 23 | let sequence = ControlledDataSequence(items: items) 24 | var iterator = 25 | sequence 26 | .timeout(for: baseDelay, scheduler: scheduler) 27 | .makeAsyncIterator() 28 | 29 | // If we don't wait for jobs to get scheduled, advancing the scheduler does virtually nothing... 30 | await sequence.iterator.waitForItemsToBeSent(items.count) 31 | await scheduler.advance(by: baseDelay) 32 | 33 | do { 34 | // It will throw an error as the sequence finished sending items but it will never send a nil. Thus, triggering a timeout 35 | while let value = try await iterator.next() { 36 | expectedItems.append(value) 37 | } 38 | XCTFail("Expected an Error") 39 | } catch (let error) { 40 | guard let timeoutError = error as? AsyncTimeSequenceError, 41 | case .timeout = timeoutError 42 | else { 43 | XCTFail("Expected Timeout Error") 44 | return 45 | } 46 | } 47 | } 48 | 49 | func testAsyncTimeoutSequenceDoesNotThrowErrorIfElementsDontWaitNTime() async { 50 | // Given 51 | let scheduler = TestAsyncScheduler() 52 | let items = [1, 5, 10, 15, 20] 53 | let baseDelay = 5.0 54 | var expectedItems = [Int]() 55 | 56 | // When 57 | var iterator = SampleDataSequence(items: items) 58 | .timeout(for: baseDelay, scheduler: scheduler) 59 | .makeAsyncIterator() 60 | 61 | await scheduler.waitForScheduledJobs(count: 1) // Make sure that the timeout is scheduled 62 | 63 | do { 64 | while let value = try await iterator.next() { 65 | expectedItems.append(value) 66 | } 67 | } catch { 68 | XCTFail("An error was not expected") 69 | } 70 | 71 | // Then 72 | XCTAssertEqual(expectedItems, [1, 5, 10, 15, 20]) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/MockSequences/InfiniteDataSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteDataSequence.swift 3 | // AsyncTimeSequencesTests 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 13/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct InfiniteDataSequence: AsyncSequence { 11 | typealias Element = T 12 | 13 | var items: [T] 14 | var delay: UInt64 15 | var iterator: InfiniteDataIterator 16 | 17 | init( 18 | items: [T], 19 | delay: UInt64 20 | ) { 21 | self.items = items 22 | self.delay = delay 23 | self.iterator = InfiniteDataIterator(items: items, delay: delay) 24 | } 25 | 26 | mutating func stop() { 27 | iterator.stop() 28 | } 29 | 30 | func makeAsyncIterator() -> InfiniteDataIterator { 31 | return iterator 32 | } 33 | } 34 | 35 | struct InfiniteDataIterator: AsyncIteratorProtocol { 36 | var items: [T] 37 | var delay: UInt64 38 | var index = 0 39 | var shouldStop = false 40 | 41 | mutating func stop() { 42 | shouldStop = true 43 | } 44 | 45 | mutating func next() async throws -> T? { 46 | if shouldStop { 47 | return nil 48 | } 49 | try? await Task.sleep(seconds: 5) 50 | 51 | if index >= items.count { 52 | index = 0 53 | } 54 | 55 | let item = items[index] 56 | 57 | index += 1 58 | 59 | return item 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/MockSequences/SampleDataSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleDataSequence.swift 3 | // AsyncTimeSequencesTests 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 13/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SampleDataSequence: AsyncSequence { 11 | typealias Element = T 12 | 13 | var items: [T] 14 | 15 | func makeAsyncIterator() -> SampleDataIterator { 16 | SampleDataIterator(items: items) 17 | } 18 | } 19 | 20 | struct SampleDataIterator: AsyncIteratorProtocol { 21 | var items: [T] 22 | fileprivate var index = 0 23 | 24 | mutating func next() async throws -> T? { 25 | guard index < items.count else { 26 | return nil 27 | } 28 | 29 | let item = items[index] 30 | index += 1 31 | 32 | return item 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/AsyncTimeSequencesTests/Support/SafeActorArrayWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeActorArrayWrapper.swift 3 | // AsyncTimeSequences 4 | // 5 | // Created by Henry Javier Serrano Echeverria on 12/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | actor SafeActorArrayWrapper { 11 | private var _elements = [T]() 12 | private var savedCount = 0 13 | private var savedContinuation: CheckedContinuation? 14 | 15 | var elements: [T] { _elements } 16 | 17 | init() {} 18 | 19 | func append(_ element: T) { 20 | _elements.append(element) 21 | guard _elements.count >= savedCount, let continuation = savedContinuation else { return } 22 | continuation.resume() 23 | savedContinuation = nil 24 | } 25 | 26 | /// This function will wait for `count` elements to be appended to the inner array until 27 | /// it returns. An internal continuation helps it resume when this condition is fulfilled 28 | func waitForElements(_ count: Int) async { 29 | guard _elements.count < count else { return } 30 | savedCount = count 31 | await withCheckedContinuation { (continuation: CheckedContinuation) in 32 | savedContinuation = continuation 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------