Skip to content

Commit e7dda73

Browse files
authored
Fix Effect.throttle (#654)
* Fix Effect.throttle Fixes #540. * Simplify * Publicize
1 parent 74c111f commit e7dda73

File tree

4 files changed

+56
-18
lines changed

4 files changed

+56
-18
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
buildConfiguration = "Debug"
2727
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2828
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29-
shouldUseLaunchSchemeArgsEnv = "YES">
29+
shouldUseLaunchSchemeArgsEnv = "YES"
30+
codeCoverageEnabled = "YES">
3031
<Testables>
3132
<TestableReference
3233
skipped = "NO">

Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@ import Dispatch
66
public enum ActionFormat {
77
/// Prints the action in a single line by only specifying the labels of the associated values:
88
///
9-
/// ```swift
10-
/// Action.screenA(.row(index:, action: .textChanged(query:)))
11-
/// ```
9+
/// ```swift
10+
/// Action.screenA(.row(index:, action: .textChanged(query:)))
11+
/// ```
1212
///
1313
case labelsOnly
1414
/// Prints the action in a multiline, pretty-printed format, including all the labels of
1515
/// any associated values, as well as the data held in the associated values:
1616
///
17-
/// ```swift
18-
/// Action.screenA(
19-
/// ScreenA.row(
20-
/// index: 1,
21-
/// action: RowAction.textChanged(
22-
/// query: "Hi"
23-
/// )
24-
/// )
17+
/// ```swift
18+
/// Action.screenA(
19+
/// ScreenA.row(
20+
/// index: 1,
21+
/// action: RowAction.textChanged(
22+
/// query: "Hi"
2523
/// )
26-
/// ```
24+
/// )
25+
/// )
26+
/// ```
2727
///
2828
case prettyPrint
2929
}

Sources/ComposableArchitecture/Internal/Throttling.swift renamed to Sources/ComposableArchitecture/Effects/Throttling.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Combine
22
import Dispatch
33

44
extension Effect {
5-
/// Turns an effect into one that can be throttled.
5+
/// Throttles an effect so that it only publishes one output per given interval.
66
///
77
/// - Parameters:
88
/// - id: The effect's identifier.
@@ -13,7 +13,7 @@ extension Effect {
1313
/// `false`, the publisher emits the first element received during the interval.
1414
/// - Returns: An effect that emits either the most-recent or first element received during the
1515
/// specified interval.
16-
func throttle<S>(
16+
public func throttle<S>(
1717
id: AnyHashable,
1818
for interval: S.SchedulerTimeType.Stride,
1919
scheduler: S,
@@ -26,19 +26,20 @@ extension Effect {
2626
return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher()
2727
}
2828

29+
let value = latest ? value : (throttleValues[id] as! Output? ?? value)
30+
throttleValues[id] = value
31+
2932
guard throttleTime.distance(to: scheduler.now) < interval else {
3033
throttleTimes[id] = scheduler.now
3134
throttleValues[id] = nil
3235
return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher()
3336
}
3437

35-
let value = latest ? value : (throttleValues[id] as! Output? ?? value)
36-
throttleValues[id] = value
37-
3838
return Just(value)
3939
.delay(
4040
for: scheduler.now.distance(to: throttleTime.advanced(by: interval)), scheduler: scheduler
4141
)
42+
.handleEvents(receiveOutput: { _ in throttleTimes[id] = scheduler.now })
4243
.setFailureType(to: Failure.self)
4344
.eraseToAnyPublisher()
4445
}

Tests/ComposableArchitectureTests/Internal/EffectThrottleTests.swift renamed to Tests/ComposableArchitectureTests/EffectThrottleTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,40 @@ final class EffectThrottleTests: XCTestCase {
134134
// A second value is emitted right away.
135135
XCTAssertEqual(values, [1, 2])
136136
}
137+
138+
func testThrottleEmitsFirstValueOnce() {
139+
var values: [Int] = []
140+
var effectRuns = 0
141+
142+
func runThrottledEffect(value: Int) {
143+
struct CancelToken: Hashable {}
144+
145+
Deferred { () -> Just<Int> in
146+
effectRuns += 1
147+
return Just(value)
148+
}
149+
.eraseToEffect()
150+
.throttle(
151+
id: CancelToken(), for: 1, scheduler: scheduler.eraseToAnyScheduler(), latest: false
152+
)
153+
.sink { values.append($0) }
154+
.store(in: &self.cancellables)
155+
}
156+
157+
runThrottledEffect(value: 1)
158+
159+
// A value emits right away.
160+
XCTAssertEqual(values, [1])
161+
162+
scheduler.advance(by: 0.5)
163+
164+
runThrottledEffect(value: 2)
165+
166+
scheduler.advance(by: 0.5)
167+
168+
runThrottledEffect(value: 3)
169+
170+
// A second value is emitted right away.
171+
XCTAssertEqual(values, [1, 2])
172+
}
137173
}

0 commit comments

Comments
 (0)