Observable periodically pause then continue - rxjs

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.

Related

RxJS error in inner stream kill the source

I have the following stream:
const source = fromEvent(document.querySelector('h1'), 'click').pipe(
switchMap(() => {
return timer(500).pipe(
switchMap(() => timer(500).pipe(tap(() => {
throw new Error('error')
})))
)
})
)
When the inner stream throws, the fromEvent source is also stopped. How can I prevent this and keep the source stream alive?
I think you could try the following:
const source = fromEvent(document.querySelector('h1'), 'click').pipe(
switchMap(() => {
return timer(500).pipe(
switchMap(() => timer(500).pipe(tap(() => {
throw new Error('error')
}))),
catchError(() => EMPTY)
)
})
)
where EMPTY is nothing but this:
export const EMPTY = new Observable<never>(subscriber => subscriber.complete());
The reason this is happens is that when an error occurs, even in a next callback(e.g tap's first argument), it will be passed through as:
this.destination.error(caughtError);
where destination is the next subscriber in the chain(downwards). Eventually catchError will be reached and this prevents the error from affecting the outermost stream(whose source is fromEvent).
You can try with retry operator after the first switchMap, like this
const source = fromEvent(document.querySelector('h1'), 'click').pipe(
switchMap(() => {
return timer(500).pipe(
switchMap(() => timer(500).pipe(tap(() => {
throw new Error('error')
})))
)
}),
retry()
)
If no parameter is passed to retry, then it will resubscribe to the source observable an infinite amount of times. Otherwise you can pass an integer which represents the number of times it will retry before eventually error.
Here a stackblitz.

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

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.

How to turn this cold observable into hot?

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

Add event listeners, and use `scan` to keep a reduced state, then remove event listeners

I am working on this sandbox here - https://stackblitz.com/edit/rxjs-g7msgv?file=index.ts
What I am trying to do is:
1) Wait for onLogin event
2) While logged in, I want to connectSocket(), and whenever the socket gets disconnected, and the app is in the foreground, I want to re-connectSocket(). (in the sandbox I have stubbed out connectSocket() to a promise that just waits 5 sec)
3) I want to repeat step 2, until onLogout event comes in
I wrote this code here, please see the sandbox and start things off by pressing the "onLogin" button.
fromEvent(document, 'onLogin')
.pipe(
switchMap(() =>
of({ isDisconnected: true, isInForeground: true }).pipe(
mergeMap(data =>
concat(
fromEvent(document, 'onDisconnect').pipe(
mergeMap(() =>
data.isDisconnected = true
)
),
fromEvent(document, 'onAppStateChange').pipe(
mergeMap(({ detail:{ state } }) =>
data.isInForeground = state === 'foreground'
)
),
).pipe(
mergeMap(({ isDisconnected, isInForeground }) => {
if (isDisconnected && isInForeground) {
return flatMap(() => connectSocket());
} else {
return EMPTY;
}
})
)
),
takeUntil(fromEvent(document, 'onLogout'))
)
)
)
.subscribe(console.log);
I use switchMap because while its running, I don't want any other login events to restart another flow.
I'm not able to get this working. I am new to rxjs.
Use startWith to Init the value and combineLatest will fire when either one of the event is triggered.
fromEvent(document, 'onLogin').pipe(
switchMap(() =>
combineLatest(
fromEvent(document, 'onDisconnect').pipe(
mapTo(true),
startWith(true)
),
fromEvent(document, 'onAppStateChange').pipe(
map(e => e.detail === 'foreground'),
startWith(true),
)
).pipe(
mergeMap(([isDisconnected, isInForeground]) =>
isDisconnected && isInForeground ? connectSocket() : EMPTY
),
takeUntil(fromEvent(document, 'onLogout'))
)
)
)

Resources