Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions Sources/OpenCombine/Publishers/Publishers.Throttle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ extension Publishers.Throttle {
private var lastEmissionTime: Context.SchedulerTimeType?

private var pendingInput: Input?
private var heldInput: Input?
private var pendingCompletion: Subscribers.Completion<Failure>?

private var demand: Subscribers.Demand = .none
Expand Down Expand Up @@ -200,6 +201,9 @@ extension Publishers.Throttle {
self.lastTime = lastTime

guard demand > .none else {
if latest || heldInput == nil {
heldInput = input
}
lock.unlock()
return .none
}
Expand All @@ -210,31 +214,36 @@ extension Publishers.Throttle {
pendingInput = input
lock.unlock()
} else if !hasScheduledOutput {
let minimumEmissionTime =
lastEmissionTime.map { $0.advanced(by: interval) }
scheduleEmissionAndUnlock(input, now: lastTime)
} else {
lock.unlock()
}

let emissionTime =
minimumEmissionTime.map { Swift.max(lastTime, $0) } ?? lastTime
return .none
}

demand -= 1
private func scheduleEmissionAndUnlock(_ input: Input,
now: Context.SchedulerTimeType) {
let minimumEmissionTime =
lastEmissionTime.map { $0.advanced(by: interval) }

pendingInput = input
lock.unlock()
let emissionTime =
minimumEmissionTime.map { Swift.max(now, $0) } ?? now

let action: () -> Void = { [weak self] in
self?.scheduledEmission()
}
demand -= 1

if emissionTime == lastTime {
scheduler.schedule(action)
} else {
scheduler.schedule(after: emissionTime, action)
}
} else {
lock.unlock()
pendingInput = input
lock.unlock()

let action: () -> Void = { [weak self] in
self?.scheduledEmission()
}

return .none
if emissionTime == now {
scheduler.schedule(action)
} else {
scheduler.schedule(after: emissionTime, action)
}
}

func receive(completion: Subscribers.Completion<Failure>) {
Expand Down Expand Up @@ -319,7 +328,12 @@ extension Publishers.Throttle {
return
}
self.demand += demand
lock.unlock()
guard pendingInput == nil, pendingCompletion == nil,
let input = heldInput.take() else {
lock.unlock()
return
}
scheduleEmissionAndUnlock(input, now: scheduler.now)
}

func cancel() {
Expand Down
28 changes: 28 additions & 0 deletions Tests/OpenCombineTests/PublisherTests/ThrottleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,34 @@ final class ThrottleTests: XCTestCase {
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
}

func testInputWithoutDemandIsHeldUntilDemand() throws {
for latest in [true, false] {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(1), scheduler: scheduler, latest: latest)
}

XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])

try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history,
[.subscription("Throttle"), .value(latest ? 2 : 1)])

XCTAssertEqual(helper.publisher.send(3), .none)
scheduler.executeScheduledActions()
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history,
[.subscription("Throttle"), .value(latest ? 2 : 1), .value(3)])
}
}

func testCancelWhileReceivingInput() throws {
let scheduler = VirtualTimeScheduler()
let subscription = CustomSubscription()
Expand Down
Loading