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().
Related
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()
I wonder if someone ever had this issue before.
Before I had an EventHandler that would look like this:
export interface EventHandler {
name: string;
canHandleEvent(event: EventEntity): boolean;
handleEvent(event: EventEntity): Promise<void>;
}
And my filter function would work normally, also my tests were passing - where I was filtering the events using:
messages.forEach(message => {
const event: EventEntity = JSON.parse(message.Body);
this.handlers
.filter(handler => handler.canHandleEvent(event)) // WORKED WELL
.forEach(handler => {
// LOGIC
});
Currently, we had to change the canHandleEvent to either Boolean or Promise. Since we had some promises to be resolved and identify whether the event can be handle or not.
export interface EventHandler {
// ...
canHandleEvent(event: EventEntity): boolean | Promise<boolean>;
}
So, in order to solve it, I used Promise.resolve and Promise.all. No luck:
messages.forEach(async message => {
const event: EventEntity = JSON.parse(message.Body);
const handlersResolved = await Promise.all(this.handlers);
handlersResolved
.filter(handler => handler.canHandleEvent(event))
.forEach(handler => {
Now, my tests pass for the Promise canHandleEvent, but they are failing for the events passed that is boolean. They look like this:
class HandlerB implements EventHandler {
name = HandlerB.name;
numRuns = 0;
canHandleEvent(event: EventEntity): boolean {
console.log('event', event)
return event.eventType === EventType.ONE_EVENT || event.eventType === EventType.SECOND_EVENT;
}
async handleEvent(event: EventEntity): Promise<void> {
return new Promise(resolve => {
setTimeout(() => {
this.numRuns += 1;
resolve();
}, 25);
});
}
}
And my test that are now failing and before was passing are:
it('Should process handlers that match, including canHandleEvent that returns Promise<boolean> TRUE', async () => {
setHandlers([handlerA, handlerB, handlerC]);
const event = await createEvent(EventType.SECOND_EVENT);
await sleep(1000);
expect(handlerA.numRuns, 'handleA').to.eql(0);
expect(handlerB.numRuns, 'handleB').to.eql(1);
expect(handlerC.numRuns, 'handleC').to.eql(1); // handlerC is Promise<boolean>, it works fine
expect(errorHandler.numRuns).to.eql(0);
handlerC.numRuns = 0;
});
it('Should allow handlers to pass even if one has an error', async () => {
setHandlers([handlerA, handlerB, errorHandler]);
const event = await createEvent(EventType.USER_REGISTRATION_STATUS);
await sleep(1000);
expect(handlerA.numRuns, 'handlerA').to.eql(1);
expect(handlerB.numRuns, 'handlerB').to.eql(1);
expect(errorHandler.numRuns, 'errorHandler').to.eql(1);
});
Any thoughts on how to solve this? I've tried to identify whether is promise or boolean before inside the .filter but still no luck:
this.handlers
.filter(async handler => {
if(typeof handler.canHandleEvent(event).then == 'function') {
const result = await Promise.resolve(handler.canHandleEvent(event))
console.log('IS PROMISE!!', result);
return result
}
console.log('IT IS NOT PROMISE', handler.canHandleEvent(event))
return handler.canHandleEvent(event)
})
Currently, we had to change the canHandleEvent to either Boolean or Promise...
Just to be clear, that's a massive semantic change that will ripple through every layer of the code that uses that method. You can't directly use filter with it anymore, for instance, and any synchronous function that uses it is now potentially asynchronous (and fundamentally, "potentially asynchronous" = "asynchronous"). But if it has to happen, it has to happen! :-)
Your original code using canHandleEvent like this:
messages.forEach(message => {
const event: EventEntity = JSON.parse(message.Body);
this.handlers
.filter(handler => handler.canHandleEvent(event)) // WORKED WELL
.forEach(handler => {
// LOGIC
});
});
has to become asynchronous, like this:
// Handles things in parallel, not series
await/*or return*/ Promise.all(messages.map(message => {
const event: EventEntity = JSON.parse(message.Body);
return Promise.all(this.handlers.map(handler => async {
if (await handler.canHandleEvent(event)) {
// LOGIC
}
}));
}));
Notice how each layer was affected. messages.forEach turned into building an array of promises via messages.map and waiting for them via await (or using .then, etc., or returning to a calling function). For each message, we do the same thing for handlers, since we can't know whether a handler can handle something synchronously. (No need for Promise.resolve, Promise.all will handle that for you.)
The code above assumes it's okay for all of this to overlap (both the messages and the handlers for a message), whereas before because it was all synchronous, they all happened in series (all of the relevant handlers for one message, in order, then all of the handlers for the next, etc.). If you need it to be in series like that, you can use for-of loops:
// Handles things in series, not parallel
// (In an `async` function)
for (const message of messages) {
const event: EventEntity = JSON.parse(message.Body);
for (const handler of this.handlers) {
if (await handler.canHandleEvent(event)) {
// LOGIC
}
}
}
In both cases, it's possible to handle the ones returning boolean differently (synchronously) from the ones returning promises, but it complicates the code.
To solve your issue, I think the most simple way is to first populate the array with the expected values, so you can properly filter.
const transformedHandlers = await Promise.all(this.handlers.map(async handler => {
return {
...handler,
eventCanBeHandled: await handler.canHandleEvent(event)
}
}))
This will transform the array so you have a key that shows what handlers can be handled.
To finish it off you use your code like you would always do but instead of checking
canHandleEvent
you use the new field that has been introduced in the const transformedhandlers
below is the example:
transformedHandlers
.filter(handler => handler.eventCanBeHandled)
.forEach(handler => {
// LOGIC
});
This should be enough to keep you code working like it used to.
sorry for my English. it's not my native language
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
I am working on a project where I am building a simple front end in Angular (typescript) / Node to make call to a back end server for executing different tasks. These tasks take time to execute and thus need to be queued on the back end server. I solved this issue by following the following tutorial: https://github.com/realpython/flask-by-example and everything seems to work just fine.
Now I am finishing things up on the front end, where most of the code has been already written in Typescript using Angular and Rxjs. I am trying to replicate the following code in Typescript:
https://github.com/dimoreira/word-frequency/blob/master/static/main.js
This code consists of two functions, where first function "getModelSummary"(getResults in the example) calls a post method via:
public getModelSummary(modelSummaryParameters: ModelSummaryParameters): Observable<ModelSummary> {
return this.http.post(`${SERVER_URL}start`, modelSummaryParameters)
.map(res => res.json())
;
}
to put the job in queue and assign a jobID to that function on the back end server. The second function "listenModelSummary", ideally should get executed right after the first function with the jobId as it's input and loops in a short interval checking if the job has been completed or not:
public listenModelSummary(jobID: string) {
return this.http.get(`${SERVER_URL}results/` + jobID).map(
(res) => res.json()
);
}
Once the job is done, it needs to return the results, which would update the front end.
I am new to Typescript, Observables and rxjs and wanted to ask for the right way of doing this. I do not want to use javascript, but want to stick to Typescript as much as possible in my front end code stack. How can I use the first function to call the second function with it's output "jobID" and have the second function run via interval until the output comes back?
Observables are great, and are the type of object returned by Angular's HttpClient class, but sometimes, in my opinion, dealing with them is a lot more complicated than using promises.
Yes, there is a slight performance hit for the extra operation to convert the Observable to a Promise, but you get a simpler programming model.
If you need to wait for the first function to complete, and then hand the returned value to another function, you can do:
async getModelSummary(modelSummaryParameters: ModelSummaryParameters): Promise<ModelSummary> {
return this.http.post(`${SERVER_URL}start`, modelSummaryParameters).toPromise();
}
async doStuff(): Promise<void> {
const modelSummary = await this.getModelSummary(params);
// not sure if you need to assign this to your viewmodel,
// what's returned, etc
this.listenModelSummary(modelSummary)
}
If you're dead-set on using Observables, I would suggest using the concatMap pattern, which would go something like this:
doStuff(modelSummaryParameters: ModelSummaryParameters): Observable<ModelSummary> {
return this.http
.post(`${SERVER_URL}start`, modelSummaryParameters)
.pipe(
concatMap(modelSummary => <Observable<ModelSummary>> this.listenModelSummary(modelSummary))
);
}
Here's an article on different mapping solutions for Observables: https://blog.angularindepth.com/practical-rxjs-in-the-wild-requests-with-concatmap-vs-mergemap-vs-forkjoin-11e5b2efe293 that might help you out.
You can try the following:
getModelSummary(modelSummaryParameters: ModelSummaryParameters): Promise<ModelSummary> {
return this.http.post(`${SERVER_URL}start`, modelSummaryParameters).toPromise();
}
async someMethodInYourComponent() {
const modelSummary = await this.get(modelSummary(params);
this.listenModelSummary(modelSummary)
}
// OR
someMethodInYourComponent() {
this.get(modelSummary(params).then(() => {
this.listenModelSummary(modelSummary);
});
}
After doing more reading/researching into rxjs, I was able to make my code work and I wanted to thank you guys for the feedback and to post my code below.
In my services I created two observables:
First one is to fetch a jobId returned by queue server:
// API: GET / FUNCTION /:jobID
public getModelSummaryQueueId(modelSummaryParameters: ModelSummaryParameters): Observable<JobId>{
return this.http.post(${SERVER_URL}start, modelSummaryParameters).map(
(jobId) => jobId.json()
)
}
Use the jobId from first segment to fetch data:
// API: GET / FUNCTION /:results
public listenModelSummary(jobId: JobId): Observable <ModelSummary>{
return this.http.get(${SERVER_URL}results/+ jobId).map(
(res) => res.json()
)
}
Below is the component that works with the 2 services above:
`
this.subscription = this.developmentService.getModelSummaryQueueId(this.modelSummaryParameters)
.subscribe((jobId) => {
return this.developmentService.listenModelSummary(jobId)
// use switchMap to pull value from observable and check if it completes
.switchMap((modelSummary) =>
// if value has not changed then invoke observable again else return
modelSummary.toString() === 'Nay!'
? Observable.throw(console.log('...Processing Request...'))
// ? Observable.throw(this.modelSummary = modelSummary)
: Observable.of(modelSummary)
)
.retryWhen((attempts) => {
return Observable
// specify number of attempts
.range(1,20)
.zip(attempts, function(i) {
return(i);
})
.flatMap((res:any) => {
// res is a counter of how many attempts
console.log("number of attempts: ", res);
res = 'heartbeat - ' + res
this.getProgressBar(res);
// this.res = res;
// delay request
return Observable.of(res).delay(100)
})
})
// .subscribe(this.displayData);
// .subscribe(modelSummary => console.log(modelSummary));
.subscribe((modelSummary) => {
console.log("FINAL RESULT: ", modelSummary)
this.modelSummary = modelSummary;
this.getProgressBar('Done');
});
});
`
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)))))
);
}