I have a dynamic array structure. Specifically, it is Google Maps' MVCArray. This structure has regular put, get, remove methods, as well as an addListener to listen to any changes. A library method (Polygon#getPaths) returns an MVCArray of MVCArray of LatLngs, since a polygon can have any number of paths, and each path can have any number of vertices.
My goal is to convert generate an Observable of PolygonPathEvent which will fire when either the parent MVCArray any child MVCArray will be changed.
Creating the Observables
First order of business is to convert the addListener to Observables.
private createMVCEventObservable<T>(array: MVCArray<T>): Observable<[T[], string, number, T?]>{
const eventNames = ['insert_at', 'remove_at', 'set_at'];
return fromEventPattern(
(handler: Function) => eventNames.map(evName => array.addListener(evName,
(index: number, previous?: LatLng) => this._zone.run(() => handler.apply(array, [[array.getArray(), evName, index, previous]])))),
(handler: Function, evListeners: MapsEventListener[]) => evListeners.forEach(evListener => evListener.remove()));
}
Combining the Observables
Now to combine, seemingly we need to use combineLatest
const pathsChanges$ = this.createMVCEventObservable(paths);
const pathChanges$ = combineLatest(paths.getArray().map(this.createMVCEventObservable));
return combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) =>
new PolygonPathEvent(pathArr, paths);
);
The problem is that the parent MVCArray of MVCArray can change, but combineLatest takes in a static array. So when there is a new path added, I don't know how to make the returned Observable also listen to this new path. Same, if a path is deleted, I don't know how to make the returned observable unsubscribe from the deleted path.
Piping to Subject (Wrong approach)
I thought about returning a Subject, and simply subscribing it to different observables whenever the parent MVCArray<MVCArray<LatLng>> changes.
const retVal: Subject<PolygonPathEvent> = new Subject();
const pathsChanges$ = this.createMVCEventObservable(paths);
const pathChanges$ = combineLatest(paths.getArray().map(this.createMVCEventObservable));
let latestSubscription = combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) =>
new PolygonPathEvent(pathArr, paths)
).subscribe(retVal);
pathsChanges$.pipe(tap( ([arrays, event, index, previous]) => {
latestSubscription.unsubscribe();
latestSubscription = combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) =>
new PolygonPathEvent(pathArr, paths)
).subscribe(retVal);
} ));
return retVal;
This works, the problem is that a subscription to the original Observables (and addListener) happens right in this method, and not when the returned Observable is subscribed to.
Conclusion
I need some kind of operator for this.
If I understand your problem right, there may be space to use switchMap to solve it.
You say that "The problem is that the parent MVCArray of MVCArray can change, but combineLatest takes in a static array". I think that the static array you refer to is the one emitted by the pathChanges$ Observable, which is created once for all at the beginning of your code snippet and gets not updated when the parent MVCArray of MVCArray changes.
If my understanding is right, what we need to do is to provide a way to notify the event of change of the parent MVCArray of MVCArray and, any time such event occurs, execute again the combineLatest function using the new updated array.
This can be accomplished with a logic similar to the following
const pathsChanges$ = this.createMVCEventObservable(paths);
pathsChanges$
.pipe(
switchMap(() => combineLatest(paths.getArray().map(this.createMVCEventObservable))),
map(pathArr => new PolygonPathEvent(pathArr, paths))
)
This logic assumes that the variable paths is passed into this piece of logic from the outside.
I could not reproduce the case and therefore I am far from sure that my answer solves your problem, even if I hope it helps.
Related
I use the following code in an angular app. I used the RxJS map call similar to how array map is used. After reading about RxJS switchmap operator, I'm not sure whether I should use map or switchmap. Should I use switchmap so the observable which is returned from the http call is closed so there is no memory leak?
getPeopleForTypeahead(term: string): Observable<IPersonForTypeahead[]> {
var peopleUrl = `https://localhost:5001/api/peoplesearch?name=${term}`;
return this.http.get<any>(peopleUrl)
.pipe(
map(pl => {
return this.peopleAsFlattened(pl.peopleList).reduce((p, c) => p.concat(c));
}),
catchError(this.handleError('getPeopleForTypeahead', []))
);
}
peopleAsFlattened = (pla: IPeopleList[]) => {
return pla.map(pl => pl.people.map(p => {
return {
id: p.id,
fullName: p.fullNames[0].firstName + " " + p.fullNames[0].lastName
};
}));
}
map and switchMap have completely different purposes:
map - transform the shape of an emission
switchMap - subscribe to an observable and emit its emissions into the stream
map
Use map when you want transform the shape of each emission. Ex: emit the user name property, instead of the entire user object:
userName$: Observable<string> = this.service.getUser(123).pipe(
map(user => user.name)
);
switchMap
Use switchMap when you want to map an emission to another observable and emit its emissions. Ex: You have an observable of some id and want to emit the resource after fetching it:
user$: Observable<User> = this.userId$.pipe(
switchMap(id => this.service.getUser(id)),
);
When user$ is subscribed to, the user returned from service.getUser(id) is emitted (not the userId string).
switchMap is not interchangeable with the map operator, nor vise versa. Although both of them has to do with mapping (as their names suggest), they have two separate use-cases.
In your particular case, the map operator is the way to go.
When to use switchMap?
You can only use switchMap(cb) when you check all these requirements:
Your callback function, cb, passed into switchMap returns an observable, observable$.
If your cb (callback function) does not return an observable, you should look into operators that don't handle higher-level observables, such as filter and map (what you actually needed); not operators that handle higher-level observables such as concatMap and well, switchMap.
You want to execute your cb sequentially before the next operation down the pipeline (after switchMap) executes.
Maybe you want to run logic inside of cb, and optionally get the return value of cb after executing, so that you can pass it down the pipeline for further processing, for example.
When you want to "discard" what will happen to cb's execution and re-execute cb every time the source observable (the thing that trickles down to switchMap(cb)) emits a new value/notification.
Applying what we hopefully learned, we know that your cb:
pl => {
return this.peopleAsFlattened(pl.peopleList).reduce((p, c) => p.concat(c));
}
returns a plain JavaScript array; not an observable. This takes using switchMap out of the question since it violates the first requirement I made up above.
Hopefully that makes sense. :)
We use switchMap when the source observable is a hot observable. In which case you prefer the behaviour that cancel the succeeding observable when source emits.
In your code, you source is a one-off http call which means it will not emit multiple times and the follow up action is not executing observable but to mutate an array. There is no need to use switchMap
I was originally searching how to return several actions in a ngrx effect, and found I need to return an array of actions.
Then I noticed that returning a simple array in the switchMap works as fine as returning an observable created from this array.
For example:
timer(1000).pipe(switchMap(val => from([val, val + 1)])).subscribe(val => console.log(val));
timer(1000).pipe(switchMap(val => [val, val + 1])).subscribe(val => console.log(val));
I expect the first to work and think it is the correct syntax.
I don't expect the second to work but it actually does and I would like to understand why.
Thanks,
Because switchMap, among other flattening-operators (mergeMap, exhaustMap, ...), takes an ObservableLike as the return type of its projection function.
An ObservableLike can be Observable, Promise or Array. If you provide an array, it is converted into a stream of its items - basically the same as if you had used from.
I would like to understand why take operator is used in code given below.
private _places = new BehaviorSubject<Place[]>(
// places for initialization
);
get places() {
return this._places.asObservable();
}
addPlace(title: string, description: string, price: number)
{
generatedId: string;
newPlace: Place;
// code to initialize newPlace
return this.http.post<{name: string}>(
'https://ionic-angular-ef2f8.firebaseio.com/offered-places.json',
{...newPlace, id: null})
.pipe(
switchMap(response => {
generatedId = response.name;
return this.places;
}),
take(1),
tap(places => {
newPlace.id = generatedId;
this._places.next(places.concat(newPlace));
})
);
}
post request returns an Observable, and we get a value from it in switchMap operator (note that we do not take a value before calling switchMap). In switchMap we replace the observable with a new observable got from _places, a BehaviourSubject object. After switchMap we use take operator.
Why don't we skip take operator, and use tap straight off? Do we take a value from an observable, because the observable is generated from a subject? Who can explain the use case of take operator in details?
UPDATE
I suspect that the reason I should use take operator after switchMap is that switchMap returns an observable received from an object of type BehaviorSubject which holds emitted values. One can subscribe to such BehaviorSubject object and take the latest emitted value - that's exactly what I did.
httpClient.post() emits one next notification and one complete notification.
However, they're using switchMap to merge another Observable to the chain (this.places). switchMap() won't complete until its source and the inner Observable complete so they're using take(1) to complete the chain after the first emission from this.places which is a BehaviorSubject that doesn't complete until you deliberately call complete() on it.
This is a very similar use-case to using takeUntil() to complete chains. takeUntil() always has to be the last operator in a chain because completing a source Observable to switchMap(), concatMap(), ... doesn't necessarily complete the whole chain. See this for more details https://medium.com/angular-in-depth/rxjs-avoiding-takeuntil-leaks-fb5182d047ef
If I have the following code:
const subject = new BehaviorSubject<[]>([]);
const observable = subject.asObservable();
subject.next([{color: 'blue'}])
observable.pipe(first()).subscribe(v => console.log(v))
According to the docs:
If called with no arguments, first emits the first value of the source Observable, then completes....
Does this mean that the source observable(the BehaviorSubject in this case) completes and you can no longer use it? As in you can no longer call .next([...]) on it.
I'm trying to understand how can an observable complete if it doesnt have the .complete() method on it?
I was trying to look at the source code of first() which under the covers uses take() and in turn take() uses lift() so I was curious if somehow first operator returns a copy of the source observable(the subject) and completes that.
The source observable is not completing, what it completes is the subscription. You could have multiple subscriptions on your Observable source, in your case one BehaviorSubject.
subject.next([{color: 'blue'}])
subject.next([{color: 'red'}])
const subs1 = observable.pipe(first()).subscribe(v => console.log(v))
const subs2 = observable.subscribe(v => console.log(v))
In the example above you clearly see that the source is not completing, just the subscription.
I have created a Stackblitz if you want to try it: https://stackblitz.com/edit/rxjs-uv6h6i
Hope I got your point!
Cheers :)
What's the best way to handle asynchronous updates in the middle of an Observable stream.
Let's say there are 3 observables:
Obs1 (gets data from API) -> pipes to Obs2
Obs2 (transforms data) -> pipes to Obs3
Obs3 (sends transformed data)
(The actual application is more complex, and there's reasons it's not done in a single Observable, this is just a simple example).
That all works well and good if it's a linear synchronous path.
But we also have async messages that will change the output of Obs2.
3 scenarios I'm asking about are:
- we fetch data, and go through Obs1, Obs2 & Obs3
- we get a message to make a change, go through Obs2 & Obs3
- we get a different message to make a change which also needs to apply the change from the previous message, through Obs2 & Obs3
The main problem here is that there are different types of asynchronous messages that will change the outcome of Obs2, but they all need to still know what the previous outcome of Obs2 was (so the any other changes from messages that happened before is still applied)
I have tried using switchMap in Obs2 with a scan in Obs1 like this:
obs1
const obs1$ = obs1$.pipe(
// this returns a function used in the reducer.
map((data) => (prevData) => 'modifiedData',
scan((data, reducer) => reducer(betsMap), {})
)
obs2
const obs2$ = obs1$.pipe(
switchMap(data =>
someChange$.pipe(map(reducer => reducer(data)))
)
)
where someChange$ is a BehaviorSubject applying a change using another reducer function.
This works fine for async message #1 that makes some change.
But when message #2 comes in and a different change is needed, the first change is lost.
the changes that should be in "prevData" in obs1$ is always undefined because it happens before the message is applied.
How can I take the output from obs2$ and apply asynchronous updates to it that remembers what all of the past updates was? (in a way where I can clear all changes if needed)
So if i got the question right, there are two problems that this question tackles:
First: How to cache the last 2 emitted values from stream.
scan definitely is the right way, if this cache logic is needed in more than one place/file, I would go for a custom pipe operator, like the following one
function cachePipe() {
return sourceObservable =>
sourceObservable.pipe(
scan((acc, cur) => {
return acc.length === 2 ? [...acc.slice(1), cur] : [...acc, cur];
}, [])
);
}
cachePipe will always return the latest 2 values passed trough the stream.
...
.pipe(
cachePipe()
)
Second: How to access data from multiple streams at the same time, upon stream event
Here rxjs's combineLatest creation operator might do the trick for you,
combineLatest(API$, async1$ ,async2$,async3$)
.pipe(
// here I have access to an array of the last emitted value of all streams
// and the data can be passed to `Obs2` in your case
)
In the pipe I can chain whatever number of observables, which resolves the second problem.
Note:
combineLatest needs for all streams, inside of it, to emit once, before the operator strats to emit their combined value, one workaround is to use startWith operator with your input streams, another way to do it is by passing the data trough BehaviorSubject-s.
Here is a demo at CodeSandbox , that uses the cachePipe() and startWith strategy to combine the source (Obs1) with the async observables that will change the data.