Recursive function with timeout and check RxJs Observable value polling - rxjs

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

Related

Angular 11 how to make one of the http request in higher order mapping conditionally

I want to use a better solution if exists so it will make the following simpler.
If this.projectguid is empty then want to use the switchMap call otherwise the other one.
Can anyone suggest me a better solution for this?
getWorksheet(): Observable<Worksheet | number> {
if(this.projectsGuid === '') {
return this.apiService.get('projects/latest')
.pipe(
switchMap((res: {id: string, name: string}) => {
this.projectsGuid = res.id
let getUrl = `projects/${this.projectsGuid}/questionnaires/${this.questionnaireId}/worksheets/latest`;
return this.apiService.get<Worksheet>(getUrl).pipe(
catchError((err) => {
return of(err.status);
})
);
})
)
} else {
let getUrl = `projects/${this.projectsGuid}/questionnaires/${this.questionnaireId}/worksheets/latest`;
return this.apiService.get<Worksheet>(getUrl).pipe(
catchError((err) => {
return of(err.status);
})
);
}
}
You could define a projectGuid$ observable that emits the this.projectsGuid value if not empty, and otherwise emits the result of the http call (mapped to just the id):
const projectGuid$ = this.projectsGuid !== ''
? of(this.projectsGuid)
: this.apiService.get('projects/latest').pipe(
map(({id}) => id),
tap(guid => this.projectsGuid = guid)
);
Then you can pipe the projectGuid to the call to fetch the worksheet:
return projectGuid$.pipe(
map(guid => `projects/${guid}/questionnaires/${this.questionnaireId}/worksheets/latest`),
switchMap(url => this.apiService.get<Worksheet>(url).pipe(
catchError(err => of(err.status))
))
);

Rxjs, retry 3 times, wait 30 seconds, then repeat

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.

RxJs - how to make observable behave like queue

I'm trying to achieve next:
private beginTransaction(): Observable() {
..
}
private test(): void {
this.beginTransaction().subscribe((): void => {
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
this.commitTransaction();
});
}
beginTransaction can be called concurrently, but should delay the observable until first or only one beginTransaction finished.
In order words: Only one transaction can be in progress at any time.
What have I tried:
private transactionInProgress: boolean = false;
private canBeginTransaction: Subject<void> = new Subject<void>();
private bla3(): void {
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 1');
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 2');
this.commitTransaction();
});
this.beginTransaction().subscribe((): void => {
console.log('beginTransaction 3');
this.commitTransaction();
});
}
private commitTransaction(): void {
this.transactionInProgress = false;
this.canBeginTransaction.next();
}
private beginTransaction(): Observable<void> {
if(this.transactionInProgress) {
return of(undefined)
.pipe(
skipUntil(this.canBeginTransaction),
tap((): void => {
console.log('begin transaction');
})
);
}
this.transactionInProgress = true;
return of(undefined);
}
What you've asked about is pretty vague and general. Without a doubt, a more constrained scenario could probably look a whole lot simpler.
Regardless, here I create a pipeline that only lets transaction(): Observable be subscribed to once at a time.
Here's how that might look:
/****
* Represents what each transaction does. Isn't concerned about
* order/timing/'transactionInProgress' or anything like that.
*
* Here is a fake transaction that just takes 3-5 seconds to emit
* the string: `Hello ${name}`
****/
function transaction(args): Observable<string> {
const name = args?.message;
const duration = 3000 + (Math.random() * 2000);
return of("Hello").pipe(
tap(_ => console.log("starting transaction")),
switchMap(v => timer(duration).pipe(
map(_ => `${v} ${name}`)
)),
tap(_ => console.log("Ending transation"))
);
}
// Track transactions
let currentTransactionId = 0;
// Start transactions
const transactionSubj = new Subject<any>();
// Perform transaction: concatMap ensures we only start a new one if
// there isn't a current transaction underway
const transaction$ = transactionSubj.pipe(
concatMap(({id, args}) => transaction(args).pipe(
map(payload => ({id, payload}))
)),
shareReplay(1)
);
/****
* Begin a new transaction, we give it an ID since transactions are
* "hot" and we don't want to return the wrong (earlier) transactions,
* just the current one started with this call.
****/
function beginTransaction(args): Observable<any> {
return defer(() => {
const currentId = currentTransactionId++;
transactionSubj.next({id: currentId, args});
return transaction$.pipe(
first(({id}) => id === currentId),
map(({payload}) => payload)
);
})
}
// Queue up 3 transactions, each one will wait for the previous
// one to complete before it will begin.
beginTransaction({message: "Dave"}).subscribe(console.log);
beginTransaction({message: "Tom"}).subscribe(console.log);
beginTransaction({message: "Tim"}).subscribe(console.log);
Asynchronous Transactions
The current setup requires transactions to be asynchronous, or you risk losing the first one. The workaround for that is not simple, so I've built an operator that subscribes, then calls a function as soon as possible after that.
Here it is:
function initialize<T>(fn: () => void): MonoTypeOperatorFunction<T> {
return s => new Observable(observer => {
const bindOn = name => observer[name].bind(observer);
const sub = s.subscribe({
next: bindOn("next"),
error: bindOn("error"),
complete: bindOn("complete")
});
fn();
return {
unsubscribe: () => sub.unsubscribe
};
});
}
and here it is in use:
function beginTransaction(args): Observable<any> {
return defer(() => {
const currentId = currentTransactionId++;
return transaction$.pipe(
initialize(() => transactionSubj.next({id: currentId, args})),
first(({id}) => id === currentId),
map(({payload}) => payload)
);
})
}
Aside: Why Use defer?
Consider re-writting beginTransaction:
function beginTransaction(args): Observable<any> {
const currentId = currentTransactionId++;
return transaction$.pipe(
initialize(() => transactionSubj.next({id: currentId, args})),
first(({id}) => id === currentId),
map(({payload}) => payload)
);
}
In this case, the ID is set at the moment you invoke beginTransaction.
// The ID is set here, but it won't be used until subscribed
const preppedTransaction = beginTransaction({message: "Dave"});
// 10 seconds later, that ID gets used.
setTimeout(
() => preppedTransaction.subscribe(console.log),
10000
);
If transactionSubj.next is called without the initialize operator, then this problem gets even worse as transactionSubj.next would also get called 10 seconds before the observable is subscribed to (You're sure to miss the output)
The problems continue:
What if you want to subscribe to the same observable twice?
const preppedTransaction = beginTransaction({message: "Dave"});
preppedTransaction.subscribe(
value => console.log("First Subscribe: ", value)
);
preppedTransaction.subscribe(
value => console.log("Second Subscribe: ", value)
);
I would expect the output to be:
First Subscribe: Hello Dave
Second Subscribe: Hello Dave
Instead, you get
First Subscribe: Hello Dave
First Subscribe: Hello Dave
Second Subscribe: Hello Dave
Second Subscribe: Hello Dave
Because you don't get a new ID on subscribing, the two subscriptions share one ID. defer fixes this problem by not assigning an id until subscription. This becomes seriously important when managing errors in streams (letting you re-try an observable after it errors).
I am not sure I have understood the problem right, but it looks to me as concatMap is the operator you are looking for.
An example could be the following
const transactionTriggers$ = from([
't1', 't2', 't3'
])
function processTransation(trigger: string) {
console.log(`Start processing transation triggered by ${trigger}`)
// do whatever needs to be done and then return an Observable
console.log(`Transation triggered by ${trigger} processing ......`)
return of(`Transation triggered by ${trigger} processed`)
}
transactionTriggers$.pipe(
concatMap(trigger => processTransation(trigger)),
tap(console.log)
).subscribe()
You basically start from a stream of events, where each event is supposed to trigger the processing of the transaction.
Then you use processTransaction function to do whatever you have to do to process a transaction. processTransactio needs to return an Observable which emits the result of the processing when the transaction has been processed and then completes.
Then in the pipe you can use tap to do further stuff with the result of the processing, if required.
You can try the code in this stackblitz.

Angular 9/rxjs: How to handle an error thrown inside switchMap?

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()

Re-execute async RxJS stream after delay

I'm using RxJS 6 to lazily step through iterable objects using code similar to example running below. This is working well but I'm having trouble solving my final use case.
Full code here
import { EMPTY, defer, from, of } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator (iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => result.done ? EMPTY : of(result.value))
);
}
function iterateValues ({ params }) {
const { values, delay: delayMilliseconds } = params;
const isIterable = typeof values[Symbol.iterator] === "function";
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
if (isIterable && delayMilliseconds > 0) {
const iterator = values[Symbol.iterator]();
// The first value is emitted immediately, the rest are emitted after time.
return stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
);
} else {
return from(values);
}
}
const options = {
params: {
// Any iterable object is walked manually. Otherwise delegate to `from()`.
values: ["Mary", "had", "a", "little", "lamb"],
// Delay _between_ values.
delay: 350,
// Delay before the stream restarts _after the last value_.
runAgainAfter: 1000,
}
};
iterateValues(options)
// Is not repeating?!
.pipe(repeat(3))
.subscribe(
v => {
console.log(v, Date.now());
},
console.error,
() => {
console.log('Complete');
}
);
I'd like to add in another option which will re-execute the stream, an indefinite number of times, after a delay (runAgainAfter). I'm having trouble composing this in cleanly without factoring the result.done case deeper. So far I've been unable to compose the run-again behavior around iterateValues.
What's the best approach to accomplish the use case?
Thanks!
Edit 1: repeat just hit me in the face. Perhaps it means to be friendly.
Edit 2: No, repeat isn't repeating but the observable is completing. Thanks for any help. I'm confused.
For posterity here is the full code sample for a revised edition is repeat-able and uses a consistent delay between items.
import { concat, EMPTY, defer, from, interval, of, throwError } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator(iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => (result.done ? EMPTY : of(result.value)))
);
}
function iterateValues({ params }) {
const { values, delay: delayMilliseconds, times = 1 } = params;
const isIterable =
values != null && typeof values[Symbol.iterator] === "function";
if (!isIterable) {
return throwError(new Error(`\`${values}\` is not iterable`));
}
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
const observable =
delayMilliseconds > 0
? defer(() => of(values[Symbol.iterator]())).pipe(
mergeMap(iterator =>
stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
)
)
)
: from(values);
return observable.pipe(repeat(times));
}
I'm gonna be honest, but there could be better solution for sure. In my solution, I ended up encapsulating delay logic in a custom runAgainAfter operator. Making it an independent part, that doesn't affect your code logic directly.
Full working code is here
And the code of runAgainAfter if anybody needs it:
import { Observable } from "rxjs";
export const runAgainAfter = delay => observable => {
return new Observable(observer => {
let timeout;
let subscription;
const subscribe = () => {
return observable.subscribe({
next(value) {
observer.next(value);
},
error(err) {
observer.error(err);
},
complete() {
timeout = setTimeout(() => {
subscription = subscribe();
}, delay);
}
});
};
subscription = subscribe();
return () => {
subscription.unsubscribe();
clearTimeout(timeout);
};
});
};
Hope it helps <3

Resources