How to turn this cold observable into hot? - rxjs

I can't seem to be able to turn this cold observable into a hot one:
const test = new BehaviorSubject('test').pipe(tap(() => console.log('I want this to be logged only once to the console!')))
const grr = test.pipe(
share(), // share() seems to not do anything
take(1), // The culprit is here, causes logging to take place 5 times (5 subscribers)
share() // share() seems to not do anything
)
grr.subscribe(() => console.log(1))
grr.subscribe(() => console.log(2))
grr.subscribe(() => console.log(3))
grr.subscribe(() => console.log(4))
grr.subscribe(() => console.log(5))
// Expected output:
// 'I want this to be logged only once to the console!'
// 1
// 2
// 3
// 4
// 5
How should I change this to produce the wanted output?

You can use publishReplay and refCount operators like this:
import { interval, BehaviorSubject } from 'rxjs';
import { publishReplay, tap, refCount } from 'rxjs/operators';
const test = new BehaviorSubject('test').
pipe(
tap(() => console.log('I want this to be logged only once to the console!')
),
publishReplay(1),
refCount()
);
test.subscribe(() => console.log(1));
test.subscribe(() => console.log(2));
test.subscribe(() => console.log(3));
test.subscribe(() => console.log(4));
Working Stackblitz: https://stackblitz.com/edit/typescript-cvcmq6?file=index.ts

Related

Is is possible to emit value on every observable completion that will go to the subscribers using single pipe?

I need to invoke multiple web request in declared order. Every consecutive request depends on the outcome of previous one. I would like to emit event after each request is completed that will go to the subscriber.
Right now I am "tapping" value of each request and emitting result using separate Subject. Is it possible to do this using single pipe with operators?
Here is code example
fromEvent(pauseButton, "click")
.pipe(
tap(()=>{
subscribedLabel.innerHTML="";
tapedLabel.innerHTML="";
}),
tap(v => (tapedLabel.innerHTML = "started")),
concatMapTo(of("phase 1 completed").pipe(delay(1000))),
tap(v => (tapedLabel.innerHTML = v)),
concatMapTo(of("phase 2 completed").pipe(delay(1000))),
tap(v => (tapedLabel.innerHTML = v))
)
.subscribe(v => {
console.log(v);
subscribedLabel.innerHTML = v;
});
https://stackblitz.com/edit/typescript-6jza7h?file=index.ts
The expected outcome is that subscribedLabel.innerHTML will change the same way as tapedLabel.innerHTML
It not clear what you're after, but this is a way you can use 4 consecutive calls and accumulate all their responses into one object.
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
concatMap(_ =>
fakeHTTP(1).pipe(
map(res => ({first: res}))
)
),
tap(_ => console.log("First Request Complete")),
concatMap(first =>
fakeHTTP(2).pipe(
map(res => ({...first, second: res}))
)
),
tap(_ => console.log("Second Request Complete")),
concatMap(second =>
fakeHTTP(3).pipe(
map(res => ({...second, third: res}))
)
),
tap(_ => console.log("Third Request Complete")),
concatMap(third =>
fakeHTTP(4).pipe(
map(res => ({...third, fourth: res}))
)
),
tap(_ => console.log("Fourth Request Complete"))
).subscribe(console.log);
The output of this is as follows:
// Wait 1s
First Request Complete
// Wait 1s
Second Request Complete
// Wait 1s
Third Request Complete
// Wait 1s
Fourth Request Complete
{"first":1,"second":2,"third":3,"fourth":4} // <- Value sent to subscribe
Update #1: Pass Values Up the Call Chain
You can pass values up the call chain, but it gets a bit more complicated. You want each step only to work on values from the previous step, but to ignore (emit unaltered) the values from further up the chain.
One way you can do this is to tag each response. I do this with a pass flag that can be true or false. The final operation is to remove the flag.
Here is what that looks like:
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
concatMap(_ =>
fakeHTTP(1)
),
tap(_ => console.log("First Request Complete")),
concatMap(first =>
fakeHTTP(2).pipe(
map(res => ({pass: false, payload: res})),
startWith({pass: true, payload: first})
)
),
tap(({pass}) => {
if(!pass) console.log("Second Request Complete")
}),
concatMap(second => second.pass ?
of(second) :
fakeHTTP(3).pipe(
map(res => ({pass: false, payload: res})),
startWith({...second, pass: true})
)
),
tap(({pass}) => {
if(!pass) console.log("Third Request Complete")
}),
concatMap(third => third.pass ?
of(third) :
fakeHTTP(4).pipe(
map(res => ({pass: false, payload: res})),
startWith({...third, pass: true})
)
),
tap(({pass}) => {
if(!pass) console.log("Second Request Complete")
}),
map(({payload}) => payload)
).subscribe(console.log);
The output of this is as follows:
// Wait 1s
First Request Complete // <- console log from tap
1 // <- console log from subscribe
// Wait 1s
Second Request Complete // <- console log from tap
2 // <- console log from subscribe
// Wait 1s
Third Request Complete // <- console log from tap
3 // <- console log from subscribe
// Wait 1s
Second Request Complete // <- console log from tap
4 // <- console log from subscribe
Update #2: When recursion is possible
You can also make recursive calls where each new call depends on the previous call and some base-case ends the recursion. RxJS jas expand as a built-in way to recurse.
In this example, each new call to fakeHTTP uses the value emitted by the previous call directly.
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
map(_ => 1),
expand(proj => proj < 4 ?
fakeHTTP(++proj) :
EMPTY
)
).subscribe(console.log);
The output of this is as follows:
// Wait 1s
1
// Wait 1s
2
// Wait 1s
3
// Wait 1s
4
Update #3: Separate observables
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
const first$ = fromEvent(button, "click").pipe(
concatMap(_ => fakeHTTP(1)),
share()
);
const second$ = first$.pipe(
concatMap(first => fakeHTTP(2)),
share()
);
const third$ = second$.pipe(
concatMap(second => fakeHTTP(3)),
share()
);
const fourth$ = third$.pipe(
concatMap(third => fakeHTTP(4))
);
merge(
first$,
second$,
third$,
fourth$
).subscribe(console.log);
Here's another, more annoying way to write almost the exact same thing.
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
map(_ => fakeHTTP(1).pipe(
share(),
)),
map(first$ => ([first$.pipe(
concatMap(firstR => fakeHTTP(2)),
share()
), first$])),
map(([second$, ...tail]) => ([second$.pipe(
concatMap(secondR => fakeHTTP(3)),
share()
),second$, ...tail])),
map(([third$, ...tail]) => ([third$.pipe(
concatMap(thirdR => fakeHTTP(4))
),third$, ...tail])),
concatMap(calls => merge(...calls))
).subscribe(console.log);

withLatestFrom unexpected behavior

I have an unexpected behavior with the operator withLatestFrom.
Output
map a
a
map a <== why a is mapped again ?
map b
b
const { Subject, operators } = window.rxjs
const { map, withLatestFrom } = operators
const createA = new Subject()
const createB = new Subject()
const a = createA.pipe(
map(() => console.log('map a'))
)
const b = createB.pipe(
withLatestFrom(a),
map(() => console.log('map b'))
)
a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })
createA.next()
createB.next()
<script src="https://unpkg.com/#reactivex/rxjs#6.6.3/dist/global/rxjs.umd.js"></script>
I found that the operator share() allows multiple subscribers.
const a = createA.pipe(
map(() => console.log('map a')),
share()
)
The problem here isn't with withLatestFrom() but rather with how subscriptions work. Observables are lazy and don't run until you subscribe. Each new subscriptions will run the observable again.
const stream$ = from([1,2,3]);
stream$.subscribe(console.log) // output: 1 2 3
stream$.subscribe(console.log) // output: 1 2 3
In this case, `from([1,2,3)] ran twice. If I alter my stream, anything I do will happen for each subscriber.
const stream$ = from([1,2,3]).pipe(
tap(_ => console.log("hi"))
);
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
The final piece of the puzzle is this: internally withLatestFrom() subscribes to the stream that you give it. Just like an explicit .subscribe() runs the observable, so does withLatestFrom() once it's subscribed to.
You can use shareReplay to cache the latest values and replay them instead of running the observable again. It's one way to manage a multicasted stream:
const createA = new Subject()
const createB = new Subject()
const a = createA.pipe(
tap(() => console.log('tap a')),
shareReplay(1)
)
const b = createB.pipe(
withLatestFrom(a),
tap(() => console.log('tap b'))
)
a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })
createA.next()
createB.next()
Now a.subscribe() and withLatestFrom(a) are both getting a buffered value that only gets run when createA.next() is executed.
As an aside, mapping a value to nothing is bad habit to get into. Consider the following code:
from([1,2,3]).pipe(
map(val => console.log(val))
).subscribe(val => console.log(val));
This will output
1
undefined
2
undefined
3
undefined
because you're actually mapping each value to nothing. tap on the other hand doesn't change the source observable, so it's a much better tool for debugging and/or side effects that don't alter the stream
from([1,2,3]).pipe(
tap(val => console.log(val))
).subscribe(val => console.log(val));
This will output
1
1
2
2
3
3

Observable periodically pause then continue

How to pause observable every 15 seconds then wait for 5s and then continue emitting?
I made this example:
const digits = interval(1000);
const pauser$ = interval(20000).pipe(mapTo(true));
pauser$.subscribe(item =>
console.log(`${new Date().toISOString()} pause fired`)
);
const pauseStopper$ = pauser$.pipe(
concatMap(_ => timer(5000)),
tap(() => console.log(`${new Date().toISOString()} pause stopped`)),
mapTo(false)
);
const observable = merge(pauser$, pauseStopper$).pipe(
startWith(false),
switchMap(paused => (paused ? NEVER : digits))
);
observable.subscribe(
item => console.log(`${new Date().toISOString()}: ${item}`),
console.error,
() => console.log("complete")
);
After first pause cycle it behaves nearly close to what I need. Any ideas on how to make this more clear?
stackblitz example
I created a player function that gets three params:
source$: Your source observable that should be paused/played
play$: Continues to emit your source$ observable
pause$: Pauses your source$ observable
const player = <T>(source$: Observable<T>, play$: Observable<void>, pause$: Observable<void>): Observable<T> =>
merge(
play$.pipe(
switchMap(() => of(source$))
),
pause$.pipe(
switchMap(() => of(NEVER))
)
).pipe(
switchMap(stream => stream)
)
This function currently throws an error as far as I can see. In the short time I have atm I could not fix the error. The error is caused by of(void 0) in the pause$ switchMap. Later I will try to fix this error. Forfeit the error the function works.
You can now use a play$ and pause$ Observable to emit the play/pause:
const play$: Observable<void> = interval(20000).pipe(
startWith(void 0),
mapTo(void 0)
);
const pause$: Observable<void> = play$.pipe(
switchMap(() => interval(15000).pipe(
take(1)
)),
mapTo(void 0)
);
const player$ = player(source$, play$, pause$);
Does this solve your problem? If there are any issues with the solution let me know and I try to adapt.

Emit before and after every retry

I have an epic, that listens for a certain action.
Once it gets the action, it should do a ajax.post
Branch
If status code is good, then emit YES
If status bad, then emit pre, wait 1s, emit post
I am struggling mightily with the last bullet, here is my code in a playground - https://rxviz.com/v/WJxGMl4O
Here is my pipeline part:
action$.pipe(
flatMap(action =>
defer(() => ajax.post('foo foo foo')).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
retryWhen(error$ =>
error$.pipe(
tap(error => console.log('got error:', error)),
merge(of('pre')), // this isnt emiting
delay(1000),
merge(of('post')) // this isnt emitting
)
)
)
)
)
I think you can achieve what you want by using catchError instead of retryWhen because retryWhen only reacts to next notifications but won't propagate them further. With catchError you get also the source Observable which you can return and thus re-subscribe. concat subscribes to all its source one after another only after the previous one completed so it'll first send the two messages pre and post and after that retry.
action$.pipe(
filter(action => action === 'hi'),
mergeMap(action =>
defer(() => resolveAfter(3)).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
catchError((error, source$) => {
console.log('retrying, got error:', error);
return staticConcat(
of('pre'),
of('post').pipe(delay(1000)),
source$,
);
}),
)
),
//take(4)
)
Your updated demo: https://rxviz.com/v/A8D7BzyJ
Here is my approach:
First, I created 2 custom operators, one that will handle 'pre' & 'post'(skipValidation) and one that will handle the logic(useValidation).
const skipValidation = src => of(src).pipe(
concatMap(
v => of('post').pipe(
startWith('pre'),
delay(1000),
),
),
);
What's important to notice in the snippet below is action$.next({ skip: true }). With that, we are emitting new values that will go through the iif operator so that we can emit 'pre' & 'post';
const useValidation = src => of(src).pipe(
filter(action => action === 'hi'),
mergeMap(action =>
defer(() => resolveAfter(3)).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
delay(1000),
retryWhen(error$ =>
error$.pipe(
tap(error => { console.log('retrying, got error:', error); action$.next({ skip: true })}),
delay(1000),
)
)
)
)
);
action$.pipe(
tap(v => console.log('v', v)), // Every emitted value will go through the `iif ` operator
mergeMap(v => iif(() => typeof v === 'object' && v.skip, skipValidation(v), useValidation(v))),
)
Here is your updated demo.

In RxJS, map not executing inner mapped observable

New to RxJS, but I'm trying to map a single element stream to another that produces an array after all internal/subsequent streams are finished/loaded. However, my inner observables don't seem to be executing. They just get returned cold.
High level, I need to execute http post to upload a list of files (in two different arrays to two different endpoints). Since they are large I emulate with a delay of 5 seconds. The requests need to be executed in parallel, but limited to concurrently executing X at a time (here 2). This all needs to be inside a pipe and the pipe should only allow the stream to continue after all posts are complete.
https://stackblitz.com/edit/rxjs-pnwa1b
import { map, mapTo, mergeMap, mergeAll, delay, tap, catchError, toArray } from 'rxjs/operators';
import { interval, merge, forkJoin, of, from, range, Observable } from 'rxjs';
const single = "name";
const first = ["abc", "def"];
const second = of("ghi", "jkl", "mno");
of(single)
.pipe(tap(val => console.log(`emit:${val}`)))
.pipe(
map(claim =>
merge(
from(first).pipe(map(photo => of(photo).pipe(delay(5000)))),
from(second).pipe(map(video => of(video).pipe(delay(5000))))
)
.pipe(
mergeAll(2)
)
.pipe(tap(val => console.log(`emit:${val}`)))
.pipe(toArray())
.pipe(tap(val => console.log(`emit:${val}`)))
)
)
.pipe(
catchError(error => {
console.log("error");
return Observable.throw(error);
})
)
.subscribe(val => console.log(`final:${val}`));
An inner subscribe would not wait until they are complete. Using forkJoin would not allow me to limit the concurrent uploads. How can I accomplish this?
Update:
Answer by #dmcgrandle was very helpful and led me to make the changes below that seem to be working:
import { map, mapTo, mergeMap, mergeAll, delay, tap, catchError, toArray } from 'rxjs/operators';
import { interval, merge, forkJoin, of, from, range, Observable, throwError } from 'rxjs';
const single = "name";
const first = ["abc", "def"];
const second = of("ghi", "jkl", "mno");
of(single)
.pipe(tap(val => console.log(`emit:${val}`)))
.pipe(
mergeMap(claim =>
merge(
from(first).pipe(map(photo => of(photo).pipe(delay(5000)).pipe(tap(val => console.log(`emit:${val}`))))),
from(second).pipe(map(video => of(video).pipe(delay(5000)).pipe(tap(val => console.log(`emit:${val}`)))))
)
),
mergeAll(2),
toArray()
)
.pipe(
catchError(error => {
console.log("error");
return throwError(error);
})
)
.subscribe(val => console.log(`final:${val}`));
If I am understanding you correctly, then I think this is a solution. Your issue was with the first map, which won't perform an inner subscribe, but rather just transform the stream into Observables of Observables, which didn't seem to be what you wanted. Instead I used mergeMap there.
Inside the from's I used concatMap to force each emission from first and second to happen in order and wait for one to complete before another started. I also set up postToEndpoint functions that return Observables to be closer to what your actual code will probably look like.
StackBlitz Demo
code:
import { mergeMap, concatMap, delay, tap, catchError, toArray } from 'rxjs/operators';
import { merge, of, from, concat, throwError } from 'rxjs';
const single = "name";
const first = ["abc", "def"];
const second = of("ghi", "jkl", "mno");
const postToEndpoint1$ = photo => of(photo).pipe(
tap(data => console.log('start of postTo1 for photo:', photo)),
delay(5000),
tap(data => console.log('end of postTo1 for photo:', photo))
);
const postToEndpoint2$ = video => of(video).pipe(
tap(data => console.log('start of postTo2 for video:', video)),
delay(5000),
tap(data => console.log('end of postTo2 for video:', video))
);
of(single).pipe(
tap(val => console.log(`initial emit:${val}`)),
mergeMap(claim =>
merge(
from(first).pipe(concatMap(postToEndpoint1$)),
from(second).pipe(concatMap(postToEndpoint2$))
)
),
toArray(),
catchError(error => {
console.log("error");
return throwError(error);
})
).subscribe(val => console.log(`final:`, val));
I hope this helps.

Resources