RxJS operator waitUntil - rxjs

a: 1---2-3-4--5---6
b: ------T---------
o: ------1234-5---6
Using RxJS, is there some operator that can accomplish the diagram above? I have stream A which is a random stream of events, given a stream B which has a single true event, can I have an output stream that doesn't emit anything until that true event, and then sends everything is had saved up until then and afterwards emits normally?
I thought maybe I could use buffer(), but it seems like there is no way to do a one time buffer like this with that operator.

const { concat, interval, of, from } = rxjs;
const { share, delay, toArray, takeUntil, mergeMap } = rxjs.operators;
const waitUntil = signal$ => source$ => {
const sharedSource$ = source$.pipe(share());
return concat(
sharedSource$.pipe(
takeUntil(signal$),
toArray(),
mergeMap(from)
),
sharedSource$
);
}
const stopWaiting$ = of('signal').pipe(delay(2000));
const source$ = interval(500).pipe(
waitUntil(stopWaiting$)
).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>

I think #ZahiC's solution is correct but personally I'd do it in a single chain using the multicast operator.
a$.pipe(
multicast(new Subject(), s => concat(
s.pipe(
buffer(b$),
take(1),
),
s
)),
)
multicast will basically spit the stream into two where concat will first subscribe to the first one that is buffered until b$ emits. Then it completes immediately because of take(1) and concat subscribe to the same steam again but this time unbuffered.

Here's my solution, using TypeScript:
export const queueUntil = <T>(signal$: Observable<any>) => (source$: Observable<T>) => {
let shouldBuffer = true;
return source$.pipe(
bufferWhen(() => shouldBuffer ? signal$.pipe(
tap(() => shouldBuffer = false),
) : source$),
concatMap(v => v),
);
};
and can be used like this:
a$.pipe(
queueUntil(b$)
)

All existing answers here (as of 2022/1/4) have the potential to skip/eat source notifications emitted on the same frame as the notifier emits (particularly if the source stream feeds new values back into itself). This solution supports this use case:
function waitUntil<T>(notifier$: Observable<any>): MonoTypeOperatorFunction<T> {
return (source$: Observable<T>) => {
const buffer$ = new ReplaySubject<T>();
let doBuffer = true;
source$.pipe(takeWhile(() => doBuffer, true)).subscribe(buffer$);
return notifier$.pipe(
take(1),
switchMap(() => {
doBuffer = false;
return concat(buffer$, source$);
}),
);
};
}

Related

Rxjs - cancel debounce in the specific case

Question about rxjs puzzle.
I have the input observable stream and it will emit after 3 secs when I type some.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
// input is HTMLInputElement
const input$ = fromEvent(input, "input");
input$
.pipe(debounce(() => interval(3000)))
.subscribe(e => console.log(e.target.value));
I would like to make a change to cancel the debounce and emit immediately once the button is clicked. But, if I don't click the button, it will wait 3 secs.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
const input$ = fromEvent(input, "input");
// add click observable stream
const click$ = fromEvent(button, "click");
input$
.pipe(debounce(() => interval(3000)))
// I can't get this to work in the mix!!
// .pipe(debounce(() => click$))
.subscribe(e => console.log(e.target.value));
How can this be achieved?
sounds like a race operator.
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
input$
.pipe(
switchMap(value => race(
click$,
timer(3000),
).pipe(
take(1),
mapTo(value),
)),
.subscribe(e => console.log(e.target.value));
Here is the solution to toggle debounce, what you have to do is to convert interval() to a stream that change interval time base on button click
Js
import { fromEvent, interval,timer} from 'rxjs';
import { debounce,scan,shareReplay,map,startWith,tap,switchMap} from 'rxjs/operators';
const input = fromEvent(document.getElementById('text'), 'input');
const debounceToggle=fromEvent(document.getElementById('toggle'),'click').pipe(
scan((acc,curr)=>!acc,false),
map(on=>on?0:3000),
startWith(3000),
shareReplay(1),
switchMap(value=>interval(value))
)
const result = input.pipe(debounce(() => {
return debounceToggle
}));
result.subscribe(x => console.log(x.target.value));
HTML
<button id="toggle">toggle debounce</button>
<input type="text" id="text"/>
Here could be another solution I think:
input$
.pipe(
debounce(
() => interval(3000).pipe(takeUntil(buttonClick$))
)
)
.subscribe(e => console.log(e.target.value));
debounce will emit the value that caused the inner observable's subscription, when it either completes/emits a value
// Called when the inner observable emits a value
// The inner obs. will complete after this as well
notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.emitValue();
}
// Called when the inner observable completes
notifyComplete(): void {
this.emitValue();
}
Source code
The following would be the simplest in my opinion:
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
merge(
input$.pipe(debounceTime(3000)),
click$
).pipe(
map(() => input.value)
).subscribe(val => console.log(val));
https://stackblitz.com/edit/rxjs-8bnhxd
Also, you are essentially "combining" 2 different events here, it doesn't make sense to me to rely on event.target.value, as it could be referring to different things which makes it hard to read.

Multiple subscriptions nested into one subscription

I find myself puzzled trying to set a very simple rxjs flow of subscriptions. Having multiple non-related subscriptions nested into another.
I'm in an angular application and I need a subject to be filled with next before doing other subscriptions.
Here would be the nested version of what I want to achieve.
subject0.subscribe(a => {
this.a = a;
subject1.subscribe(x => {
// Do some stuff that require this.a to exists
});
subject2.subscribe(y => {
// Do some stuff that require this.a to exists
});
});
I know that nested subscriptions are not good practice, I tried using flatMap or concatMap but didn't really get how to realize this.
It's always a good idea to separate the data streams per Observable so you can easily combine them later on.
const first$ = this.http.get('one').pipe(
shareReplay(1)
)
The shareReplay is used to make the Observable hot so it won't call http.get('one') per each subscription.
const second$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('second', firstCallResult))
);
const third$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('third', firstCallResult))
);
After this you can perform subscriptions to the Observables you need:
second$.subscribe(()=>{}) // in this case two requests will be sent - the first one (if there were no subscribes before) and the second one
third$.subscribe(() => {}) // only one request is sent - the first$ already has the response cached
If you do not want to store the first$'s value anywhere, simply transform this to:
this.http.get('one').pipe(
flatMap(firstCallResult => combineLatest([
this.http.post('two', firstCallResult),
this.http.post('three', firstCallResult)
])
).subscribe(([secondCallResult, thirdCallResult]) => {})
Also you can use BehaviorSubject that stores the value in it:
const behaviorSubject = new BehaviorSubject<string>(null); // using BehaviorSubject does not require you to subscribe to it (because it's a hot Observable)
const first$ = behaviorSubject.pipe(
filter(Boolean), // to avoid emitting null at the beginning
flatMap(subjectValue => this.http.get('one?' + subjectValue))
)
const second$ = first$.pipe(
flatMap(firstRes => this.http.post('two', firstRes))
)
const third$ = first$.pipe(
flatMap(()=>{...})
)
behaviorSubject.next('1') // second$ and third$ will emit new values
behaviorSubject.next('2') // second$ and third$ will emit the updated values again
You can do that using the concat operator.
const first = of('first').pipe(tap((value) => { /* doSomething */ }));
const second = of('second').pipe(tap((value) => { /* doSomething */ }));
const third = of('third').pipe(tap((value) => { /* doSomething */ }));
concat(first, second, third).subscribe();
This way, everything is chained and executed in the same order as defined.
EDIT
const first = of('first').pipe(tap(value => {
// doSomething
combineLatest(second, third).subscribe();
}));
const second = of('second').pipe(tap(value => { /* doSomething */ }));
const third = of('third').pipe(tap(value => { /* doSomething */ }));
first.subscribe();
This way, second and third are running asynchronously as soon as first emits.
You could do something like this:
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(() => subject1),
tap(x => {
// Do some stuff that require this.a to exists
}),
switchMap(() => subject2),
tap(y => {
// Do some stuff that require this.a to exists
})
);
if you want to trigger this, simply call this.subject$.next();
EDIT:
Here is an possible approach with forkJoin, that shout call the subjects parallel.
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(
() => forkJoin(
subject1,
subject2
)),
tap([x,y] => {
// Do some stuff that require this.a to exists
})
);

In RxJS, how to prevent operators from emitting values if the source observable hasn't emitted any?

I have three observables foo$, bar$ and baz$ that I merge together to form another observable. This is working as expected:
The stream starts with >>>
Each value are emitted one by one
The stream ends with <<<
const foo$ = of('foo');
const bar$ = of('bar');
const baz$ = of('baz');
merge(foo$, bar$, baz$).pipe(startWith('>>>'), endWith('<<<')).subscribe(str => {
console.log(str);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of} = rxjs; const {startWith, endWith} = rxjs.operators;</script>
Now if none of the three observables above emit a value, I do not want to output neither >>> nor <<<. So startWith and endWith can only "run" if merge(foo$, bar$, baz$) actually emits a value.
In order to simulate that I'm rejecting all values emitted by foo$, bar$ and baz$ with a filtering function.
const foo$ = of('foo').pipe(filter(() => false));
const bar$ = of('bar').pipe(filter(() => false));
const baz$ = of('baz').pipe(filter(() => false));
merge(foo$, bar$, baz$).pipe(startWith('>>>'), endWith('<<<')).subscribe(str => {
console.log(str);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of} = rxjs; const {startWith, endWith, filter} = rxjs.operators</script>
However as you can see in the output, both startWith and endWith have emitted their value even though the merge() hasn't produced any.
Question: How can I prevent startWith and endWith from executing if the observable did not emit a single value?
The first condition is simple. You can just prepend the first emission with concatMap:
mergeMap((v, index) => index === 0 ? of('>>>', v) : of(v))
The second condition is more tricky. You want basically the right opposite to defaultIfEmpty. I can't think of any simple solution so I'd probably use endWith anyway and just ignore the emission if it's the first and only emission (which means the source just completed without emitting anything):
endWith('<<<'),
filter((v, index) => index !== 0 || v !== '<<<'),
Complete example:
const foo$ = of('foo');//.pipe(filter(() => false));
const bar$ = of('bar');//).pipe(filter(() => false));
const baz$ = of('baz');//.pipe(filter(() => false));
merge(foo$, bar$, baz$).pipe(
mergeMap((v, index) => index === 0 ? of('>>>', v) : of(v)),
endWith('<<<'),
filter((v, index) => index !== 0 || v !== '<<<'),
).subscribe(console.log);
Live demo: https://stackblitz.com/edit/rxjs-n2alkc?file=index.ts
You could assign the merged observable to a variable, take the first element from the stream and then map to the observable you want to execute when atleast one value has been emitted.
const foo$ = of('foo').pipe(filter(() => false))
const bar$ = of('bar').pipe(filter(() => false))
const baz$ = of('baz').pipe(filter(() => false))
const merged$ = merge(foo$, bar$, baz$);
merged$.pipe(
take(1),
switchMap(() => merged$.pipe(
startWith('>>>'),
endWith('<<<')
))
).subscribe(console.log);
https://stackblitz.com/edit/rxjs-phspsc
From the documentation of startWith:
Returns an Observable that emits the items you specify as arguments before it begins to emit items emitted by the source Observable.
So startWith and endWith will always run.
I dont know what your expected result should be, but if you only want to concatenate the strings for each emitted value you could use the map or switchMap operators.
EDIT:
Example with map to concat each value:
const foo$ = of('foo').pipe(filter(() => false));
const bar$ = of('bar').pipe(filter(() => false));
const baz$ = of('baz').pipe(filter(() => false));
merge(foo$, bar$, baz$).pipe(map(v => `>>>${v}<<<`)).subscribe(str => {
console.log(str);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of} = rxjs; const {startWith, endWith, filter, map} = rxjs.operators</script>
I see that all answers are done combining operators, but doing this solution with only operators is kind of tricky, why not just create your own observable? I think this solution is the easiest one to understand. No hidden cleverness, no complex operator combinations, no subscription repetitions...
Solution:
const foo$ = of('foo')//.pipe(filter(() => false));
const bar$ = of('bar')//.pipe(filter(() => false));
const baz$ = of('baz')//.pipe(filter(() => false));
function wrapIfOnEmit$(...obs) {
return new Observable(observer => {
let hasEmitted;
const subscription = merge(...obs).subscribe((data) => {
if (!hasEmitted) {
observer.next('>>>');
hasEmitted = true;
}
observer.next(data);
},
(error) => observer.error(error),
() => {
if (hasEmitted) observer.next('<<<');
observer.complete();
})
return () => subscription.unsubscribe();
})
}
wrapIfOnEmit$(foo$, bar$, baz$).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of, Observable} = rxjs; const {filter} = rxjs.operators</script>
Hope this helps!
One way to achieve that is to use a materialize-dematerialize operators pair:
source$.pipe(
// turn all events on stream into Notifications
materialize(),
// wrap elements only if they are present
switchMap((event, index) => {
// if its first event and its a value
if (index === 0 && event.kind === 'N') {
const startingNotification = new Notification('N', '>>>', undefined);
return of(startingNotification, event);
}
// if its a completion event and it not a first event
if (index > 0 && event.kind === 'C') {
const endingNotification = new Notification('N', '<<<', undefined);
return of(endingNotification, event);
}
return of(event);
}),
// turn Notifications back to events on stream
dematerialize()
)
Play with this code in a playground:
https://thinkrx.io/gist/5a7a7f1338737e452ff6a1937b5fe05a
for convenience, I've added an empty and error source there as well
Hope this helps

Any reason to use shareReplay(1) in a BehaviorSubject pipe?

I'm using a library that exposes data from a service class using a pretty common BehaviorSubject pattern. The only notable difference with the implementation and what I have seen/used myself is the addition of a pipe with a shareReplay(1) operator. I'm not sure if the shareReplay is required. What effect, if any, does the shareReplay have in this case?
// "rxjs": "^6.3.0"
this.data = new BehaviorSubject({});
this.data$ = this.data.asObservable().pipe(
shareReplay(1)
)
Note: I've read a number of articles on shareReplay, and I've seen questions about different combinations of shareReplay and Subject, but not this particular one
Not in your example but imagine if there was some complex logic in a map function that transformed the data then the share replay would save that complex logic being run for each subscription.
const { BehaviorSubject } = rxjs;
const { map, shareReplay } = rxjs.operators;
const bs$ = new BehaviorSubject('initial value');
const obs$ = bs$.pipe(
map(val => {
console.log('mapping');
return 'mapped value';
}),
shareReplay({bufferSize:1, refCount: true})
);
obs$.subscribe(val => { console.log(val); });
obs$.subscribe(val => { console.log(val); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.min.js"></script>
Compare without the share, the map happens twice.
const { BehaviorSubject } = rxjs;
const { map } = rxjs.operators;
const bs$ = new BehaviorSubject('initial value');
const obs$ = bs$.pipe(
map(val => {
console.log('mapping');
return 'mapped value';
})
);
obs$.subscribe(val => { console.log(val); });
obs$.subscribe(val => { console.log(val); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.min.js"></script>
With this pattern (use of shareReplay(1)), the service protects itself from the user using the next() function of the BehaviorSubject while sending the last value of the BehaviorSubject (which would not have been the case without shareReplay(1)).

rxjs switchMap need to return subscribed observable

Here here the requirement:
When click start button, emit event x times every 100ms, each emit correspond an UI update. When x times emit complete, it will trigger a final UI update, look simple right?
Here is my code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel$ = interval(100)
.pipe(
take(x),
share()
)
var startLight$ = start$
.pipe(
switchMap(() => {
intervel$
.pipe(last())
.subscribe(() => {
// Update UI
})
return intervel$
}),
share()
)
startLight$
.subscribe(function (e) {
//Update UI
})
Obviously, subscribe inside switchMap is anti-pattern, so I tried to refactor my code:
const startInterval$ = start$
.pipe(
switchMapTo(intervel$),
)
startInterval$.pipe(last())
.subscribe(() => {
//NEVER Receive value
})
const startLight$ = startInterval$.pipe(share())
The problem is that intervel$ stream is generated inside switchMap and can not be accessed outside, you can only access the stream who generate interval$, i.e. start$ which never complete!
Is there is smarter way to handle such kind of problem or it was an inherent limitation of rxjs?
You were very close. Use last() inside intervel$ to only emit the final one to the subscribe below. Working StackBlitz. Here are details from the StackBlitz:
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100)
.pipe(
tap(() => console.log('update UI')), // Update UI here
take(x),
last()
);
const startInterval$ = start$
.pipe( switchMapTo(intervel$));
startInterval$
.subscribe(() => {
console.log('will run once');
});
Update
If you do not wish to use tap(), then you can simply cause start$ to finish by taking only the first emission and then completing with either take(1) or first(). Here is a new StackBlitz showing this.
const start$ = fromEvent(document.getElementById('start'), 'click')
.pipe(
first()
);
const intervel$ = interval(100)
.pipe(
take(x)
);
const startInterval$ = start$
.pipe(
switchMapTo(intervel$)
);
startInterval$
.subscribe(
() => console.log('Update UI'),
err => console.log('Error ', err),
() => console.log('Run once at the end')
);
The downside to this approach (or any approach that completes the Observable) is that once completed it won't be reused. So for example, clicking multiple times on the button in the new StackBlitz won't work. Which approach to use (the first one that can be clicked over and over or the one that completes) depends on the results you need.
Yet Another Option
Create two intervel$ observables, one for the intermediate UI updates and one for the final one. Merge them together and only do the UI updating in the subscribe. StackBlitz for this option
code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel1$ = interval(100)
.pipe(
take(x)
);
const intervel2$ = interval(100)
.pipe(
take(x+1),
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
A more idiomatic way, same logic as previous one (By Guichi)
import { switchMapTo, tap, take, last, share, mapTo } from 'rxjs/operators';
import { fromEvent, interval, merge } from 'rxjs';
const x = 5;
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100);
const intervel1$ = intervel$
.pipe(
take(x)
);
const intervel2$ = intervel1$
.pipe(
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
Reflection
The key problem of the original question is to 'use the same observable in different ways', i.e. during the progress and the final. So merge is an pretty decent logic pattern to target this kind of problem
Put your update logic inside the switchMap and tap() , tap will run multiple time and only last emission will be taken by subscribe()
const startInterval$ = start$
.pipe(
switchMap(()=>intervel$.pipe(tap(()=>//update UI),last()),
)
startInterval$
.subscribe(() => {
// will run one time
})

Resources