Repository navigation
Drain buffered coalesced values before completion #8247
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
Changes from all commits
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 |
|---|---|---|
|
|
@@ -78,13 +78,12 @@ private final class CoalesceLatestInner<Downstream: Subscriber, Context: Schedul | |
| private var hasReceivedReplay = false | ||
| private var windowStart: Context.SchedulerTimeType? | ||
| private var pendingValue: Input? | ||
| private var readyValue: Input? | ||
| private var pendingCompletion: Subscribers.Completion<Never>? | ||
| private var downstreamDemand: Subscribers.Demand = .none | ||
| private var isDelivering = false | ||
| private var trailingScheduled = false | ||
| private var isCancelled = false | ||
| private var demand: Subscribers.Demand = .none | ||
| /// Latest value that reached an emission point while downstream demand | ||
| /// was zero; conflated (newer emissions overwrite it) and delivered from | ||
| /// `request(_:)` when demand arrives. | ||
| private var undeliveredValue: Input? | ||
|
|
||
| init(downstream: Downstream, interval: Context.SchedulerTimeType.Stride, scheduler: Context) { | ||
| self.downstream = downstream | ||
|
|
@@ -95,14 +94,18 @@ private final class CoalesceLatestInner<Downstream: Subscriber, Context: Schedul | |
| func receive(subscription: Subscription) { | ||
| upstreamSubscription = subscription | ||
| downstream.receive(subscription: self) | ||
| guard !isCancelled else { | ||
| subscription.cancel() | ||
| return | ||
| } | ||
| subscription.request(.unlimited) | ||
| } | ||
|
|
||
| func receive(_ input: Input) -> Subscribers.Demand { | ||
| guard !isCancelled else { return .none } | ||
| if !hasReceivedReplay { | ||
| hasReceivedReplay = true | ||
| deliver(input) | ||
| enqueueForDelivery(input) | ||
| return .none | ||
| } | ||
| let now = scheduler.now | ||
|
|
@@ -116,18 +119,19 @@ private final class CoalesceLatestInner<Downstream: Subscriber, Context: Schedul | |
| // callback cannot emit it out of order after this value. | ||
| pendingValue = nil | ||
| windowStart = now | ||
| deliver(input) | ||
| enqueueForDelivery(input) | ||
| } | ||
| return .none | ||
| } | ||
|
|
||
| func receive(completion: Subscribers.Completion<Never>) { | ||
| guard !isCancelled else { return } | ||
| pendingCompletion = completion | ||
| if let value = pendingValue { | ||
| pendingValue = nil | ||
| deliver(value) | ||
| enqueueForDelivery(value) | ||
| } | ||
| downstream.receive(completion: completion) | ||
| drainReadyValue() | ||
| } | ||
|
|
||
| private func scheduleTrailingEmission(at deadline: Context.SchedulerTimeType) { | ||
|
|
@@ -153,33 +157,61 @@ private final class CoalesceLatestInner<Downstream: Subscriber, Context: Schedul | |
| } | ||
| pendingValue = nil | ||
| windowStart = scheduler.now | ||
| deliver(value) | ||
| enqueueForDelivery(value) | ||
| } | ||
|
|
||
| /// Hands a value to the downstream only when it has outstanding demand; | ||
| /// otherwise conflates it into the latest-value slot for `request(_:)`. | ||
| private func deliver(_ value: Input) { | ||
| guard demand > .none else { | ||
| undeliveredValue = value | ||
| return | ||
| func request(_ demand: Subscribers.Demand) { | ||
| guard !isCancelled, demand > .none else { return } | ||
| downstreamDemand += demand | ||
| drainReadyValue() | ||
| } | ||
|
|
||
| private func enqueueForDelivery(_ value: Input) { | ||
| readyValue = value | ||
|
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.
When downstream still has outstanding demand but Useful? React with 👍 / 👎. |
||
| drainReadyValue() | ||
| } | ||
|
|
||
| private func drainReadyValue() { | ||
| guard !isDelivering else { return } | ||
| isDelivering = true | ||
| defer { | ||
| isDelivering = false | ||
| finishPendingCompletionIfPossible() | ||
| } | ||
|
|
||
| while !isCancelled, | ||
| downstreamDemand > .none, | ||
| let value = readyValue { | ||
| readyValue = nil | ||
| downstreamDemand -= .max(1) | ||
| let additionalDemand = downstream.receive(value) | ||
| guard !isCancelled else { return } | ||
| downstreamDemand += additionalDemand | ||
| } | ||
| demand -= 1 | ||
| demand += downstream.receive(value) | ||
| } | ||
|
|
||
| func request(_ additionalDemand: Subscribers.Demand) { | ||
| guard !isCancelled else { return } | ||
| demand += additionalDemand | ||
| guard demand > .none, let value = undeliveredValue else { return } | ||
| undeliveredValue = nil | ||
| demand -= 1 | ||
| demand += downstream.receive(value) | ||
| private func finishPendingCompletionIfPossible() { | ||
| guard !isCancelled, | ||
| !isDelivering, | ||
| readyValue == nil, | ||
| let completion = pendingCompletion | ||
| else { return } | ||
|
|
||
| pendingCompletion = nil | ||
| // Combine completion is not demand-gated, but it cannot overtake a | ||
| // buffered value. Once demand drains that value, completion follows. | ||
| downstreamDemand = .none | ||
| isCancelled = true | ||
| downstream.receive(completion: completion) | ||
| upstreamSubscription = nil | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+193
to
207
Contributor
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.
For the |
||
|
|
||
| func cancel() { | ||
| isCancelled = true | ||
| pendingValue = nil | ||
| undeliveredValue = nil | ||
| readyValue = nil | ||
| pendingCompletion = nil | ||
| downstreamDemand = .none | ||
| upstreamSubscription?.cancel() | ||
| upstreamSubscription = nil | ||
| } | ||
|
|
||
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.
With unlimited demand, if a downstream receiving the replay reentrantly sends a leading value, then another value inside that new window, then completes (
send(2); send(3); send(completion:)), this line enqueues the pending trailing value throughreadyValue = valuewhileisDeliveringis still true, overwriting the held leading value before the outer drain can emit it. Fresh evidence beyond the prior fixed thread is the extra in-window value before completion; the old sink behavior would emit[1, 2, 3], but this change emits[1, 3], so leading-edge semantics are still broken for completing reentrant streams.Useful? React with 👍 / 👎.