Skip to main content
Code Review

Return to Answer

Really should use "async algorithms" implementation
Source Link
Rob
  • 2.7k
  • 16
  • 27

HoweverIn the latter part of this answer, below, I offer my suggestions on what I would change in your implementation. But, nowadays, the right solution is to use the debounce and throttle from Apple’s Swift Async Algorithms library.

For example:

import AsyncAlgorithms
final class AsyncAlgorithmsTests: XCTestCase {
 // a stream of individual keystrokes with a pause after the first five characters
 func keystrokes() -> AsyncStream<String> {
 AsyncStream<String> { continuation in
 Task {
 continuation.yield("h")
 continuation.yield("e")
 continuation.yield("l")
 continuation.yield("l")
 continuation.yield("o")
 try await Task.sleep(seconds: 2)
 continuation.yield(",")
 continuation.yield(" ")
 continuation.yield("w")
 continuation.yield("o")
 continuation.yield("r")
 continuation.yield("l")
 continuation.yield("d")
 continuation.finish()
 }
 }
 }
 // A stream of the individual keystrokes aggregated together as strings (as we
 // want to search the whole string, not for individual characters)
 //
 // e.g.
 // h
 // he
 // hel
 // hell
 // hello
 // ...
 //
 // As the `keystrokes` sequence has a pause after the fifth character, this will
 // also pause after "hello" and before "hello,". We can use that pause to test
 // debouncing and throttling
 func strings() -> AsyncStream<String> {
 AsyncStream<String> { continuation in
 Task {
 var string = ""
 for await keystroke in keystrokes() {
 string += keystroke
 continuation.yield(string)
 }
 continuation.finish()
 }
 }
 }
 func testDebounce() async throws {
 let debouncedSequence = strings().debounce(for: .seconds(1))
 // usually you'd just loop through the sequence with something like
 //
 // for await string in debouncedSequence {
 // sendToServer(string)
 // }
 // but I'm just going to directly await the yielded values and test the resulting array
 let result: [String] = await debouncedSequence.reduce(into: []) { 0ドル.append(1ドル) }
 XCTAssertEqual(result, ["hello", "hello, world"])
 }
 func testThrottle() async throws {
 let throttledSequence = strings().throttle(for: .seconds(1))
 let result: [String] = await throttledSequence.reduce(into: []) { 0ドル.append(1ドル) }
 XCTAssertEqual(result, ["h", "hello,"])
 }
}
// MARK: - Task.sleep(seconds:)
extension Task where Success == Never, Failure == Never {
 /// Suspends the current task for at least the given duration
 /// in seconds.
 ///
 /// If the task is canceled before the time ends,
 /// this function throws `CancellationError`.
 ///
 /// This function doesn't block the underlying thread.
 public static func sleep(seconds duration: TimeInterval) async throws {
 try await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
// MARK: - TimeInterval
extension TimeInterval {
 static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)
}

Given that you were soliciting a "code review", if you really wanted to write your own "debounce" and "throttle" and did not want to use Async Algorithms for some reason, my previous answer, below, addresses some observations on your implementation:


You can add an actor-isolated function to Limiter:


You might consider using debounce and throttle from Apple’s Swift Async Algorithms library.

However, you can add an actor-isolated function to Limiter:


You might consider using debounce and throttle from Apple’s Swift Async Algorithms library.

In the latter part of this answer, below, I offer my suggestions on what I would change in your implementation. But, nowadays, the right solution is to use the debounce and throttle from Apple’s Swift Async Algorithms library.

For example:

import AsyncAlgorithms
final class AsyncAlgorithmsTests: XCTestCase {
 // a stream of individual keystrokes with a pause after the first five characters
 func keystrokes() -> AsyncStream<String> {
 AsyncStream<String> { continuation in
 Task {
 continuation.yield("h")
 continuation.yield("e")
 continuation.yield("l")
 continuation.yield("l")
 continuation.yield("o")
 try await Task.sleep(seconds: 2)
 continuation.yield(",")
 continuation.yield(" ")
 continuation.yield("w")
 continuation.yield("o")
 continuation.yield("r")
 continuation.yield("l")
 continuation.yield("d")
 continuation.finish()
 }
 }
 }
 // A stream of the individual keystrokes aggregated together as strings (as we
 // want to search the whole string, not for individual characters)
 //
 // e.g.
 // h
 // he
 // hel
 // hell
 // hello
 // ...
 //
 // As the `keystrokes` sequence has a pause after the fifth character, this will
 // also pause after "hello" and before "hello,". We can use that pause to test
 // debouncing and throttling
 func strings() -> AsyncStream<String> {
 AsyncStream<String> { continuation in
 Task {
 var string = ""
 for await keystroke in keystrokes() {
 string += keystroke
 continuation.yield(string)
 }
 continuation.finish()
 }
 }
 }
 func testDebounce() async throws {
 let debouncedSequence = strings().debounce(for: .seconds(1))
 // usually you'd just loop through the sequence with something like
 //
 // for await string in debouncedSequence {
 // sendToServer(string)
 // }
 // but I'm just going to directly await the yielded values and test the resulting array
 let result: [String] = await debouncedSequence.reduce(into: []) { 0ドル.append(1ドル) }
 XCTAssertEqual(result, ["hello", "hello, world"])
 }
 func testThrottle() async throws {
 let throttledSequence = strings().throttle(for: .seconds(1))
 let result: [String] = await throttledSequence.reduce(into: []) { 0ドル.append(1ドル) }
 XCTAssertEqual(result, ["h", "hello,"])
 }
}
// MARK: - Task.sleep(seconds:)
extension Task where Success == Never, Failure == Never {
 /// Suspends the current task for at least the given duration
 /// in seconds.
 ///
 /// If the task is canceled before the time ends,
 /// this function throws `CancellationError`.
 ///
 /// This function doesn't block the underlying thread.
 public static func sleep(seconds duration: TimeInterval) async throws {
 try await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
// MARK: - TimeInterval
extension TimeInterval {
 static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)
}

Given that you were soliciting a "code review", if you really wanted to write your own "debounce" and "throttle" and did not want to use Async Algorithms for some reason, my previous answer, below, addresses some observations on your implementation:


You can add an actor-isolated function to Limiter:

Async algorithms
Source Link
Rob
  • 2.7k
  • 16
  • 27

You might consider using debounce and throttle from Apple’s Swift Async Algorithms library.


You might consider using debounce and throttle from Apple’s Swift Async Algorithms library.

Debounce network requests, but the accumulation of user input
Source Link
Rob
  • 2.7k
  • 16
  • 27
// test throttling as user enters "hello, world" into a text field
func testThrottler() async throws {
 // Given
 let promise = expectation(description: "Ensure first task fired")
 let throttler = Limiter(policy: .throttle, duration: 1)
 var valuefulfillmentCount = ""
0
 var fulfillmentCountpromise.expectedFulfillmentCount = 02
 promise.expectedFulfillmentCountvar value = 2""
 func sendToServeraccumulateAndSendToServer(_ input: String) async {
 awaitvalue throttler.submit+= {input
 await throttler.submit value{ +=[value] input
in
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "h")
 case 1: XCTAssertEqual(value, "hwor""hello,")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await sendToServeraccumulateAndSendToServer("h")
 await sendToServeraccumulateAndSendToServer("e")
 await sendToServeraccumulateAndSendToServer("l")
 await sendToServeraccumulateAndSendToServer("l")
 await sendToServeraccumulateAndSendToServer("o")
 try await Task.sleep(seconds: 2)
 await sendToServeraccumulateAndSendToServer("wor"",")
 await sendToServeraccumulateAndSendToServer("ld"" ")
 await accumulateAndSendToServer("w")
 await accumulateAndSendToServer("o")
 await accumulateAndSendToServer("r")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("d")
 wait(for: [promise], timeout: 10)
}

Repeat that process for all the tests, and you'll see it works fine.

  1. In debounce, the test for isCancelled is redundant. The try await Task.sleep will throw an error if the task was canceled.

  2. As a matter of convention, Apple uses operation for the name of the closure parameters, presumably to avoid confusion with Task instances.

  3. I would change the Task to be a Task<Void, Error>?. Then you can simplify debounce to:

    func debounce(operation: @escaping () async -> Void) {
     task?.cancel()
     task = Task {
     trydefer await{ sleep()task = nil }
     try await operationTask.sleep(seconds: duration)
     task =await niloperation()
     }
    }
    
  4. When throttling network requests for user input, you generally want to throttle the network requests, but not the accumulation of the user input. So I have pulled the value += input out of the throttler/debouncer. I also use a capture list of [value] to make sure that we avoid race conditions between the accumulation of user input and the network requests.

actor Limiter {
 private let policy: Policy
 private let duration: TimeInterval
 private var task: Task<Void, Error>?
 init(policy: Policy, duration: TimeInterval) {
 self.policy = policy
 self.duration = duration
 }
 func submit(operation: @escaping () async -> Void) {
 switch policy {
 case .throttle: throttle(operation: operation)
 case .debounce: debounce(operation: operation)
 }
 }
}
// MARK: - Limiter.Policy
extension Limiter {
 enum Policy {
 case throttle
 case debounce
 }
}
// MARK: - Private utility methods
private extension Limiter {
 func throttle(operation: @escaping () async -> Void) {
 guard task == nil else { return }
 task = Task {
 try?defer await{ sleep()task = nil }
 tasktry =await nilTask.sleep(seconds: duration)
 }
 Task {
 await operation()
 }
 }
 func debounce(operation: @escaping () async -> Void) {
 task?.cancel()
 task = Task {
 try await sleep()
 await operation()
 defer { task = nil }
 }
 }
 try funcawait Task.sleep(seconds: duration) async throws {
 try await Task.sleep(nanoseconds: UInt64operation(duration * .nanosecondsPerSecond))
 }
}
// MARK: - TimeInterval
extension TimeInterval {}
 static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)}
}

Which uses these extensions

// MARK: - Task.sleep(seconds:)
extension Task where Success == Never, Failure == Never {
 /// Suspends the current task for at least the given duration
 /// in seconds.
 ///
 /// If the task is canceled before the time ends,
 /// this function throws `CancellationError`.
 ///
 /// This function doesn't block the underlying thread.
 public static func sleep(seconds duration: TimeInterval) async throws {
 try await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
// MARK: - TimeInterval
extension TimeInterval {
 static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)
}
final class LimiterTests: XCTestCase {

 // test throttling as user enters "hello, world" into a text field
 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
 var value = ""

 func sendToServeraccumulateAndSendToServer(_ input: String) async {
  await throttler.submit {
 value += input
 await throttler.submit { [value] in
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "h")
 case 1: XCTAssertEqual(value, "hwor""hello,")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await sendToServeraccumulateAndSendToServer("h")
 await sendToServeraccumulateAndSendToServer("e")
 await sendToServeraccumulateAndSendToServer("l")
 await sendToServeraccumulateAndSendToServer("l")
 await sendToServeraccumulateAndSendToServer("o")
 try await Task.sleep(seconds: 2)
 await sendToServeraccumulateAndSendToServer("wor"",")
 await sendToServeraccumulateAndSendToServer("ld"" ")
 await accumulateAndSendToServer("w")
 await accumulateAndSendToServer("o")
 await accumulateAndSendToServer("r")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("d")
 wait(for: [promise], timeout: 10)
 }

 // test debouncing as user enters "hello, world" into a text field

 func testDebouncer() async throws {
 // Given
 let promise = expectation(description: "Ensure last task fired")
 let debouncer = Limiter(policy: .debounce, duration: 1)
 var value = ""
 var fulfillmentCount = 0
 promise.expectedFulfillmentCount = 2
 func sendToServeraccumulateAndSendToServer(_ input: String) async {
  await debouncer.submit {
 value += input
 await debouncer.submit { [value] in
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "o""hello")
 case 1: XCTAssertEqual(value, "old""hello, world")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await sendToServeraccumulateAndSendToServer("h")
 await sendToServeraccumulateAndSendToServer("e")
 await sendToServeraccumulateAndSendToServer("l")
 await sendToServeraccumulateAndSendToServer("l")
 await sendToServeraccumulateAndSendToServer("o")
 try await Task.sleep(seconds: 2)
 await sendToServeraccumulateAndSendToServer("wor"",")
 await sendToServeraccumulateAndSendToServer("ld"" ")
 await accumulateAndSendToServer("w")
 await accumulateAndSendToServer("o")
 await accumulateAndSendToServer("r")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("d")
 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
 XCTAssertLessThanOrEqual(.now, end)
 promise.fulfill()
 }
 // When
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 try await Task.sleep(seconds: 2)
 end = .now + 1
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 try await Task.sleep(seconds: 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
 XCTAssertGreaterThanOrEqual(.now, end)
 promise.fulfill()
 }
 // When
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 try await Task.sleep(seconds: 2)
 end = .now + 1
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 try await Task.sleep(seconds: 2)
 wait(for: [promise], timeout: 10)
 }
 private func sleep(_ duration: TimeInterval) async {
 try? await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
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) async {
 await throttler.submit {
 value += input

 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "h")
 case 1: XCTAssertEqual(value, "hwor")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await sendToServer("h")
 await sendToServer("e")
 await sendToServer("l")
 await sendToServer("l")
 await sendToServer("o")
 await sleep(2)
 await sendToServer("wor")
 await sendToServer("ld")
 wait(for: [promise], timeout: 10)
}

Repeat that process for all the tests, and you'll see it works fine.

  1. In debounce, the test for isCancelled is redundant. The try await sleep will throw an error if the task was canceled.

  2. As a matter of convention, Apple uses operation for the name of the closure parameters, presumably to avoid confusion with Task instances.

  3. I would change the Task to be a Task<Void, Error>?. Then you can simplify debounce to:

    func debounce(operation: @escaping () async -> Void) {
     task?.cancel()
     task = Task {
     try await sleep()
     await operation()
     task = nil
     }
    }
    
actor Limiter {
 private let policy: Policy
 private let duration: TimeInterval
 private var task: Task<Void, Error>?
 init(policy: Policy, duration: TimeInterval) {
 self.policy = policy
 self.duration = duration
 }
 func submit(operation: @escaping () async -> Void) {
 switch policy {
 case .throttle: throttle(operation: operation)
 case .debounce: debounce(operation: operation)
 }
 }
}
// MARK: - Limiter.Policy
extension Limiter {
 enum Policy {
 case throttle
 case debounce
 }
}
// MARK: - Private utility methods
private extension Limiter {
 func throttle(operation: @escaping () async -> Void) {
 guard task == nil else { return }
 task = Task {
 try? await sleep()
 task = nil
 }
 Task {
 await operation()
 }
 }
 func debounce(operation: @escaping () async -> Void) {
 task?.cancel()
 task = Task {
 try await sleep()
 await operation()
  task = nil }
 }
 func sleep() async throws {
 try await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
// MARK: - TimeInterval
extension TimeInterval {
 static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)
}
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) async {
  await throttler.submit {
 value += input
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "h")
 case 1: XCTAssertEqual(value, "hwor")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await sendToServer("h")
 await sendToServer("e")
 await sendToServer("l")
 await sendToServer("l")
 await sendToServer("o")
 await sleep(2)
 await sendToServer("wor")
 await sendToServer("ld")
 wait(for: [promise], timeout: 10)
 }
 func testDebouncer() async throws {
 // Given
 let promise = expectation(description: "Ensure last task fired")
 let debouncer = Limiter(policy: .debounce, duration: 1)
 var value = ""
 var fulfillmentCount = 0
 promise.expectedFulfillmentCount = 2
 func sendToServer(_ input: String) async {
  await debouncer.submit {
 value += input
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "o")
 case 1: XCTAssertEqual(value, "old")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await sendToServer("h")
 await sendToServer("e")
 await sendToServer("l")
 await sendToServer("l")
 await sendToServer("o")
 await sleep(2)
 await sendToServer("wor")
 await 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
 XCTAssertLessThanOrEqual(.now, end)
 promise.fulfill()
 }
 // When
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await sleep(2)
 end = .now + 1
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: 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
 XCTAssertGreaterThanOrEqual(.now, end)
 promise.fulfill()
 }
 // When
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await sleep(2)
 end = .now + 1
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await sleep(2)
 wait(for: [promise], timeout: 10)
 }
 private func sleep(_ duration: TimeInterval) async {
 try? await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
// test throttling as user enters "hello, world" into a text field
func testThrottler() async throws {
 // Given
 let promise = expectation(description: "Ensure first task fired")
 let throttler = Limiter(policy: .throttle, duration: 1)
 var fulfillmentCount = 0
 promise.expectedFulfillmentCount = 2
 var value = ""
 func accumulateAndSendToServer(_ input: String) async {
 value += input
 await throttler.submit { [value] in
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "h")
 case 1: XCTAssertEqual(value, "hello,")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await accumulateAndSendToServer("h")
 await accumulateAndSendToServer("e")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("o")
 try await Task.sleep(seconds: 2)
 await accumulateAndSendToServer(",")
 await accumulateAndSendToServer(" ")
 await accumulateAndSendToServer("w")
 await accumulateAndSendToServer("o")
 await accumulateAndSendToServer("r")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("d")
 wait(for: [promise], timeout: 10)
}
  1. In debounce, the test for isCancelled is redundant. The Task.sleep will throw an error if the task was canceled.

  2. As a matter of convention, Apple uses operation for the name of the closure parameters, presumably to avoid confusion with Task instances.

  3. I would change the Task to be a Task<Void, Error>?. Then you can simplify debounce to:

    func debounce(operation: @escaping () async -> Void) {
     task?.cancel()
     task = Task {
     defer { task = nil }
     try await Task.sleep(seconds: duration)
     await operation()
     }
    }
    
  4. When throttling network requests for user input, you generally want to throttle the network requests, but not the accumulation of the user input. So I have pulled the value += input out of the throttler/debouncer. I also use a capture list of [value] to make sure that we avoid race conditions between the accumulation of user input and the network requests.

actor Limiter {
 private let policy: Policy
 private let duration: TimeInterval
 private var task: Task<Void, Error>?
 init(policy: Policy, duration: TimeInterval) {
 self.policy = policy
 self.duration = duration
 }
 func submit(operation: @escaping () async -> Void) {
 switch policy {
 case .throttle: throttle(operation: operation)
 case .debounce: debounce(operation: operation)
 }
 }
}
// MARK: - Limiter.Policy
extension Limiter {
 enum Policy {
 case throttle
 case debounce
 }
}
// MARK: - Private utility methods
private extension Limiter {
 func throttle(operation: @escaping () async -> Void) {
 guard task == nil else { return }
 task = Task {
 defer { task = nil }
 try await Task.sleep(seconds: duration)
 }
 Task {
 await operation()
 }
 }
 func debounce(operation: @escaping () async -> Void) {
 task?.cancel()
 task = Task {
 defer { task = nil }
 try await Task.sleep(seconds: duration) await operation()
 }
 }
}

Which uses these extensions

// MARK: - Task.sleep(seconds:)
extension Task where Success == Never, Failure == Never {
 /// Suspends the current task for at least the given duration
 /// in seconds.
 ///
 /// If the task is canceled before the time ends,
 /// this function throws `CancellationError`.
 ///
 /// This function doesn't block the underlying thread.
 public static func sleep(seconds duration: TimeInterval) async throws {
 try await Task.sleep(nanoseconds: UInt64(duration * .nanosecondsPerSecond))
 }
}
// MARK: - TimeInterval
extension TimeInterval {
 static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)
}
final class LimiterTests: XCTestCase {

 // test throttling as user enters "hello, world" into a text field
 func testThrottler() async throws {
 // Given
 let promise = expectation(description: "Ensure first task fired")
 let throttler = Limiter(policy: .throttle, duration: 1)
 var fulfillmentCount = 0
 promise.expectedFulfillmentCount = 2
 var value = ""

 func accumulateAndSendToServer(_ input: String) async {
 value += input
 await throttler.submit { [value] in
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "h")
 case 1: XCTAssertEqual(value, "hello,")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await accumulateAndSendToServer("h")
 await accumulateAndSendToServer("e")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("o")
 try await Task.sleep(seconds: 2)
 await accumulateAndSendToServer(",")
 await accumulateAndSendToServer(" ")
 await accumulateAndSendToServer("w")
 await accumulateAndSendToServer("o")
 await accumulateAndSendToServer("r")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("d")
 wait(for: [promise], timeout: 10)
 }

 // test debouncing as user enters "hello, world" into a text field

 func testDebouncer() async throws {
 // Given
 let promise = expectation(description: "Ensure last task fired")
 let debouncer = Limiter(policy: .debounce, duration: 1)
 var value = ""
 var fulfillmentCount = 0
 promise.expectedFulfillmentCount = 2
 func accumulateAndSendToServer(_ input: String) async {
 value += input
 await debouncer.submit { [value] in
 // Then
 switch fulfillmentCount {
 case 0: XCTAssertEqual(value, "hello")
 case 1: XCTAssertEqual(value, "hello, world")
 default: XCTFail()
 }
 promise.fulfill()
 fulfillmentCount += 1
 }
 }
 // When
 await accumulateAndSendToServer("h")
 await accumulateAndSendToServer("e")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("o")
 try await Task.sleep(seconds: 2)
 await accumulateAndSendToServer(",")
 await accumulateAndSendToServer(" ")
 await accumulateAndSendToServer("w")
 await accumulateAndSendToServer("o")
 await accumulateAndSendToServer("r")
 await accumulateAndSendToServer("l")
 await accumulateAndSendToServer("d")
 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
 XCTAssertLessThanOrEqual(.now, end)
 promise.fulfill()
 }
 // When
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 try await Task.sleep(seconds: 2)
 end = .now + 1
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 await throttler.submit(operation: test)
 try await Task.sleep(seconds: 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
 XCTAssertGreaterThanOrEqual(.now, end)
 promise.fulfill()
 }
 // When
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 try await Task.sleep(seconds: 2)
 end = .now + 1
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 await debouncer.submit(operation: test)
 try await Task.sleep(seconds: 2)
 wait(for: [promise], timeout: 10)
 }
}
incorporate race comment from below
Source Link
Rob
  • 2.7k
  • 16
  • 27
Loading
Source Link
Rob
  • 2.7k
  • 16
  • 27
Loading
default

AltStyle によって変換されたページ (->オリジナル) /