mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-20 09:11:33 +01:00
Having brought back an explicit interface for `throttle` [here](https://github.com/pointfreeco/swift-composable-architecture/pull/2368) we should also consider providing an explicit interface for `debounce`. Though a clock-based debounce is a matter of: ```swift return .run { send in try await withTaskCancellation(id: CancelID.debounce) { try await clock.sleep(for: .seconds(1)) await send(.action) } } ``` A dedicated operator simplifies and flattens: ```swift return .send(.action) .debounce(id: CancelID.debounce, for: .seconds(1), clock: clock) ``` This PR only brings back the scheduler-based API, so alongside #2368 we are reintroducing two Combine-forward APIs for this work. Before we release a 1.1 that fully commits these APIs, we should consider if we can make clock-based APIs work, instead (or in addition). If in addition, we should also consider introducing the Combine-based APIs as soft-deprecated to begin with.
99 lines
2.4 KiB
Swift
99 lines
2.4 KiB
Swift
import Combine
|
|
@_spi(Internals) import ComposableArchitecture
|
|
import XCTest
|
|
|
|
@MainActor
|
|
final class EffectDebounceTests: BaseTCATestCase {
|
|
var cancellables: Set<AnyCancellable> = []
|
|
|
|
func testDebounce() async {
|
|
let mainQueue = DispatchQueue.test
|
|
var values: [Int] = []
|
|
|
|
@discardableResult
|
|
func runDebouncedEffect(value: Int) -> Task<Void, Never> {
|
|
Task {
|
|
struct CancelToken: Hashable {}
|
|
|
|
let effect = Effect.send(value)
|
|
.debounce(id: CancelToken(), for: 1, scheduler: mainQueue)
|
|
|
|
for await action in effect.actions {
|
|
values.append(action)
|
|
}
|
|
}
|
|
}
|
|
|
|
runDebouncedEffect(value: 1)
|
|
|
|
// Nothing emits right away.
|
|
XCTAssertEqual(values, [])
|
|
|
|
// Waiting half the time also emits nothing
|
|
await mainQueue.advance(by: 0.5)
|
|
XCTAssertEqual(values, [])
|
|
|
|
// Run another debounced effect.
|
|
runDebouncedEffect(value: 2)
|
|
|
|
// Waiting half the time emits nothing because the first debounced effect has been canceled.
|
|
await mainQueue.advance(by: 0.5)
|
|
XCTAssertEqual(values, [])
|
|
|
|
// Run another debounced effect.
|
|
runDebouncedEffect(value: 3)
|
|
|
|
// Waiting half the time emits nothing because the second debounced effect has been canceled.
|
|
await mainQueue.advance(by: 0.5)
|
|
XCTAssertEqual(values, [])
|
|
|
|
// Waiting the rest of the time emits the final effect value.
|
|
await mainQueue.advance(by: 0.5)
|
|
XCTAssertEqual(values, [3])
|
|
|
|
// Running out the scheduler
|
|
await mainQueue.run()
|
|
XCTAssertEqual(values, [3])
|
|
}
|
|
|
|
func testDebounceIsLazy() async {
|
|
let mainQueue = DispatchQueue.test
|
|
var values: [Int] = []
|
|
var effectRuns = 0
|
|
|
|
@discardableResult
|
|
func runDebouncedEffect(value: Int) -> Task<Void, Never> {
|
|
Task {
|
|
struct CancelToken: Hashable {}
|
|
|
|
let effect = Effect.publisher {
|
|
Deferred { () -> Just<Int> in
|
|
effectRuns += 1
|
|
return Just(1)
|
|
}
|
|
}
|
|
.debounce(id: CancelToken(), for: 1, scheduler: mainQueue)
|
|
|
|
for await action in effect.actions {
|
|
values.append(action)
|
|
}
|
|
}
|
|
}
|
|
|
|
runDebouncedEffect(value: 1)
|
|
|
|
XCTAssertEqual(values, [])
|
|
XCTAssertEqual(effectRuns, 0)
|
|
|
|
await mainQueue.advance(by: 0.5)
|
|
|
|
XCTAssertEqual(values, [])
|
|
XCTAssertEqual(effectRuns, 0)
|
|
|
|
await mainQueue.advance(by: 0.5)
|
|
|
|
XCTAssertEqual(values, [1])
|
|
XCTAssertEqual(effectRuns, 1)
|
|
}
|
|
}
|