I have an observable const numbers = from([1,2,3]) which will emit 1, 2, 3, then complete.
I need to map this to another observable e.g. like this:
const mapped = numbers.pipe(
concatMap(number => Observable.create(observer => {
observer.next(number);
}))
);
But now the resulting observable mapped emits 1, 2, 3 but not the complete event.
How can I preserve the complete event in mapped?
Your code gives me just "1" (with RxJS 6); are you sure you see 3 values?
Rx.from([1,2,3]).pipe(
op.concatMap(number => Rx.Observable.create(observer => {
observer.next(number);
}))
).forEach(x => console.log(x)).then(() => console.log('done'))
You're never completing the created Observable (it emits one value but never calls observer.complete()). This works:
Rx.from([1,2,3]).pipe(
op.concatMap(number => Rx.Observable.create(observer => {
observer.next(number); observer.complete();
}))
).forEach(x => console.log(x)).then(() => console.log('done'))
This all shows how hard it is to use Rx.Observable.create() correctly. The point of using Rx is to write your code using higher-level abstractions. A large part of this is preferring to use operators in preference to observers. E.g. in your case (which is admittedly simple):
Rx.from([1,2,3])
.pipe(op.concatMap(number => Rx.of(number)))
.forEach(x => console.log(x)).then(() => console.log('done'))
Related
How to combine the distinct, switchMap and mergeMap operators, so that when the source emits repeated values (detected by distinct.keySelector), the previous subscription is canceled (as in the switchMap), but if the value is not repeated follow the behavior of mergeMap?
Example:
source = from(1, 2, 1, 2, 3) // 'abcde'
result = source.pipe(delay(), combination() // '--cde'
I'm currently doing something like:
const activeSubscriptions = new Map();
source$.pipe(
mergeMap((value) => {
const pendingSubscription = activeSubscriptions.get(value);
if (pendingSubscription) {
pendingSubscription.unsubscribe();
activeSubscriptions.delete(value);
}
const request$ = new Subject();
const subscription = this.service.get(value).subscribe({
complete: () => request$.complete(),
error: (err) => request$.error(err),
next: (value) => request$.next(value),
});
activeSubscriptions.set(value, subscription);
return request$;
})
);
But looking for a better way to do that.
Thank you in advance
I think you can use the windowToggle operator for this:
src$ = src$.pipe(shareReplay(1));
src$.pipe(
ignoreElements(),
windowToggle(src$.pipe(observeOn(asyncScheduler)), openValue => src$.pipe(skip(1), filter(v => v === openValue))),
mergeMap(
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
),
)
A replacement for observeOn(asyncScheduler) could also be delay(0), the important thing is to make sure the order in which the src$'s subscribers receive the value is correct. In this case, we want to make sure that when src$ emits, the clean-up takes place first, so that's why we're using src$.pipe(observeOn(asyncScheduler)).
ignoreElements() is used because each window is paired to only one value, the one which has created the window. The first argument(s) passed to windowToggle will describe the observable(s) which can create the windows. So, we only need those, since we're able to get the last value with the help of
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
By the way, a window is nothing but a Subject.
Lastly, if you want to perform async operations inside the window's pipe, you'll have to make sure that everything is unsubscribed when the window is completed(closed). To do that, you could try this:
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
switchMap(val => /* some async action which uses `val` */),
takeUntil(window.pipe(isEmpty()))
)
where isEmpty will emit either true or false when the source(in this case, the window) completes. false means that the source had emitted at least one value before emitting a complete notification, and true otherwise. In this case, I'd say it's irrelevant whether it's true or false, since the window will not emit any values by itself(because we have used ignoreElements, which ignores everything except error and complete notifications).
Is there a way to keep track of a value with pipeable operators?
To give a tangible example, let's say that I want to :
Over a data stream S:
Memorize S current data
Make a HTTP request
Do stuff with the response
Make another HTTP request using the result of the operation
Create an object containing a value base on 1. and the response
Then merge these and exploit the objects I created.
Basically,
const data = from([1, 2, 3]).pipe(
// Memorize here
map(a => req1(a)),
flatMap(a => a),
map(b => syncOp(b)),
map(c => req2(c)),
flatMap(d => d),
map(e => ({id: _memorized_, value: e}))
merge(data).subscribe(f => console.log(f.id, f.value))
Related stackblitz
Any input on the matter will be greatly appreciated.
Note: If possible, I'd prefer not to carry the value I need all the way down via the creation of an object at the top.
You can easily do that by just restructuring your operators and making memorized a local variable:
const data = from([1, 2, 3]).pipe(
// Memorize here
mergeMap(memorized => req1(memorized).pipe(
flatMap(a => a),
map(b => syncOp(b)),
map(c => req2(c)),
flatMap(d => d),
map(e => ({id: memorized, value: e}))
));
merge(data).subscribe(f => console.log(f.id, f.value));
I have this piece of RxJS code
this.listItems$ = this.store.select(EntityState.relationshipItems).pipe(
map(fn => fn(12)),
mergeMap(items => items),
map(this.toListItem),
toArray<ListItem>(),
tap(x => console.log(x))
);
Using mergeMap(items => items) I'm trying to "flatten" the array, then map each item to another object, and then convert it back to an array.
However, the flow doesn't even reach the last tap. I can see the toListItem function is called, but I don't understand why it stops there.
Transforming it to
this.listItems$ = this.store.select(EntityState.relationshipItems).pipe(
map(fn => fn(12)),
map(items => items.map(this.toListItem)),
tap(x => console.log(x))
);
makes it work, but I'd like to understand why the above one doesn't work.
That's because this.store.select(...) is a Subject that never completes (if it did then you could select data just once which doesn't make sense).
However, toArray collects all emissions from its source and when its source completes it emits a single array. But the source is this.store.select(...) that never completes so toArray never emits anything.
So probably the easiest workaround would be just restructuring your chain:
this.listItems$ = this.store.select(EntityState.relationshipItems).pipe(
map(fn => fn(12)),
mergeMap(items => from(items).pipe(
map(this.toListItem),
toArray<ListItem>(),
tap(x => console.log(x))
)),
);
Now the source is from that completes after iterating items so toArray will receive complete notification and emit its content as well.
I have a scenario where I need to make a request to an endpoint, and then based on the return I need to either produce multiple items or just pass an item through (specifically I am using redux-observable and trying to produce multiple actions based on an api return if it matters).
I have a simplified example below but it doesn't feel like idiomatic rx and just feels weird. In the example if the value is even I want to produce two items, but if odd, just pass the value through. What is the "right" way to achieve this?
test('url and response can be flatMap-ed into multiple objects based on array response and their values', async () => {
const fakeUrl = 'url';
axios.request.mockImplementationOnce(() => Promise.resolve({ data: [0, 1, 2] }));
const operation$ = of(fakeUrl).pipe(
mergeMap(url => request(url)),
mergeMap(resp => resp.data),
mergeMap(i =>
merge(
of(i).pipe(map(num => `number was ${num}`)),
of(i).pipe(
filter(num => num % 2 === 0),
map(() => `number was even`)
)
)
)
);
const result = await operation$.pipe(toArray()).toPromise();
expect(result).toHaveLength(5);
expect(axios.request).toHaveBeenCalledTimes(1);
});
Personally I'd do it in a very similar way. You just don't need to be using the inner merge for both cases:
...
mergeMap(i => {
const source = of(`number was ${i}`);
return i % 2 === 0 ? merge(source, of(`number was even`)) : source;
})
I'm using concat to append a value after source Observable completes. Btw, in future RxJS versions there'll be endWith operator that will make it more obvious. https://github.com/ReactiveX/rxjs/pull/3679
Try to use such combo - partition + merge.
Here is an example (just a scratch)
const target$ = Observable.of('single value');
const [streamOne$, streamTwo$] = target$.partition((v) => v === 'single value');
// some actions with your streams - mapping/filtering etc.
const result$ = Observable.merge(streamOne$, streamTwo$)';
Within an observable chain, I need to perform some async work, then return the source value to the next observable so I had to pipe(mapTo(x)) after the async work.
A more complete example:
// fake async work with just 1 null value
someAsyncWork = () => of(null)
of('1', '2', '3').pipe(
// some async work
concatMap(no => someAsyncWork().pipe(mapTo(no))),
concatMap(no => `Some async work [${no}] done!`)
).subscribe(message => console.log(message))
I cannot use tap(no => someAsyncWork()) because that would cause the next observable to run before someAsyncWork() returns.
While my current approach works, it somewhat clutters the code...and I have this pattern repeated in many places within the codebase.
Question: Anyway to do this without pipe(mapTo(no)) - in a more concise/readable way?
Perhaps the simplest thing to do would be to write your own pipeable operator.
For example:
const concatTap = <T>(project: (value: T) => Observable<any>) =>
concatMap((value: T) => project(value).pipe(mapTo(value)));
However, that assumes the observable for the async operation emits only a single value. To guard against multiple values being emitted you could do something like this:
const concatTap = <T>(project: (value: T) => Observable<any>) =>
concatMap((value: T) => concat(project(value).pipe(ignoreElements()), of(value)));
You could use concatTap like this:
of('1', '2', '3').pipe(
concatTap(() => someAsyncWork()),
concatMap(no => `Some async work [${no}] done!`)
).subscribe(message => console.log(message));
I'm sure you could choose a better name than I did. concatTap was the first thing that popped into my head. Naming things is hard.