I'm using rxjs to connect to a WebSocket service, and in case of failure, I want to retry 3 times, wait 30 seconds, then repeat infinitely, how can I do this?
I found a solution, first, create the following operator:
function retryWithDelay<T>(
repetitions: number,
delay: number
): (a: Observable<T>) => Observable<T> {
let count = repetitions;
return (source$: Observable<T>) =>
source$.pipe(
retryWhen((errors) =>
errors.pipe(
delayWhen(() => {
count--;
if (count === 0) {
count = repetitions;
return timer(delay);
}
return timer(0);
})
)
)
);
}
Then, use use it like this:
function connect(url: string) {
return webSocket({ url })
.pipe(retryWithDelay(3, 30000));
}
You can do this by doing the following:
//emit value every 200ms
const source = Rx.Observable.interval(200);
//output the observable
const example = source
.map(val => {
if (val > 5) {
throw new Error('The request failed somehow.');
}
return val;
})
.retryWhen(errors => errors
//log error message
.do(val => console.log(`Some error that occur ${val}, pauze for 30 seconds.`))
//restart in 30 seconds
.delayWhen(val => Rx.Observable.timer(30 * 1000))
);
const subscribe = example
.subscribe({
next: val => console.log(val),
error: val => console.log(`This will never happen`)
});
See the working example: https://jsbin.com/goxowiburi/edit?js,console
Be aware that this is an infinite loop and you are not introducing unintended consequences into your code.
Related
What is the correct way in RXJS to remap an observable into a timer start value, without interrupting the original stream?
obs.pipe(take(1000), startTimer())
.subscribe(start => {
// show how long it took to finish streaming 1000 values:
const duration = Date.now() - start;
console.log(duration);
});
I want startTimer to remap into once-off subscription with start, but without interrupting the original stream, i.e. in this case subscribe is to be triggered only after all 1000 values have finished streaming.
How do I implement such startTimer? It's supposed to result into a once-off Date.now() value to help measure full stream duration.
Or is there maybe a standard solution for this already that I'm missing?
update-1
The expected result is like the one below, but without the need for creating start as an external variable, and instead make it part of the stream:
const start = Date.now();
obs.pipe(take(1000))
.subscribe({
complete() {
const duration = Date.now() - start;
console.log(duration);
}
});
The reason I want to make it part of a stream is because the original observable and subscribers are very much detached from each other, as in sitting in unrelated source files.
P.S. Alternatively, a solution that emits duration in the end would also be good, if that is at all possible.
update-2
In the end, I used a generic drain operand, designed to drain an observable stream, and then produce an observable at the end:
/**
* Drains the source observable till it completes, and then posts a new value-observable.
*/
function drain<T>(value: T | Observable<T> | (() => T | Observable<T>)) {
const v = () => {
const a = typeof value === 'function' ? value.call(null) : value;
return a instanceof Observable ? a : of(a);
}
return s => defer(() => s.pipe(filter(_ => false), c => concat(c, v()))) as Observable<T>;
}
Using this operand, I can rewrite startTimer like this:
const startTimer = () => drain(Date.now);
Some code that does what you describe pretty much exactly the way you describe it:
function logRunTime<T>(prefix: string): MonoTypeOperatorFunction<T> {
return s => defer(() => {
const start = Date.now();
return s.pipe(
tap({
complete: () => console.log(`${prefix}: ${Date.now() - start}ms`)
})
);
});
}
interval(1000).pipe(
take(10),
logRunTime("Ten Seconds of Interval")
).subscribe(console.log);
Output:
0
1
2
3
4
5
6
7
8
9
Ten Seconds of Interval: 10014ms
Update 1
do not make the original observable stop emitting values [...] we just do not want the source values
It seem to me that either you keep emitting the values or you don't.
Here is a version that drops the source emissions.
Is this what you're after?
function reduceRunTime<T>(prefix: string): OperatorFunction<T, string> {
return s => defer(() => {
const start = Date.now();
return s.pipe(
filter(_ => false),
c => concat(c, of(null)),
map(_ => `${prefix}: ${Date.now() - start}ms`)
);
}) as Observable<string>;
}
interval(1000).pipe(
take(10),
reduceRunTime("Ten Seconds of Interval")
).subscribe(console.log);
Output:
Ten Seconds of Interval: 10013ms
Update 2
If you don't want a string, this will emit the start time once the observable completes.
function startTimer() {
return s => s.pipe(
filter(_ => false),
c => concat(c, of(Date.now()))
) as Observable<number>;
}
Update 3
Two separate behaviours
I think update 2 may have been cleaned up too much. Consider this example:
const timed$ = interval(500).pipe(
take(5),
startTimer()
);
const logDiff = (start: number) => console.log(Date.now() - start);
timed$.subscribe(logDiff);
setTimeout(() => {
timed$.subscribe(logDiff);
}, 1000);
setTimeout(() => {
timed$.subscribe(logDiff);
}, 5000);
The output:
2521
3507
7511
Notably, because Observables are lazy (do nothing until subscribed), but Date.now is called when the observable is created. Your startTime may well be set long before the observable even starts. Making a 2.5s observable appear to require 7.5s.
Using defer fixes this problem as it doesn't create the observable until it is subscribed.
Updated startTimer
function startTimer() {
return s => defer(() => s.pipe(
filter(_ => false),
c => concat(c, of(Date.now()))
)) as Observable<number>;
}
New output for example above:
2521
2507
2511
Now you can do fun things like run the same observable 10 times and average out the runtime to get a better idea of how long it will take.
const average = arr => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length;
concat(...Array.from(Array(10)).map(_ => timed$)).pipe(
map(start => Date.now() - start),
tap(console.log),
toArray()
).subscribe(runs => console.log("Average Runtime: ", average(runs)));
Output:
2515
2506
2506
2506
2507
2505
2506
2506
2507
2507
Average Runtime: 2507.1
I added a start, stop, pause button. Start will start a count down timer which will start from a value, keep decrementing until value reaches 0. We can pause the timer on clicking the pause button. On click of Stop also timer observable completes.
However, once the timer is completed ( either when value reaches 0 or
when clicked on stop button ), I am not able to start properly. I
tried adding repeatWhen operator. It starts on clicking twice. Not at
the first time.
Also, at stop, value is not resetting back to the initial value.
const subscription = merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
tap(val => {
console.log(val);
}),
switchMap(val => (val ? interval(10).pipe(takeUntil(stopClick$)) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
repeatWhen(() => startClick$),
startWith(startValue)
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Stackblitz Code link is available here
This is a pretty complicated usecase. There are two issues I think:
You have two subscriptions to startClick$ and the order of subscriptions matters in this case. When the chain completes repeatWhen is waiting for startClick$ to emit. However, when you click the button the emission is first propagated into the first subscription inside merge(...) and does nothing because the chain has already completed. Only after that it resubscribes thanks to repeatWhen but you have to press the button again to trigger the switchMap() operator.
When you use repeatWhen() it'll resubscribe every time the inner Observable emits so you want it to emit on startClick$ but only once. At the same time you don't want it to complete so you need to use something like this:
repeatWhen(notifier$ => notifier$.pipe(
switchMap(() => startClick$.pipe(take(1))),
)),
So to avoid all that I think you can just complete the chain using takeUntil(stopClick$) and then immediatelly resubscribe with repeat() to start over.
merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
switchMap(val => (val ? interval(10) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
startWith(startValue),
takeUntil(stopClick$),
repeat(),
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Your updated demo: https://stackblitz.com/edit/rxjs-tum4xq?file=index.ts
Here's an example stopwatch that counts up instead of down. Perhaps you can re-tool it.
type StopwatchAction = "START" | "STOP" | "RESET" | "END";
function createStopwatch(
control$: Observable<StopwatchAction>,
interval = 1000
): Observable<number>{
return defer(() => {
let toggle: boolean = false;
let count: number = 0;
const ticker = timer(0, interval).pipe(
map(x => count++)
);
const end$ = of("END");
return concat(
control$,
end$
).pipe(
catchError(_ => end$),
switchMap(control => {
if(control === "START" && !toggle){
toggle = true;
return ticker;
}else if(control === "STOP" && toggle){
toggle = false;
return EMPTY;
}else if(control === "RESET"){
count = 0;
if(toggle){
return ticker;
}
}
return EMPTY;
})
);
});
}
Here's an example of this in use:
const start$: Observable<StopwatchAction> = fromEvent(startBtn, 'click').pipe(mapTo("START"));
const reset$: Observable<StopwatchAction> = fromEvent(resetBtn, 'click').pipe(mapTo("RESET"));
createStopwatch(merge(start$,reset$)).subscribe(seconds => {
secondsField.innerHTML = seconds % 60;
minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
hoursField.innerHTML = Math.floor(seconds / 3600);
});
You can achieve that in another way without completing the main observable or resubscribing to it using takeUntil, repeatWhen, or other operators, like the following:
create a simple state to handle the counter changes (count, isTicking)
merge all the observables that affecting the counter within one observable.
create intermediate observable to interact with the main merge observable (start/stop counting).
interface CounterStateModel {
count: number;
isTicking: boolean;
}
// Setup counter state
const initialCounterState: CounterStateModel = {
count: startValue,
isTicking: false
};
const patchCounterState = new Subject<Partial<CounterStateModel>>();
const counterCommands$ = merge(
startClick$.pipe(mapTo({ isTicking: true })),
pauseBtn$.pipe(mapTo({ isTicking: false })),
stopClick$.pipe(mapTo({ ...initialCounterState })),
patchCounterState.asObservable()
);
const counterState$: Observable<CounterStateModel> = counterCommands$.pipe(
startWith(initialCounterState),
scan(
(counterState: CounterStateModel, command): CounterStateModel => ({
...counterState,
...command
})
),
shareReplay(1)
);
const isTicking$ = counterState$.pipe(
map(state => state.isTicking),
distinctUntilChanged()
);
const commandFromTick$ = isTicking$.pipe(
switchMap(isTicking => (isTicking ? timer(0, 10) : NEVER)),
withLatestFrom(counterState$, (_, counterState) => ({
count: counterState.count
})),
tap(({ count }) => {
if (count) {
patchCounterState.next({ count: count - 1 });
} else {
patchCounterState.next({ ...initialCounterState });
}
})
);
const commandFromReset$ = stopClick$.pipe(mapTo({ ...initialCounterState }));
merge(commandFromTick$, commandFromReset$)
.pipe(startWith(initialCounterState))
.subscribe(
state => (counterDisplayHeader.innerHTML = state.count.toString())
);
Also here is the working version:
https://stackblitz.com/edit/rxjs-o86zg5
I perform http requests to my db and have noticed that if I send all the requests at once, some of them will get a timeout errors. I'd like to add a delay between calls so the server doesn't get overloaded. I'm trying to find the RxJS solution to this problem and don't want to add a setTimeout.
Here is what I currently do:
let observables = [];
for(let int = 0; int < 10000; int++){
observables.push(new Observable((observer) => {
db.add(doc[int], (err, result)=>{
observer.next();
observer.complete();
})
}))
}
forkJoin(observables).subscribe(
data => {
},
error => {
console.log(error);
},
() => {
db.close();
}
);
You can indeed achieve this with Rxjs quite nicely. You'll need higher order observables, which means you'll emit an observable into an observable, and the higher order observable will flatten this out for you.
The nice thing about this approach is that you can easily run X requests in // without having to manage the pool of requests yourself.
Here's the working code:
import { Observable, Subject } from "rxjs";
import { mergeAll, take, tap } from "rxjs/operators";
// this is just a mock to demonstrate how it'd behave if the API was
// taking 2s to reply for a call
const mockDbAddHtppCall = (id, cb) =>
setTimeout(() => {
cb(null, `some result for call "${id}"`);
}, 2000);
// I have no idea what your response type looks like so I'm assigning
// any but of course you should have your own type instead of this
type YourRequestType = any;
const NUMBER_OF_ITEMS_TO_FETCH = 10;
const calls$$ = new Subject<Observable<YourRequestType>>();
calls$$
.pipe(
mergeAll(3),
take(NUMBER_OF_ITEMS_TO_FETCH),
tap({ complete: () => console.log(`All calls are done`) })
)
.subscribe(console.log);
for (let id = 0; id < NUMBER_OF_ITEMS_TO_FETCH; id++) {
calls$$.next(
new Observable(observer => {
console.log(`Starting a request for ID "${id}""`);
mockDbAddHtppCall(id, (err, result) => {
if (err) {
observer.error(err);
} else {
observer.next(result);
observer.complete();
}
});
})
);
}
And a live demo on Stackblitz: https://stackblitz.com/edit/rxjs-z1x5m9
Please open the console of your browser and note that the console log showing when a call is being triggered starts straight away for 3 of them, and then wait for 1 to finish before picking up another one.
Looks like you could use an initial timer to trigger the http calls. e.g.
timer(delayTime).pipe(combineLatest(()=>sendHttpRequest()));
This would only trigger the sendHttpRequest() method after the timer observable had completed.
So with your solution. You could do the following...
observables.push(
timer(delay + int).pipe(combineLatest(new Observable((observer) => {
db.add(doc[int], (err, result)=>{
observer.next();
observer.complete();
}))
}))
Where delay could probably start off at 0 and you could increase it using the int index of your loop by some margin.
Timer docs: https://www.learnrxjs.io/learn-rxjs/operators/creation/timer
Combine latest docs: https://www.learnrxjs.io/learn-rxjs/operators/combination/combinelatest
merge with concurrent value:
mergeAll and mergeMap both allow you to define the max number of subscribed observables. mergeAll(1)/mergeMap(LAMBDA, 1) is basically concatAll()/concatMap(LAMBDA).
merge is basically just the static mergeAll
Here's how you might use that:
let observables = [...Array(10000).keys()].map(intV =>
new Observable(observer => {
db.add(doc[intV], (err, result) => {
observer.next();
observer.complete();
});
})
);
const MAX_CONCURRENT_REQUESTS = 10;
merge(...observables, MAX_CONCURRENT_REQUESTS).subscribe({
next: data => {},
error: err => console.log(err),
complete: () => db.close()
});
Of note: This doesn't batch your calls, but it should solve the problem described and it may be a bit faster than batching as well.
mergeMap with concurrent value:
Perhaps a slightly more RxJS way using range and mergeMap
const MAX_CONCURRENT_REQUESTS = 10;
range(0, 10000).pipe(
mergeMap(intV =>
new Observable(observer => {
db.add(doc[intV], (err, result) => {
observer.next();
observer.complete();
});
}),
MAX_CONCURRENT_REQUESTS
)
).subscribe({
next: data => {},
error: err => console.log(err),
complete: () => db.close()
});
I'm using Angular (9) powered Bootstrap (6.1.0) TypeAhead and defining its search function like so:
search = (text$: Observable<string>) => {
return text$.pipe(
debounceTime(200),
distinctUntilChanged(),
// switchMap allows returning an observable rather than maps array
switchMap((searchText) => {
if (!searchText || searchText.trim().length == 0) {
// when the user erases the searchText
this.dealerRepUserID = 0;
this.dealerRepChanging.emit(this.dealerRepUserID);
return EMPTY;
}
else if (this.dealerID == this.hostOrganizationID) {
// get a list of host reps
return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive);
} else {
// get a list of dealer reps
return this.myService.getDealerReps(this.dealerID, searchText);
}
})
);
}
The function must return an Observable. How do I catch an error thrown inside the switchMap?
Have you try the catchError
import { catchError } from 'rxjs/operators';
return text$.pipe(
debounceTime(200),
distinctUntilChanged(),
// switchMap allows returning an observable rather than maps array
switchMap((searchText) => {
if (!searchText || searchText.trim().length == 0) {
// when the user erases the searchText
this.dealerRepUserID = 0;
this.dealerRepChanging.emit(this.dealerRepUserID);
return EMPTY;
}
else if (this.dealerID == this.hostOrganizationID) {
// get a list of host reps
return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive).pipe(catchError(error => of());
} else {
// get a list of dealer reps
return this.myService.getDealerReps(this.dealerID, searchText).pipe(catchError(error => of());
}
})
);
Here is my app effect
public loadDataPerformance$: Observable<Action> = createEffect(() => {
return this.actions$.pipe(
ofType(RiskProfileActions.loadDataPerformance),
withLatestFrom(
this.store$.select(fromRoot.getAnalyticsFilterSelectedOptions),
this.store$.pipe(select(fromFactoryPerformance.getFactoryId))
),
switchMap(([{ recordDate }, filters, factoryId]) =>
this.riskProfileApiService.getDataPerformanceData(filters, factoryId, recordDate).pipe(
map((riskDataPerformanceData: PerformanceDataModel) =>
RiskProfileActions.loadRiskScoreBreakdownPerformanceSuccess(riskDataPerformanceData)
),
catchError(error => of(RiskProfileActions.loadRiskScoreBreakdownPerformanceFail(error)))
)
)
);
});
The switchMap by itself won't throw any error, the thing that might do something unexpected are the returned observables this.myService.getHostRepsAutoComplete and this.myService.getDealerReps. A tricky moment with the catching errors is that whenever there is an error the observable which is throwing the error is being killed.
For example
observable$.pipe(
switchMap(() => observable2$),
catchError(() => doSomethingFunction())
).subscribe()
observable$ will be completed once there is an error, that will complete your search stream and you will get no more data after the error.
As Phat Tran Ky showed in his example the handling of errors should happen inside the new streams in the switchMap operator
observable$.pipe(
switchMap(() => observable2$.pipe(catchError(() => doSomethingFunction())),
)
).subscribe()
By doing so whenever there is an error thrown from inside it will kill the inner observable (observable2$) but won't kill the outer subscriptions on the outer observable observable$
A further enhancement that you can do in order to handle your errors in one spot might be to merge the inner observable in one, for example, something like
observable$.pipe(
switchMap(() => {
return merge(
observable1$.pipe(filter(() => ${your if else condition for case 1})),
observable2$.pipe(filter(() => ${your if else condition for case 2})),
observable3$.pipe(filter(() => ${your if else condition for case 3})),
).pipe(catchError((error) => yourErrorHandlerFunction(error)))
})),
)
).subscribe()
I have the recursive function: repeatAlert that is called again if data.answered === null:
....
Edit
this.repeatAlert(id).subscribe( val => console.log(val));
console.log('1stCall Alert: ', new Date().getMinutes());
....
find(id: number): Observable<any> {
return this.http.get(`${this.resourceUrl}ByAlertId/${id}`
}
repeatAlert(id: number) {
this.find(id).subscribe((data: AlertInt) => {
if (data.answered === null ) {
this.sendNotification('Alert ', data.text);
console.log('Call Alert: ', new Date().getMinutes(), data.id);
setTimeout(() => {
if (data.answered === null) {
this.repeatAlert(id);
}
}, data.repeating * 1000 * 60);
}
});
}
When I change the value of data.answered in the database, I can't read with this observable find(id) the change of data.answered. So it keeps calling repeatAlert forever ...
What am I doing wrong?
Extra question: Is it better a loop or recursive function ?
You are doing polling. I suggest something like following:
find(id: number): Observable<any> {
return this.http.get(`${this.resourceUrl}ByAlertId/${id}`;
}
repeatAlert(id: number) {
// request data from the endpoint and execute necessary code
const data$ = this.find(id).pipe(
tap(data => {
if (data.answered === null) {
this.sendNotification('Alert ', data.text);
}
})
);
// polling will start if on the first try we don't have data.answered
const startPolling = (duration: number) => timer(duration, duration).pipe(
//take(10), // let's say we want to stop after 10 tries
concatMap(() => data$),
takeWhile(data => data.answered === null), // when to stop polling
);
// if data.answered is null on the first try switch to polling otherwise end
return data$.pipe(
switchMap(data => data.answered === null ?
startPolling(data.repeating * 1000 * 60) :
of(data)
),
);
}
Also note that I changed your repeatAlert, it's better to return Observable from the method and subscribe yourself to avoid memory leaks. You should subscribe and unsubscribe yourself. Also, I suggest you to use take(10) for example so that polling doesn't continue indefinitely, it's up to you.
timer(dueTime, period) works like this: It will emit first event after dueTime and continue emitting events after every period.
Edit takeWhile condition is true and not condition is false
I turns out that this code is also working
repeatAlert(id: number) {
this.alertServ.find(id).subscribe((data: AlertInt) => {
if (data.answered === null) {
this.sendNotification( 'Alert ', data.text);
setTimeout(() => {
this.repeatAlert(id);
}, data.repeating * 1000 * 60);
}
});
}
I forget in the backend to send the data.answered field ... so was always null