I have the following use case of a simple BLE device setup process using RxAndroidBle:
Connect to a BLE device.
Start listening to notification characteristic and set up a parser to parse each incoming notification. Parser will then use a PublishSubject to publish parsed data.
Perform a write to write characteristic (negotiate secure connection).
Wait for parser PublishSubject to deliver the parsed response from device - public key (which arrived through the notification characteristic as a response to our write).
Perform another write to the write characteristic (set connection as secure).
Deliver a Completable saying if the process has completed successfully or not.
Right now my solution (not working) looks like this:
deviceService.connectToDevice(macAddress)
.andThen(Completable.defer { deviceService.setupCharacteristicNotification() })
.andThen(Completable.defer { deviceService.postNegotiateSecurity() })
.andThen(Completable.defer {
parser.notificationResultSubject
.flatMapCompletable { result ->
when (result) {
DevicePublicKeyReceived -> Completable.complete()
else -> Completable.error(Exception("Unexpected notification parse result: ${result::class}"))
}
}
})
.andThen(Completable.defer { deviceService.postSetSecurity() })
And the DeviceService class:
class DeviceService {
/**
* Observable keeping shared RxBleConnection for reuse by different calls
*/
private var connectionObservable: Observable<RxBleConnection>? = null
fun connectToDevice(macAddress: String): Completable {
return Completable.fromAction {
connectionObservable =
rxBleClient.getBleDevice(macAddress)
.establishConnection(false)
.compose(ReplayingShare.instance())
}
}
fun setupCharacteristicNotification(): Completable =
connectionObservable?.let {
it
.switchMap { connection ->
connection.setupNotification(UUID_NOTIFICATION_CHARACTERISTIC)
.map { notificationObservable -> notificationObservable.doOnNext { bytes -> parser.parse(bytes) }.ignoreElements() }
.map { channel ->
Observable.merge(
Observable.never<RxBleConnection>().startWith(connection),
channel.toObservable()
)
}
.ignoreElements()
.toObservable<RxBleConnection>()
}
.doOnError { Timber.e(it, "setup characteristic") }
.take(1).ignoreElements()
} ?: Completable.error(CONNECTION_NOT_INITIALIZED)
fun postNegotiateSecurity(): Completable {
val postLength = negotiateSecurity.postNegotiateSecurityLength()
val postPGK = negotiateSecurity.postNegotiateSecurityPGKData()
return connectionObservable?.let {
it.take(1)
.flatMapCompletable { connection ->
postLength
.flatMapSingle { connection.write(it.bytes.toByteArray()) }
.doOnError { Timber.e(it, "post length") }
.flatMap {
postPGK
.flatMapSingle { connection.write(it.bytes.toByteArray()) }
.doOnError { Timber.e(it, "post PGK") }
}
.take(1).ignoreElements()
}
} ?: Completable.error(CONNECTION_NOT_INITIALIZED)
}
fun postSetSecurity(): Completable =
connectionObservable?.let {
it.take(1)
.flatMapCompletable { connection ->
negotiateSecurity.postSetSecurity()
.flatMapSingle { connection.write(it.bytes.toByteArray()) }
.take(1).ignoreElements()
}
} ?: Completable.error(CONNECTION_NOT_INITIALIZED)
}
private fun RxBleConnection.write(bytes: ByteArray): Single<ByteArray> =
writeCharacteristic(UUID_WRITE_CHARACTERISTIC, bytes)
The problem is that it gets stuck in deviceService.postNegotiateSecurity() and never gets past. I don't get any data in the parser as well, so I assume I'm incorrectly subscribing to the notification characteristic.
negotiateSecurity.postNegotiateSecurityLength() and negotiateSecurity.postNegotiateSecurityPGKData() are methods which prepare data to be sent and deliver it as Observable<SendFragment>. Because of data frame size limit, one frame might be encoded as several fragments, which are then emitted by these Observables.
Recap:
postNegotiateSecurity() is never completed
negotiateSecurity.postNegotiateSecurityLength() may emit one or more times
negotiateSecurity.postNegotiateSecurityPGKData() may emit one or more times
Analysis (omitted logs for readability):
it.take(1)
.flatMapCompletable { connection ->
postLength
.flatMapSingle { connection.write(it.bytes.toByteArray()) }
.flatMap {
postPGK // may emit more than one value
.flatMapSingle { connection.write(it.bytes.toByteArray()) }
}
.take(1) // first emission from the above `flatMap` will finish the upstream
.ignoreElements()
}
Every emission from postLength will start a characteristic write. Every succeeded write will start subscription to postPGK. If postLength will emit more than once — more subscriptions to postPGK will be made.
Every subscription to postPGK most likely will result in multiple emissions. Every emission will then be flatMapped to a characteristic write. Every write succeeded write will emit a value.
After the first emission from the above mentioned characteristic write the upstream will be disposed (because of .take(1) operator).
If the postNegotiateSecurity() is actually started it will also finish or error (given that both postLength and postPGK will emit at least one value) since there is no additional logic here.
Conclusion
postNegotiateSecurity() will most probably complete (but not in an intended manner) as the first packet from postPGK will finish it. I would assume that the peripheral expects full data before it will notify anything therefore it is waiting for the PGK to be fully transmitted which will not happen as shown above.
Logs from the application with RxBleLog.setLogLevel(RxBleLog.VERBOSE) set on could help with understanding of what actually happened.
Related
I'm working on a driver that will read data from the network. It doesn't know how much is in a response, other than that when it tries to read and gets 0 bytes back, it is done. So my blocking Swift code looks naively like this:
func readAllBlocking() -> [Byte] {
var buffer: [Byte] = []
var fullBuffer: [Byte] = []
repeat {
buffer = read() // synchronous, blocking
fullBuffer.append(buffer)
} while buffer.count > 0
return fullBuffer
}
How can I rewrite this as a promise that will keep on running until the entire result is read? After trying to wrap my brain around it, I'm still stuck here:
func readAllNonBlocking() -> EventLoopFuture<[Byte]> {
///...?
}
I should add that I can rewrite read() to instead of returning a [Byte] return an EventLoopFuture<[Byte]>
Generally, loops in synchronous programming are turned into recursion to get the same effect with asynchronous programming that uses futures (and also in functional programming).
So your function could look like this:
func readAllNonBlocking(on eventLoop: EventLoop) -> EventLoopFuture<[Byte]> {
// The accumulated chunks
var accumulatedChunks: [Byte] = []
// The promise that will hold the overall result
let promise = eventLoop.makePromise(of: [Byte].self)
// We turn the loop into recursion:
func loop() {
// First, we call `read` to read in the next chunk and hop
// over to `eventLoop` so we can safely write to `accumulatedChunks`
// without a lock.
read().hop(to: eventLoop).map { nextChunk in
// Next, we just append the chunk to the accumulation
accumulatedChunks.append(contentsOf: nextChunk)
guard nextChunk.count > 0 else {
promise.succeed(accumulatedChunks)
return
}
// and if it wasn't empty, we loop again.
loop()
}.cascadeFailure(to: promise) // if anything goes wrong, we fail the whole thing.
}
loop() // Let's kick everything off.
return promise.futureResult
}
I would like to add two things however:
First, what you're implementing above is to simply read in everything until you see EOF, if that piece of software is exposed to the internet, you should definitely add a limit on how many bytes to hold in memory maximally.
Secondly, SwiftNIO is an event driven system so if you were to read these bytes with SwiftNIO, the program would actually look slightly differently. If you're interested what it looks like to simply accumulate all bytes until EOF in SwiftNIO, it's this:
struct AccumulateUntilEOF: ByteToMessageDecoder {
typealias InboundOut = ByteBuffer
func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
// `decode` will be called if new data is coming in.
// We simply return `.needMoreData` because always need more data because our message end is EOF.
// ByteToMessageHandler will automatically accumulate everything for us because we tell it that we need more
// data to decode a message.
return .needMoreData
}
func decodeLast(context: ChannelHandlerContext, buffer: inout ByteBuffer, seenEOF: Bool) throws -> DecodingState {
// `decodeLast` will be called if NIO knows that this is the _last_ time a decode function is called. Usually,
// this is because of EOF or an error.
if seenEOF {
// This is what we've been waiting for, `buffer` should contain all bytes, let's fire them through
// the pipeline.
context.fireChannelRead(self.wrapInboundOut(buffer))
} else {
// Odd, something else happened, probably an error or we were just removed from the pipeline. `buffer`
// will now contain what we received so far but maybe we should just drop it on the floor.
}
buffer.clear()
return .needMoreData
}
}
If you wanted to make a whole program out of this with SwiftNIO, here's an example that is a server which accepts all data until it sees EOF and then literally just writes back the number of received bytes :). Of course, in the real world you would never hold on to all the received bytes to count them (you could just add each individual piece) but I guess it serves as an example.
import NIO
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
struct AccumulateUntilEOF: ByteToMessageDecoder {
typealias InboundOut = ByteBuffer
func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
// `decode` will be called if new data is coming in.
// We simply return `.needMoreData` because always need more data because our message end is EOF.
// ByteToMessageHandler will automatically accumulate everything for us because we tell it that we need more
// data to decode a message.
return .needMoreData
}
func decodeLast(context: ChannelHandlerContext, buffer: inout ByteBuffer, seenEOF: Bool) throws -> DecodingState {
// `decodeLast` will be called if NIO knows that this is the _last_ time a decode function is called. Usually,
// this is because of EOF or an error.
if seenEOF {
// This is what we've been waiting for, `buffer` should contain all bytes, let's fire them through
// the pipeline.
context.fireChannelRead(self.wrapInboundOut(buffer))
} else {
// Odd, something else happened, probably an error or we were just removed from the pipeline. `buffer`
// will now contain what we received so far but maybe we should just drop it on the floor.
}
buffer.clear()
return .needMoreData
}
}
// Just an example "business logic" handler. It will wait for one message
// and just write back the length.
final class SendBackLengthOfFirstInput: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
// Once we receive the message, we allocate a response buffer and just write the length of the received
// message in there. We then also close the channel.
let allData = self.unwrapInboundIn(data)
var response = context.channel.allocator.buffer(capacity: 10)
response.writeString("\(allData.readableBytes)\n")
context.writeAndFlush(self.wrapOutboundOut(response)).flatMap {
context.close(mode: .output)
}.whenSuccess {
context.close(promise: nil)
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("ERROR: \(error)")
context.channel.close(promise: nil)
}
}
let server = try ServerBootstrap(group: group)
// Allow us to reuse the port after the process quits.
.serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
// We should allow half-closure because we want to write back after having received an EOF on the input
.childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: true)
// Our program consists of two parts:
.childChannelInitializer { channel in
channel.pipeline.addHandlers([
// 1: The accumulate everything until EOF handler
ByteToMessageHandler(AccumulateUntilEOF(),
// We want 1 MB of buffering max. If you remove this parameter, it'll also
// buffer indefinitely.
maximumBufferSize: 1024 * 1024),
// 2: Our "business logic"
SendBackLengthOfFirstInput()
])
}
// Let's bind port 9999
.bind(to: SocketAddress(ipAddress: "127.0.0.1", port: 9999))
.wait()
// This will never return.
try server.closeFuture.wait()
Demo:
$ echo -n "hello world" | nc localhost 9999
11
I have this publisher and subscribers (example code):
import Combine
let publisher = PassthroughSubject<ComplexStructOrClass, Never>()
let sub1 = publisher.sink { (someString) in
// Async work...
}
let sub2 = publisher.sink { (someString) in
// Async work, but it has to wait until sub1 has finished his work
}
So the publisher constant has 2 subscribers. When I use the method send on the publisher constant, it should send the value first to sub1 and after sub1 finished processing (with a callback or something like that), publisher should and notify sub2.
So in the comments its stated that Combine is made for this. What publisher do I need to use? A PassthroughSubject may be the wrong decision.
Usecase
I need to publish values throughout the lifetime of my app to a dynamic number of subscribers, for a few different publishers (I hope I can make a protocol). So a subscriber can be added and removed from a publisher at any given time. A subscriber look as follows:
It has a NSPersistentContainer
A callback should be made by the publisher when a new value has arrived. That process looks like:
the publisher will create a backgroundContext of the container of the subscriber, because it knows a subscriber has a container
the publisher sends the context along with the new published value to the subscriber
the publisher waits until it receives a callback of the subscriber. The subscriber shouldn't save the context, but the publisher must hold a reference to the context. The subscriber gives a callback of an enum, which has a ok case and some error cases.
When a subscriber gives a callback with an error enum case, the publisher must rollback the contexts it created for each subscriber.
When a subscriber gives a callback with the ok case, the publisher repeats step 1 till 5 for every subscriber
This step will only be reached when no subscriber gave a error enum case or there are no subscribers. The publisher will save all the contexts created by the subscribers.
Current code, no Combine
This is some code without using Combine:
// My publisher
protocol NotiPublisher {
// Type of message to send
associatedtype Notification
// List of subscribers for this publisher
static var listeners: Set<AnyNotiPublisher<Notification>> { get set }
}
// My subscriber
protocol NotificationListener: Hashable {
associatedtype NotificationType
var container: NSPersistentContainer { get }
// Identifier used to find this subscriber in the list of 'listeners' in the publisher
var identifier: Int32 { get }
var notify: ((_ notification: NotificationType, _ context: NSManagedObjectContext, #escaping CompletionHandlerAck) -> ()) { get }
}
// Type erased version of the NotificationListener and some convience methods here, can add them if desired
// In a extension of NotiPublisher, this method is here
static func notify(queue: DispatchQueue, notification: Notification, completionHander: #escaping CompletionHandlerAck) throws {
let dispatchGroup = DispatchGroup()
var completionBlocks = [SomeCompletionHandler]()
var contexts = [NSManagedObjectContext]()
var didLoop = false
for listener in listeners {
if didLoop {
dispatchGroup.wait()
} else {
didLoop = true
}
dispatchGroup.enter()
listener.container.performBackgroundTask { (context) in
contexts.append(context)
listener.notify(notification, context, { (completion) in
completionBlocks.append(completion)
dispatchGroup.leave()
})
}
}
dispatchGroup.notify(queue: queue) {
let err = completion.first(where: { element in
// Check if an error has occured
})
if err == nil {
for context in contexts {
context.performAndWait {
try! context.save()
}
}
}
completionHander(err ?? .ok(true))
}
}
This is pretty complex code, I am wondering if I can make use of the power of Combine to make this code more readable.
I wrote the following to chain async operations from a publisher using flatMap that allows you to return another publisher. I'm not a fan, and it might not meet your need to dynamically change the subs, but it might help someone:
let somePublisher = Just(12)
let anyCancellable = somePublisher.flatMap{ num in
//We can return a publisher from flatMap, so lets return a Future one because we want to do some async work
return Future<Int,Never>({ promise in
//do an async thing using dispatch
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
print("flat map finished processing the number \(num)")
//now just pass on the value
promise(.success(num))
})
})
}.flatMap{ num in
//We can return a publisher from flatMap, so lets return a Future one because we want to do some async work
return Future<Int,Never>({ promise in
//do an async thing using dispatch
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
print("second flat map finished processing the number \(num)")
//now just pass on the value
promise(.success(num))
})
})
}.sink { num in
print("This sink runs after the async work in the flatMap/Future publishers")
}
You can try to use serial OperationQueue to receive values. Seems like it will wait until sink completes before call another.
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
publisher
.receive(on: queue)
.sink { _ in }
As I port some Objective-C code to Swift, I'm trying to better understand the new Combine framework and how I can use it to re-create a common design pattern.
In this case, the design pattern is a single object (Manager, Service, etc) that any number of "clients" can register with as a delegate to receive callbacks. It's a basic 1:Many pattern using delegates.
Combine looks ideal for this, but the sample code is a bit thin. Below is a working example but I'm not sure if it's correct or being used as intended. In particular, I'm curious about reference cycles between the objects.
class Service {
let tweets = PassthroughSubject<String, Never>()
func start() {
// Simulate the need send to send updates.
DispatchQueue.global(qos: .utility).async {
while true {
self.sendTweet()
usleep(100000)
}
}
}
func sendTweet() {
tweets.send("Message \(Date().timeIntervalSince1970)")
}
}
class Client : Subscriber {
typealias Input = String
typealias Failure = Never
let service:Service
var subscription:Subscription?
init(service:Service) {
self.service = service
// Is this a retain cycle?
// Is this thread-safe?
self.service.tweets.subscribe(self)
}
func receive(subscription: Subscription) {
print("Received subscription: \(subscription)")
self.subscription = subscription
self.subscription?.request(.unlimited)
}
func receive(_ input: String) -> Subscribers.Demand {
print("Received tweet: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("Received completion")
}
}
// Dependency injection is used a lot throughout the
// application in a similar fashion to this:
let service = Service()
let client = Client(service:service)
// In the real world, the service is started when
// the application is launched and clients come-and-go.
service.start()
Output is:
Received subscription: PassthroughSubject
Received tweet: Message 1560371698.300811
Received tweet: Message 1560371698.4087949
Received tweet: Message 1560371698.578027
...
Is this even remotely close to how Combine was intended to be used?
lets check it! the simplest way is add deinit to both classes and limit the live of service
class Service {
let tweets = PassthroughSubject<String, Never>()
func start() {
// Simulate the need send to send updates.
DispatchQueue.global(qos: .utility).async {
(0 ... 3).forEach { _ in
self.sendTweet()
usleep(100000)
}
}
}
func sendTweet() {
tweets.send("Message \(Date().timeIntervalSince1970)")
}
deinit {
print("server deinit")
}
}
now it is easy to check that
do {
let service = Service()
//_ = Client(service:service)
// In the real world, the service is started when
// the application is launched and clients come-and-go.
service.start()
}
will finished as expected
server deinit
modify it with subscribing client
do {
let service = Service()
_ = Client(service:service)
service.start()
}
and you immediately know the result
Received subscription: PassthroughSubject
Received tweet: Message 1580816649.7355099
Received tweet: Message 1580816649.8548698
Received tweet: Message 1580816650.001649
Received tweet: Message 1580816650.102639
there is a memory cycle, as you expected :-)
Generally, there is very low probability, that you need your own subscriber implementation.
First modify the service, so the client will know when no more messages will arrive
func start() {
// Simulate the need send to send updates.
DispatchQueue.global(qos: .utility).async {
// send some tweets
(0 ... 3).forEach { _ in
self.sendTweet()
usleep(100000)
}
// and send "finished"
self.tweets.send(completion: .finished)
}
}
and next use "build-in" subcriber in your publisher by invoking his .sink method. .sink return AnyCancelable (it is a reference type) which you have to store somewhere.
var cancelable: AnyCancellable?
do {
let service = Service()
service.start()
// client
cancelable = service.tweets.sink { (s) in
print(s)
}
}
now, everythig works, es expected ...
Message 1580818277.2908669
Message 1580818277.4674711
Message 1580818277.641886
server deinit
But what about cancelable? Let check it!
var cancelable: AnyCancellable?
do {
let service = Service()
service.start()
// client
cancelable = service.tweets.sink { (s) in
print(s)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print(cancelable)
}
it prints
Message 1580819227.5750608
Message 1580819227.763901
Message 1580819227.9366078
Message 1580819228.072041
server deinit
Optional(Combine.AnyCancellable)
so you have to release it "manualy", if you don't need it anymore. .sink is there again!
var cancelable: AnyCancellable?
do {
let service = Service()
service.start()
// client
cancelable = service.tweets.sink(receiveCompletion: { (completion) in
print(completion)
// this inform publisher to "unsubscribe" (not necessery in this scenario)
cancelable?.cancel()
// and we can release our canceleble
cancelable = nil
}, receiveValue: { (message) in
print(message)
})
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print(cancelable)
}
and result
Message 1580819683.462331
Message 1580819683.638145
Message 1580819683.74383
finished
server deinit
nil
Combine has almost everything you need in real word application, the trouble is a lack of documentation, but a lot of sources are available on the internet.
Custom Combine Subscriber should also conform to Cancellable protocol that provides a method to forward cancellation to the received subscription object from Publisher. That way you do not have to expose Subscription property. According to doc:
If you create a custom Subscriber, the publisher sends a Subscription object when you first subscribe to it. Store this subscription, and then call its cancel() method when you want to cancel publishing. When you create a custom subscriber, you should implement the Cancellable protocol, and have your cancel() implementation forward the call to the stored subscription.
https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine
I have a network request that can Succeed or Fail
I have encapsulated it in an observable.
I have 2 rules for the request
1) There can never be more then 1 request at the same time
-> there is a share operator i can use for this
2) When the request was Succeeded i don't want to repeat the same
request again and just return the latest value
-> I can use shareReplay(1) operator for this
The problem arises when the request fails, the shareReplay(1) will just replay the latest error and not restart the request again.
The request should start again at the next subscription.
Does anyone have an idea how i can turn this into a Observable chain?
// scenario 1
let obs: Observable<Int> = request().shareReplay(1)
// outputs a value
obs.subscribe()
// does not start a new request but outputs the same value as before
obs.subscribe()
// scenario 2 - in case of an error
let obs: Observable<Int> = request().shareReplay(1)
// outputs a error
obs.subscribe()
// does not start a new request but outputs the same value as before, but in this case i want it to start a new request
obs.subscribe()
This seems to be a exactly doing what i want, but it consists of keeping state outside the observable, anyone know how i can achieve this in a more Rx way?
enum Err: Swift.Error {
case x
}
enum Result<T> {
case value(val: T)
case error(err: Swift.Error)
}
func sample() {
var result: Result<Int>? = nil
var i = 0
let intSequence: Observable<Result<Int>> = Observable<Int>.create { observer in
if let result = result {
if case .value(let val) = result {
return Observable<Int>.just(val).subscribe(observer)
}
}
print("do work")
delay(1) {
if i == 0 {
observer.onError(Err.x)
} else {
observer.onNext(1)
observer.onCompleted()
}
i += 1
}
return Disposables.create {}
}
.map { value -> Result<Int> in Result.value(val: value) }
.catchError { error -> Observable<Result<Int>> in
return .just(.error(err: error))
}
.do(onNext: { result = $0 })
.share()
_ = intSequence
.debug()
.subscribe()
delay(2) {
_ = intSequence
.debug()
.subscribe()
_ = intSequence
.debug()
.subscribe()
}
delay(4) {
_ = intSequence
.debug()
.subscribe()
}
}
sample()
it only generates work when we don't have anything cached, but thing again we need to use side effects to achieve the desired output
As mentioned earlier, RxSwift errors need to be treated as fatal errors. They are errors your stream usually cannot recover from, and usually errors that would not even be user facing.
For that reason - a stream that emits an .error or .completed event, will immediately dispose and you won't receive any more events there.
There are two approaches to tackling this:
Using a Result type like you just did
Using .materialize() (and .dematerialize() if needed). These first operator will turn your Observable<Element> into a Observable<Event<Element>>, meaning instead of an error being emitted and the sequence terminated, you will get an element that tells you it was an error event, but without any termination.
You can read more about error handling in RxSwift in Adam Borek's great blog post about this: http://adamborek.com/how-to-handle-errors-in-rxswift/
If an Observable sequence emits an error, it can never emit another event. However, it is a fairly common practice to wrap an error-prone Observable inside of another Observable using flatMap and catch any errors before they are allowed to propagate through to the outer Observable. For example:
safeObservable
.flatMap {
Requestor
.makeUnsafeObservable()
.catchErrorJustReturn(0)
}
.shareReplay(1)
.subscribe()
The API I use requires multiple requests to get search results. It's designed this way because searches can take a long time (> 5min). The initial response comes back immediately with metadata about the search, and that metadata is used in follow up requests until the search is complete. I do not control the API.
1st request is a POST to https://api.com/sessions/search/
The response to this request contains a cookie and metadata about the search. The important fields in this response are the search_cookie (a String) and search_completed_pct (an Int)
2nd request is a POST to https://api.com/sessions/results/ with the search_cookie appended to the URL. eg https://api.com/sessions/results/c601eeb7872b7+0
The response to the 2nd request will contain either:
The search results if the query has completed (aka search_completed_pct == 100)
Metadata about the progress of search, search_completed_pct is the progress of the search and will be between 0 and 100.
If the search is not complete, I want to make a request every 5 seconds until it's complete (aka search_completed_pct == 100)
I've found numerous posts here that are similar, many use Dispatch Groups and for loops, but that approach did not work for me. I've tried a while loop and had issues with variable scoping. Dispatch groups also didn't work for me. This smelled like the wrong way to go, but I'm not sure.
I'm looking for the proper design to make these recursive calls. Should I use delegates or are closures + loop the way to go? I've hit a wall and need some help.
The code below is the general idea of what I've tried (edited for clarity. No dispatch_groups(), error handling, json parsing, etc.)
Viewcontroller.swift
apiObj.sessionSearch(domain) { result in
Log.info!.message("result: \(result)")
})
ApiObj.swift
func sessionSearch(domain: String, sessionCompletion: (result: SearchResult) -> ()) {
// Make request to /search/ url
let task = session.dataTaskWithRequest(request) { data, response, error in
let searchCookie = parseCookieFromResponse(data!)
********* pseudo code **************
var progress: Int = 0
var results = SearchResults()
while (progress != 100) {
// Make requests to /results/ until search is complete
self.getResults(searchCookie) { searchResults in
progress = searchResults.search_pct_complete
if (searchResults == 100) {
completion(searchResults)
} else {
sleep(5 seconds)
} //if
} //self.getResults()
} //while
********* pseudo code ************
} //session.dataTaskWithRequest(
task.resume()
}
func getResults(cookie: String, completion: (searchResults: NSDictionary) -> ())
let request = buildRequest((domain), url: NSURL(string: ResultsUrl)!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
let theResults = getJSONFromData(data!)
completion(theResults)
}
task.resume()
}
Well first off, it seems weird that there is no API with a GET request which simply returns the result - even if this may take minutes. But, as you mentioned, you cannot change the API.
So, according to your description, we need to issue a request which effectively "polls" the server. We do this until we retrieved a Search object which is completed.
So, a viable approach would purposely define the following functions and classes:
A protocol for the "Search" object returned from the server:
public protocol SearchType {
var searchID: String { get }
var isCompleted: Bool { get }
var progress: Double { get }
var result: AnyObject? { get }
}
A concrete struct or class is used on the client side.
An asynchronous function which issues a request to the server in order to create the search object (your #1 POST request):
func createSearch(completion: (SearchType?, ErrorType?) -> () )
Then another asynchronous function which fetches a "Search" object and potentially the result if it is complete:
func fetchSearch(searchID: String, completion: (SearchType?, ErrorType?) -> () )
Now, an asynchronous function which fetches the result for a certain "searchID" (your "search_cookie") - and internally implements the polling:
func fetchResult(searchID: String, completion: (AnyObject?, ErrorType?) -> () )
The implementation of fetchResult may now look as follows:
func fetchResult(searchID: String,
completion: (AnyObject?, ErrorType?) -> () ) {
func poll() {
fetchSearch(searchID) { (search, error) in
if let search = search {
if search.isCompleted {
completion(search.result!, nil)
} else {
delay(1.0, f: poll)
}
} else {
completion(nil, error)
}
}
}
poll()
}
This approach uses a local function poll for implementing the polling feature. poll calls fetchSearch and when it finishes it checks whether the search is complete. If not it delays for certain amount of duration and then calls poll again. This looks like a recursive call, but actually it isn't since poll already finished when it is called again. A local function seems appropriate for this kind of approach.
The function delay simply waits for the specified amount of seconds and then calls the provided closure. delay can be easily implemented in terms of dispatch_after or a with a cancelable dispatch timer (we need later implement cancellation).
I'm not showing how to implement createSearch and fetchSearch. These may be easily implemented using a third party network library or can be easily implemented based on NSURLSession.
Conclusion:
What might become a bit cumbersome, is to implement error handling and cancellation, and also dealing with all the completion handlers. In order to solve this problem in a concise and elegant manner I would suggest to utilise a helper library which implements "Promises" or "Futures" - or try to solve it with Rx.
For example a viable implementation utilising "Scala-like" futures:
func fetchResult(searchID: String) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, f: poll)
}
}
}
poll()
return promise.future!
}
You would start to obtain a result as shown below:
createSearch().flatMap { search in
fetchResult(search.searchID).map { result in
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
This above contains complete error handling. It does not yet contain cancellation. Your really need to implement a way to cancel the request, otherwise the polling may not be stopped.
A solution implementing cancellation utilising a "CancellationToken" may look as follows:
func fetchResult(searchID: String,
cancellationToken ct: CancellationToken) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID, cancellationToken: ct).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, cancellationToken: ct) { ct in
if ct.isCancelled {
promise.reject(CancellationError.Cancelled)
} else {
poll()
}
}
}
}
}
poll()
return promise.future!
}
And it may be called:
let cr = CancellationRequest()
let ct = cr.token
createSearch(cancellationToken: ct).flatMap { search in
fetchResult(search.searchID, cancellationToken: ct).map { result in
// if we reach here, we got a result
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
Later you can cancel the request as shown below:
cr.cancel()