-
Notifications
You must be signed in to change notification settings - Fork 429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add throttle(while:on:) #58
Changes from all commits
73e8b06
f5ab55c
a88287c
9c0aec4
27cefb7
3ea05c7
1cc110f
ec56cda
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1552,6 +1552,96 @@ extension SignalProtocol { | |
} | ||
} | ||
|
||
/// Conditionally throttles values sent on the receiver whenever | ||
/// `shouldThrottle` is true, forwarding values on the given scheduler. | ||
/// | ||
/// - note: While `shouldThrottle` remains false, values are forwarded on the | ||
/// given scheduler. If multiple values are received while | ||
/// `shouldThrottle` is true, the latest value is the one that will | ||
/// be passed on. | ||
/// | ||
/// - note: If the input signal terminates while a value is being throttled, | ||
/// that value will be discarded and the returned signal will | ||
/// terminate immediately. | ||
/// | ||
/// - note: If `shouldThrottle` completes before the receiver, and its last | ||
/// value is `true`, the returned signal will remain in the throttled | ||
/// state, emitting no further values until it terminates. | ||
/// | ||
/// - parameters: | ||
/// - shouldThrottle: A boolean property that controls whether values | ||
/// should be throttled. | ||
/// - scheduler: A scheduler to deliver events on. | ||
/// | ||
/// - returns: A signal that sends values only while `shouldThrottle` is false. | ||
public func throttle<P: PropertyProtocol>(while shouldThrottle: P, on scheduler: SchedulerProtocol) -> Signal<Value, Error> | ||
where P.Value == Bool | ||
{ | ||
return Signal { observer in | ||
let initial: ThrottleWhileState<Value> = .resumed | ||
let state = Atomic(initial) | ||
let schedulerDisposable = SerialDisposable() | ||
|
||
let disposable = CompositeDisposable() | ||
disposable += schedulerDisposable | ||
|
||
disposable += shouldThrottle.producer | ||
.skipRepeats() | ||
.startWithValues { shouldThrottle in | ||
let valueToSend = state.modify { state -> Value? in | ||
guard !state.isTerminated else { return nil } | ||
|
||
if shouldThrottle { | ||
state = .throttled(nil) | ||
} else { | ||
defer { state = .resumed } | ||
|
||
if case let .throttled(value?) = state { | ||
return value | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The returned value from an action of let valueToSend = state.modify { state -> Value? in
guard !state.isTerminated else { return nil }
if shouldThrottle {
state = .throttled(nil)
return nil
} else {
state = .resumed
if case let .throttled(value) = state {
return value
} else {
return nil
}
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call! I've pushed 58e5bed. I ended up having a single There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍 |
||
|
||
if let value = valueToSend { | ||
schedulerDisposable.innerDisposable = scheduler.schedule { | ||
observer.send(value: value) | ||
} | ||
} | ||
} | ||
|
||
disposable += self.observe { event in | ||
let eventToSend = state.modify { state -> Event<Value, Error>? in | ||
switch event { | ||
case let .value(value): | ||
switch state { | ||
case .throttled: | ||
state = .throttled(value) | ||
return nil | ||
case .resumed: | ||
return event | ||
case .terminated: | ||
return nil | ||
} | ||
|
||
case .completed, .interrupted, .failed: | ||
state = .terminated | ||
return event | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as https://github.com/ReactiveCocoa/ReactiveSwift/pull/58/files#r82397528: let eventToSend = state.modify { state -> Event<Value, Error>? in
switch event {
case let .value(value):
switch state {
case .throttled:
state = .throttled(value)
return nil
case .resumed:
return event
case .terminated:
return nil
}
case .completed, .interrupted, .failed:
state = .terminated
return event
}
} |
||
|
||
if let event = eventToSend { | ||
schedulerDisposable.innerDisposable = scheduler.schedule { | ||
observer.action(event) | ||
} | ||
} | ||
} | ||
|
||
return disposable | ||
} | ||
} | ||
|
||
/// Debounce values sent by the receiver, such that at least `interval` | ||
/// seconds pass after the receiver has last sent a value, then forward the | ||
/// latest value on the given scheduler. | ||
|
@@ -1638,6 +1728,21 @@ private struct ThrottleState<Value> { | |
var pendingValue: Value? = nil | ||
} | ||
|
||
private enum ThrottleWhileState<Value> { | ||
case resumed | ||
case throttled(Value?) | ||
case terminated | ||
|
||
var isTerminated: Bool { | ||
switch self { | ||
case .terminated: | ||
return true | ||
case .resumed, .throttled: | ||
return false | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could just be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That requires explicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stylistically, this could also become a switch statement if you prefer, which would add exhaustively checking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
And fail to compile if one adds a new |
||
} | ||
|
||
extension SignalProtocol { | ||
/// Combines the values of all the given signals, in the manner described by | ||
/// `combineLatestWith`. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1040,6 +1040,38 @@ extension SignalProducerProtocol { | |
return lift { $0.throttle(interval, on: scheduler) } | ||
} | ||
|
||
/// Conditionally throttles values sent on the receiver whenever | ||
/// `shouldThrottle` is true, forwarding values on the given scheduler. | ||
/// | ||
/// - note: While `shouldThrottle` remains false, values are forwarded on the | ||
/// given scheduler. If multiple values are received while | ||
/// `shouldThrottle` is true, the latest value is the one that will | ||
/// be passed on. | ||
/// | ||
/// - note: If the input signal terminates while a value is being throttled, | ||
/// that value will be discarded and the returned signal will | ||
/// terminate immediately. | ||
/// | ||
/// - note: If `shouldThrottle` completes before the receiver, and its last | ||
/// value is `true`, the returned signal will remain in the throttled | ||
/// state, emitting no further values until it terminates. | ||
/// | ||
/// - parameters: | ||
/// - shouldThrottle: A boolean property that controls whether values | ||
/// should be throttled. | ||
/// - scheduler: A scheduler to deliver events on. | ||
/// | ||
/// - returns: A producer that sends values only while `shouldThrottle` is false. | ||
public func throttle<P: PropertyProtocol>(while shouldThrottle: P, on scheduler: SchedulerProtocol) -> SignalProducer<Value, Error> | ||
where P.Value == Bool | ||
{ | ||
// Using `Property.init(_:)` avoids capturing a strong reference | ||
// to `shouldThrottle`, so that we don't extend its lifetime. | ||
let shouldThrottle = Property(shouldThrottle) | ||
|
||
return lift { $0.throttle(while: shouldThrottle, on: scheduler) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This keeps a strong reference to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first thing I tried was to wrap let shouldThrottle = Property(shouldThrottle)
return lift { $0.throttle(while: shouldThrottle, on: scheduler) } But that also increases the lifetime of the underlying property. Is there any way to capture a reference to this property that won't extend its lifetime? It looks like composing properties fundamentally alters the lifetime of the source property, extending it to be the union of all transitive property lifetimes. Is that intentional? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's said it might be worth discussing if the source capturing should be dropped. It could be an oversight (especially with the arguments I made in #58 (comment)... ehm), since in ReactiveCocoa/ReactiveCocoa#2922 @mdiep and I focused just on how Edit: Edited many times. Edit 2: It seems the capturing is originated from ReactiveCocoa/ReactiveCocoa#2788. The argument to capture was different though, and it was actually invalidated by ReactiveCocoa/ReactiveCocoa#2922... Edit 3: Opened a PR. #117 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #117 is merged. |
||
} | ||
|
||
/// Debounce values sent by the receiver, such that at least `interval` | ||
/// seconds pass after the receiver has last sent a value, then | ||
/// forward the latest value on the given scheduler. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is 4 notes excessive? 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps just two notes (
shouldThrottle
+ termination behavior), or no notes at all?