Why does the rxjs share operator not work as expected in this setTimeout() example? - javascript

I don't understand why the rxjs share operator does not work with setTimeout().
I'm trying to understand this blogpost. In this example, the concept of "shared subscription" does not seem to work as expected.
const observable1 = Observable.create(observer => {
observer.next(`I am alive.`);
setTimeout(() => {
observer.next(`I am alive again.`);
}, 1000);
}).pipe(share());
observable1.subscribe(x => console.log(x));
observable1.subscribe(x => console.log(x));
Expected:
I am alive.
I am alive again.
Actual:
I am alive.
I am alive again.
I am alive again.
Reproducable stackblitz.

That is the expected output.
From official docs on share() operator:
Returns a new Observable that multicasts (shares) the original Observable. As long as there is at least one Subscriber this Observable will be subscribed and emitting data.
That means as soon as an observer subscribes, the observable starts emitting data.
So when the first subscribe statement observable1.subscribe(x => console.log(x)); executes, an observer subscribes and data is emitted by observer.next('I am alive.); statement.
When second subscribe statement executes, another observer subscribes and it receives only the data emitted from that point of time. This is the data emitted by observer.next('I am alive again.'); in setTimeout() method.
We can see this clearly in this StackBlitz demo where we are logging Observer1 and Observer2 text along with the received data.
I think the point of confusion is seeing two I am alive again. statements. It is logged twice because we are logging it in each subscriber. Move these log statements to the observable and they will only be logged once. This makes it more evident that the observable is executed only once.

This is the supposed behaviour of share(). It monitores and shares only one action. Here is an example taken from learnrxjs.com. As you can see only the tap()-operator is monitored. The mapTo()-operator is ignored.
// RxJS v6+
import { timer } from 'rxjs';
import { tap, mapTo, share } from 'rxjs/operators';
//emit value in 1s
const source = timer(1000);
//log side effect, emit result
const example = source.pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***')
);
/*
***NOT SHARED, SIDE EFFECT WILL BE EXECUTED
TWICE***
output:
"***SIDE EFFECT***"
"***RESULT***"
"***SIDE EFFECT***"
"***RESULT***"
*/
const subscribe = example.subscribe(val => console.log(val));
const subscribeTwo = example.subscribe(val => console.log(val));
//share observable among subscribers
const sharedExample = example.pipe(share());
/*
***SHARED, SIDE EFFECT EXECUTED ONCE***
output:
"***SIDE EFFECT***"
"***RESULT***"
"***RESULT***"
*/
const subscribeThree = sharedExample.subscribe(val => console.log(val));
const subscribeFour = sharedExample.subscribe(val => console.log(val));

Related

Can this rxjs merge logic be simplified?

I have an observable (onAuthStateChanged) from the Firebase client that:
emits null immediately if the user is not signed in, and
emits null and then a user object a few moments later if the user is signed in.
const observable = new Observable((obs) => {
return app.auth().onAuthStateChanged(
obs.next,
obs.error,
obs.complete
)
})
What I want is to:
ignore any emitted null values for the first 1000ms of the app lifecycle (null coming after 1000ms is accepted)
always emit user object regardless of what time it comes
if no user object comes in the first 1000ms, then emit null at the 1000ms mark
Here is what I've done (and it seems to work). However, I'm reluctant to use this code as it doesn't seem that concise:
const o1 = observable.pipe(skipUntil(timer(1000)))
const o2 = observable.pipe(
takeUntil(o1),
filter((user) => user !== null)
)
const o3 = timer(1000).pipe(takeUntil(o2), mapTo(null))
merge(o1, o2, o3).subscribe({
next: setUser,
error: console.log,
complete: () => console.log("error: obs completed, this shouldn't happen"),
})
Is there a way to do this without merge? I tried going through the docs but I'm quite lost.
Thanks for your help!
You could use concat instead of merge. Think of it as using the first source until it completes, then use the second source.
const nonNullUser = firebaseUser.pipe(
filter(user => user !== null),
takeUntil(timer(1000))
);
const user = concat(nonNullUser, firebaseUser);
user.subscribe(...);
I just realized that this solution will not explicitly perform step #3 "emit null at the 1000ms mark". I was thinking subscribing to firebaseUser would emit the latest value. But, I'm not sure if that's true for your scenario.
If not, we could easily achieve this by adding shareReplay like this:
const firebaseUser = observable.pipe(shareReplay(1));
While I liked the answer from #BizzyBob I was genuinely intrigued by these requirements that I wanted to see what other options were available. Here's what I produced:
const auth$ = observable.pipe(
startWith(null)
)
const null$ = timer(1000).pipe(
switchMap(_=>auth$)
)
const valid$ = auth$.pipe(
filter(user=>!!user)
)
const user$ = race(null$, valid$);
We have our source auth$ observable which gets your Firebase data. However, startWith() will immediately emit null before any values coming from Firebase.
I declared two observables for null and non-null cases, null$ and valid$.
The null$ observable will subscribe to auth$ after 1000ms. When this happens it immediately emits null thanks to the startWith() operator.
The valid$ observable subscribes to auth$ immediately but only emits valid user data thanks to filter(). It won't emit startWith(null) because it is caught by the filter.
Last, we declare user$ by using the race() operator. This operator accepts a list of observables as its parameters. The first observable to emit a value wins and is the resulting subscription.
So in our race, valid$ has 1000ms to emit a valid user. If it doesn't, race() will subscribe to null$ resulting in the immediate null, and all future values coming from Firebase.

Struggling with flatMap vs concatMap in rxJs

I am struggling to understand the difference between the flatMap and concatMap in rxJs.
The most clear answer that I could understand was that here difference-between-concatmap-and-flatmap
So I went and tried things out by my self.
import "./styles.css";
import { switchMap, flatMap, concatMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
import { Observable } from "rxjs";
function createObs1() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(1);
subscriber.complete();
}, 900);
});
}
function createObs2() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(2);
//subscriber.next(22);
//subscriber.next(222);
subscriber.complete();
}, 800);
});
}
function createObs3() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(3);
//subscriber.next(33);
//subscriber.next(333);
subscriber.complete();
}, 700);
});
}
function createObs4() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 600);
});
}
function createObs5() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(5);
subscriber.complete();
}, 500);
});
}
createObs1()
.pipe(
flatMap((resp) => {
console.log(resp);
return createObs2();
}),
flatMap((resp) => {
console.log(resp);
return createObs3();
}),
flatMap((resp) => {
console.log(resp);
return createObs4();
}),
flatMap((resp) => {
console.log(resp);
return createObs5();
})
)
.subscribe((resp) => console.log(resp));
console.log("hellooo");
I have used that playground here playground example
Questions
1)
From my understanding the use of flatMap should mix the outputs so that the console logs are like (1,3,2,4,5). I have tried more than 30 times and always come on the same row (1, 2, 3, 4, 5)
What am I doing wrong or have undestood wrong?
2)
If on createObs2() and createObs3() you remove the comments and include the code with multiple emitted events then things get messy. Even if you change to concatMap it messes things and results come mixed. Multiple numbers that I expect only once come multiple times. The result can be (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) Why this happens?
How I test the example on playground. I just remove only 1 letter from the last console.log("hello"). Only one change for example console.log("heloo") and is then observed and project is compiled again and output printed in console.
Edit: The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library.
createObs1().subscribe( (resp1) => {
console.log(resp1);
createObs2().subscribe( (resp2) => {
console.log(resp2);
createObs3().subscribe( (resp3) => {
console.log(resp3);
createObs4().subscribe( (resp4) => {
console.log(resp4);
createObs5().subscribe( (resp5) => {
console.log(resp5);
})
})
})
})
})
Your test scenario is not really sufficient to see the differences between these two operators. In your test case, each observable only emits 1 time. If an observable only emits a single value, there is really no different between concatMap and flatMap (aka mergeMap). The differences can only be seen when there are multiple emissions.
So, let's use a different scenario. Let's have a source$ observable that simply emits an incrementing integer every 1 second. Then, within our "Higher Order Mapping Operator" (concatMap & mergeMap), we will return an observable that emits a variable number of times every 1 second, then completes.
// emit number every second
const source$ = interval(1000).pipe(map(n => n+1));
// helper to return observable that emits the provided number of times
function inner$(max: number, description: string): Observable<string> {
return interval(1000).pipe(
map(n => `[${description}: inner source ${max}] ${n+1}/${max}`),
take(max),
);
}
Then let's define two separate observables based on the source$ and the inner$; one using concatMap and one using flatMap and observe the output.
const flatMap$ = source$.pipe(
flatMap(n => inner$(n, 'flatMap$'))
);
const concatMap$ = source$.pipe(
concatMap(n => inner$(n, 'concatMap$'))
);
Before looking the differences in the output, let's talk about what these operators have in common. They both:
subscribe to the observable returned by the passed in function
emit emissions from this "inner observable"
unsubscribe from the inner observable(s)
What's different, is how they create and manage inner subscriptions:
concatMap - only allows a single inner subscription at a time. As it receives emissions, it will only subscribe to one inner observable at a time. So it will initially subscribe to the observable created by "emission 1", and only after it completes, will it subscribe to the observable created by "emission 2". This is consistent with how the concat static method behaves.
flatMap (aka mergeMap) - allows many inner subscriptions. So, it will subscribe to the inner observables as new emissions are received. This means that emissions will not be in any particular order as it will emit whenever any of its inner observables emit. This is consistent with how the merge static method behaves (which is why I personally prefer the name "mergeMap").
Here's a StackBlitz that shows the output for the above observables concatMap$ and mergeMap$:
Hopefully, the above explanation helps to clear up your questions!
#1 - "use of flatMap should mix the outputs"
The reason this wasn't working as you expected was because only one emission was going through the flatMap, which means you only ever had a single "inner observable" emitting values. As demonstrated in the above example, once flatMap receives multiple emissions, it can have multiple inner observables that emit independently.
#2 - "...and include the code with multiple emitted events then things get messy."
The "things get messy" is due to having multiple inner subscription that emit values.
For the part you mention about using concatMap and still getting "mixed" output, I would not expect that. I have seen weird behavior in StackBlitz with observable emissions when "auto save" is enabled (seems like sometimes it doesn't completely refresh and old subscriptions seem to survive the auto refresh, which gives very messy console output). Maybe code sandbox has a similar problem.
#3 - "The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library"
This makes sense. You don't want to mess around with nested subscriptions, because there isn't a great way to guarantee the inner subscriptions will be cleaned up.
In most cases with http calls, I find that switchMap is the ideal choice because it will drop emissions from inner observables you no longer care about. Imagine you have a component that reads an id from a route param. It uses this id to make an http call to fetch data.
itemId$ = this.activeRoute.params.pipe(
map(params => params['id']),
distinctUntilChanged()
);
item$ = this.itemId$.pipe(
switchMap(id => http.get(`${serverUrl}/items/${id}`)),
map(response => response.data)
);
We want item$ to emit only the "current item" (corresponds to the id in the url). Say our UI has a button the user can click to navigate to the next item by id and your app finds itself with a click-happy user who keeps smashing that button, which changes the url param even faster than the http call can return the data.
If we chose mergeMap, we would end up with many inner observables that would emit the results of all of those http calls. At best, the screen will flicker as all those different calls come back. At worst (if the calls came back out of order) the UI would be left displaying data that isn't in sync with the id in the url :-(
If we chose concatMap, the user would be forced to wait for all the http calls to be completed in series, even though we only care about that most recent one.
But, with switchMap, whenever a new emission (itemId) is received, it will unsubscribe from the previous inner observable and subscribe to the new one. This means it will not ever emit the results from the old http calls that are no longer relevant. :-)
One thing to note is that since http observables only emit once, the choice between the various operators (switchMap, mergeMap, concatMap) may not seem to make a difference, since they all perform the "inner observable handling" for us. However, it's best to future-proof your code and choose the one that truly gives you the behavior you would want, should you start receiving more than a single emission.
Every time the first observable emits, a second observable is created in the flatMap and starts emitting. However, the value from the first observable is not passed along any further.
Every time that second observable emits, the next flatMap creates a third observable, and so on. Again, the original value coming into the flatMap is not passed along any further.
createObs1()
.pipe(
flatMap(() => createObs2()), // Merge this stream every time prev observable emits
flatMap(() => createObs3()), // Merge this stream every time prev observable emits
flatMap(() => createObs4()), // Merge this stream every time prev observable emits
flatMap(() => createObs5()), // Merge this stream every time prev observable emits
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
So, it's only the values emitted from createObs5() that actually get emitted to the observer. The values emitted from the previous observables have just been triggering the creation of new observables.
If you were to use merge, then you would get what you may have been expecting:
createObs1()
.pipe(
merge(createObs2()),
merge(createObs3()),
merge(createObs4()),
merge(createObs5()),
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
// 4
// 3
// 2
// 1

How do I sequence actions in RxJS/redux-observable vs redux-saga?

I've started learning RxJs deeply, one of reasons is to master redux-observable side effects approach, tho I find sagas more convenient and "declarative". I've already learned merge/flat/concat/switchMap operators, but it didn't help me to figure out how to sequence things in rxjs.
Here's an example of what I mean by "sequencing", on instance of Timer app where start may be scheduled after some period of time, implemented with redux-saga:
export function* timerSaga() {
while (true) {
yield take('START');
const { startDelay } = yield select(); // scheduled delay
const [cancelled] = yield race([
take('CANCEL_START'),
delay(startDelay)
]);
if (!cancelled) {
yield race([
call(function*() {
while (true) {
yield delay(10);
yield put({ type: 'TICK' });
}
}),
take(['STOP', 'RESET']
]);
}
}
}
I find that example very logically consistent and clear. I have no idea how to implement that with redux-observable. Please, simply give me peace of code that reproduces same logic but with rxjs operators.
Between sagas (generators) and epics (observables), it's important to change
the way you think about how events arrive at your code.
Generators satisfy the iterator and iterable protocols, which involve pulling
values/events (in this case, Redux actions) from the source, and blocking
execution until those events arrive.
Observables are push rather than pull. We describe and name streams of events
that we're interested in, and then we subscribe to them. There are no blocking
calls because all of our code is triggered by events when they occur.
This code duplicates the behavior in the saga example.
import { interval, timer } from 'rxjs';
import { withLatestFrom, mapTo, exhaustMap, takeUntil } from 'rxjs/operators';
import { ofType } from 'redux-observable';
const myEpic = (action$, state$) => {
// A stream of all the "cancel start" actions
const cancelStart$ = action$.pipe(ofType('CANCEL_START'));
// This observable will emit delayed start events that are not cancelled.
const delayedCancellableStarts$ = action$.pipe(
// When a start action occurs...
ofType('START'),
// Grab the latest start delay value from state...
withLatestFrom(state$, (_, { startDelay }) => startDelay),
exhaustMap(
// ...and emit an event after our delay, unless our cancel stream
// emits first, then do nothing until the next start event arrives.
// exhaustMap means we ignore all other start events while we handle
// this one.
(startDelay) => timer(startDelay).pipe(takeUntil(cancelStart$))
)
);
// On subscribe, emit a tick action every 10ms
const tick$ = interval(10).pipe(mapTo({ type: 'TICK' }));
// On subscribe, emit only STOP or RESET actions
const stopTick$ = action$.pipe(ofType('STOP', 'RESET'));
// When a start event arrives, start ticking until we get a message to
// stop. Ignore all start events until we stop ticking.
return delayedCancellableStarts$.pipe(
exhaustMap(() => tick$.pipe(takeUntil(stopTick$)))
);
};
Importantly, even though we're creating and naming these observable streams, their behavior is lazy - none of them are 'activated' until subscribed to, and that happens when you provide this epic function to the redux-observable middleware.
I assume take() return an observable, haven't test the code. It can probably be transformed to rx fashion like below.
The key here is repeat() and takeUntil()
// outter condition for starting ticker
forkJoin(take('START'), select())
.pipe(
switchMap(([, startDelay]) =>
// inner looping ticker
timer(10).pipe(switchMap(_ => put({type: 'TICK'})), repeat(),
takeUntil(race(
take('CANCEL_START'),
delay(startDelay)
))
)
/////////////////////
)
)

switchMap operation only running on first call?

I have an angular application that makes a request to an Http service and calls a switchMap on another Http service. For some reason the request in the switchMap only runs the first time the parent call is called. Otherwise the parent request fires and the switchMap one doesn't, here is the code:
this._receivableService.newTenantDebitCredit(tenantCredit)
.take(1)
.switchMap(result =>
// Refresh the lease receivables before giving result
this._receivableService.getAll({
refresh: true,
where: { leaseId: this.leaseId }
}).take(1).map(() => result)
)
.subscribe(
...
)
How can I make the getAll request in the switch map run every time the newTenantDebitCredit method is called above it?
Edit: Here is the entirety of the function that is called on click. when i click the button the first time for a given unit both methods are executed. If I try a Unit that has already had that method called (without a refresh) only the first method is executed. I realize a lot of this may not be clear it's a rather large project at this point.
public submitTenantCredit() {
this.isLoading = true;
let tenantCredit: NewTenantDebitCreditData;
let receivableDefinitions: ReceivableDefinition[] = [];
// construct receivable defintions for NewTenantDebitData model
receivableDefinitions = this._constructReceivableDefinitions();
// construct data we will be POSTing to server.
tenantCredit = new NewTenantDebitCreditData({
siteId: this._apiConfig.siteId,
leaseId: this.leaseId,
isCredit: true,
receivables: receivableDefinitions,
reason: this.actionReason
});
// make service call and handle response
this._receivableService.newTenantDebitCredit(tenantCredit)
.take(1)
.switchMap(result =>
// Refresh the lease receivables before giving result
this._receivableService.getAll({
refresh: true,
where: { leaseId: this.leaseId }
}).take(1).map(() => result)
)
.take(1)
.subscribe(
(receivables) => {
this.closeReasonModal();
let refreshLeaseId = this.leaseId;
this.leaseId = refreshLeaseId;
this.isLoading = false;
this.refreshBool = !this.refreshBool;
this._debitCreditService.refreshUnitInfo();
this._notifications.success(`The tenant credit for ${this.customerName} - Unit ${this.unitNumber} was submitted successfully`);
},
(error) => {
console.error(error);
this.isLoading = false;
}
)
}
If it helps newTenantDebitCredit() is a HTTP POST request and getAll() is a GET request.
You used take operator. When your service observable will emit then take operator will execute first and take will chain only first emit from observable. Subsequent emit will not taken by your code.
If you want to take all emits from observable then remove take from your code.
Hope it will help.
Testing the Rx code in isolation, here's a mockup. The console logs happen each time, so I think the Rx you're using is ok.
The best guess at a likely culprit is this.refreshBool = !this.refreshBool, but we'd need to see the internals of newTenantDebitCredit and getAll to be definitive.
// Some mocking
const _receivableService = {
newTenantDebitCredit: (tc) => {
console.log('inside newTenantDebitCredit')
return Rx.Observable.of({prop1:'someValue'})
},
getAll: (options) => {
console.log('inside getAll')
return Rx.Observable.of({prop2:'anotherValue'})
}
}
const tenantCredit = {}
// Test
_receivableService.newTenantDebitCredit(tenantCredit)
.take(1)
.switchMap(result => {
console.log('result', result)
return _receivableService.getAll({
refresh: true,
where: { leaseId: this.leaseId }
})
.take(1)
.map(() => result)
})
.take(1)
.subscribe(
(receivables) => {
console.log('receivables', receivables)
//this.refreshBool = !this.refreshBool;
},
(error) => {
console.error(error);
}
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
First of all, this has nothing to do with the switchMap operator.
Normaly removing the take(1) would cause this behaviour. In this case it wouldn't because it itsn't a so called hot observable.
The problem is that you are using a http.post. This is a cold observable which means it will only return a value once. That is also the reason why you don't need to unsubscribe. It will NEVER fire twice. Possible sollutions might be:
Using web sockets to get realtime data.
Creating a timer which will periodically fetch the data.
Simply get the data again whenever you need it.
The way you are asking the question
How can I make the getAll request in the switch map run every time the newTenantDebitCredit method is called above it?
actually sounds to me as if you are calling only newTenantDebitCredit from somewhere in your code, expecting the second request to happen; so I think this might be a misunderstanding of how observable chains work. Let's make an example:
const source$ = Observable.of(42);
source$
.map(value => 2 * value)
.subscribe(console.log);
source$
.subscribe(console.log);
What would you expect this to log? If your answer is "It would log 84 twice", then that is wrong: it logs 84 and 42.
Conceptually, your situation is the same. The second request only happens when the observable returned by newTenantDebitCredit() emits; it will not happen anytime some caller calls newTenantDebitCredit. This is because observable chains do not mutate an observable in-place, they only ever return a new observable.
If you want the second request to happen, you have to actually change the definition of the newTenantDebitCredit method to return an observable set up to perform the second request; alternatively, set up a chained observable that you subscribe to instead of calling newTenantDebitCredit.
Not really an answer but I did solve my problem. It will almost certainly be of no use to anyone BUT it was an issue in the receivableService it was not properly cheeking the boolean: refresh and was pulling values from cache after the first time.

Making a lazy, cached observable that only execute the source once

I'm trying to use an rxjs observable to delegate, but share, a piece of expensive work across the lifetime of an application.
Essentially, something like:
var work$ = Observable.create((o) => {
const expensive = doSomethingExpensive();
o.next(expensive);
observer.complete();
})
.publishReplay(1)
.refCount();
Now, this works fine and does exactly what I want, except for one thing: if all subscribers unsubscribe, then when the next one subscribes, my expensive work happens again. I want to keep it.
now, I could use a subject, or I could remove the refCount() and use connect manually (and never disconnect). But that would make the expensive work happen the moment I connect, not the first time a subscriber tries to consume work$.
Essentially, I want something akin to refCount that only looks at the first subscription to connect, and never disconnect. A "lazy connect".
Is such a thing possible at all?
How does publishReplay() actually work
It internally creates a ReplaySubject and makes it multicast compatible. The minimal replay value of ReplaySubject is 1 emission. This results in the following:
First subscription will trigger the publishReplay(1) to internally subscribe to the source stream and pipe all emissions through the ReplaySubject, effectively caching the last n(=1) emissions
If a second subscription is started while the source is still active the multicast() will connect us to the same replaySubject and we will receive all next emissions until the source stream completes.
If a subscription is started after the source is already completed the replaySubject has cached the last n emissions and it will only receive those before completing.
const source = Rx.Observable.from([1,2])
.mergeMap(i => Rx.Observable.of('emission:'+i).delay(i * 100))
.do(null,null,() => console.log('source stream completed'))
.publishReplay(1)
.refCount();
// two subscriptions which are both in time before the stream completes
source.subscribe(val => console.log(`sub1:${val}`), null, () => console.log('sub1 completed'));
source.subscribe(val => console.log(`sub2:${val}`), null, () => console.log('sub2 completed'));
// new subscription after the stream has completed already
setTimeout(() => {
source.subscribe(val => console.log(`sub_late-to-the-party:${val}`), null, () => console.log('sub_late-to-the-party completed'));
}, 500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>

Categories

Resources