├── .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 |
--------------------------------------------------------------------------------