Skip to content

Commit 63b0780

Browse files
authored
Address Effect.throttle sendability (#3325)
1 parent f02d8f6 commit 63b0780

File tree

1 file changed

+35
-35
lines changed

1 file changed

+35
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import Combine
1+
@preconcurrency import Combine
22
import Dispatch
33
import Foundation
44

5-
extension Effect {
5+
extension Effect where Action: Sendable {
66
/// Throttles an effect so that it only publishes one output per given interval.
77
///
88
/// The throttling of an effect is with respect to actions being sent into the store. So, if
@@ -23,12 +23,13 @@ extension Effect {
2323
/// `false`, the publisher emits the first element received during the interval.
2424
/// - Returns: An effect that emits either the most-recent or first element received during the
2525
/// specified interval.
26-
public func throttle<S: Scheduler>(
26+
public func throttle<S: Scheduler & Sendable>(
2727
id: some Hashable & Sendable,
2828
for interval: S.SchedulerTimeType.Stride,
2929
scheduler: S,
3030
latest: Bool
31-
) -> Self {
31+
) -> Self
32+
where S.SchedulerTimeType.Stride: Sendable {
3233
switch self.operation {
3334
case .none:
3435
return .none
@@ -42,45 +43,44 @@ extension Effect {
4243
publisher
4344
.receive(on: scheduler)
4445
.flatMap { value -> AnyPublisher<Action, Never> in
45-
throttleLock.lock()
46-
defer { throttleLock.unlock() }
46+
throttleState.withValue {
47+
guard let throttleTime = $0.times[id] as! S.SchedulerTimeType? else {
48+
$0.times[id] = scheduler.now
49+
$0.values[id] = nil
50+
return Just(value).eraseToAnyPublisher()
51+
}
4752

48-
guard let throttleTime = throttleTimes[id] as! S.SchedulerTimeType? else {
49-
throttleTimes[id] = scheduler.now
50-
throttleValues[id] = nil
51-
return Just(value).eraseToAnyPublisher()
52-
}
53-
54-
let value = latest ? value : (throttleValues[id] as! Action? ?? value)
55-
throttleValues[id] = value
53+
let value = latest ? value : ($0.values[id] as! Action? ?? value)
54+
$0.values[id] = value
5655

57-
guard throttleTime.distance(to: scheduler.now) < interval else {
58-
throttleTimes[id] = scheduler.now
59-
throttleValues[id] = nil
60-
return Just(value).eraseToAnyPublisher()
61-
}
56+
guard throttleTime.distance(to: scheduler.now) < interval else {
57+
$0.times[id] = scheduler.now
58+
$0.values[id] = nil
59+
return Just(value).eraseToAnyPublisher()
60+
}
6261

63-
return Just(value)
64-
.delay(
65-
for: scheduler.now.distance(to: throttleTime.advanced(by: interval)),
66-
scheduler: scheduler
67-
)
68-
.handleEvents(
69-
receiveOutput: { _ in
70-
throttleLock.sync {
71-
throttleTimes[id] = scheduler.now
72-
throttleValues[id] = nil
62+
return Just(value)
63+
.delay(
64+
for: scheduler.now.distance(to: throttleTime.advanced(by: interval)),
65+
scheduler: scheduler
66+
)
67+
.handleEvents(
68+
receiveOutput: { _ in
69+
throttleState.withValue {
70+
$0.times[id] = scheduler.now
71+
$0.values[id] = nil
72+
}
7373
}
74-
}
75-
)
76-
.eraseToAnyPublisher()
74+
)
75+
.eraseToAnyPublisher()
76+
}
7777
}
7878
}
7979
.cancellable(id: id, cancelInFlight: true)
8080
}
8181
}
8282
}
8383

84-
var throttleTimes: [AnyHashable: Any] = [:]
85-
var throttleValues: [AnyHashable: Any] = [:]
86-
let throttleLock = NSRecursiveLock()
84+
private let throttleState = LockIsolated<(times: [AnyHashable: Any], values: [AnyHashable: Any])>(
85+
(times: [:], values: [:])
86+
)

0 commit comments

Comments
 (0)