I'm trying to read files by using bindNodeCallback and fs readdir, stat.
Here's my code:
import { readdir, stat, Stats } from "fs";
import { bindNodeCallback, Observable, of } from "rxjs";
import { catchError, filter, flatMap, map, mergeMap, switchMap, tap } from 'rxjs/operators';
const readDirBindCallback: (path: string) => Observable<string[]> = bindNodeCallback(readdir);
const fileStateBindCallback: (path: string) => Observable<Stats> = bindNodeCallback(stat);
readDirBindCallback('/Users/.../WebstormProjects/copy')
.pipe(
flatMap(x => x),
tap(console.log),
switchMap(status => {
console.log(status);
return fileStateBindCallback('/Users/.../WebstormProjects/copy/' + status);
})
)
.subscribe(result => {
console.log(result)
});
"switchMap" has been called multiple times correctly.
The problem is fileStateBindCallback has been called only once, the final subscibe logged only once.
Why is this happened?
If I subscribe the fileStateBindCallback manually in switchMap block. It would run as I expect. But that is not a good pratice and not fit in my request.
I think the problem is the switchMap operator.
switchMap can only have one active inner observable at a time. If an outer values comes in and there is an active inner obs., it will be unsubscribed and a new one will be created based on the newly arrived value and the provided function to switchMap.
readdir will return an array.
flatMap(arr => arr) will simply emit the array's items separately and synchronously.
Let's assume your directory has N items.
flatMap will emits these items one by one.
So item 1 is passed along and switchMap will create an inner obs (fileStateBindCallback), which involves asynchronous operation.
Then, item 2 is sent, but as this happens synchronously, the current inner observable(that one created due to item 1) handled by switchMap will be unsubscribed and a new one will be created for item 2.
And so forth, until item N finally arrives. Remember that the array's items are emitted synchronously. The item N-1's inner obs. will be unsubscribed and a new one will be created for item N. But since N is the last item in the array, it can't be interrupted by any subsequent emissions, meaning that its inner observable will have the time to emit & complete.
What you might be looking for is concatMap.
When Nth item arrives, it will wait for the current inner observable created due to the N-1th item to complete, before creating a new one based on the Nth item.
So, replacing switchMap with concatMap should do the job. If the order is not important, you can opt for mergeMap.
Related
I have an observable that returns items, I need only one specific item and I want to add some extra fields to it from a http request which is also observable. Code below is how I'm trying to achieve that but it doesn't work. I need all observables to complete to get the full item data in ngOnInit. What am I missing?
ngOnInit() {
myItemsObservable$(this.store, items, items.data)
.map(items => items.find(
item => {
return item.id === id
}
))
.concatMap(item => {
return this.apiService.get(`/items/${item.id}/extradata`).map(extra => ({
...item,
extra
}))
})
.subscribe(item => {
// I expect item to have extra fields here.
this.item = item
})
// this.item here should already be complete.
}
Let's look at a simplified version of your code:
1 function ngOnInit() {
2 myItemsObservable$().subscribe(item => this.item = item);
3 console.log(this.item); // undefined
4 }
You are essentially calling two functions that get executed immediately one after the other.
Line 2 creates a subscription object which initiates the flow of data inside the observable. But... execution is not paused after line 2. So then, Line 3 is executed, before the asynchronous tasks within the observable have been completed. This is why this.item is still undefined on line 3.
Hopefully, you can see why your comment is not correct:
// this.item here should already be complete.
You are passing a function (item => this.item = item) to the subscribe() method that handles emissions from the observable when they occur. This is the place in your code where you actually have the emitted value.
So, if we move the console.log() inside the subscribe, this.item would no longer be undefined:
1 function ngOnInit() {
2 myItemsObservable$().subscribe(item => {
3 this.item = item;
4 console.log(this.item); // not undefined :-)
5 });
6 }
To address the two parts of your question:
How to add extra fields in RxJS observable
You are already doing this. You've used the map and concatMap operators to modify values emitted by the source observable into your desired value.
...and wait for it to complete?
Well, you don't "wait" for it per se. With RxJS, you are defining the behavior of how the data flows. The only place you have access to the actual data is inside the subscribe.
But... instead of subscribing, then copying the data from the observable to another variable, you can simply reference the observable directly in other parts of your code.
Let's break your code up into a few different parts so it's easier to see how we can reference different observable sources without subscribing:
id$ = this.route.paramMap.pipe( // This could come from a form control input
params => params.get('id') // or some other observable source.
);
allItems$ = myItemsObservable$(this.store, items, items.data);
getItem$(id) {
return this.allItems$.pipe(
map(items => items.find(i.id === id))
);
}
getExtraData$(id) {
return this.apiService.get(`/items/${id}/extradata`);
}
item$ = this.id$.pipe(
switchMap(id => getItem$(id)),
switchMap(item => this.getExtraData(item.id).pipe(
map(extra => ({ ...item, ...extra }))
))
);
}
See how the definition of item$ starts with the id$? This means that whenever id$ emits a new value, item$ will automatically call getItem$(), then getExtraData() then emit this new item. We didn't need to subscribe to make that happen.
We can simply define an observable to start with another observable then .pipe() the emissions and transform them to suit our needs.
We've essentially designed an observable that will emit any time that item in the store changes, or whenever our selected id$ emits a new value. In a sense, we've built up item$ to represent our item and it will always be up to date, including having its "extra data" appended. This is very powerful. Now we can just use it.
Notice the definition of item$ doesn't need to be in ngOnInit; it can actually go directly on your component.
It's true we could subscribe in our component... but we can usually just use the AsyncPipe in the template:
<div *ngIf="item$ | async as item">
<h1>{{ item.name }}</h1>
<ul>
<li>{{ item.description }}</li>
<li>{{ item.someProperty }}</li>
</ul>
</div>
If you find yourself often subscribing in your component, only to copy the data to a local variable, just to be consumed by your template; I would encourage you to pause and ask yourself it its really necessary. Most of the time you can define an observable that emits exactly the data your view needs without ever needing to subscribe.
RxJS provides many operators and static functions that make it easy to create observables with a variety of common behaviors.
Are you using an old version of angular and rxjs? In the current way, map is not a method of observables.
Instead, I think you would be looking for something like this;
function ngOnInit() {
myItemsObservable$(this.store, items, items.data)
.pipe(
filter(item => item.id === id), // assuming your observable emits each item separately. Otherwise, use your map statement above.
take(1), // do you need this? I'm guessing you wouldn't.
concatMap(item => this.apiService.get(`,/items/${item.id}/extradata`)
.pipe(
map(extra => Object.assign({}, item, {extra})) // if I understand what you are wanting here, otherwise replace "{extra}" with "extra", or go back to your original notation
)
)
)
.subscribe(
item => {
// I expect item to have extra fields here.
this.item = item
}
)
// this.item here should already be complete.
}
I'm using Angular 10 and trying to collect window:keyup events for a certain amount of time using RXJS - but unfortunately not very successful at all. Basically I want to input data and if no key is pressed for a certain amount of time, the request should be submitted.
fromEvent(window, 'keyup').pipe(
map((ev: KeyboardEvent) => ev.key),
scan((acc, value) => acc + value),
debounceTime(500)
).subscribe(key => {
console.log(key);
});
That's how my basic approach looks like, which is basically doing what I want, but I'm unable to delete the scan result within the subscription result.
Is there a better way to achieve my desired behavior?
One approach I have found is to use a Subject alongside, and have it emit the results of an Observable which sets itself up and destroys itself for each produced string:
const result$ = new Subject<string>();
fromEvent(window, 'keyup').pipe(
exhaustMap(key => fromEvent(window, 'keyup').pipe(
takeUntil(result$),
startWith(key),
map((event: KeyboardEvent) => event.key),
scan((acc, curr) => acc + curr),
debounceTime(500),
tap(val => result$.next(val)),
)),
).subscribe();
result$.subscribe(console.log); // here you would do your request logic,
// and make sure to set it to unsubscribe
// (with a takeUntil and a destroy$ Subject, for example).
When a first key is pressed, it sets up the outer Observable. It then creates the inner Observable, watching every key pressed until the debounce time of 500ms passes, at which point it causes the Subject to emit. The emission of that very same Subject then kills the inner Observable, allowing the outer Observable to start up again. Don't forget to use an unsubscription method on BOTH the Subject and the outer Observable.
Angular 7 docs provide this example of practical usage of rxjs Observables in implementing an exponential backoff for an AJAX request:
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) {
return pipe(
retryWhen(attempts => range(1, maxTries)
.pipe(
zip(attempts, (i) => i),
map(i => i * i),
mergeMap(i => timer(i * ms))
)
)
);
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
While I understand the concept of both Observables and backoff, I can’t quite figure out, how exactly retryWhen will calculate time intervals for resubscribing to the source ajax.
Specifically, how do zip, map, and mapMerge work in this setup?
And what’s going to be contained in the attempts object when it’s emitted into retryWhen?
I went through their reference pages, but still can’t wrap my head around this.
I have spent quite some time researching this (for learning purposes) and will try to explain the workings of this code as thoroughly as possible.
First, here’s the original code, annotated:
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) { // (1)
return pipe( // (2)
retryWhen(attempts => range(1, maxTries) // (3)
.pipe(
zip(attempts, (i) => i), // (4)
map(i => i * i), // (5)
mergeMap(i => timer(i * ms)) // (6)
)
)
); // (7)
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
Easy enough, we’re creating custom backoff operator out of retryWhen operator. We’ll be able to apply this later within pipe function.
In this context, pipe method returns a custom operator.
Our custom operator is going to be a modified retryWhen operator. It takes a function argument. This function is going to be called once — specifically, when this retryWhen is first encountered/invoked. By the way, retryWhen gets into play only when the source observable produces an error. It then prevents error from propagating further and resubscribes to the source. If the source produces a non-error result (whether on first subscription or on a retry), retryWhen is passed over and is not involved.
A few words on attempts. It’s an observable. It is not the source observable. It is created specifically for retryWhen. It has one use and one use only: whenever subscription (or re-subscription) to the source observable results in an error, attempts fires a next. We are given attempts and are free to use it in order to react in some way to each failed subscription attempt to the source observable.
So that’s what we are going to do.
First we create range(1, maxTries), an observable that has an integer for every retry we are willing to perform. range is ready to fire all it’s numbers right then and there, but we have to hold its horses: we only need a new number when another retry happens. So, that’s why we...
... zip it with the attempts. Meaning, marry each emitted value of attempts with a single value of range.
Remember, function we’re currently in is going to be called only once, and at that time, attempts will have only fired next once — for the initial failed subscription. So, at this point, our two zipped observables have produced just one value.
Btw, what are the values of the two observables zipped into one? This function decides that: (i) => i. For clarity it can be written (itemFromRange, itemFromAttempts) => itemFromRange. Second argument is not used, so it’s dropped, and first is renamed into i.
What happens here, is we simply disregard the values fired by attempts, we are only interested in the fact that they are fired. And whenever that happens we pull the next value from range observable...
...and square it. This is for the exponential part of the exponential backoff.
So, now whenever (re-)subscription to source fails, we have an ever increasing integer on our hands (1, 4, 9, 16...). How do we transform that integer into a time delay until next re-subscription?
Remember, this function we are currently inside of, it must return an observable, using attempts as input. This resulting observable is only built once. retryWhen then subscribes to that resulting observable and: retries subscribing to source observable whenever resulting observable fires next; calls complete or error on source observable whenever resulting observable fires those corresponding events.
Long story short, we need to make retryWhen wait a bit. delay operator could maybe be used, but setting up exponential growth of the delay would likely be pain. Instead, mergeMap operator comes into play.
mergeMap is a shortcut for two operators combined: map and mergeAll. map simply converts every increasing integer (1, 4, 9, 16...) into a timer observable which fires next after passed number of milliseconds. mergeAll forces retryWhen to actually subscribe to timer. If that last bit didn’t happen, our resulting observable would just fire next immediately with timer observable instance as value.
At this point, we’ve built our custom observable which will be used by retryWhen to decide when exactly to attempt to re-subscribe to source observable.
As it stands I see two problems with this implementation:
As soon as our resulting observable fires its last next (causing the last attempt to resubscribe), it also immediately fires complete. Unless the source observable returns result very quickly (assuming that the very last retry will be the one that succeeds), that result is going to be ignored.
This is because as soon as retryWhen hears complete from our observable, it calls complete on source, which may still be in the process of making AJAX request.
If all retries were unsuccessful, source actually calls complete instead of more logical error.
To solve both these issues, I think that our resulting observable should fire error at the very end, after giving the last retry some reasonable time to attempt to do its job.
Here’s my implementation of said fix, which also takes into account deprecation of zip operator in latest rxjs v6:
import { delay, dematerialize, map, materialize, retryWhen, switchMap } from "rxjs/operators";
import { concat, pipe, range, throwError, timer, zip } from "rxjs";
function backoffImproved(maxTries, ms) {
return pipe(
retryWhen(attempts => {
const observableForRetries =
zip(range(1, maxTries), attempts)
.pipe(
map(([elemFromRange, elemFromAttempts]) => elemFromRange),
map(i => i * i),
switchMap(i => timer(i * ms))
);
const observableForFailure =
throwError(new Error('Could not complete AJAX request'))
.pipe(
materialize(),
delay(1000),
dematerialize()
);
return concat(observableForRetries, observableForFailure);
})
);
}
I tested this code and it seems to work properly in all cases. I can’t be bothered to explain it in detail right now; I doubt anyone will even read the wall of text above.
Anyway, big thanks to #BenjaminGruenbaum and #cartant for setting me onto right path for wrapping my head around all this.
Here is a different version that can be easily extended/modified:
import { Observable, pipe, throwError, timer } from 'rxjs';
import { mergeMap, retryWhen } from 'rxjs/operators';
export function backoff(maxRetries = 5): (_: Observable<any>) => Observable<any> {
return pipe(
retryWhen(errors => errors.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
if (retryAttempt > maxRetries) {
return throwError(error);
} else {
const waitms = retryAttempt * retryAttempt * 1000;
console.log(`Attempt ${retryAttempt}: retrying in ${waitms}ms`);
return timer(waitms);
}
}),
))
);
};
Ref retryWhen
I have created an observable, which is given below
private permissionSubject = new Subject<any>();
permissionObservable$ = this.permissionSubject.asObservable();
constructor( public apiService: ApiService) { }
updatePermissionsData(permissionData){
this.permissionSubject.next(permissionData);
}
getPermissions(){
return this.apiService.get('/getUserPrivileges')
.map(data => data)
}
Here what I am doing is, whenever I am getting the data, I am pushing the data to Observable
Ex: consider an observable -> [1, 2] and pushing 3 as it is new, data
now observable will become [1, 2, 3]
But I want to remove 1, 2 value from Observable before pushing 3 to it. How can I do that?
Is Observable.empty() will do that, if it can, how can I update my code?
I have seen many questions in stackoverflow, but nothing helped :-( that's why I am asking this question again...
Updated code
Subscribing observable
checkPermissions() {
this.checkPermService.permissionObservable$.subscribe(
// Below one is getting executed for so many times whenever
observable get new data (stream data)
data => {
this.saveMenuItemsOnPermissions(data)
}
)
}
I think there is a misunderstanding of how Observables work. You have no buffer/memory structure in your code.
Your Code explained
// instance of a subject.
// Subjects don't have memory!! The stream is pushed to subscribers once.
private permissionSubject = new Subject<any>();
// Here you make a restriction on `permissionObservable$`, so it listens, but doesn't publish
permissionObservable$ = this.permissionSubject.asObservable();
// constructor instanciates apiService
constructor( public apiService: ApiService) { }
// each time this function is called, permissionData is pushed through
// permissionObservable and permissionObservable$ subscribers.
updatePermissionsData(permissionData){
this.permissionSubject.next(permissionData);
}
// calls a service and waits for subscription (http call I suppose)
// the map function is useless BTW
getPermissions(){
return this.apiService.get('/getUserPrivileges')
.map(data => data)
}
Observable.empty()
create an Observable that emits no items but terminates normally
Observable.empty() is not a method !! It is an observable whose purpose is to :
emit nothing
hang the stream
Edit:
If you just want to ignore the 2 first elements of an observable, you can use skip operator.
Skip operator:
Skip allows you to ignore the first x emissions from the source.
Generally skip is used when you have an observable that always emits
certain values on subscription that you wish to ignore. Perhaps those
first few aren't needed or you are subscribing to a Replay or
BehaviorSubject and do not need to act on the initial values. Reach
for skip if you are only concerned about later emissions.
// Below one is getting executed for so many times whenever observable get new data (stream data)
checkPermissions() {
this.checkPermService.permissionObservable$.skip(2)
.subscribe( data => {
this.saveMenuItemsOnPermissions(data)
})
}
There are 2 important points to bear in mind:
Subscription must occur before observable starts emitting
checkPermissions will ignore the 2 first received elements during subscription, but it will take all the following others.
I have three observable sources in my code that emit values of the same type.
const setTitle$ = params$.do(
params => this.titleService.setTitle( `${params[1].appname} - ${this.pagename}` )
).switchMap(
() => Observable.of(true)
);
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
);
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
);
When i use them in race like this
setTitle$.race(
openDocument$,
saveDocument$
).subscribe();
works only setTitle and when i subscribe manually to another two sorces like
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
).subscribe();
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
).subscribe();
then they work too. Help me understand why it's going on and how to force to work all sources in race, merge, etc.
From the documentation, the .race() operator does this:
The observable to emit first is used.
That is why, you will only get ONE emission, because only one out of the three observables that emits first will get emitted.
What you are looking for is .forkJoin() or .combineLatest().
If you want all the observables to execute in parallel and wait for ALL of them to come back as one observables, use .forkJoin():
Observable
.forkJoin([...setTitle$, openDocument$, saveDocument$])
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
//all three observables must be completed. If any of it was not completed, the other 2 observables will wait for it
})
If you however wants to listen to every emission of all the observables regardless when they are emitted, use .combineLatest():
Observable
.combineLatest(setTitle$, openDocument$, saveDocument$)
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
// as long as any of the observables completed, it will be emitted here.
});
Problem was with shared params source.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).share();
I have shared it with share operator. But in this article from the first comment to my question i found this:
When using multiple async pipes on streams with default values, the .share() operator might cause problems:
The share() will publish the first value of the stream on the first subscription. The first async pipe will trigger that subscription and get that initial value. The second async pipe however will subscribe after that value has already been emitted and therefore miss that value.
The solution for this problem is the .shareReplay(1) operator, which will keep track of the previous value of the stream. That way all the async pipes will get the last value.
I replaced share() with shareReplay(1) and all sources began emitting values.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).shareReplay(1);
Thanks to everyone for help!