Add multiple events to the Calendar with Concurrency - swift

I created a function to add my course events to the calendar app using EventKit.
After learning the swift concurrency, I want to update my code to make the progress much faster, namely using the detached task or TaskGroup to add these events.
Synchronize code without detached task or task group:
func export_test() {
Task.detached {
for i in 0...15 {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
do {
try eventStore.save(courseEvent, span: .futureEvents)
} catch { print(error.localizedDescription) }
print("Task \(i): Finished")
}
}
}
Doing the same thing using the TaskGroup :
func export_test() {
Task.detached {
await withTaskGroup(of: Void.self) { group in
for i in 0...15 {
group.addTask {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
do {
try eventStore.save(courseEvent, span: .futureEvents)
} catch { print(error.localizedDescription) }
print("Task \(i): Finished")
}
}
}
}
}
The output of the TaskGroup version:
Task 0: Start
Task 1: Start
Task 2: Start
Task 4: Start
Task 3: Start
Task 5: Start
Task 6: Start
Task 7: Start
Task 0: Finished
Task 8: Start
Task 1: Finished
Task 9: Start
Sometimes, only a few tasks will been done, and others will not, or even never been started (I created 16 tasks but only printed 9 in this example). Sometimes, all of these events can be added.
In my point of view, I have created 16 child tasks in the TaskGroup.
Each child task will add one event to the Calendar. I think in this way, I can take the full advantage of the multi-core performance (maybe it's actually not. 🙃)
If I put the for-loop inside the group.addTask closure, it will always have the expected result, but in this way, we only have a single loop so the TaskGroup may no longer needed.
I'm really exhausted🙃🙃.
snapshot:
Snapshot

To see all status messages that the tasks have finished you have to await each result and print it there
func export_test() {
Task.detached {
await withTaskGroup(of: String.self) { group in
for i in 0...15 {
group.addTask {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
do {
try eventStore.save(courseEvent, span: .futureEvents)
} catch { print(error.localizedDescription) }
return "Task \(i): Finished"
}
}
for await taskStatus in group {
print(taskStatus)
}
}
}
}

After a long time researching every aspect of the code. I found that the problem is try eventStore.save(courseEvent, span: .futureEvents).
Alternatively, I use try eventStore.save(..., span: ..., commit: false) and eventStore.commit() to solve the problem.
I think this is caused by the data races because I'm using swift concurrency. While one event is saving, another one calls save method to save again, leading to data conflicts (just my guess.)
To solve this, luckily, we can commit a batch of events later using eventStore.commit() to avoid data conflicts. The result is what I expected !!đŸ„ł
And after that optimization, the performance of this function is up to 25% faster (exactly 136ms faster). (haha. Perfect.)
Final Code (in Swift 5.7):
func export_test() {
Task.detached {
await withTaskGroup(of: Void.self) { group in
for i in 0...15 {
group.addTask {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
try eventStore.save(courseEvent, span: .futureEvents, commit: false)
}
}
}
eventStore.commit()
}
}

Related

Swift's "async let" error thrown depends on the order the tasks are made

I'm trying to understand async let error handling and it's not making a lot of sense in my head. It seems that if I have two parallel requests, the first one throwing an exception doesn't cancel the other request. In fact it just depends on the order in which they are made.
My testing setup:
struct Person {}
struct Animal {}
enum ApiError: Error { case person, animal }
class Requester {
init() {}
func getPeople(waitingFor waitTime: UInt64, throwError: Bool) async throws -> [Person] {
try await waitFor(waitTime)
if throwError { throw ApiError.person }
return []
}
func getAnimals(waitingFor waitTime: UInt64, throwError: Bool) async throws -> [Animal] {
try await waitFor(waitTime)
if throwError { throw ApiError.animal }
return []
}
func waitFor(_ seconds: UInt64) async throws {
do {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * seconds)
} catch {
print("Error waiting", error)
throw error
}
}
}
The exercise.
class ViewController: UIViewController {
let requester = Requester()
override func viewDidLoad() {
super.viewDidLoad()
Task {
async let animals = self.requester.getAnimals(waitingFor: 1, throwError: true)
async let people = self.requester.getPeople(waitingFor: 2, throwError: true)
let start = Date()
do {
// let (_, _) = try await (people, animals)
let (_, _) = try await (animals, people)
print("No error")
} catch {
print("error: ", error)
}
print(Date().timeIntervalSince(start))
}
}
}
For simplicity, from now on I'll just past the relevant lines of code and output.
Scenario 1:
async let animals = self.requester.getAnimals(waitingFor: 1, throwError: true)
async let people = self.requester.getPeople(waitingFor: 2, throwError: true)
let (_, _) = try await (animals, people)
Results in:
error: animal
1.103397011756897
Error waiting CancellationError()
This one works as expected. The slower request, takes 2 seconds, but was cancelled after 1 second (when the fastest one throws)
Scenario 2:
async let animals = self.requester.getAnimals(waitingFor: 2, throwError: true)
async let people = self.requester.getPeople(waitingFor: 1, throwError: true)
let (_, _) = try await (animals, people)
Results in:
error: animal
2.2001450061798096
Now this one is not expected for me. The people request takes 1 second to throw an error and we still wait 2 seconds and the error is animal.
My expectation is that this should have been 1 second and people error.
Scenario 3:
async let animals = self.requester.getAnimals(waitingFor: 2, throwError: true)
async let people = self.requester.getPeople(waitingFor: 1, throwError: true)
let (_, _) = try await (people, animals)
Results in:
error: person
1.0017549991607666
Error waiting CancellationError()
Now this is expected. The difference here is that I swapped the order of the requests but changing to try await (people, animals).
It doesn't matter which method throws first, we always get the first error, and the time spent also depends on that order.
Is this behaviour expected/normal? Am I seeing anything wrong, or am I testing this wrong?
I'm surprised this isn't something people are not talking about more. I only found another question like this in developer forums.
Please help. :)
From https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md
async let (l, r) = {
return await (left(), right())
// ->
// return (await left(), await right())
}
meaning that the entire initializer of the async let is a single task,
and if multiple asynchronous function calls are made inside it, they
are performed one-by one.
Here is a more structured approach with behavior that makes sense.
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
.task {
let requester = Requester()
let start = Date()
await withThrowingTaskGroup(of: Void.self) { group in
let animalTask = Task {
try await requester.getAnimals(waitingFor: 1, throwError: true)
}
group.addTask { animalTask }
group.addTask {
try await requester.getPeople(waitingFor: 2, throwError: true)
}
do {
for try await _ in group {
}
group.cancelAll()
} catch ApiError.animal {
group.cancelAll()
print("animal threw")
} catch ApiError.person {
group.cancelAll()
print("person threw")
} catch {
print("someone else")
}
}
print(Date().timeIntervalSince(start))
}
}
}
The idea is to add each task to a throwing group and then loop through each task.
Cora hit the nail on the head (+1). The async let of a tuple will just await them in order. Instead, consider a task group.
But you do not need to cancel the other items in the group. See “Task Group Cancellation” discussion in the withThrowingTaskGroup(of:returning:body:) documentation:
Throwing an error in one of the tasks of a task group doesn’t immediately cancel the other tasks in that group. However, if you call
next() in the task group and propagate its error, all other tasks are
canceled. For example, in the code below, nothing is canceled and the
group doesn’t throw an error:
withThrowingTaskGroup { group in
group.addTask { throw SomeError() }
}
In contrast, this example throws SomeError and cancels all of the tasks in the group:
withThrowingTaskGroup { group in
group.addTask { throw SomeError() }
try group.next()
}
An individual task throws its error in the corresponding call to Group.next(), which gives you a chance to handle the individual error or to let the group rethrow the error.
Or you can waitForAll, which will cancel the other tasks:
let start = Date()
do {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { let _ = try await self.requester.getAnimals(waitingFor: 1, throwError: true) }
group.addTask { let _ = try await self.requester.getPeople(waitingFor: 2, throwError: true) }
try await group.waitForAll()
}
} catch {
print("error: ", error)
}
print(Date().timeIntervalSince(start))
Bottom line, task groups do not dictate the order in which the tasks are awaited. (They also do not dictate the order in which they complete, either, so you often have to collating task group results into an order-independent structure or re-order the results.)
You asked how you would go about collecting the results. There are a few options:
You can define group tasks such that they do not “return” anything (i.e. child of Void.self), but update an actor (Creatures, below) in the addTask calls and then extract your tuple from that:
class ViewModel1 {
let requester = Requester()
func fetch() async throws -> ([Animal], [Person]) {
let results = Creatures()
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await results.update(with: self.requester.getAnimals(waitingFor: animalsDuration, throwError: shouldThrowError)) }
group.addTask { try await results.update(with: self.requester.getPeople(waitingFor: peopleDuration, throwError: shouldThrowError)) }
try await group.waitForAll()
}
return await (results.animals, results.people)
}
}
private extension ViewModel1 {
/// Creatures
///
/// A private actor used for gathering results
actor Creatures {
var animals: [Animal] = []
var people: [Person] = []
func update(with animals: [Animal]) {
self.animals = animals
}
func update(with people: [Person]) {
self.people = people
}
}
}
You can define group tasks that return enumeration case with associated value, and then extracts the results when done:
class ViewModel2 {
let requester = Requester()
func fetch() async throws -> ([Animal], [Person]) {
try await withThrowingTaskGroup(of: Creatures.self) { group in
group.addTask { try await .animals(self.requester.getAnimals(waitingFor: animalsDuration, throwError: shouldThrowError)) }
group.addTask { try await .people(self.requester.getPeople(waitingFor: peopleDuration, throwError: shouldThrowError)) }
return try await group.reduce(into: ([], [])) { previousResult, creatures in
switch creatures {
case .animals(let values): previousResult.0 = values
case .people(let values): previousResult.1 = values
}
}
}
}
}
private extension ViewModel2 {
/// Creatures
///
/// A private enumeration with associated types for the types of results
enum Creatures {
case animals([Animal])
case people([Person])
}
}
For the sake of completeness, you don't have to use task group if you do not want. E.g., you can manually cancel earlier task if prior one canceled.
class ViewModel3 {
let requester = Requester()
func fetch() async throws -> ([Animal], [Person]) {
let animalsTask = Task {
try await self.requester.getAnimals(waitingFor: animalsDuration, throwError: shouldThrowError)
}
let peopleTask = Task {
do {
return try await self.requester.getPeople(waitingFor: peopleDuration, throwError: shouldThrowError)
} catch {
animalsTask.cancel()
throw error
}
}
return try await (animalsTask.value, peopleTask.value)
}
}
This is not a terribly scalable pattern, which is why task groups might be a more attractive option, as they handle the cancelation of pending tasks for you (assuming you iterate through the group as you build the results).
FWIW, there are other task group alternatives, too, but there is not enough in your question to get too specific in this regard. For example, I can imagine some protocol-as-type implementations if all of the tasks returned an array of objects that conformed to a Creature protocol.
But hopefully the above illustrate a few patterns for using task groups to enjoy the cancelation capabilities while still collating the results.

Swift serial queue that only retains one pending task

I need a serial queue that instead of building up a backlog of tasks, only performs the task it's doing and the latest one queued since that started. Any job waiting in the queue for execution should be discarded if a new one comes in before it starts. Been trying to make it work using actor, async and await as follows but it's a bit advanced considering I only learned this stuff today. Is this close?
actor Worker {
var task: Task <Void, Never>? = nil
var next: Item? = nil
var latestResult = false
func analyseItem(_ item: Item) async -> Bool {
// make our item next (but could be overwritten)
next = item
// let anything that's running complete
await task?.value
// start a task for the latest request
task = Task {
latestResult = next?.processItem()
task = nil
}
return latestResult
}
}
I made a small project in Playgrounds
import Combine
import Foundation
import _Concurrency
actor Analyzer {
var timestamp = Date()
var flag = false
func analyzeData(timestamp: Date) async throws {
if flag { return }
flag = true
print("analyzing #\(timestamp)...")
try await Task.sleep(nanoseconds: 3_000_000_000)
print("analyzing #\(timestamp) complete.")
flag = false
}
}
let analyzer = Analyzer()
var cancellable: AnyCancellable
cancellable = Timer.publish(every: 2, on: .main, in: .default)
.autoconnect()
.receive(on: DispatchQueue.main)
.map({ output -> Date in
Task {
try? await analyzer.analyzeData(timestamp: output)
}
return output
})
.sink(receiveValue: { date in
// print(date)
})
Since flag is isolated, tasks will be analyzed every 4 seconds, because the timers fired every 2 and tasks last 3 seconds.

Swift 5.5 concurrency: Are scheduled Tasks serial to one another

i saw this thread Swift 5.5 Concurrency: how to serialize async Tasks to replace an OperationQueue with maxConcurrentOperationCount = 1?
but i am not clear on whether two scheduled Tasks will execute serially. What i mean is, if i had a piece of code
func fetchImages() {
Task.init {
let fetch = await loadImages()
}
Task.init {
let fetch1 = await loadImages1()
}
}
will first task always finish before second task starts? The application i am trying to get is to execute task 1 as soon as possible (save time) but task 2 relies on the result of task 1 so it needs to wait for task 1 to finish before proceeding. Task 2 also is only conditionally triggered upon an event so they cannot be in the same Task.
You asked:
will first task always finish before second task starts?
No, it will not. Whenever you see await, that is a “suspension point” at which Swift concurrency is free to switch to another task. In short, these can run concurrently. Let me illustrate that with Xcode Instruments:
import os.log
private let log = OSLog(subsystem: "Test", category: .pointsOfInterest)
class Foo {
func fetchImages() {
Task {
let fetch = await loadImages()
print("done loadImages")
}
Task {
let fetch1 = await loadImages1()
print("done loadImages1")
}
}
func loadImages() async {
// start “points of interest” interval
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: #function, signpostID: id, "start")
// perform 3-second asynchronous task
try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
// end “points of interest” interval
os_signpost(.end, log: log, name: #function, signpostID: id, "end")
}
func loadImages1() async {
// start “points of interest” interval
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: #function, signpostID: id, "start")
// perform 1-second asynchronous task
try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
// end “points of interest” interval
os_signpost(.end, log: log, name: #function, signpostID: id, "end")
}
}
Profiling (in Xcode, either press command-i or choose from the menu, “Product” » “Profile”) this with the “time profiler” in Instruments, you can see that these run concurrently:
The trick is to have the second task await the first one. E.g.,
func fetchImages() {
let firstTask = Task {
let fetch = await loadImages()
print("done loadImages")
}
Task {
_ = await firstTask.result
let fetch1 = await loadImages1()
print("done loadImages1")
}
}
Or you can store the first task in some property:
var firstTask: Task<Void, Never>? // you may need to adjust the `Success` and `Failure` types to match your real example
func fetchImages() {
firstTask = Task {
let fetch = await loadImages()
print("done loadImages")
}
Task {
_ = await firstTask?.result
let fetch1 = await loadImages1()
print("done loadImages1")
}
}
When you do that, you can see the sequential execution:
FWIW, this concept, of using await on the prior task was the motivating idea behind that other answer that you referenced. That is a more generalized rendition of the above. Hopefully this illustrates the mechanism outlined in that other answer.

How to make an async Swift function "#synchronized"?

I'd like to create an async function which itself using async calls. I also want to ensure that only one call is actively processed in any moment. So I want an async #synchronized function.
How to do that? Wrapping the function's body inside the dispatchQueue.sync {} does not work as it expects synchronised code. Also it seems that DispatchQueue in general is not designed to have async code blocks / tasks to execute.
This code communicates with hardware, so async in nature, that's why I want an async interface for my library. (I don't want to block the app while the stages of communication happen.) But certain operations can't be executed parallel on the hardware, so I have to go through synchronisation so the certain operations won't happen at the same time.
You can have every Task await the prior one. And you can use actor make sure that you are only running one at a time. The trick is, because of actor reentrancy, you have to put that "await prior Task" logic in a synchronous method.
E.g., you can do:
actor Experiment {
private var previousTask: Task<Void, Error>?
func startSomethingAsynchronous() {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
Now I am using os_signpost so I can watch this serial behavior from Xcode Instruments. Anyway, you could start three tasks like so:
import os.log
private let log = OSLog(subsystem: "Experiment", category: .pointsOfInterest)
class ViewController: NSViewController {
let experiment = Experiment()
func startExperiment() {
for _ in 0 ..< 3 {
Task { await experiment.startSomethingAsynchronous() }
}
os_signpost(.event, log: log, name: "Done starting tasks")
}
...
}
And Instruments can visually demonstrate the sequential behavior (where the ⓱ shows us where the submitting of all the tasks finished), but you can see the sequential execution of the tasks on the timeline:
I actually like to abstract this serial behavior into its own type:
actor SerialTasks<Success> {
private var previousTask: Task<Success, Error>?
func add(block: #Sendable #escaping () async throws -> Success) {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
return try await block()
}
}
}
And then the asynchronous function for which you need this serial behavior would use the above, e.g.:
class Experiment {
let serialTasks = SerialTasks<Void>()
func startSomethingAsynchronous() async {
await serialTasks.add {
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
The accepted answer does indeed make tasks run serially but it does not wait for a task to be finished and does not propagate errors. I use the following alternative to support the catching of errors.
actor SerialTaskQueue<T> {
private let dispatchQueue: DispatchQueue
init(label: String) {
dispatchQueue = DispatchQueue(label: label)
}
#discardableResult
func add(block: #Sendable #escaping () async throws -> T) async throws -> T {
try await withCheckedThrowingContinuation { continuation in
dispatchQueue.sync {
let semaphore = DispatchSemaphore(value: 0)
Task {
defer {
semaphore.signal()
}
do {
let result = try await block()
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
semaphore.wait()
}
}
}
}

How to convert DispatchQueue debounce to Swift Concurrency task?

I have an existing debouncer utility using DispatchQueue. It accepts a closure and executes it before the time threshold is met. It can be used like this:
let limiter = Debouncer(limit: 5)
var value = ""
func sendToServer() {
limiter.execute {
print("\(Date.now.timeIntervalSince1970): Fire! \(value)")
}
}
value.append("h")
sendToServer() // Waits until 5 seconds
value.append("e")
sendToServer() // Waits until 5 seconds
value.append("l")
sendToServer() // Waits until 5 seconds
value.append("l")
sendToServer() // Waits until 5 seconds
value.append("o")
sendToServer() // Waits until 5 seconds
print("\(Date.now.timeIntervalSince1970): Last operation called")
// 1635691696.482115: Last operation called
// 1635691701.859087: Fire! hello
Notice it is not calling Fire! multiple times, but just 5 seconds after the last time with the value from the last task. The Debouncer instance is configured to hold the last task in queue for 5 seconds no matter how many times it is called. The closure is passed into the execute(block:) method:
final class Debouncer {
private let limit: TimeInterval
private let queue: DispatchQueue
private var workItem: DispatchWorkItem?
private let syncQueue = DispatchQueue(label: "Debouncer", attributes: [])
init(limit: TimeInterval, queue: DispatchQueue = .main) {
self.limit = limit
self.queue = queue
}
#objc func execute(block: #escaping () -> Void) {
syncQueue.async { [weak self] in
if let workItem = self?.workItem {
workItem.cancel()
self?.workItem = nil
}
guard let queue = self?.queue, let limit = self?.limit else { return }
let workItem = DispatchWorkItem(block: block)
queue.asyncAfter(deadline: .now() + limit, execute: workItem)
self?.workItem = workItem
}
}
}
How can I convert this into a concurrent operation so it can be called like below:
let limit = Debouncer(limit: 5)
func sendToServer() {
await limiter.waitUntilFinished
print("\(Date.now.timeIntervalSince1970): Fire! \(value)")
}
sendToServer()
sendToServer()
sendToServer()
However, this wouldn't debounce the tasks but suspend them until the next one gets called. Instead it should cancel the previous task and hold the current task until the debounce time. Can this be done with Swift Concurrency or is there a better approach to do this?
Tasks have the ability to use isCancelled or checkCancellation, but for the sake of a debounce routine, where you want to wait for a period of time, you might just use the throwing rendition of Task.sleep(nanoseconds:), whose documentation says:
If the task is canceled before the time ends, this function throws CancellationError.
Thus, this effectively debounces for 2 seconds.
var task: Task<(), Never>?
func debounced(_ string: String) {
task?.cancel()
task = Task {
do {
try await Task.sleep(nanoseconds: 2_000_000_000)
logger.log("result \(string)")
} catch {
logger.log("canceled \(string)")
}
}
}
Note, Apple’s swift-async-algorithms has a debounce for asynchronous sequences.
Based on #Rob's great answer, here's a sample using an actor and Task:
actor Limiter {
enum Policy {
case throttle
case debounce
}
private let policy: Policy
private let duration: TimeInterval
private var task: Task<Void, Never>?
init(policy: Policy, duration: TimeInterval) {
self.policy = policy
self.duration = duration
}
nonisolated func callAsFunction(task: #escaping () async -> Void) {
Task {
switch policy {
case .throttle:
await throttle(task: task)
case .debounce:
await debounce(task: task)
}
}
}
private func throttle(task: #escaping () async -> Void) {
guard self.task?.isCancelled ?? true else { return }
Task {
await task()
}
self.task = Task {
try? await sleep()
self.task?.cancel()
self.task = nil
}
}
private func debounce(task: #escaping () async -> Void) {
self.task?.cancel()
self.task = Task {
do {
try await sleep()
guard !Task.isCancelled else { return }
await task()
} catch {
return
}
}
}
private func sleep() async throws {
try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
}
}
The tests are inconsistent in passing so I think my assumption on the order of the tasks firing is incorrect, but the sample is a good start I think:
final class LimiterTests: XCTestCase {
func testThrottler() async throws {
// Given
let promise = expectation(description: "Ensure first task fired")
let throttler = Limiter(policy: .throttle, duration: 1)
var value = ""
var fulfillmentCount = 0
promise.expectedFulfillmentCount = 2
func sendToServer(_ input: String) {
throttler {
value += input
// Then
switch fulfillmentCount {
case 0:
XCTAssertEqual(value, "h")
case 1:
XCTAssertEqual(value, "hwor")
default:
XCTFail()
}
promise.fulfill()
fulfillmentCount += 1
}
}
// When
sendToServer("h")
sendToServer("e")
sendToServer("l")
sendToServer("l")
sendToServer("o")
await sleep(2)
sendToServer("wor")
sendToServer("ld")
wait(for: [promise], timeout: 10)
}
func testDebouncer() async throws {
// Given
let promise = expectation(description: "Ensure last task fired")
let limiter = Limiter(policy: .debounce, duration: 1)
var value = ""
var fulfillmentCount = 0
promise.expectedFulfillmentCount = 2
func sendToServer(_ input: String) {
limiter {
value += input
// Then
switch fulfillmentCount {
case 0:
XCTAssertEqual(value, "o")
case 1:
XCTAssertEqual(value, "old")
default:
XCTFail()
}
promise.fulfill()
fulfillmentCount += 1
}
}
// When
sendToServer("h")
sendToServer("e")
sendToServer("l")
sendToServer("l")
sendToServer("o")
await sleep(2)
sendToServer("wor")
sendToServer("ld")
wait(for: [promise], timeout: 10)
}
func testThrottler2() async throws {
// Given
let promise = expectation(description: "Ensure throttle before duration")
let throttler = Limiter(policy: .throttle, duration: 1)
var end = Date.now + 1
promise.expectedFulfillmentCount = 2
func test() {
// Then
XCTAssertLessThan(.now, end)
promise.fulfill()
}
// When
throttler(task: test)
throttler(task: test)
throttler(task: test)
throttler(task: test)
throttler(task: test)
await sleep(2)
end = .now + 1
throttler(task: test)
throttler(task: test)
throttler(task: test)
await sleep(2)
wait(for: [promise], timeout: 10)
}
func testDebouncer2() async throws {
// Given
let promise = expectation(description: "Ensure debounce after duration")
let debouncer = Limiter(policy: .debounce, duration: 1)
var end = Date.now + 1
promise.expectedFulfillmentCount = 2
func test() {
// Then
XCTAssertGreaterThan(.now, end)
promise.fulfill()
}
// When
debouncer(task: test)
debouncer(task: test)
debouncer(task: test)
debouncer(task: test)
debouncer(task: test)
await sleep(2)
end = .now + 1
debouncer(task: test)
debouncer(task: test)
debouncer(task: test)
await sleep(2)
wait(for: [promise], timeout: 10)
}
private func sleep(_ duration: TimeInterval) async {
await Task.sleep(UInt64(duration * 1_000_000_000))
}
}