TakeUntil with Checkbox Blocks Stream Even When Evaluation is True - javascript

I'm trying to use takeUntil to stop a stream once a checkbox is switched off. I'm in Angular so for now I'm just concatenating a sting on a class property, not worrying about concatenating results of observables yet.
this.checked$ is a BehaviorSubject that starts false and gets nexted when my checkbox is de/activated. That part is for sure working at least somewhat because below will start to display the dots. However, adding the (commented out below) takeUntil(this.checked$) results in no dots displaying at all.
const source$ = interval(250).pipe(mapTo("."));
this.checked$
.pipe(
filter(c => {
return c === true;
}),
switchMap(() => {
console.log("switchMap");
// return source$.pipe(takeUntil(this.checked$));
return source$;
})
)
.subscribe(data => {
console.log("in subscribe", data, this.dots);
this.dots += data;
});
What is incorrect about how I am using BehaviorSubject and takeUntil here?
Here is a full example is on StackBlitz.

Note that takeUntil emits values only until the first checked$ is emited, here are good examples. Because checked$ is a BehaviorSubject it emits the current value immediately when you subscribe to it, as a result takeUntil stops.
A solution might be to switch between source$ and an empty observable inside the switchMap, like:
const source$ = interval(250).pipe(mapTo("."));
this.checked$
.pipe(
switchMap(c => {
console.log("switchMap");
return c ? source$ : EMPTY;
})
)
.subscribe(data => {
console.log("in subscribe", data, this.dots);
this.dots += data;
});
StackBlitz example
You can also fix your original solution by skipping the first value of checked$ subject, but I recommend you the previous solution because it's simpler.
const source$ = interval(250).pipe(mapTo("."));
this.checked$
.pipe(
filter(c => {
return c === true;
}),
switchMap(() => {
console.log("switchMap");
// return source$.pipe(takeUntil(this.checked$));
return source$.pipe(takeUntil(this.checked$.pipe(skip(1))));
})
)
.subscribe(data => {
console.log("in subscribe", data, this.dots);
this.dots += data;
});
StackBlitz example
Another solution is to use Subject instead of BehaviorSubject. The difference is that the Subject doesn't hold current value and emits nothing when you subscribe to it. It emits only when next() is called. If you replace BehaviorSubject with Subject in your original solution, it will work as expected.
StackBlitz example

Related

RxJS: How to delay next emitted value?

I have async function handleParamsChanges which can take a few seconds to resolve. And I'm calling it when observable emits value:
this._activatedRoute.params
.subscribe(params => {
this.handleParamsChanges(params).then(() => {
// new value can be processed now
});
});
How could I modify my code so that if observable emits 2 values one after another, first handleParamsChanges is called for first value, and only after this promise resolves, it's called with second value, and so on.
Edit:
Here is the solution I came up with, but I'm guessing there is a better way to do this:
const params$ = this._activatedRoute.params;
const canExecute$ = new BehaviorSubject(true);
combineLatest(params$, canExecute$)
.pipe(
filter(([_, canExecute]) => canExecute),
map(([params]) => params),
distinctUntilChanged()
)
.subscribe(async params => {
canExecute$.next(false);
try {
await this.handleParamsChanges(params);
} catch (e) {
console.log(e);
} finally {
canExecute$.next(true);
}
})
I'm using canExecute$ to delay processing of new value.
I need to use distinctUntilChanged here to avoid creating infinite loop.
What you're looking for is concatMap. It waits for the previous "inner observable" to complete before subscribing again. Also you can greatly simplify your pipe:
params$.pipe(
concatMap(params => this.handleParamsChanges(params)),
).subscribe()

rxjs chain can be triggered parallel but internal logic should be synchrounous (.next with concat)

Fields in my case can be finalized (actions linked to a field were executed).
When they are done, I need to update 2 lists:
alreadyExecutedFields: string[] --> plain array
remainingFieldsToExecute: BehaviorSubject<string[]> --> behavior subject, because a .next needs to trigger other logic.
This logic can be triggered parallel, but I want to prevent that because there's a splice within this logic which can behave incorrect then. (splicing an index, that was removed in the parallel chain.)
So when a field needs to be finalized, I call:
this.finalize$.next(field);
And the finalize$ chain looks like this:
this.finalize$.pipe(
concatMap((field: string) => {
return new Promise<void>((resolve) => {
console.log('START', field);
this.alreadyExecutedFields.push(field);
const remainingFieldsToExecute = this.remainingFieldsToExecute$.value;
remainingFieldsToExecute.splice(remainingFieldsToExecute.indexOf(field), 1);
this.remainingFieldsToExecute$.next(remainingFieldsToExecute);
console.log('END', field);
resolve();
});
}),
).subscribe(() => { });
But for some reason, when 2 finalize$.next calls happen right after each other, the concatMap doesn't await the promise of the previous one.
Also when I tried to put a timeout around the END log and the resolve, it doesn't await the previous resolve.
What does work in my case is instead of using a concatMap, using a setInterval with a flag, which locks the part of the code where the lists are being updated.
But how can this be done in a better way? Or in a correct way with or without concat pipes.
to modify alreadyExecutedFields you can use tap operator that used for side-effects
to extract value from remainingFieldsToExecute you can use withLatestFrom
then to modify remainingFieldsToExecute - you can again use tap
alreadyExecutedFields: string[] = [];
remainingFieldsToExecute$: BehaviorSubject<string[]> = new BehaviorSubject<
string[]
>(['1', '2', '3', '4', '5']);
finalize$ = new BehaviorSubject('1');
ngOnInit() {
this.finalize$
.pipe(
tap(field => this.alreadyExecutedFields.push(field)),
withLatestFrom(this.remainingFieldsToExecute$),
tap(([field, remainings]) => {
remainings.splice(remainings.indexOf(field), 1);
this.remainingFieldsToExecute$.next(remainings);
})
)
.subscribe(([_, data]) => {
console.group();
console.log('*** NEW EMIT ***');
console.log('current field:', _);
console.log('remainingFieldsToExecute', data);
console.log('alreadyExecutedFields:', this.alreadyExecutedFields);
console.groupEnd();
});
this.finalize$.next('2');
this.finalize$.next('3');
}
demo: https://stackblitz.com/edit/angular-ivy-ensh7w?file=src/app/app.component.ts
logs:
It could be smplified to:
this.finalize$
.pipe(
mergeMap(field =>
this.remainingFieldsToExecute$.pipe(
map(remFields => {
this.alreadyExecutedFields.push(field);
remFields.splice(remFields.indexOf(field), 1);
return remFields;
})
)
)
)
.subscribe()

RxJS Emit a value only if the first (source) observable emits a value and not other combined observables

So what I want to achieve here is to get the result of combineLatest([subject1$, subject2$]) only when source$ emits a value.
The problem is that whichever operator I use the subscription gets lunched any time subject1$ or subject2$ emits a value (that's how switchMap works). Now, I'm having a hard time in finding a suitable operator for this, I guess I'm looking more for some sort of pattern.
const source$ = fromEvent(document, 'click');
const subject1$ = new Subject() // can emit one or more values in unknown time
const subject2$ = new Subject() // can emit one or more values in unknown time
setInterval(() => { subject1$.next(1) }, 1000)
setInterval(() => { subject2$.next('a') }, 2000)
source$
.pipe(
switchMap(() => combineLatest([subject1$, subject2$])), // obviously does not work
)
.subscribe(([subject1, subject2]) => {
console.log(subject1, subject2);
});
Many thanks,
Use withLatestFrom
source$.pipe(
withLatestFrom(combineLatest([subject1$, subject2$]))
)
.subscribe(([_,[subject1, subject2]]) => {
console.log(subject1, subject2);
})

SwitchMap not working as intended in Angular

I have a search service returning results, if the user submits twice, only the last search should return results.
I have the following code in a service, view (updated elsewhere) has all the post body info needed for the request.
This method is called from elsewhere in the code using this.searchService.getResults()
getResults() {
const view: SearchView = this.getCurrentSearch();
if (!view.query || view.query === '') {
return;
}
this.checkPermissions(view).subscribe(searchView => {
//do some stuff
this.cacheService
.cachedPost(this.url, searchView, () => {
return this.http.post(this.url, searchView);
})
.switchMap(res => this.pageTransform(res))
.subscribe(
// results here
);
});
}
`
From running tests I can see that if I fire 3 searches in quick succession, they all resolve, whereas I only want the last to.
That's because you're calling the method getResults() multiple times which creates multiple Rx chains and all of them are processed. If you want switchMap to work correctly you need to keep a reference to only one chain and push values to it:
private search$ = new Subject();
private searchSubscription = search$
.switchMap(view => this.checkPermissions(view)
.concatMap(searchView => this.cacheService.cachedPost(this.url, searchView, () => {
return this.http.post(this.url, searchView);
})
.concatMap(res => this.pageTransform(res))
)
.subscribe(...);
getResults() {
const view: SearchView = this.getCurrentSearch();
...
this.search$.next(view);
}
I didn't test this code for obvious reasons but I think you'll get the point. Also don't forget to unsubscribe in ngOnDestroy() with this.searchSubscription.unsubscribe().

RxJS: show loading if request is slow

I thought of using RxJS to solve elegantly this problem, but after trying various approaches, I couldn't find out how to do it...
My need is quite common: I do a Rest call, ie. I have a Promise.
If the response comes quickly, I just want to use the result.
If it is slow to come, I want to display a spinner, until the request completes.
This is to avoid a flash of a the spinner, then the data.
Maybe it can be done by making two observables: one with the promise, the other with a timeout and showing the spinner as side effect.
I tried switch() without much success, perhaps because the other observable doesn't produce a value.
Has somebody implemented something like that?
Based on #PhiLho's answer, I wrote a pipeable operator, which does exactly that:
export function executeDelayed<T>(
fn : () => void,
delay : number,
thisArg? : any
) : OperatorFunction<T, T> {
return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
let timerSub = timer(delay).subscribe(() => fn());
return source.pipe(
tap(
() => {
timerSub.unsubscribe();
timerSub = timer(delay).subscribe(() => fn());
},
undefined,
() => {
timerSub.unsubscribe();
}
)
);
}
}
Basically it returns a function, which gets the Observable source.
Then it starts a timer, using the given delay.
If this timer emits a next-event, the function is called.
However, if the source emits a next, the timer is cancelled and a new one is startet.
In the complete of the source, the timer is finally cancelled.
This operator can then be used like this:
this.loadResults().pipe(
executeDelayed(
() => this.startLoading(),
500
)
).subscribe(results => this.showResult())
I did not wirte many operators myself, so this operator-implementation might not be the best, but it works.
Any suggestions on how to optimize it are welcome :)
EDIT:
As #DauleDK mentioned, a error won't stop the timer in this case and the fn will be called after delay. If thats not what you want, you need to add an onError-callback in the tap, which calls timerSub.unsubscribe():
export function executeDelayed<T>(
fn : () => void,
delay : number,
thisArg? : any
) : OperatorFunction<T, T> {
return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
let timerSub = timer(delay).subscribe(() => fn());
return source.pipe(
tap(
() => {
timerSub.unsubscribe();
timerSub = timer(delay).subscribe(() => fn());
},
() => timerSub.unsubscribe(), // unsubscribe on error
() => timerSub.unsubscribe()
)
);
}
}
Here is an example that I have used. We assume here that you get the data that you want to send to the server as an Observable as well, called query$. A query coming in will then trigger the loadResults function, which should return a promise and puts the result in the results$ observable.
Now the trick is to use observable$.map(() => new Date()) to get the timestamp of the last emitted value.
Then we can compare the timestamps of the last query and the last response that came in from the server.
Since you also wanted to not only show a loading animation, but wanted to wait for 750ms before showing the animation, we introduce the delayed timestamp. See the comments below for a bit more explanation.
At the end we have the isLoading$ Observable that contains true or false. Subscribe to it, to get notified when to show/hide the loading animation.
const query$ = ... // From user input.
const WAIT_BEFORE_SHOW_LOADING = 750;
const results$ = query$.flatMapLatest(loadResults);
const queryTimestamp$ = query$.map(() => new Date());
const resultsTimestamp$ = results$.map(() => new Date());
const queryDelayTimestamp$ = (
// For every query coming in, we wait 750ms, then create a timestamp.
query$
.delay(WAIT_BEFORE_SHOW_LOADING)
.map(() => new Date())
);
const isLoading$ = (
queryTimestamp$.combineLatest(
resultsTimestamp$,
queryDelayTimestamp$,
(queryTimestamp, resultsTimestamp, delayTimestamp) => {
return (
// If the latest query is more recent than the latest
// results we got we can assume that
// it's still loading.
queryTimestamp > resultsTimestamp &&
// But only show the isLoading animation when delay has passed
// as well.
delayTimestamp > resultsTimestamp
);
}
)
.startWith(false)
.distinctUntilChanged()
);
OK, thinking more about it in my commuting, I found a solution...
You can find my experiment ground at http://plnkr.co/edit/Z3nQ8q
In short, the solution is to actually subscribe to the observable handing the spinner (instead of trying to compose it in some way).
If the result of the Rest request comes before the observable fires, we just cancel the spinner's disposable (subscription), so it does nothing.
Otherwise, the observable fires and display its spinner. We can then just hide it after receiving the response.
Code:
function test(loadTime)
{
var prom = promiseInTime(loadTime, { id: 'First'}); // Return data after a while
var restO = Rx.Observable.fromPromise(prom);
var load = Rx.Observable.timer(750);
var loadD = load.subscribe(
undefined,
undefined,
function onComplete() { show('Showing a loading spinner'); });
restO.subscribe(
function onNext(v) { show('Next - ' + JSON.stringify(v)); },
function onError(e) { show('Error - ' + JSON.stringify(e)); loadD.dispose(); },
function onComplete() { show('Done'); loadD.dispose(); }
);
}
test(500);
test(1500);
Not sure if that's an idiomatic way of doing this with RxJS, but it seems to work...
Other solutions are welcome, of course.
Just before fetching the data, ie. creating the spinner, set timeout for a function, which creates the spinner. Lets say you are willing to wait half a second, until showing spinner... it would be something like:
spinnerTimeout = setTimeout(showSpinner, 500)
fetch(url).then(data => {
if (spinner) {
clearTimeout(spinnerTimeout) //this is critical
removeSpinnerElement()
}
doSomethingWith(data)
});
EDIT: if it's not obvious, clearTimer stops the showSpinner from executing, if the data arrived sooner than 500ms(ish).
Here is my solution :
public static addDelayedFunction<T>(delayedFunction: Function, delay_ms: number): (mainObs: Observable<T>) => Observable<T> {
const stopTimer$: Subject<void> = new Subject<void>();
const stopTimer = (): void => {
stopTimer$.next();
stopTimer$.complete();
};
const catchErrorAndStopTimer = (obs: Observable<T>): Observable<T> => {
return obs.pipe(catchError(err => {
stopTimer();
throw err;
}));
};
const timerObs: Observable<any> = of({})
.pipe(delay(delay_ms))
.pipe(takeUntil(stopTimer$))
.pipe(tap(() => delayedFunction()));
return (mainObs: Observable<T>) => catchErrorAndStopTimer(
of({})
.pipe(tap(() => timerObs.subscribe()))
.pipe(mergeMap(() => catchErrorAndStopTimer(mainObs.pipe(tap(stopTimer)))))
);
}

Categories