iOS Combine Start new request only if previous has finished - ios

I have network request that triggers every last cell in switui appearas. Sometimes if user scrolls fast enough down -> up -> request will trigger before first one finishes. Without combine or reactive approach I have completion block and bool value to handle this:
public func load() {
guard !isLoadingPosts else { return }
isLoadingPosts = true
postsDataProvider.loadMorePosts { _ in
self.isLoadingPosts = false
}
}
I was wondering if with combine this can be resolved more elegantly, without the need to use bool value. For example execute request only if previous has finished?

It looks like you want to skip making the call if it's already in progress.
Since you didn't share any of the Combine code you might have, I'll assume that you have a publisher-returning function like this:
func loadMorePosts() -> AnyPublisher<[Post], Error> {
//...
}
Then you can use a subject to initiate a load call, a flatMap(maxPublishers:_:) downstream, with a number of publishers limited to 1:
let loadSubject = PassthroughSubject<Void, Never>()
loadSubject
.flatMap(maxPublishers: .max(1)) {
loadMorePosts()
}
.sink(
receiveCompletion: { _ in },
receiveValue: { posts in
// update posts
})
.store(in: &cancellables)
The above pipeline subscribes to the subject, but if another value arrives before flatMap is ready to receive it, it would simply be dropped.
Then the load function becomes:
func load() {
loadSubject.send(())
}

Related

Dynamically terminate Observable based on another observable | RxSwift

I have array of Observables, say [Observable <WriteTaskResult>]
I want to perform all write tasks keeping their order, and if any one of them fails then I want to perform Observable<ResetTaskResult>
Following function will return observable of type BatchTasksResult for tracking tasks progress.
Sample Code:
enum BatchTasksResult{
case elapsedTime(Double)
case failedFatal
case rolledback
case success
}
func writeBlocks(tasks: [WriteTask]) -> Observable<BatchTasksResult>{
return Observable.create {(observable) -> Disposable in
let allTasks: [Observable<WriteTaskResult>] = self.writeSomewhere(tasks)
Observable.concat(allTasks)
.subscribe { writeTaskResult in
observable.onNext(.elapsedTime(writeTaskResult.totalTime))
}
onError: { (err) in
// Perform Observable<ResetTaskResult>
// if ResetTask was successful then observable.onNext(.rolledback)
// if ResetTask failed then observable.onNext(.failedFatal)
}
onCompleted: {
observable.onNext(.success)
}
.disposed(by: disposeBag)
return Disposables.create()
}
}
How do I trigger rollback logic using Observable from onError of allTasks' observable?
Simple solution seems nested observable, but that's not good practice, I guess? I tried FlatMap, but it can't really solve "If any sinlge task fails, then rollback and reset" Any other solution to this?
There's no need to add the extra level of indirection with the create function. Every Observable operator already creates a new object.
And when you do use Observable.create, do not dispose in an external dispose bag and return a Disposables.create(). Just return the disposable that you just created.
Here's the appropriate way to do what you want:
func writeBlocks(tasks: [WriteTask], resetTask: Single<ResetTaskResult>) -> Observable<BatchTasksResult> {
// create the array of write tasks and concat them. You seem to have that down.
let result = Observable.concat(tasks.map(writeSomewhere(task:)).map { $0.asObservable() })
.share() // the share is needed because you are using the value twice below.
return Observable.merge(
// push out the elapsed time for each task.
result.map { BatchTasksResult.elapsedTime($0.totalTime) },
// when the last one is done, push out the success event.
result.takeLast(1).map { _ in BatchTasksResult.success }
)
.catch { _ in
resetTask // the resetTask will get subscribed to if needed.
.map { _ in BatchTasksResult.rolledback } // if successful emit a rollback
.catch { _ in Single.just(BatchTasksResult.failedFatal) } // otherwise emit the failure.
.asObservable()
}
}
func writeSomewhere(task: WriteTask) -> Single<WriteTaskResult> {
// create a Single that performs the write and emits a result.
}

Rx: How to modify a shared source observable within a retry

Top Level Question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
Solution(s) I have considered:
The suggestion of using defer from this post doesn't seem to naturally port over if the source observable needs to be shared.
Use case (to fully elaborate the question)
Say I have a server connection object that, when initialized, connects to an url. Once it is created, I can also use it to get a data stream for a particular input.
class ServerConnection {
var url: URL
init(url: URL)
func getDataStream(input: String) -> Observable<Data> // the observable also errors when the instance is destroyed.
}
However, one particular url or another may be broken or overloaded. So I may want to obtain the address of a mirror and generate a new ServerConnection object. Let's say I have such a function.
// At any point in time, gets the mirror of the url with the lowest load
func getLowestLoadMirror(url: URL) -> URL {}
Ideally, I want this "mirror url" switching should be an implementation detail. The user of my code may only care about the data they receive. So we would want to encapsulate this logic in a new class:
class ServerConnectionWithMirrors {
private var currentConnection: BehaviorRelay<ServerConnection>
init(startingURL: URL)
func dataStream(for inputParams: String) -> Observable<Data>
}
// usage
let connection = ServerConnectionWithMirrors(startingURL: "www.example.com")
connection.dataStream(for: "channel1")
.subscribe { channel1Data in
// do something with channel1Data
}.disposed(by: disposeBag)
connection.dataStream(for: "channel2")
.subscribe { channel2Data in
// do something with channel2Data
}.disposed(by: disposeBag)
How should I write the dataStream() function for ServerConnectionWithMirrors? I should be using retries, but I need to ensure that the retries, when faced with a particular error (ServerOverLoadedError) update the value on the behaviorRelay.
Here is code that I have so far that demonstrates the crux at what I am trying to do. One problem is that multiple subscribers to the behaviorRelay may all update it in rapid succession when they get an error, where only one update would do.
func dataStream(for inputParams: String) -> Observable<Data> {
self.currentConnection.asObservable()
.flatMapLatest { server in
return server.getDataStream(input: inputParams)
}
.retryWhen { errors in
errors.flatMapLatest { error in
if error is ServerOverLoadedError {
self.currentConnection.accept(ServerConnection(url: getLowestLoadURL()))
} else {
return Observable.error(error)
}
}
}
}
The answer to your top level question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
You cannot modify a retry's source observable from within the retry. (full stop) You cannot do this whether it is shared or not. What you can do is make the source observable in such a way that it naturally updates its data for every subscription.
That is what the question you referred to is trying to explain.
func getData(from initialRequest: URLRequest) -> Observable<Data> {
return Observable.deferred {
var correctRequest = initialRequest
let correctURL = getLowestLoadMirror(url: initialRequest.url!)
correctRequest.url = correctURL
return Observable.just(correctRequest)
}
.flatMapLatest {
getDataFromServer(request: $0)
}
.retryWhen { error in
error
.do(onNext: {
guard $0 is ServerOverloadedError else { throw $0 }
})
}
}
With the above code, every time deferred is retried, it will call its closure and every time its closure is called, the URL will the lowest load will be used.

How can I branch out multiple API calls from the result of one API call and collect them after all are finished with Combine?

So, I have this sequence of API calls, where I fetch a employee details, then fetch the company and project details that the employee is associated with. After both fetching are complete, I combine both and publish a fetchCompleted event. I've isolated the relevant code below.
func getUserDetails() -> AnyPublisher<UserDetails, Error>
func getCompanyDetails(user: UserDetails) -> AnyPublisher<CompanyDetails, Error>
func getProjectDetails(user: UserDetails) -> AnyPublisher<ProjectDetails, Error>
If I do this,
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
let companyPublisher = getUserDetails()
.flatMap { getCompanyDetails($0) }
let projectPublisher = getUserDetails()
.flatMap { getProjectDetails($0) }
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
return cvs.eraseToAnyPublisher()
}
getUserDetails() will get called twice. What I need is fetch the userDetails once and with that, branch the stream into two, map it to fetch the company details and project details and re-combine both.
Is there a elegant(flatter) way to do the following.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
getUserDetails()
.sink {
let companyPublisher = getCompanyDetails($0)
let projectPublisher = getProjectDetails($0)
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
}
return cvs.eraseToAnyPublisher()
}
The whole idea of Combine is that you construct a pipeline down which data flows. Actually what flows down can be a value or a completion, where a completion could be a failure (error). So:
You do not need to make a signal that the pipeline has produced its value; the arrival of that value at the end of the pipeline is that signal.
Similarly, you do not need to make a signal that the pipeline's work has completed; a publisher that has produced all the values it is going to produce produces the completion signal automatically, so the arrival of that completion at the end of the pipeline is that signal.
After all, when you receive a letter, the post office doesn't call you up on the phone and say, "You've got mail." Rather, the postman hands you the letter. You don't need to be told you've received a letter; you simply receive it.
Okay, let's demonstrate. The key to understanding your own pipeline is simply to track what kind of value is traveling down it at any given juncture. So let's construct a model pipeline that does the sort of thing you need done. I will posit three types of value:
struct User {
}
struct Project {
}
struct Company {
}
And I will imagine that it is possible to go online and fetch all of that information: the User independently, and the Project and Company based on information contained in the User. I will simulate that by providing utility functions that return publishers for each type of information; in real life these would probably be deferred futures, but I will simply use Just to keep things simple:
func makeUserFetcherPublisher() -> AnyPublisher<User,Error> {
Just(User()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeProjectFetcherPublisher(user:User) -> AnyPublisher<Project,Error> {
Just(Project()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeCompanyFetcherPublisher(user:User) -> AnyPublisher<Company,Error> {
Just(Company()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
Now then, let's construct our pipeline. I take it that our goal is to produce, as the final value in the pipeline, all the information we have collected: the User, the Project, and the Company. So our final output will be a tuple of those three things. (Tuples are important when you are doing Combine stuff. Passing a tuple down the pipeline is extremely common.)
Okay, let's get started. In the beginning there is nothing, so we need an initial publisher to kick off the process. That will be our user fetcher:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
What's coming out the end of that pipeline is a User. We now want to feed that User into the next two publishers, fetching the corresponding Project and Company. The way to insert a publisher into the middle of a pipeline is with flatMap. And remember, our goal is to produce the tuple of all our info. So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
// at this point, the value is a User
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
}
// at this point, the value is a tuple: (User,Project,Company)
So what goes into flatMap, where the question mark is? Well, we must produce a publisher that produces the tuple we have promised. The tuple-making publisher par excellence is Zip. We have three values in our tuple, so this is a Zip3:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
let result = Publishers.Zip3(/* ? */)
return result.eraseToAnyPublisher()
}
So what are we zipping? We must zip publishers. Well, we know two of those publishers — they are the publishers we have already defined!
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
// ?
let result = Publishers.Zip3(/* ? */, pub1, pub2)
return result.eraseToAnyPublisher()
}
We're almost done! What goes in the missing slot? Remember, it must be a publisher. And what's our goal? We want to pass on the very same User that arrived from upstream. And what's the publisher that does that? It's Just! So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
let just = Just(user).setFailureType(to:Error.self)
let result = Publishers.Zip3(just, pub1, pub2)
return result.eraseToAnyPublisher()
}
And we're done. No muss no fuss. This is a pipeline that produces a (User,Project,Company) tuple. Whoever subscribes to this pipeline does not need some extra signal; the arrival of the tuple is the signal. And now the subscriber can do something with that info. Let's create the subscriber:
myWonderfulPipeline.sink {
completion in
if case .failure(let error) = completion {
print("error:", error)
}
} receiveValue: {
user, project, company in
print(user, project, company)
}.store(in: &self.storage)
We didn't do anything very interesting — we simply printed the tuple contents. But you see, in real life the subscriber would now do something useful with that data.
You can use the zip operator to get a Publisher which emits a value whenever both of its upstreams emitted a value and hence zip together getCompanyDetails and getProjectDetails.
You also don't need a Subject to signal the fetch being finished, you can just call map on the flatMap.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.map { _ in UserFetchState.fetchComplete }
.eraseToAnyPublisher()
}
However, you shouldn't need a UserFetchState to signal the state of your pipeline (and especially shouldn't throw away the fetched CompanyDetails and ProjectDetails objects in the middle of your pipeline. You should simply return the fetched CompanyDetails and ProjectDetails as a result of your flatMap.
func getCompleteUserDetails() -> AnyPublisher<(CompanyDetails, ProjectDetails), Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.eraseToAnyPublisher()
}

How do I stop RxSwift ble scanner once it has found a match?

I have a ble scanner that works and looks like this:
func scan(serviceId: String) -> Observable<[BleHandler.BlePeripheral]> {
knownDevices = []
return waitForBluetooth()
.flatMap { _ in self.scanForPeripheral(serviceId: serviceId) }
.map { _ in self.knownDevices }
}
private func waitForBluetooth() -> Observable<BluetoothState> {
return self.manager
.observeState()
.startWith(self.manager.state)
.filter { $0 == .poweredOn }
.take(1)
}
Then in the viewModel class it filters matches from core data:
func scanAndFilter() -> Observable<[LocalDoorCoreDataObject]> {
let persistingDoors: [LocalDoorCoreDataObject] = coreDataHandler.fetchAll(fetchRequest: NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject"))
return communicationService
.scanForDevices(register: false)
.map{ peripherals in
print("🐶 THIS WILL GO ON FOR ETERNITY", peripherals.count)
self.knownDevices = peripherals
return persistingDoors
.filter { door in peripherals.contains(where: { $0.identifier.uuidString == door.dPeripheralId }) }
}
}
And in the view I want to connect when the scan is completed:
private func scanAndConnect(data: LocalDoorCoreDataObject) {
viewModel.scanRelay().subscribe(
onNext: {
print("🐶SCANNED NAME", $0.first?.dName)},
onCompleted: {
print("🐶COMPLETED SCAN")
self.connectToFilteredPeripheral(localDoor: data)
}).disposed(by: disposeBag)
}
It never reaches onCompleted as it will just scan for eternity even after having found and filtered the core data match. In Apple's framework coreBluetooth I could simply call manager.stopScan() after it has found what I want, but that doesn't seem to be available on the Rx counterpart. How does it work for RxSwift
You can create a new Observable that looks for devices and then completes as soon as it finds the device(s) you're looking for. This would be something like:
func scanAndFilter() -> Observable<[LocalDoorCoreDataObject]> {
return Observable.deferred { in
let persistingDoors: [LocalDoorCoreDataObject] = coreDataHandler.fetchAll(fetchRequest: NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject"))
return communicationService
.scanForDevices(register: false)
.filter { /* verify if the device(s) you're looking for is/are in this list */ }
.take(1)
}
}
The filter operator will make sure that only lists that contain the device you're looking for are passed on and the take(1) operator will take the first emitted value and complete immediately.
The deferred call makes sure that the fetch request that is performed in the first line is not executed when you call scanAndFilter() but only when somebody actually subscribes to the resulting Observable.
If you only want one event to exit the filter operator, then just use .take(1). The Observable will shut down after it emits a single value. If the BLE function is written correctly, it will call stopScan() when the Disposable is disposed of.
I have no idea why the other answer says to "always make sure to wrap all function that return Observables into a .deferred. I've been using RxSwift since 2015 and I've only ever needed deferred once. Certainly not every time I called a function that returned an Observable.

Swift Combine: Buffer upstream values and emit them at a steady rate?

Using the new Combine framework in iOS 13.
Suppose I have an upstream publisher sending values at a highly irregular rate - sometimes seconds or minutes may go by without any values, and then a stream of values may come through all at once. I'd like to create a custom publisher that subscribes to the upstream values, buffers them and emits them at a regular, known cadence when they come in, but publishes nothing if they've all been exhausted.
For a concrete example:
t = 0 to 5000ms: no upstream values published
t = 5001ms: upstream publishes "a"
t = 5002ms: upstream publishes "b"
t = 5003ms: upstream publishes "c"
t = 5004ms to 10000ms: no upstream values published
t = 10001ms: upstream publishes "d"
My publisher subscribed to the upstream would produce values every 1 second:
t = 0 to 5000ms: no values published
t = 5001ms: publishes "a"
t = 6001ms: publishes "b"
t = 7001ms: publishes "c"
t = 7001ms to 10001ms: no values published
t = 10001ms: publishes "d"
None of the existing publishers or operators in Combine seem to quite do what I want here.
throttle and debounce would simply sample the upstream values at a certain cadence and drop ones that are missing (e.g. would only publish "a" if the cadence was 1000ms)
delay would add the same delay to every value, but not space them out (e.g. if my delay was 1000ms, it would publish "a" at 6001ms, "b" at 6002ms, "c" at 6003ms)
buffer seems promising, but I can't quite figure out how to use it - how to force it to publish a value from the buffer on demand. When I hooked up a sink to buffer it seemed to just instantly publish all the values, not buffering at all.
I thought about using some sort of combining operator like zip or merge or combineLatest and combining it with a Timer publisher, and that's probably the right approach, but I can't figure out exactly how to configure it to give the behavior I want.
Edit
Here's a marble diagram that hopefully illustrates what I'm going for:
Upstream Publisher:
-A-B-C-------------------D-E-F--------|>
My Custom Operator:
-A----B----C-------------D----E----F--|>
Edit 2: Unit Test
Here's a unit test that should pass if modulatedPublisher (my desired buffered publisher) works as desired. It's not perfect, but it stores events (including the time received) as they're received and then compares the time intervals between events, ensuring they are no smaller than the desired interval.
func testCustomPublisher() {
let expectation = XCTestExpectation(description: "async")
var events = [Event]()
let passthroughSubject = PassthroughSubject<Int, Never>()
let cancellable = passthroughSubject
.modulatedPublisher(interval: 1.0)
.sink { value in
events.append(Event(value: value, date: Date()))
print("value received: \(value) at \(self.dateFormatter.string(from:Date()))")
}
// WHEN I send 3 events, wait 6 seconds, and send 3 more events
passthroughSubject.send(1)
passthroughSubject.send(2)
passthroughSubject.send(3)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(6000)) {
passthroughSubject.send(4)
passthroughSubject.send(5)
passthroughSubject.send(6)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {
// THEN I expect the stored events to be no closer together in time than the interval of 1.0s
for i in 1 ..< events.count {
let interval = events[i].date.timeIntervalSince(events[i-1].date)
print("Interval: \(interval)")
// There's some small error in the interval but it should be about 1 second since I'm using a 1s modulated publisher.
XCTAssertTrue(interval > 0.99)
}
expectation.fulfill()
}
}
wait(for: [expectation], timeout: 15)
}
The closest I've gotten is using zip, like so:
public extension Publisher where Self.Failure == Never {
func modulatedPublisher(interval: TimeInterval) -> AnyPublisher<Output, Never> {
let timerBuffer = Timer
.publish(every: interval, on: .main, in: .common)
.autoconnect()
return timerBuffer
.zip(self, { $1 }) // should emit one input element ($1) every timer tick
.eraseToAnyPublisher()
}
}
This properly attunes the first three events (1, 2, and 3), but not the second three (4, 5, and 6). The output:
value received: 1 at 3:54:07.0007
value received: 2 at 3:54:08.0008
value received: 3 at 3:54:09.0009
value received: 4 at 3:54:12.0012
value received: 5 at 3:54:12.0012
value received: 6 at 3:54:12.0012
I believe this is happening because zip has some internal buffering capacity. The first three upstream events are buffered and emitted on the Timer's cadence, but during the 6 second wait, the Timer's events are buffered - and when the second set ups upstream events are fired, there are already Timer events waiting in the queue, so they're paired up and fired off immediately.
This is an interesting problem. I played with various combinations of Timer.publish, buffer, zip, and throttle, but I couldn't get any combination to work quite the way you want. So let's write a custom subscriber.
What we'd really like is an API where, when we get an input from upstream, we also get the ability to control when the upstream delivers the next input. Something like this:
extension Publisher {
/// Subscribe to me with a stepping function.
/// - parameter stepper: A function I'll call with each of my inputs, and with my completion.
/// Each time I call this function with an input, I also give it a promise function.
/// I won't deliver the next input until the promise is called with a `.more` argument.
/// - returns: An object you can use to cancel the subscription asynchronously.
func step(with stepper: #escaping (StepEvent<Output, Failure>) -> ()) -> AnyCancellable {
???
}
}
enum StepEvent<Input, Failure: Error> {
/// Handle the Input. Call `StepPromise` when you're ready for the next Input,
/// or to cancel the subscription.
case input(Input, StepPromise)
/// Upstream completed the subscription.
case completion(Subscribers.Completion<Failure>)
}
/// The type of callback given to the stepper function to allow it to continue
/// or cancel the stream.
typealias StepPromise = (StepPromiseRequest) -> ()
enum StepPromiseRequest {
// Pass this to the promise to request the next item from upstream.
case more
// Pass this to the promise to cancel the subscription.
case cancel
}
With this step API, we can write a pace operator that does what you want:
extension Publisher {
func pace<Context: Scheduler, MySubject: Subject>(
_ pace: Context.SchedulerTimeType.Stride, scheduler: Context, subject: MySubject)
-> AnyCancellable
where MySubject.Output == Output, MySubject.Failure == Failure
{
return step {
switch $0 {
case .input(let input, let promise):
// Send the input from upstream now.
subject.send(input)
// Wait for the pace interval to elapse before requesting the
// next input from upstream.
scheduler.schedule(after: scheduler.now.advanced(by: pace)) {
promise(.more)
}
case .completion(let completion):
subject.send(completion: completion)
}
}
}
}
This pace operator takes pace (the required interval between outputs), a scheduler on which to schedule events, and a subject on which to republish the inputs from upstream. It handles each input by sending it through subject, and then using the scheduler to wait for the pace interval before asking for the next input from upstream.
Now we just have to implement the step operator. Combine doesn't give us too much help here. It does have a feature called “backpressure”, which means a publisher cannot send an input downstream until the downstream has asked for it by sending a Subscribers.Demand upstream. Usually you see downstreams send an .unlimited demand upstream, but we're not going to. Instead, we're going to take advantage of backpressure. We won't send any demand upstream until the stepper completes a promise, and then we'll only send a demand of .max(1), so we make the upstream operate in lock-step with the stepper. (We also have to send an initial demand of .max(1) to start the whole process.)
Okay, so need to implement a type that takes a stepper function and conforms to Subscriber. It's a good idea to review the Reactive Streams JVM Specification, because Combine is based on that specification.
What makes the implementation difficult is that several things can call into our subscriber asynchronously:
The upstream can call into the subscriber from any thread (but is required to serialize its calls).
After we've given promise functions to the stepper, the stepper can call those promises on any thread.
We want the subscription to be cancellable, and that cancellation can happen on any thread.
All this asynchronicity means we have to protect our internal state with a lock.
We have to be careful not to call out while holding that lock, to avoid deadlock.
We'll also protect the subscriber from shenanigans involving calling a promise repeatedly, or calling outdated promises, by giving each promise a unique id.
Se here's our basic subscriber definition:
import Combine
import Foundation
public class SteppingSubscriber<Input, Failure: Error> {
public init(stepper: #escaping Stepper) {
l_state = .subscribing(stepper)
}
public typealias Stepper = (Event) -> ()
public enum Event {
case input(Input, Promise)
case completion(Completion)
}
public typealias Promise = (Request) -> ()
public enum Request {
case more
case cancel
}
public typealias Completion = Subscribers.Completion<Failure>
private let lock = NSLock()
// The l_ prefix means it must only be accessed while holding the lock.
private var l_state: State
private var l_nextPromiseId: PromiseId = 1
private typealias PromiseId = Int
private var noPromiseId: PromiseId { 0 }
}
Notice that I moved the auxiliary types from earlier (StepEvent, StepPromise, and StepPromiseRequest) into SteppingSubscriber and shortened their names.
Now let's consider l_state's mysterious type, State. What are all the different states our subscriber could be in?
We could be waiting to receive the Subscription object from upstream.
We could have received the Subscription from upstream and be waiting for a signal (an input or completion from upstream, or the completion of a promise from the stepper).
We could be calling out to the stepper, which we want to be careful in case it completes a promise while we're calling out to it.
We could have been cancelled or have received completion from upstream.
So here is our definition of State:
extension SteppingSubscriber {
private enum State {
// Completed or cancelled.
case dead
// Waiting for Subscription from upstream.
case subscribing(Stepper)
// Waiting for a signal from upstream or for the latest promise to be completed.
case subscribed(Subscribed)
// Calling out to the stopper.
case stepping(Stepping)
var subscription: Subscription? {
switch self {
case .dead: return nil
case .subscribing(_): return nil
case .subscribed(let subscribed): return subscribed.subscription
case .stepping(let stepping): return stepping.subscribed.subscription
}
}
struct Subscribed {
var stepper: Stepper
var subscription: Subscription
var validPromiseId: PromiseId
}
struct Stepping {
var subscribed: Subscribed
// If the stepper completes the current promise synchronously with .more,
// I set this to true.
var shouldRequestMore: Bool
}
}
}
Since we're using NSLock (for simplicity), let's define an extension to ensure we always match locking with unlocking:
fileprivate extension NSLock {
#inline(__always)
func sync<Answer>(_ body: () -> Answer) -> Answer {
lock()
defer { unlock() }
return body()
}
}
Now we're ready to handle some events. The easiest event to handle is asynchronous cancellation, which is the Cancellable protocol's only requirement. If we're in any state except .dead, we want to become .dead and, if there's an upstream subscription, cancel it.
extension SteppingSubscriber: Cancellable {
public func cancel() {
let sub: Subscription? = lock.sync {
defer { l_state = .dead }
return l_state.subscription
}
sub?.cancel()
}
}
Notice here that I don't want to call out to the upstream subscription's cancel function while lock is locked, because lock isn't a recursive lock and I don't want to risk deadlock. All use of lock.sync follows the pattern of deferring any call-outs until after the lock is unlocked.
Now let's implement the Subscriber protocol requirements. First, let's handle receiving the Subscription from upstream. The only time this should happen is when we're in the .subscribing state, but .dead is also possible in which case we want to just cancel the upstream subscription.
extension SteppingSubscriber: Subscriber {
public func receive(subscription: Subscription) {
let action: () -> () = lock.sync {
guard case .subscribing(let stepper) = l_state else {
return { subscription.cancel() }
}
l_state = .subscribed(.init(stepper: stepper, subscription: subscription, validPromiseId: noPromiseId))
return { subscription.request(.max(1)) }
}
action()
}
Notice that in this use of lock.sync (and in all later uses), I return an “action” closure so I can perform arbitrary call-outs after the lock has been unlocked.
The next Subscriber protocol requirement we'll tackle is receiving a completion:
public func receive(completion: Subscribers.Completion<Failure>) {
let action: (() -> ())? = lock.sync {
// The only state in which I have to handle this call is .subscribed:
// - If I'm .dead, either upstream already completed (and shouldn't call this again),
// or I've been cancelled.
// - If I'm .subscribing, upstream must send me a Subscription before sending me a completion.
// - If I'm .stepping, upstream is currently signalling me and isn't allowed to signal
// me again concurrently.
guard case .subscribed(let subscribed) = l_state else {
return nil
}
l_state = .dead
return { [stepper = subscribed.stepper] in
stepper(.completion(completion))
}
}
action?()
}
The most complex Subscriber protocol requirement for us is receiving an Input:
We have to create a promise.
We have to pass the promise to the stepper.
The stepper could complete the promise before returning.
After the stepper returns, we have to check whether it completed the promise with .more and, if so, return the appropriate demand upstream.
Since we have to call out to the stepper in the middle of this work, we have some ugly nesting of lock.sync calls.
public func receive(_ input: Input) -> Subscribers.Demand {
let action: (() -> Subscribers.Demand)? = lock.sync {
// The only state in which I have to handle this call is .subscribed:
// - If I'm .dead, either upstream completed and shouldn't call this,
// or I've been cancelled.
// - If I'm .subscribing, upstream must send me a Subscription before sending me Input.
// - If I'm .stepping, upstream is currently signalling me and isn't allowed to
// signal me again concurrently.
guard case .subscribed(var subscribed) = l_state else {
return nil
}
let promiseId = l_nextPromiseId
l_nextPromiseId += 1
let promise: Promise = { request in
self.completePromise(id: promiseId, request: request)
}
subscribed.validPromiseId = promiseId
l_state = .stepping(.init(subscribed: subscribed, shouldRequestMore: false))
return { [stepper = subscribed.stepper] in
stepper(.input(input, promise))
let demand: Subscribers.Demand = self.lock.sync {
// The only possible states now are .stepping and .dead.
guard case .stepping(let stepping) = self.l_state else {
return .none
}
self.l_state = .subscribed(stepping.subscribed)
return stepping.shouldRequestMore ? .max(1) : .none
}
return demand
}
}
return action?() ?? .none
}
} // end of extension SteppingSubscriber: Publisher
The last thing our subscriber needs to handle is the completion of a promise. This is complicated for several reasons:
We want to protect against a promise being completed multiple times.
We want to protect against an older promise being completed.
We can be in any state when a promise is completed.
Thus:
extension SteppingSubscriber {
private func completePromise(id: PromiseId, request: Request) {
let action: (() -> ())? = lock.sync {
switch l_state {
case .dead, .subscribing(_): return nil
case .subscribed(var subscribed) where subscribed.validPromiseId == id && request == .more:
subscribed.validPromiseId = noPromiseId
l_state = .subscribed(subscribed)
return { [sub = subscribed.subscription] in
sub.request(.max(1))
}
case .subscribed(let subscribed) where subscribed.validPromiseId == id && request == .cancel:
l_state = .dead
return { [sub = subscribed.subscription] in
sub.cancel()
}
case .subscribed(_):
// Multiple completion or stale promise.
return nil
case .stepping(var stepping) where stepping.subscribed.validPromiseId == id && request == .more:
stepping.subscribed.validPromiseId = noPromiseId
stepping.shouldRequestMore = true
l_state = .stepping(stepping)
return nil
case .stepping(let stepping) where stepping.subscribed.validPromiseId == id && request == .cancel:
l_state = .dead
return { [sub = stepping.subscribed.subscription] in
sub.cancel()
}
case .stepping(_):
// Multiple completion or stale promise.
return nil
}
}
action?()
}
}
Whew!
With all that done, we can write the real step operator:
extension Publisher {
func step(with stepper: #escaping (SteppingSubscriber<Output, Failure>.Event) -> ()) -> AnyCancellable {
let subscriber = SteppingSubscriber<Output, Failure>(stepper: stepper)
self.subscribe(subscriber)
return .init(subscriber)
}
}
And then we can try out that pace operator from above. Since we don't do any buffering in SteppingSubscriber, and the upstream in general isn't buffered, we'll stick a buffer in between the upstream and our pace operator.
var cans: [AnyCancellable] = []
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let erratic = Just("A").delay(for: 0.0, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher()
.merge(with: Just("B").delay(for: 0.3, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
.merge(with: Just("C").delay(for: 0.6, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
.merge(with: Just("D").delay(for: 5.0, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
.merge(with: Just("E").delay(for: 5.3, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
.merge(with: Just("F").delay(for: 5.6, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
.handleEvents(
receiveOutput: { print("erratic: \(Double(DispatchTime.now().rawValue) / 1_000_000_000) \($0)") },
receiveCompletion: { print("erratic: \(Double(DispatchTime.now().rawValue) / 1_000_000_000) \($0)") }
)
.makeConnectable()
let subject = PassthroughSubject<String, Never>()
cans += [erratic
.buffer(size: 1000, prefetch: .byRequest, whenFull: .dropOldest)
.pace(.seconds(1), scheduler: DispatchQueue.main, subject: subject)]
cans += [subject.sink(
receiveCompletion: { print("paced: \(Double(DispatchTime.now().rawValue) / 1_000_000_000) \($0)") },
receiveValue: { print("paced: \(Double(DispatchTime.now().rawValue) / 1_000_000_000) \($0)") }
)]
let c = erratic.connect()
cans += [AnyCancellable { c.cancel() }]
return true
}
And here, at long last, is the output:
erratic: 223394.17115897 A
paced: 223394.171495405 A
erratic: 223394.408086369 B
erratic: 223394.739186984 C
paced: 223395.171615624 B
paced: 223396.27056174 C
erratic: 223399.536717127 D
paced: 223399.536782847 D
erratic: 223399.536834495 E
erratic: 223400.236808469 F
erratic: 223400.236886323 finished
paced: 223400.620542561 E
paced: 223401.703613078 F
paced: 223402.703828512 finished
Timestamps are in units of seconds.
The erratic publisher's timings are, indeed, erratic and sometimes close in time.
The paced timings are always at least one second apart even when the erratic events occur less than one second apart.
When an erratic event occurs more than one second after the prior event, the paced event is sent immediately following the erratic event without further delay.
The paced completion occurs one second after the last paced event, even though the erratic completion occurs immediately after the last erratic event. The buffer doesn't send the completion until it receives another demand after it sends the last event, and that demand is delayed by the pacing timer.
I've put the the entire implementation of the step operator in this gist for easy copy/paste.
EDIT
There's an even simpler approach to the original one outlined below, which doesn't require a pacer, but instead uses back-pressure created by flatMap(maxPublishers: .max(1)).
flatMap sends a demand of 1, until its returned publisher, which we could delay, completes. We'd need a Buffer publisher upstream to buffer the values.
// for demo purposes, this subject sends a Date:
let subject = PassthroughSubject<Date, Never>()
let interval = 1.0
let pub = subject
.buffer(size: .max, prefetch: .byRequest, whenFull: .dropNewest)
.flatMap(maxPublishers: .max(1)) {
Just($0)
.delay(for: .seconds(interval), scheduler: DispatchQueue.main)
}
ORIGINAL
I know this is an old question, but I think there's a much simpler way to implement this, so I thought I'd share.
The idea is similar to a .zip with a Timer, except instead of a Timer, you would .zip with a time-delayed "tick" from a previously sent value, which can be achieved with a CurrentValueSubject. CurrentValueSubject is needed instead of a PassthroughSubject in order to seed the first ever "tick".
// for demo purposes, this subject sends a Date:
let subject = PassthroughSubject<Date, Never>()
let pacer = CurrentValueSubject<Void, Never>(())
let interval = 1.0
let pub = subject.zip(pacer)
.flatMap { v in
Just(v.0) // extract the original value
.delay(for: .seconds(interval), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in
pacer.send() // send the pacer "tick" after the interval
})
}
What happens is that the .zip gates on the pacer, which only arrives after a delay from a previously sent value.
If the next value comes earlier than the allowed interval, it waits for the pacer.
If, however, the next value comes later, then the pacer already has a new value to provide instantly, so there would be no delay.
If you used it like in your test case:
let c = pub.sink { print("\($0): \(Date())") }
subject.send(Date())
subject.send(Date())
subject.send(Date())
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
subject.send(Date())
subject.send(Date())
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
subject.send(Date())
subject.send(Date())
}
the result would be something like this:
2020-06-23 19:15:21 +0000: 2020-06-23 19:15:21 +0000
2020-06-23 19:15:21 +0000: 2020-06-23 19:15:22 +0000
2020-06-23 19:15:21 +0000: 2020-06-23 19:15:23 +0000
2020-06-23 19:15:22 +0000: 2020-06-23 19:15:24 +0000
2020-06-23 19:15:22 +0000: 2020-06-23 19:15:25 +0000
2020-06-23 19:15:32 +0000: 2020-06-23 19:15:32 +0000
2020-06-23 19:15:32 +0000: 2020-06-23 19:15:33 +0000
Could Publishers.CollectByTime be useful here somewhere?
Publishers.CollectByTime(upstream: upstreamPublisher.share(), strategy: Publishers.TimeGroupingStrategy.byTime(RunLoop.main, .seconds(1)), options: nil)
Just wanted to mention that I adapted Rob's answer from earlier and converted it to a custom Publisher, in order to allow for a single unbroken pipeline (see comments below his solution). My adaptation is below, but all the credit still goes to him. It also still makes use of Rob's step operator and SteppingSubscriber, as this custom Publisher uses those internally.
Edit: updated with buffer as part of the modulated operator, otherwise that would be required to be attached to buffer the upstream events.
public extension Publisher {
func modulated<Context: Scheduler>(_ pace: Context.SchedulerTimeType.Stride, scheduler: Context) -> AnyPublisher<Output, Failure> {
let upstream = buffer(size: 1000, prefetch: .byRequest, whenFull: .dropNewest).eraseToAnyPublisher()
return PacePublisher<Context, AnyPublisher>(pace: pace, scheduler: scheduler, source: upstream).eraseToAnyPublisher()
}
}
final class PacePublisher<Context: Scheduler, Source: Publisher>: Publisher {
typealias Output = Source.Output
typealias Failure = Source.Failure
let subject: PassthroughSubject<Output, Failure>
let scheduler: Context
let pace: Context.SchedulerTimeType.Stride
lazy var internalSubscriber: SteppingSubscriber<Output, Failure> = SteppingSubscriber<Output, Failure>(stepper: stepper)
lazy var stepper: ((SteppingSubscriber<Output, Failure>.Event) -> ()) = {
switch $0 {
case .input(let input, let promise):
// Send the input from upstream now.
self.subject.send(input)
// Wait for the pace interval to elapse before requesting the
// next input from upstream.
self.scheduler.schedule(after: self.scheduler.now.advanced(by: self.pace)) {
promise(.more)
}
case .completion(let completion):
self.subject.send(completion: completion)
}
}
init(pace: Context.SchedulerTimeType.Stride, scheduler: Context, source: Source) {
self.scheduler = scheduler
self.pace = pace
self.subject = PassthroughSubject<Source.Output, Source.Failure>()
source.subscribe(internalSubscriber)
}
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.subscribe(subscriber)
subject.send(subscription: PaceSubscription(subscriber: subscriber))
}
}
public class PaceSubscription<S: Subscriber>: Subscription {
private var subscriber: S?
init(subscriber: S) {
self.subscriber = subscriber
}
public func request(_ demand: Subscribers.Demand) {
}
public func cancel() {
subscriber = nil
}
}

Resources