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

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>

Related

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

Filtering RxJS stream after emission of other observable until timer runs out

I want to achieve the following behavior in RxJS but could not find a way using the available operators:
Stream A: Generated by a continuous stream of events (e.g. browser scroll)
Stream B: Generated by another arbitrary event (e.g. some kind of user input)
When B emits a value, I want to pause the processing of A, until a specified amount of time has passed. All values emitted by A in this timeframe are thrown away.
When B emits another value during this interval, the interval is reset.
After the interval has passed, the emitted values of A are no longer filtered.
// Example usage.
streamA$
.pipe(
unknownOperator(streamB$, 800),
tap(val => doSomething(val))
)
// Output: E.g. [event1, event2, <skips processing because streamB$ emitted>, event10, ...]
// Operator API.
const unknownOperator = (pauseProcessingWhenEmits: Observable<any>, pauseIntervalInMs: number) => ...
I thought that throttle could be used for this use case, however it will not let any emission through, until B has emitted for the first time (which might be never!).
streamA$
.pipe(
// If B does not emit, this never lets any emission of A pass through!
throttle(() => streamB$.pipe(delay(800)), {leading: false}),
tap(val => doSomething(val))
)
An easy hack would be to e.g. subscribe manually to B, store the timestamp when a value was emitted in the Angular component and then filter until the specified time has passed:
(obviously goes against the side-effect avoidance of a reactive framework)
streamB$
.pipe(
tap(() => this.timestamp = Date.now())
).subscribe()
streamA$
.pipe(
filter(() => Date.now() - this.timestamp > 800),
tap(val => doSomething(val))
)
I wanted to check with the experts here if somebody knows an operator (combination) that does this without introducing side-effects, before I build my own custom operator :)
I think this would be an approach:
bModified$ = b$.pipe(
switchMap(
() => of(null).pipe(
delay(ms),
switchMapTo(subject),
ignoreElements(),
startWith(null).
)
)
)
a$.pipe(
multicast(
new Subject(),
subject => merge(
subject.pipe(
takeUntil(bModified$)
),
NEVER,
)
),
refCount(),
)
It may seem that this is not a problem whose solution would necessarily involve multicasting, but in the above approach I used a sort of local multicasting.
It's not that expected multicasting behavior because if you subscribe to a$ multiple times(let's say N times), the source will be reached N times, so the multicasting does not occur at that level.
So, let's examine each relevant part:
multicast(
new Subject(),
subject => merge(
subject.pipe(
takeUntil(bModified$)
),
NEVER,
)
),
The first argument will indicate the type of Subject to be used in order to achieve that local multicasting. The second argument is a function, more accurately called a selector. Its single argument is the argument specified before(the Subject instance). This selector function will be called every time a$ is being subscribed to.
As we can see from the source code:
selector(subject).subscribe(subscriber).add(source.subscribe(subject));
the source is subscribed, with source.subscribe(subject). What's achieved through selector(subject).subscribe(subscriber) is a new subscriber that will be part of the Subject's observers list(it's always the same Subject instance), because merge internally subscribes to the provided observables.
We used merge(..., NEVER) because, if the subscriber that subscribed to the selector completes, then, next time the a$ stream becomes active again, the source would have to be resubscribed. By appending NEVER, the observable resulted form calling select(subject) will never complete, because, in order for merge to complete, all of its observables have to complete.
subscribe(subscriber).add(source.subscribe(subject)) creates a connection between subscribed and the Subject, such that when subscriber completes, the Subject instance will have its unsubscribe method called.
So, let's assume we have subscribed to a$: a$.pipe(...).subscribe(mySubscriber). The Subject instance in use will have one subscriber and if a$ emits something, mySubscriber will receive it(through the subject).
Now let's cover the case when bModified$ emits
bModified$ = b$.pipe(
switchMap(
() => of(null).pipe(
delay(ms),
switchMapTo(subject),
ignoreElements(),
startWith(null).
)
)
)
First of all, we're using switchMap because one requirement is that when b$ emits, the timer should reset. But, the way I see this problem, 2 things have to happen when b$ emits:
start a timer (1)
pause a$'s emissions (2)
(1) is achieved by using takeUntil in the Subject's subscribers. By using startWith, b$ will emit right away, so that a$'s emissions are ignored. In the switchMap's inner observable we're using delay(ms) to specify how long the timer should take. After it elapses, with the help of switchMapTo(subject), the Subject will now get a new subscriber, meaning that a$'s emissions will be received by mySubscriber(without having to resubscribe to the source). Lastly, ignoreElements is used because otherwise when a$ emits, it would mean that b$ also emit, which will cause a$ to be stopped again. What comes after switchMapTo(subject) are a$'s notifications.
Basically, we're able to achieve the pausable behavior this way: when the Subject instance as one subscriber(it will have at most one in this solution), it is not paused. When it has none, it means it is paused.
EDIT: alternatively, you could have a look at the pause operator from rxjs-etc.

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

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));

asynchronously add epic to middleware in redux-observable

I'm trying to evaluate redux-observable. Just looking through the doc and I'm trying to get the async epic loading thing going. I created a fork of the jsbin from the docs which basically attempts to add the async usage of the BehaviorSubject stuff.
http://jsbin.com/bazoqemiqu/edit?html,js,output
In that 'PING PONG' example, I added an 'OTHER' action and then use BehaviorSubject.next (as described in the docs) to add that epic. However, when I run the example, what happens is that the PING action is fired, followed by an endless stream of 'OTHER' actions, but never the PONG action. To see this, I added the reduxLogger. View it in the dev tools console as the jsbin console doesn't render it correctly.
My question is what am I doing wrong? Why does the PONG action never get dispatched?
Your otherEpic is an infinite "loop" (over time)
const otherEpic$ = action$ =>
action$
.delay(1000)
.mapTo({ type: OTHER });
This epic has the behavior "when any action at all is received, wait 1000ms and then emit another action of type OTHER". And since the actions your Epics emit go through the normal store.dispatch cycle like any other action, that means after the first PING is received, it will emit an OTHER after 1000ms, which will then be recursively received by the same epic again, wait another 1000ms and emit another OTHER, repeat forever.
I'm not sure if this was known, but wanted to point it out.
You next() into the BehaviorSubject of epic$ before your rootEpic has started running/been subscribed to it.
BehaviorSubjects will keep the last value emitted and provide that immediately when someone subscribes. Since your rootEpic has not yet been called and subscribed to the by the middleware, you're replacing the initial value, so only the otherEpic is emitted and ran through the epic$.mergeMap stuff.
In a real application with async/bundle splitting, when you would call epic$.next(newEpic) should always be after the middleware has subscribed to your rootEpic and received the initial epic you provided to your BehaviorSubject.
Here's a demo of that working: http://jsbin.com/zaniviz/edit?js,output
const epic$ = new BehaviorSubject(combineEpics(epic1, epic2, ...etc));
const rootEpic = (action$, store) =>
epic$.mergeMap(epic => {console.log(epic)
return epic(action$, store)
});
const otherEpic = action$ =>
action$.ofType(PONG)
.delay(1000)
.mapTo({ type: OTHER });
const epicMiddleware = createEpicMiddleware(rootEpic);
const store = createStore(rootReducer,
applyMiddleware(loggerMiddleware, epicMiddleware)
);
// any time AFTER the epicMiddleware
// has received the rootEpic
epic$.next(otherEpic);
The documentation says "sometime later" in the example, which I now see isn't clear enough. I'll try and clarify this further.
You may also find this other question on async loading of Epics useful if you're using react-router with Webpack's require.enquire() splitting.
Let me know if I can clarify any of these further 🖖

How to create an Observable that only fires when it has subscribers, and provides the latest value to new subscribers immediately

I'm trying to create a stream/observable that...
Only outputs events when it has subscribers
Provides any new subscribers with the latest value.
The concrete case is that I need an observable that makes an Async API call whenever a particular event happens, but only if it has subscribers. I'm trying to avoid unnecessary API calls.
I've managed to create a stream that only fires when it has subscribers like this...
let dataStream = Rx.Observable
.interval(1000) // Fire an event every second
.singleInstance() // Only do something when we have subscribers
.startWith(null) // kick start as soon as something subscribes
.flatMapLatest(interval => SomeAPI.someDataGet()) // get data, returns a promise
And this works. If I console.log(...) in the SomeAPI.someDataGet method, I only see it firing when the stream has subscribers. And my implementation looks really nice because I do this to subscribe and unsubscribe which fits in very nicely with React component lifecycle methods.
let sub1;
sub1 = dataStream.subscribe(x => console.log('sub1', x));
sub1.dispose();
I also want any new subscribers to receive the latest value the instant they subscribe. This is where I'm struggling. If I do this...
let sub1, sub2;
sub1 = dataStream.subscribe(x => console.log('sub1', x));
setTimeout( () => {
sub2 = dataStream.subscribe(x => console.log('sub2', x));
}, 1500)
...I don't see the console.log for sub2 until the next interval.
If my understanding is correct. I need a Hot Observable. So I have tried to create a stream like this...
let dataStream = Rx.Observable
.interval(1000) // Fire an event every second
.singleInstance() // Only do something when we have subscribers
.startWith(null) // kick start as soon as something subscribes
.flatMapLatest(interval => SomeAPI.someDataGet()) // get data
.publish() // Make this a hot observable;
Which as I understand it, should make dataStream a hot observable.
However, in my tests the second subscription still doesn't receive data until the next interval. In addition, this would introduce the requirement to connect and disconnect the dataStream when subscribing which is something I would like to avoid if possible.
I'm brand new to RxJS and I would not be surprised if I've misunderstood what's happening here.
Instead of .publish(), use .shareReplay(1).

Categories

Resources