VUE with vue-rx / rxjs : How to create a subscription with $watchAsObservable using filter and interval - rxjs

I need the help in the following vue-rx / RxJs question.
I have to make a subscription in vue-rx that watches a props value, and when it is true then it calls a http request in every 500 ms, and stops it when it is false, or when the returned value is 'COMPLETED'.
I tried something like this:
export default {
props: ['started'],
subscriptions() {
return {
currentHttpState: this.$watchAsObservable('started')
.pipe(pluck('newValue'),filter(value => value === false))
.switchMap(() => interval(500).pipe(switchMap(() => this.callHttpRequest()),distinctUntilChanged())),
Thank you for the help!

I'm not too familiar with vue (or vue-rx), so this may just be half the answer (the RxJS bit).
I'm assuming this.$watchAsObservable('started') with pluck('newValue') is a stream of true and false? (reflecting the value of the started prop)
If so, I would use switchMap to switch between an interval/timer and nothing.
currentHttpState: this.$watchAsObservable('started').pipe(
pluck('newValue'),
map(val => val? timer(0,500) : EMPTY),
switchMap(timer$ => timer$.pipe(
switchMap(_ => this.callHttpRequest()),
takeWhile(result => result.status !== 'COMPLETED')
)
)
that second switchMap will have the effect that if a call takes over 500ms to complete, it will be dropped and you'll never see the results. This also unsubscribed if the takeWhile() condition isn't met - so you'll have to change that to meet your specific requirements.

Related

How do I create an observable which emits a value from an ngrx selector, then another value after a delay?

Given an ngrx selector:
store.select('count')
I want to create an observable that will emit values emitted by the selector, then emit another specific value after a delay.
Using concat doesn't work as (I assume) the selector doesn't complete, so the 0 is never emitted:
this.count$ = concat(store.select('count'), of(0).pipe(delay(2000)));
Stackblitz: https://stackblitz.com/edit/so-selector-delay?file=src/app/my-counter/my-counter.component.ts
- click 'Increment' button - Current Count should change to 1 then back to 0 after 2 seconds.
If you want to emit the store.select('count') value, then essentially reset it to 0 after not receiving an emission for 2 seconds, you can use a switchMap to create a source that emits two values:
The emitted count
The "default" value of 0 after 2000ms
The trick here is that the second value (0) will NOT be emitted if another store.select('count') value is received, because switchMap will create a new source and "switch" to that:
this.count$ = store.select('count').pipe(
switchMap(count => concat(
of(count),
of(0).pipe(delay(2000))
))
);
Here's a working StackBlitz demo.
It might even be worth creating a custom operator:
this.count$ = this.store.select('count').pipe(
resetAfterDelay(0, 2000)
);
export function resetAfterDelay<T>(defaultValue: T, delayMs: number) {
return switchMap((value: T) => concat(
of(value),
of(defaultValue).pipe(delay(delayMs))
));
}
StackBlitz
Below is an approach using combineLatest and BehaviorSubject
We are hold a value in a subject and create a timer that emits value 0 after 2s. So we have two Observables, one emits immediately and the other after 2s. We combine this two and the effect is a single observable as desired
valueHolderSubject$ = new BehaviorSubject(0);
...
this.count$ = combineLatest([
store.select("count").pipe(
tap(x => this.valueHolderSubject$.next(x)),
tap(() =>
timer(2000).subscribe({
next: () => {
this.valueHolderSubject$.next(0);
}
})
),
map(() => this.valueHolderSubject$.value),
distinctUntilChanged()
),
this.valueHolderSubject$
]).pipe(map(([, x]) => x));
Demo Here
A per my comments on the answer from BizzyBob, I was getting unreliable results. But I refined it to work using:
this.count$ = merge(
store.select('count'),
store.select('count').pipe(delay(2000), map(_ => 0))
);
See stackblitz: https://stackblitz.com/edit/so-selector-delay-merge-working?file=src/app/my-counter/my-counter.component.ts

Cancel repeated subscription in mergeMap

How to combine the distinct, switchMap and mergeMap operators, so that when the source emits repeated values (detected by distinct.keySelector), the previous subscription is canceled (as in the switchMap), but if the value is not repeated follow the behavior of mergeMap?
Example:
source = from(1, 2, 1, 2, 3) // 'abcde'
result = source.pipe(delay(), combination() // '--cde'
I'm currently doing something like:
const activeSubscriptions = new Map();
source$.pipe(
mergeMap((value) => {
const pendingSubscription = activeSubscriptions.get(value);
if (pendingSubscription) {
pendingSubscription.unsubscribe();
activeSubscriptions.delete(value);
}
const request$ = new Subject();
const subscription = this.service.get(value).subscribe({
complete: () => request$.complete(),
error: (err) => request$.error(err),
next: (value) => request$.next(value),
});
activeSubscriptions.set(value, subscription);
return request$;
})
);
But looking for a better way to do that.
Thank you in advance
I think you can use the windowToggle operator for this:
src$ = src$.pipe(shareReplay(1));
src$.pipe(
ignoreElements(),
windowToggle(src$.pipe(observeOn(asyncScheduler)), openValue => src$.pipe(skip(1), filter(v => v === openValue))),
mergeMap(
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
),
)
A replacement for observeOn(asyncScheduler) could also be delay(0), the important thing is to make sure the order in which the src$'s subscribers receive the value is correct. In this case, we want to make sure that when src$ emits, the clean-up takes place first, so that's why we're using src$.pipe(observeOn(asyncScheduler)).
ignoreElements() is used because each window is paired to only one value, the one which has created the window. The first argument(s) passed to windowToggle will describe the observable(s) which can create the windows. So, we only need those, since we're able to get the last value with the help of
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
By the way, a window is nothing but a Subject.
Lastly, if you want to perform async operations inside the window's pipe, you'll have to make sure that everything is unsubscribed when the window is completed(closed). To do that, you could try this:
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
switchMap(val => /* some async action which uses `val` */),
takeUntil(window.pipe(isEmpty()))
)
where isEmpty will emit either true or false when the source(in this case, the window) completes. false means that the source had emitted at least one value before emitting a complete notification, and true otherwise. In this case, I'd say it's irrelevant whether it's true or false, since the window will not emit any values by itself(because we have used ignoreElements, which ignores everything except error and complete notifications).

RxJS design pattern for multiple async calls

I am new to RxJS and haven't been able to find clear answers on the following use case:
In a mobile app (Angular/Ionic), I need to (1) make simultaneous HTTP calls and only return data when all have completed (like $q.all). I want to (2) throw an error if the calls work correctly but there is a nested value in one of the responses that meets a certain criteria (ie user is not authenticated properly). Because it's a mobile app, I want to (3) build in a few retry attempts if the calls don't work correctly (for any reason). If after a certain number of retry attempts the calls still fail, I want to (4) throw an error.
Based on my research seems like forkJoin works the same as q.all. I have a provider that returns the following (observableArray holds the http calls).
return Observable.forkJoin(observableArray)
And then I can pipe in some operators, which is where I'm starting to struggle. For checking the nested value (2), I am using an underscore method to iterate over each response in my response array. This doesn't seem clean at all.
For retrying the calls (3), I am using retryWhen and delayWhen. But I am unsure how to limit this to 3 or 4 attempts.
And if the limit is hit, how would I throw an error back to the subscribers (4)?
.pipe(
map(
res => {
_.each(res, (obs) => {
if (!obs['success']) throw new Error('success was false')
})
}
),
retryWhen(attempts =>
attempts.pipe(
tap(err => console.log('error:', err),
delayWhen(() => timer(1000))
)
)
))
There are couple of tricks here to make your code clean.
1. Use Observable.iif():
iif accepts a condition function and two Observables. When an Observable returned by the operator is subscribed, condition function will be called. Based on what boolean it returns at that moment, consumer will subscribe either to the first Observable (if condition was true) or to the second (if condition was false).
2. Use JavaScript array native's every():
The every() method tests whether all elements in the array pass the test implemented by the provided function.
3. Use take() to terminate your retryWhen
Emits only the first count values emitted by the source Observable.
So your code boils down to:
.pipe(
switchMap((res) => iif(
() => res.every(({success}) => success),
res,//if every element in res is successful, return it
throwError('success was false') //else if any of it is false, throw error.
)
),
retryWhen(err => err.delay(1000).take(4))
)
Edit:
If you want to catch the error at your subscribe, you will need to rethrow the error. .take() will actually just terminate the sequence, aka completing it:
.pipe(
switchMap((res) => iif(
() => res.every(({success}) => success),
res,//if every element in res is successful, return it
throwError('success was false') //else if any of it is false, throw error.
)
),
retryWhen(err => {
return errors.scan((errorCount, err) =>
(errorCount >= 4 ? //is retried more than 4 times?
throwError(err) : //if yes, throw the error
errorCount++ //else just increment the count
), 0);
})
)
A possibility to consider is to pipe the various operators you need, e.g. retry and map, into each Observable contained in the observableArray you pass to forkJoin.
The code could look like something similar to this
const observableArray = new Array<Observable<any>>();
const maxRetries = 4;
function pipeHttpObservable(httpObs: Observable<any>): Observable<any> {
return httpObs
.pipe(
map(data => data.success ? data : throwError('success was false')),
retryWhen(err => err.delay(1000).take(maxRetries))
)
}
observableArray.push(pipeHttpObservable(httpObs1));
observableArray.push(pipeHttpObservable(httpObs2));
.....
observableArray.push(pipeHttpObservable(httpObsN));
forkJoin(observableArray).subscribe(result => do stuff with the results)

RxJS: How to ignore item in stream when outdated

I'm trying to subscribe to a "loading" Observable which returns a boolean. The goal is to have a delay for the items in the stream if loading is true, but not if loading is false. It should also be possible to ignore an outdated item, lets assume following case:
In the stream are two items:
Item A with value true, and is passed immediately
Item B with value false, and is passed 200ms later
Item A has value true, because of that it will be delayed by 500ms, Item B will not be delayed. In this situation Item B will arrive before Item A. I'm searching for an solution to completely ignore Item A for the mentioned case.
Here you see my current not-working solution:
this.loading$
.pipe(
distinctUntilChanged(),
delayWhen(loading => timer(loading ? 500 : 0))
)
.subscribe(loading => {
// Do something
});
Just replace delayWhen with switchMap and it should work. switchMap can cancel outdated inner stream. And you probably an one-time observable rather than an timer:
this.loading$
.pipe(
distinctUntilChanged(),
switchMap(
loading =>
of(loading).pipe(delay(loading ? 500 : 0)))
)
.subscribe(loading => {
// Do something
});

RxJs Observable: Execute function if empty/filtered

I've got an Observable that listens to some user input from a text box. If the observed string's length is >=3 (filter), it executes some HTTP call (switchMap).
Now I'd like to detect somehow if the user input has been filtered. Reason:
If the HTTP call has been done, it should show the results.
If the user input got filtered (== is invalid), it should clear the results.
Here's the code I'd like to have (see: ifFiltered):
this.userInput.valueChanges
.filter(val => val && val.length >= 3)
.ifFiltered(() => this.results = [])
.switchMap(val => getDataViaHTTP())
.subscribe(val => this.results = val);
I know, I could place that logic within the filter function for this simple example. But what if I have 10 different filters?
Did I miss any method that satisfies my needs?
Thanks in advance!
Either use partition like here RxJS modeling if else control structures with Observables operators
Or instead of filter use map and pipe the object if the former filter condition is true or null otherwise. so you can catch the null where ever you want in your chain with a filter.
Last option call some function in the else part of the filter function
We've had a similar case and tried it with partition as mentioned above but found it much handier to use throw here. So for your code
this.userInput.valueChanges
.do(val => {
if (!val || val.length < 3) {
throw new ValueTooShortError();
}
})
.switchMap(val => getDataViaHTTP())
.do(val => this.results = val)
.catch(() => this.results = [])
.subscribe();
I suggest having a common event stream, creating two filtered streams, and merging the two before subscription:
var o = this.userInput.valueChanges;
var empty= o.filter(t=> t.length < 3)
.map(t=>[])
var nonempty = o.filter(t=> t.length >= 3)
.switchMap(t=> getDataViaHTTP());
empty.merge(nonempty).subscribe(val => this.results = val);
I found another nice solution for my use case using Validators:
(I know that this is no solution using Observables as the question stated. Instead it's using Angular2 features to workaround the problem nicely.)
this.userInput.validator = Validators.compose([
Validators.required,
Validators.minLength(3)
]);
this.userInput.valueChanges
.filter(val => this.userInput.valid)
.switchMap(val => getDataViaHTTP())
.subscribe(val => this.results = val);
Now I can use the userInput.valid property and/or the userInput.statusChanges Observable to keep track of the input value.
May be it's late, but wanted to post for the members still seeking a more cleaner approach to validate IF EMPTY inside .map:
of(fooBar).pipe(
map(
(val) =>
({
...val,
Foo: (val.Bar
? val.Foo.map((e) => ({
title: e.Title,
link: e.Link,
}))
: []) as fooModal[],
}));
This code returns a empty array if val.bar is missing, but it's just an example you can use any validation & expression instead.

Resources