I have a case where I would like to validate form and then if everything is ok go to api request.
I've written some code and it works fine but errors dispose my stream. I know I could add .catch error at the end of flat map but then next flat map would be executed.
Can I add catch error at the end of stream without disposing it? Or the only way to deal with it is separate it to two streams validation and server responses?
enum Response {
case error(message: String)
case success
}
let start = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
}
.flatMapLatest { [unowned self] code -> Observable<String> in
return self.apiClient.rxSendData(code)
.retry(1)
}
.map { _ in return Response.success }
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
I'm making some assumptions about code you aren't showing. Your validate function is especially odd to me. It looks like it emits a String (which is ignored, if validation was successful and doesn't emit anything (or maybe an error) if validation failed?
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
.catchError { _ in Observable.empty() } // empty() doesn't emit a value so the next flatMap won't be executed.
}
.flatMapLatest { [unowned self] _ -> Observable<Response> in
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
If validate emits an error when validation fails, and you want to capture that error, then something like this would work:
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<Response> in
return self.validate(characters: fields)
.map { _ in Response.success }
.catchError { Observable.just(Response.error(message: $0.localizedDescription)) }
}
.flatMapLatest { [unowned self] validation -> Observable<Response> in
// here, the above flatMap emits a value no matter what, so we have to switch on it to determine if we want to continue or just push the Response down the pipe.
switch validation {
case .error:
return Observable.just(validation)
case .success:
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
Have you considered the materialize operator? It converts an observable sequence into an observable sequence of event objects detailing what happened that cannot error but completes when the input sequence completes. You can then share that.
Something like:
let code = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
self.validate(characters: fields)
.materialize()
}
.share(replay: 1)
code
.compactMap { $0.error }
.subscribe() // Show error from `self.validate`
.disposed(by: bag)
let request = code
.compactMap { $0.element }
// Will get to this flat map only if `self.validate` did not error
.flatMapLatest { [unowned self] code -> Observable<String> in
self.apiClient.rxSendData(code)
.retry(1)
.materialize()
}
.share(replay: 1)
request
.compactMap { $0.error }
.subscribe() // Show error from `self.apiClient.rxSendData`
.disposed(by: bag)
request
.compactMap { $0.element }
// Do something as a result of the request being successful
The chains would not cease upon self.validate and self.apiClient.rxSendData emitting errors.
Related
Pretty new to project reactor here, I am struggling to put a conditional check inside my Mono stream. This part of my application is receiving an object from Kafka. Let's say the object is like this.
data class SomeEvent(val id: String, val type: String)
I have a function that handles this object like this.
fun process(someEvent: SomeEvent): Mono<String> {
val id = someEvent.id
val checkCondition = someEvent.type == "thisType"
return repoOne.getItem(id)
.map {item ->
// WHAT DO I DO HERE TO PUT A CONDITIONAL CHECK
createEntryForItem(item)
}
.flatMap {entry ->
apiService.sendEntry(entry)
}
.flatMap {
it.bodyToMono(String::class.java)
}
.flatMap {body ->
Mono.just(body)
}
}
So, what I want to do is check whether checkCondition is true and if it is, I want to call a function repoTwo.getDetails(id) that returns a Mono<Details>.
createEntryForItem returns an object of type Entry
apiService.sendEntry(entry) returns a Mono<ClientResponse>
It'd be something like this (in my mind).
fun process(someEvent: SomeEvent): Mono<String> {
val id = someEvent.id
val checkCondition = someEvent.type == "thisType"
return repoOne.getItem(id)
.map {item ->
if (checkCondition) {
repoTwo.getDetails(id).map {details ->
createEntryForItem(item, details)
}
} else {
createEntryForItem(item)
}
}
.flatMap {entry ->
apiService.sendEntry(entry)
}
.flatMap {
it.bodyToMono(String::class.java)
}
.flatMap {body ->
Mono.just(body)
}
}
But, obviously, this does not work because the expression inside the if statement is cast to Any.
How should I write it to achieve what I want to achieve?
UPDATED: The location of where I like to have the conditional check.
You should use flatMap() and not map() after getItem().
return repoOne.getItem(id)
.flatMap {item ->
if (checkCondition) {
repoTwo.getDetails(id).map {details ->
createEntryForItem(item, details)
}
} else {
Mono.just(createEntryForItem(item))
}
}
In a map{} you can transform the value. Because you want to call getDetails() (which returns a reactive type and not a value) to do that you have to use flatMap{}. And that's why you need to wrap your item in a Mono by calling Mono.just(createEntryForItem(item)) on the else branch.
Just split it to another function. Your code will be cleaner too.
repoOne.getItem(id)
.map { createEntry(it, checkCondition) }.
.flatMap.....
private fun createEntry(item, checkCondition): Item {
return if (checkCondition) {
repoTwo.getDetails(id).map { createEntryForItem(item, it) }
} else {
createEntryForItem(item)
}
}
I'm wrapping some legacy completion-block code in an Observable. It will emit one event (next or error), and then complete. The problem is that calling onNext(), onCompleted() only sends the completed event to the observer. Why doesn't the next event get delivered?
UPDATE: The people stream actually works as expected. The issue turns out to be in the next stream, filteredPeople. The inner completed event is passed along to it, and I'm just returning it, which terminates the stream.
I need to filter out completed events from inner streams.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople().asObservable().materialize()
}
.share()
// this is bound to a search field
let filterText = PublishSubject<String>()
let filteredPeople = Observable.combineLatest(people, filterText) { peopleEvent, filter in
// this is the problem. the completed event from people is being returned, and it terminates the stream
guard let people = peopleEvent.element else { return peopleEvent }
if filterText.isEmpty { return .next(people) }
return .next(people.filter { ... })
}
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
filteredPeople.subscribe(
onNext: { event in
// ?! doesn't get called
},
onCompleted: {
// we get here, but why?
},
onError: {event in
...
}).disposed(by: disposeBag)
You haven't posted the code that is causing the problem. The code below works as expected:
struct Person { }
class PeopleService {
static func fetch(_ result: #escaping (Result<[Person], Error>) -> Void) {
result(.success([]))
}
}
let disposeBag = DisposeBag()
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
let people = Observable<Void>.just(())
.flatMapLatest { _ in
return fetchPeople().asObservable().materialize()
}
.share()
people.subscribe(
onNext: { event in
print("onNext does get called")
print("in fact, it will get called twice, once with a .next(.next([Person])) event")
print("and once with a .next(.completed) event.")
},
onCompleted: {
print("this prints after onNext gets called")
})
.disposed(by: disposeBag)
I fixed it by filtering out completed events from the inner stream. I am not sure this is the right way, but I can't think of a better solution.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople()
.asObservable()
.materialize()
// Our work is done, but don't end the parent stream
.filter { !$0.isCompleted }
}
.share()
I ran into a situation where I would fetch an API which will generate json data of registered users. I would then have to loop through each user and fetch their avatar from remote url and save it to disk. I can perform this second task inside subscribe but this is not a best practice. I am trying to implement it with map, flatMap etc.
Here is my sample code:
self.dataManager.getUsers()
.observeOn(MainScheduler.instance)
.subscribeOn(globalScheduler)
.map{ [unowned self] (data) -> Users in
var users = data
// other code for manipulating users goes here
// then below I am trying to use another loop to fetch their avatars
if let cats = users.categories {
for cat in cats {
if let profiles = cat.profiles {
for profile in profiles {
if let thumbnail = profile.thumbnail,
let url = URL(string: thumbnail) {
URLSession.shared.rx.response(request: URLRequest(url: url))
.subscribeOn(MainScheduler.instance)
.subscribe(onNext: { response in
// Update Image
if let img = UIImage(data: response.data) {
try? Disk.save(img, to: .caches, as: url.lastPathComponent)
}
}, onError: { (error) in
}).disposed(by: self.disposeBag)
}
}
}
}
}
return users
}
.subscribe(onSuccess: { [weak self] (users) in
}).disposed(by: disposeBag)
There are 2 problems in this code. First is with the rx on URLSession which execute the task in background on another thread and there is no way to acknowledge the main subscribe back when this operation will finish. Second is with the loop and rx which is not efficient as it should generate multiple observables and then process it.
Any idea to improve this logic is welcome.
This was a fun puzzle.
The "special sauce" that solves the problem is in this line:
.flatMap {
Observable.combineLatest($0.map {
Observable.combineLatest(
Observable.just($0.0),
URLSession.shared.rx.data(request: $0.1)
.materialize()
)
})
}
The map before the line creates an Observable<[(URL, URLRequest)]> and the line in question converts it to an Observable<[(URL, Event<Data>)]>.
The line does this by:
Set up the network call to create an Observable<Data>
Materialize it to create an Observable<Event<Data>> (this is done so an error in one download won't shutdown the entire stream.)
Lift the URL back into an Observable which gives us an Observable<URL>
Combine the observables from steps 2 & 3 to produce an Observable<(URL, Event<Data>)>.
Map each array element to produce [Observable<(URL, Event<Data>)>]
Combine the observables in that array to finally produce Observable<[(URL, Event<Data>)]>
Here is the code
// manipulatedUsers is for the code you commented out.
// users: Observable<Users>
let users = self.dataManager.getUsers()
.map(manipulatedUsers) // manipulatedUsers(_ users: Users) -> Users
.asObservable()
.share(replay: 1)
// this chain is for handling the users object. You left it blank in your code so I did too.
users
.observeOn(MainScheduler.instance)
.subscribe(onNext: { users in
})
.disposed(by: disposeBag)
// This navigates through the users structure and downloads the images.
// images: Observable<(URL, Event<Data>)>
let images = users.map { $0.categories ?? [] }
.map { $0.flatMap { $0.profiles ?? [] } }
.map { $0.compactMap { $0.thumbnail } }
.map { $0.compactMap { URL(string: $0) } }
.map { $0.map { ($0, URLRequest(url: $0)) } }
.flatMap {
Observable.combineLatest($0.map {
Observable.combineLatest(
Observable.just($0.0),
URLSession.shared.rx.data(request: $0.1)
.materialize()
)
})
}
.flatMap { Observable.from($0) }
.share(replay: 1)
// this chain filters out the errors and saves the successful downloads.
images
.filter { $0.1.element != nil }
.map { ($0.0, $0.1.element!) }
.map { ($0.0, UIImage(data: $0.1)!) }
.observeOn(MainScheduler.instance)
.bind(onNext: { url, image in
try? Disk.save(image, to: .caches, as: url.lastPathComponent)
return // need two lines here because this needs to return Void, not Void?
})
.disposed(by: disposeBag)
// this chain handles the download errors if you want to.
images
.filter { $0.1.error != nil }
.bind(onNext: { url, error in
print("failed to download \(url) because of \(error)")
})
.disposed(by: disposeBag)
with RxSwift 3.6.1 I made this extension to ObservableType to get a new token after an error request:
public extension ObservableType where E == Response {
public func retryWithToken() -> Observable<E> {
return retryWhen { error -> Observable<Response> in
return error.flatMap({ (error) -> Observable<Response> in
if let myApiError: MyApiError = error as? MyApiError {
if (myApiError == MyApiError.tokenError) {
return Session.shared.myProvider.request(.generateToken)
} else {
return Observable.error(myApiError)
}
}
return Observable.error(error)
})
}
}
}
and then I can use it:
Session.shared.myProvider.rx
.request(.mySampleRequest)
.filterSuccessfulStatusCodes()
.retryWithToken()
.subscribe { event in
....
}.disposed(by: self.disposeBag)
but with RxSwift 4.0.0 now the sequence expect a
PrimitiveSequence<SingleTrait, Response>
someone can explain to me how to do the same with RxSwift 4.0.0? I try with an extension to PrimitiveSequence but I've some compilation errors.
I believe that has nothing to do with RxSwift but is a Moya change. MoyaProvider.rx.request returns Single which is a typealias for PrimitiveSequence which is not an ObservableType.
You declare your function upon the ObservableType.
So just do asObservable() before retryWithToken()
Suppose you have a branch in your promise chain that could either return nothing or an AnyObject promise. What would you specify as the return type of the 'then' closure? For example:
func sample() -> Promise<AnyObject> {
return Promise { fulfill, reject in
fulfill(1)
}
.then { _ -> Void in
if false {
return Promise { fulfill, reject in
fulfill(0)
}
}
}
}
If I put Void as the return type for the 'then' closure I get a seg fault; if I put Promise as return type then I get an error:
missing return in a closure expected to return Promise<AnyObject>
Any suggestions?
Thanks
Based on the code sample, I see no reason to return an AnyObject. If you want to optionally return Void or an Object, then make a promise that contains an optional.
func sample() -> Promise<AnyObject?> {
return Promise { fulfill, reject in
functionForGettingObjectWithCallback() { result: AnyObject? in
fulfill(result)
}
}
}