Cancel an Observable from the producer side, not consumer side - rxjs

Cancelling from the consumer side, might be called using takeUntil, but that's not necessarily very dynamic. In this case, though, I am looking to cancel an Observable from the producer side of the equation, in the same way you might wish to cancel a Promise inside a promise chain (which is not very possible with the native utility).
Say I have this Observable being returned from a method. (This Queue library is a simple persistent queue that read/writes to a text file, we need to lock read/writes so nothing gets corrupted).
Queue.prototype.readUnique = function () {
var ret = null;
var lockAcquired = false;
return this.obsEnqueue
.flatMap(() => acquireLock(this))
.flatMap(() => {
lockAcquired = true;
return removeOneLine(this)
})
.flatMap(val => {
ret = val; // this is not very good
return releaseLock(this);
})
.map(() => {
return JSON.parse(ret);
})
.catch(e => {
if (lockAcquired) {
return releaseLock(this);
}
else {
return genericObservable();
}
});
};
I have 2 different questions -
If I cannot acquire the lock, how can I "cancel" the observable, to just send back an empty Observable with no result(s)? Would I really have to do if/else logic in each return call to decide whether the current chain is cancelled and if so, return an empty Observable? By empty, I mean an Observable that simple fires onNext/onComplete without any possibility for errors and without any values for onNext. Technically, I don't think that's an empty Observable, so I am looking for what that is really called, if it exists.
If you look at this particular sequence of code:
.flatMap(() => acquireLock(this))
.flatMap(() => {
lockAcquired = true;
return removeOneLine(this)
})
.flatMap(val => {
ret = val;
return releaseLock(this);
})
.map(() => {
return JSON.parse(ret);
})
what I am doing is storing a reference to ret at the top of the method and then referencing it again a step later. What I am looking for is a way to pass the value fired from removeOneLine() to JSON.parse(), without having to set some state outside the chain (which is simply inelegant).

According to your definition of cancel, it is to prevent an observable from sending a value downstream. To prevent an observable from pushing a value, you can use filter:
It can be as simple as:
observable.filter(_ => lockAcquired)
This will only send a notification downstream if lockAcquired is true.

1) It depends on how your method acquireLock works - but I am assuming that it throws an error if it cannot acquire the lock, in that case you could create your stream with a catch and set the fallback stream to an empty one:
return Rx.Observable.catch(
removeLine$,
Rx.Observable.empty()
);
2) To spare the stateful external variable you could simply chain a mapTo:
let removeLine$ = acquireLock(this)
.flatMap(() => this.obsEnqueue
.flatMap(() => removeOneLine(this))
.flatMap(val => releaseLock(this).mapTo(val))
.map(val => JSON.parse(val))
.catch(() => releaseLock(this))
);

Related

Filtered send queue in rxjs

So I'm relatively inexperienced with rxjs so if this is something that would be a pain or really awkward to do, please tell me and I'll go a different route. So in this particular use case, I was to queue up updates to send to the server, but if there's an update "in flight" I want to only keep the latest item which will be sent when the current in flight request completes.
I am kind of at a loss of where to start honestly. It seems like this would be either a buffer type operator and/or a concat map.
Here's what I would expect to happen:
const updateQueue$ = new Subject<ISettings>()
function sendToServer (settings: ISettings): Observable {...}
...
// we should send this immediately because there's nothing in-flight
updateQueue$.next({ volume: 25 });
updateQueue$.next({ volume: 30 });
updateQueue$.next({ volume: 50 });
updateQueue$.next({ volume: 65 });
// lets assume that our our original update just completed
// I would now expect a new request to go out with `{ volume: 65 }` and the previous two to be ignored.
I think you can achieve what you want with this:
const allowNext$ = new Subject<boolean>()
const updateQueue$ = new Subject<ISettings>()
function sendToServer (settings: ISettings): Observable { ... }
updateQueue$
.pipe(
// Pass along flag to mark the first emitted value
map((value, index) => {
const isFirstValue = index === 0
return { value, isFirstValue }
}),
// Allow the first value through immediately
// Debounce the rest until subject emits
debounce(({ isFirstValue }) => isFirstValue ? of(true) : allowNext$),
// Send network request
switchMap(({ value }) => sendToServer(value)),
// Push to subject to allow next debounced value through
tap(() => allowNext$.next(true))
)
.subscribe(response => {
...
})
This is a pretty interesting question.
If you did not have the requirement of issuing the last in the queue, but simply ignoring all requests of update until the one on the fly completes, than you would simply have to use exhaustMap operator.
But the fact that you want to ignore all BUT the last request for update makes the potential solution a bit more complex.
If I understand the problem well, I would proceed as follows.
First of all I would define 2 Subjects, one that emits the values for the update operation (i.e. the one you have already defined) and one dedicated to emit only the last one in the queue if there is one.
The code would look like this
let lastUpdate: ISettings;
const _updateQueue$ = new Subject<ISettings>();
const updateQueue$ = _updateQueue$
.asObservable()
.pipe(tap(settings => (lastUpdate = settings)));
const _lastUpdate$ = new Subject<ISettings>();
const lastUpdate$ = _lastUpdate$.asObservable().pipe(
tap(() => (lastUpdate = null)),
delay(0)
);
Then I would merge the 2 Observables to obtain the stream you are looking for, like this
merge(updateQueue$, lastUpdate$)
.pipe(
exhaustMap(settings => sendToServer(settings))
)
.subscribe({
next: res => {
// do something with the response
if (lastUpdate) {
// emit only if there is a new "last one" in the queue
_lastUpdate$.next(lastUpdate);
}
},
});
You may notice that the variable lastUpdate is used to control that the last update in the queue is used only once.

How do I get my observable to have it's values for use in an NGRX effect

To be honest I am a total noob at NGRX and only limited experience in rxjs. But essentially I have code similar to this:
#Effect()
applyFilters = this.actions$.pipe(
ofType<ApplyFilters>(MarketplaceActions.ApplyFilters),
withLatestFrom(this.marketplaceStore.select(appliedFilters),
this.marketplaceStore.select(catalogCourses)),
withLatestFrom(([action, filters, courses]) => {
return [courses,
this.combineFilters([
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES)
])
];
}),
map(([courses, filters]) => {
console.log('[applyFilters effect] currently applied filters =>', filters);
console.log('courseFilters', filters);
const filteredCourses = (courses as ShareableCourse[]).filter(x => (filters as number[]).includes(+x.id));
console.log('all', courses);
console.log('filtered', filteredCourses);
return new SetCatalogCourses(filteredCourses);
})
);
Helper method:
private combineFilters(observables: Observable<number[]>[]): number[] {
if (!observables.some(x => x)) {
return [];
} else {
let collection$ = (observables[0]);
const result: number[] = [];
for (let i = 0; i < observables.length; i++) {
if (i >= 1) {
collection$ = concat(collection$, observables[i]) as Observable<number[]>;
}
}
collection$.subscribe((x: number[]) => x.forEach(y => result.push(y)));
return result;
}
}
So essentially the store objects gets populated, I can get them. I know that the observables of 'this.getCourseIdsFromFiltersByFilterType(args)' do work as on the console log of the 'filters' they are there. But the timing of the operation is wrong. I have been reading up and am just lost after trying SwitchMap, MergeMap, Fork. Everything seems to look okay but when I am trying to actually traverse the collections for the result of the observables from the service they are not realized yet. I am willing to try anything but in the simplest form the problem is this:
Two observables need to be called either in similar order or pretty close. Their 'results' are of type number[]. A complex class collection that has a property of 'id' that this number[] should be able to include. This works just fine when all the results are not async or in a component.(I event dummied static values with variables to check my 'filter' then 'includes' logic and it works) But in NGRX I am kind of lost as it needs a return method and I am simply not good enough at rxjs to formulate a way to make it happy and ensure the observables are fully realized for their values from services to be used appropriately. Again I can see that my console log of 'filters' is there. Yet when I do a 'length' of it, it's always zero so I know somewhere there is a timing problem. Any help is much appreciated.
If I understand the problem, you may want to try to substitute this
withLatestFrom(([action, filters, courses]) => {
return [courses,
this.combineFilters([
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES)
])
];
}),
with something like this
switchMap(([action, filters, courses]) => {
return forkJoin(
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES
).pipe(
map(([trainingFilters, industryFilters]) => {
return [courses, [...trainingFilters, ...industryFilters]]
})
}),
Now some explanations.
When you exit this
withLatestFrom(this.marketplaceStore.select(appliedFilters),
this.marketplaceStore.select(catalogCourses)),
you pass to the next operator this array [action, filters, courses].
The next operator has to call some remote APIs and therefore has to create a new Observable. So you are in a situation when an upstream Observable notifies something which is taken by an operator which create a new Observable. Similar situations are where operators such as switchMap, mergeMap (aka flatMap), concatMap and exhastMap have to be used. Such operators flatten the inner Observable and return its result. This is the reason why I would use one of these flattening operators. Why switchMap in your case? It is not really a short story. Maybe reading this can cast some light.
Now let's look at the function passed to switchMap
return forkJoin(
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES
).pipe(
map(([trainingFilters, industryFilters]) => {
return [courses, [...trainingFilters, ...industryFilters]]
})
This function first executes 2 remote API calls in parallel via forkJoin, then take the result of these 2 calls and map it to a new Array containing both courses and the concatenation of trainingFilters and industryFilters

Implement a loop logic within an rxjs pipe

I have a class, QueueManager, which manages some queues.
QueueManager offers 3 APIs
deleteQueue(queueName: string): Observable<void>
createQueue(queueName: string): Observable<string>
listQueues(): Observable<string>: Observable`
deleteQueue is a fire-and-forget API, in the sense that it does not return any signal when it has completed its work and deleted the queue. At the same time createQueue fails if a queue with the same name already exists.
listQueues() returns the names of the queues managed by QueueManager.
I need to create a piece of logic which deletes a queue and recreates it. So my idea is to do something like
call the deleteQueue(queueName) method
start a loop calling the listQueues method until the result returned shows that queueName is not there any more
call createQueue(queueName)
I do not think I can use retry or repeat operators since they resubscribe to the source, which in this case would mean to issue the deleteQueue command more than once, which is something I need to avoid.
So what I have thought to do is something like
deleteQueue(queueName).pipe(
map(() => [queueName]),
expand(queuesToDelete => {
return listQueues().pipe(delay(100)) // 100 ms of delay between checks
}),
filter(queues => !queues.includes(queueName)),
first() // to close the stream when the queue to cancel is not present any more in the list
)
This logic seems actually to work, but looks to me a bit clumsy. Is there a more elegant way to address this problem?
The line map(() => [queueName]) is needed because expand also emits values from its source observable, but I don't think that's obvious from just looking at it.
You can use repeat, you just need to subscribe to the listQueues observable, rather than deleteQueue.
I've also put the delay before listQueues, otherwise you're waiting to emit a value that's already returned from the API.
const { timer, concat, operators } = rxjs;
const { tap, delay, filter, first, mapTo, concatMap, repeat } = operators;
const queueName = 'A';
const deleteQueue = (queueName) => timer(100);
const listQueues = () => concat(
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['B'])),
);
const source = deleteQueue(queueName).pipe(
tap(() => console.log('queue deleted')),
concatMap(() =>
timer(100).pipe(
concatMap(listQueues),
tap(queues => console.log('queues', queues)),
repeat(),
filter(queues => !queues.includes(queueName)),
first()
)
)
);
source.subscribe(x => console.log('next', x), e => console.error(e), () => console.log('complete'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.js"></script>

Observable - Getting the value of the latest emission

I have a form and I allow the user to click as many times as he wants on a refresh button. Of course, I use debounceTime operator but I don't know how to:
either cancel the previous http requests
or indicate to my service to return the value of the latest emission.
For example:
t1: click => received data in 2000ms
t2: click => received data in 200ms
Therefore, I will get the data from t1 moment whereas the latest one is at t2.
I've tried with pipe(last()), switchMap but I don't return data.
My component:
this.filtersForm.valueChanges.pipe(debounceTime(500)).subscribe(
form => {
this.service.setFilters(form); // Set private field in service (1)
this.onSubmit();
}
);
onSubmit() {
if (this.filtersForm.valid) {
this.service.notifFiltersHasChanged();
}
}
Service:
ctor(...) {
this.filters$.subscribe(f => this.getData());
}
notifFiltersHasChanged() {
this.filters$.next(this._filters); // (1) _filters is set by setFilters method
}
getData(): void {
// ...
this.backEndService.getAll(this._filters).subscribe(data => this._data = data);
}
BackEndService:
getAll(filters: any): Observable<Data> {
return this.httpClient.get<Data>(url).pipe(last());
}
The main trick is to use a single subscription (or even zero, if you'll use | async pipe in your template). So you source from an Observable and chain through your services.
Heres an updated example of yours:
Component
onDestroy$ = new Subject<void>();
constructor(){
this.filtersForm.valueChanges.pipe(
// accept only valid values
filter(() => this.filtersForm.valid),
// debounce them
debounceTime(500),
// when a value comes in -- we switch to service request
// subsequent values would cancel this request
switchMap(formValues => this.service.getData(formValues)),
// this is needed to unsubscribe from the service
// when component is destroyed
takeUntil(this.onDestroy$)
)
.subscribe(data=>{
// do what you need with the data
})
}
ngOnDestroy() {
this.onDestroy$.next(void 0);
}
Service
// service becomes stateless
// its only responsible for parsing and passing data
getData(filters): Observable<Data> {
return this.backEndService.getAll(filters);
}
BackEndService
getAll(filters: any): Observable<Data> {
return this.httpClient.get<Data>(url).pipe(last());
}
Another way would be to have a Subject, that you would push to. Otherwise it would be the same chaining on top of that Subject.
Hope this helps

How to merge the value of a Subject "next" into the main stream with Rxjs

The method retryWhen returns a Subject, how can I merge it's next value with the main stream ?
dataStream.
retryWhen(errors => {
return errors.
delay(1000).
take(3).
concat(Rx.Observable.throw(errors));
}).
subscribe(
x => console.log('onNext:', x),
e => {
e.subscribe(function (value) {
console.log('err', value);
});
},
_ => console.log('onCompleted'));
Right now it works but I have to subscribe to the e in the error handler to get the value of the error thrown by dataStream. Is it possible to simplify this ?
updates:
I would like to code a "data-access routine" with resilience. Currently It will retry 3 times with a delay of 1sec and in the case of an error, It will throw the "error". The problem is it doesn't throw an error it throws a "Subject" and I have to subscribe to it to get the error.
let getPromiseStream = function (endpoint) {
return Rx.Observable.
just(endpoint).
flatMap(requestUrl => {
return _this.$http.get(requestUrl);
});
};
let itemsStream = getPromiseStream('/items');
let locksStream = getPromiseStream('/error');
let usersStream = getPromiseStream('/users');
let favsStream = getPromiseStream('/favs');
let dataStream = Rx.Observable.
zip(
itemsStream,
locksStream,
usersStream,
favsStream,
(items, locks, users, favs) => {
return {items: items.data, locks: locks.data, users: users.data, favs: favs.data};
});
retryWhen can be a little difficult to use (I had to look it up again to remember its behavior). Essentially retryWhen will take all errors that you receive from the source Observable and convert them into onNext calls. The function you pass in takes in that Observable and then lets you play with the events passing through. If you want to continue retrying it should simply pass the event through. If it should error then you need to convert the onNext into a onError or if you just want it to stop trying then you should convert it into an onCompleted.
So onto your code you want to delay a certain number of times and then error the whole thing out if it really is more than just a transient network error.
dataStream.
retryWhen(errors => {
return errors
.flatMap((err, count) => {
return count < 3 ?
//This will just get flattened out and passed through
Rx.Observable.just(err) :
//If you have received 3 errors it is time to throw
//This error will get passed all the way to the final
//onError method if it isn't caught along the way
Rx.Observable.throw(err);
})
.delay(1000);
})
.subscribe(
x => console.log('onNext:', x),
e => console.log('err', value),
_ => console.log('onCompleted'));
Additional note: I would suggest that you put the retryWhen on each Observable that feeds into your zip rather than on the whole thing. As in the latter case a failure from one will retry all the source Observables not just the one that failed.

Resources