How to change stream interval dynamically when another stream emits - rxjs

So I have a stream made of an array of strings.
const msgs = ['Searching...', 'Gathering the Data...', 'Loading the Dashboard...'];
const msgs$ = Observable.from(msgs);
I need to emit one of these messages sequentially every 3 seconds until dataFetched$ stream emits - which basically means all data arrived to page. I cannot cycle through them. Once the last message is seen but data hasn't arrived, it shouldn't change.
I can go over each string in msgs over time with RxJS zip function
const longInt$: Observable<number> = Observable.interval(3000);
const longMsgsOverTime$ = Observable.zip(msgs$, longInt$, msg => msg);
Then when the dataFetched$ stream emits I need to switch to shortInt$ which is
const longInt$: Observable<number> = Observable.interval(1500);
And continue displaying the loading messages until the last one is visible. Each message should only be seen once.
It's quite mind bending for me - I can tick some of the requirements but not make it fully working.
After many tries I arrived to conclusion that we need to wrap msgs array in a Subject to prevent cycling though all of them again after we switched from longInt$ to shortInt$.
===================EDIT=====================
Following's ggradnig's answer and code I concocted this ugliness (for debugging purposes):
setLoaderMsg(){
console.log('%c setting loader msg', 'border: 10px dotted red;');
const msgs$ = Observable.from(['Searching...', 'Gathering the Data...', 'Loading the Dashboard...', 'Something something...', 'yet another msg...', 'stop showing me...']),
shortInt$ = Observable.interval(250).pipe(
tap(v => console.log('%c short v', 'background: green;',v))
),
longInt$ = Observable.interval(3000).pipe(
tap(v => console.log('%c long v', 'background: red;',v)),
takeUntil(this.dataFetched$)
),
// interval$ = Observable.interval(3000).pipe(takeUntil(this.dataFetched$)).pipe(concat(Observable.interval(250))),
interval$ = longInt$.pipe(concat(shortInt$));
Observable.zip(msgs$, interval$, msg => msg)
// Observable.zip(msgs$, interval$)
.pipe(tap(v => {
console.log('%c v', 'background: black; border: 1px solid red; color: white;', v);
this.loaderMsg = v;
}))
.subscribe(
() => {}, //next
() => {}, //error
// () => this.loading = false //complete
() => {
console.log('%c complete', 'background: yellow; border: 2px solid red');
// this.loading = false;
}
);
}
Below is a screengrab of my chrome console to see what happens. The shortInt$ stream would log green message, as you can see it never happens, even though the dataFetched$ stream emitted (orange dashed border).
Below the "fetch data emit" log we should see green background messages that would signify that the shortInt$ stream is emitting values.
================================= 2nd EDIT ========================
Below is dataFetched$ observable:
fetchData(){
console.log('%c fetching now', 'border: 2px dashed purple');
// this.initLoader();
this.dataFetched$ = Observable.combineLatest([
this.heroTableService.getHeroTableData([SubAggs.allNetworks.key], AggFieldsMap.legends.name),
this.researchService.benchmark(this.getSubAggsForOverviewTbl()),
this.multiLineChartService.getEngagementOverTime(this.getSubAggsForNetworkEngagementsOverTimeTable())
]).pipe(
share(),
delay(7000),
tap(v => {
console.log('%c fetch data emit', 'border: 2px dashed orange', v);
})
);
const sub = this.dataFetched$.subscribe(([heroTableRes, benchmarkRes, netByEngRes]: [ITable[], IBenchmarkResponse, any]) => {
// this.loading = false; ==> moved to dashboard
//xavtodo: split this shit into .do() or .tap under each http request call
//hero table logic here
this.heroTableData = heroTableRes;
//////////////////////////////////////////////////////
//engagement-by-network-table
this.engagementByNetworkData = this.researchService.extractDataForEngagementByNetworkTable(benchmarkRes, this.getSubAggsForOverviewTbl());
//////////////////////////////////////////////////////
//network eng over time logic here MULTI LINE CHART
this.engagementMultiLineData = this.multiLineChartService.extractEngagementData(netByEngRes, this.getSubAggsForNetworkEngagementsOverTimeTable());
this.networks = this.multiLineChartService.getNetworks();
this.multilineChartEngagements = Util.getNetworksWithAltNames(this.networks);
this.publishedContentData = this.multiLineChartService.extractPublishedData(netByEngRes);
//combined multiline chart
this.multiLineChartData = {
publishedData: this.publishedContentData,
engagementData: this.engagementMultiLineData
};
});
this.addSubscription(sub);
}

Sounds like a task for takeUntil and concat.
The first observable is longInt$. It completes as soon as dataFetched$ emits. For this requirement, you can use takeUntil. It takes another observable as a parameter and once that observable emits, the source observable will complete.
From the docs:
Emits the values emitted by the source Observable until a notifier Observable emits a value.
So, the first step would be:
const longInt$: Observable<number> = Observable.interval(3000)
.pipe(takeUntil(dataFetched$));
Next, you want to concat your other observable, shortInt$. Concat will let the first observable complete, then let the second observable take over.
From the docs:
Creates an output Observable which sequentially emits all values from given Observable and then moves on to the next.
This would look something like this:
const longMsgsOverTime$ = Observable.zip(msgs$,
longInt$.pipe(concat(shortInt$)), msg => msg);
Note: If you are using RxJS 5 or lower, replace the pipe(..) statements with the corresponding operator. For example, .pipe(concat(...)) turns into .concat(...)
Here is an example on StackBlitz: https://stackblitz.com/edit/angular-mtyfxd

Related

How to use rxjs buffer with takeWhile

I am working on webrtc. The application sends icecandidates to backend firestore server.
The problem is the call to signaling server is made multiple times as onicecandidate is triggered multiple time. I want collect all the icecandidates and make a single call to signaling server.
The idea is to buffer all the events untill iceGathering is finished. This below attempt does not work
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, 'icecandidate');
const takeWhile$ = source
.pipe(
takeWhile(val=> val.currentTarget.iceGatheringState === 'gathering'
))
const buff = source.pipe(buffer(takeWhile$));
buff.subscribe(() => {
// this.pc.onicecandidate = onicecandidateCallback;
})
Method 1:
You are almost there.
The takeWhile$ takes values and emits them while condition is met. So in buff, whenever takeWhile$ emits a value, buff emits a buffer of icecandidate events.
So you only need to emit one value in takeWhile$.
So what you need is takeLast() operator to only emit the last value.
When you put takeLast(1) in takeWhile$, it only emits last value and in buff, last emitted value leads to creation of buffer of icecandidate events.
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, "icecandidate");
const takeWhile$ = source.pipe(
takeWhile(val => val.currentTarget.iceGatheringState === "gathering"),
takeLast(1)
);
const buff = source.pipe(buffer(takeWhile$));
buff.subscribe((bufferValues) => {
// bufferValues has a buffer of icecandidate events
// this.pc.onicecandidate = onicecandidateCallback;
});
You'll have access to buffer of icecandidate events in the subscription as bufferValues in above code.
Method 2:
You can also use reduce operator to achieve same scenario
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, "icecandidate");
const takeWhile$ = source.pipe(
takeWhile(val => val.currentTarget.iceGatheringState === "gathering"),
reduce((acc, val) => [...acc,val], [])
);
takeWhile$.subscribe((bufferValues) => {
// bufferValues has a buffer of icecandidate events
// this.pc.onicecandidate = onicecandidateCallback;
})

Merge main Observable stream with updates

I need to merge main stream with updates stream this way:
Main: ----A-------B-------------C---|--
Upd: -D-----E---------F---G--------|--
========================================
Result:----A--AE---B----BF--BG---C---|--
I.e., when Main emitted, the result should always be Main (or Main with empty Upd). When Upd was emitted without previous Main, it should be ignored. If Upd was emitted after Main, then they should be combined.
Consider this TypeScript code:
interface Item {
Id: number;
Data: string;
}
function mergeUpdates(main: Item[], upd: Item[]) {
if (!upd || upd.length === 0) {
return main;
}
const result = main;
// const result = {...main};
for (const updatedItem of upd) {
const srcIndex = result.findIndex(_ => _.Id === updatedItem.Id);
if (srcIndex >= 0) {
result[srcIndex] = updatedItem;
} else {
result.push(updatedItem);
}
}
return result;
}
const main$ = new Subject<Item[]>();
const upd$ = new Subject<Item[]>();
const result$ = combineLatest(main$, upd$).pipe( // combineLatest is wrong operator!
map(([main, upd]) => mergeUpdates(main, upd)));
$result.subscribe(r => console.log(r.map(_ => _.Data).join(',')));
main$.next([{Id:1, Data:'Data1'}, {Id:2, Data:'Data2'}]);
upd$.next([{Id:1, Data:'Updated1'}]);
upd$.next([{Id:1, Data:'Updated2'}]);
main$.next([{Id:1, Data:'Data1_Orig'}, {Id:2, Data:'Data2'}]);
// Expexted result:
// 'Data1,Data2'
// 'Updated1,Data2'
// 'Updated2,Data2'
// 'Data1_Orig,Data2'
The only solution I have in mind is to use 'combineLatest' and mark items in upd$ stream as processed, thus do not use it again when data from main$ emitted later. I believe this is not the best approach as it cause unwanted side effects.
Is there any better solution for this task?
Thank you in advance.
Here would be my approach:
main$.pipe(
switchMap(
mainValue => merge(
of(mainValue),
upd$.pipe(
map(updVal => mainValue + updVal)
)
)
)
)
switchMap - make sure the inner observable's emitted values will be combined with the latest outer value
merge(of(a), upd$.pipe()) - emit the main value first, then listen to any notifications upd$ emits and combine them with the current main value
If another outer value comes in, the inner subscriber will be unsubscribed, meaning that the upd subject won't have redundant subscribers.

takeUntil failing to prevent emissions from observable

I am trying to create my own click, hold and drag events using Rxjs and the mousedown, mouseup and mousemove events. My attempts use a number of streams that begin with a mousedown event, each with a takeUntil that listens for emissions from the other streams. Basically once one of the streams has "claimed" the action (i.e. passed all the requirements and emitted a value) the other observables should complete with no emissions.
I have looked at other answers and thought it might have something to do with the timer running async but it happens between streams that do not rely the timer e.g. drag and click. I have been playing around in codesandbox.io using rxjs v6.
The takeUntil's also have to sit on the inner observables as I don't want the outer observables to run once and complete.
The code is shown below:
const mouse_Down$ = fromEvent(document, "mousedown").pipe(
tap(event => event.preventDefault())
);
const mouse_Up$ = fromEvent(document, "mouseup").pipe(
tap(event => event.preventDefault())
);
const mouse_Move$ = fromEvent(document, "mousemove");
const mouse_drag$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Move$.pipe(takeUntil(merge(mouse_Up$, mouse_Hold$, mouse_drag$)))
)
).subscribe(event => console.log("Drag"));
const mouse_Hold$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
timer(1000).pipe(takeUntil(merge(mouse_drag$, mouse_Click$)))
)
).subscribe(event => console.log("Hold"));
const mouse_Click$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Up$.pipe(takeUntil(mouse_drag$, mouse_Hold$))
)
).subscribe(event => console.log("Click"));
Expected behaviour:
If the user moves the mouse within 1s of the mousedown event the mouse_drag$ stream should begin emitting and the mouse_Click$/mouse_Hold$'s inner observables should complete (thanks to the takeUntil(mouse_drag$) without emitting and await the next mouse_down$ emmission.
If the mouse button remains down for more than 1s without moving the mouse_Hold$ should emit and mouse_drag$/mouse_click$'s inner observable should complete (thanks to the takeUntil(mouse_Hold$) without emitting and await the next mouse_down$ emmission.
Actual Behaviour: Currently the mouse_Drag$ will emit, the mouse_Hold$ will emit after one second and the mouse_Click$ will emit when the button is released.
My question is why doesn't the emitting mouse_Drag$ stream cause the mouse_Hold$ and mouse_Click$'s inner observable to complete without emitting?
The take until needs to be at the end of your chain
This will cancel the whole chain.
const { fromEvent } = rxjs;
const { tap, takeUntil, mergeMap, merge } = rxjs.operators;
const mouse_Down$ = fromEvent(document, "mousedown").pipe(
tap(event => event.preventDefault())
);
const mouse_Up$ = fromEvent(document, "mouseup").pipe(
tap(event => event.preventDefault())
);
const mouse_Move$ = fromEvent(document, "mousemove");
const mouse_drag$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Move$
),
takeUntil(mouse_Up$)
).subscribe(event => console.log("Drag"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
To clarify:
that you want emit from mouse_Hold$ if mouse is hold more then 1 second.
You want to get values from mouse_drag$ if LESS then 1 second pass after mouse dropdown and mouseMove.
You do not have to complete anything since otherwise all behaviour will work only once.
So plan:
3. mouse_drag$ - If mousedown - check mouseMove for 1 second. If mouseMove emits - switch to mouseMove values
4. mouse_Hold$ - if mouseDown - check mouseMove for 1 second. If mouseMove doesn't emit - switch to mouseHold and make it emit 'Hold'
let Rx = window['rxjs'];
const {defer, of, timer, fromEvent, merge, race} = Rx;
const {switchMap, repeat, tap, takeUntil, filter} = Rx.operators;
const {ajax} = Rx.ajax;
console.clear();
const mouse_Down$ = fromEvent(document, "mousedown");
const mouse_Up$ = fromEvent(document, "mouseup");
const mouse_Move$ = fromEvent(document, "mousemove");
const timer$ = timer(2000);
mouse_Hold$ = mouse_Down$.pipe(
switchMap((downEvent) => {
return timer$.pipe(
switchMap((time) => of('HOLD'))
);
}),
takeUntil(merge(mouse_Up$, mouse_Move$)),
repeat(mouse_Down$)
)
mouse_Hold$.subscribe(console.warn);
mouse_drags$ = mouse_Down$.pipe(
switchMap(() => mouse_Move$),
takeUntil(mouse_Up$, $mouse_Hold),
repeat(mouse_Down$)
)
mouse_drags$.subscribe(console.log);
Here is a codepen: https://codepen.io/kievsash/pen/oOmMwp?editors=0010

BufferTime with leading option

I have some events that I'd like to buffer but I'd like to buffer only after the first element.
[------bufferTime------]
Input over time:
[1, 2, 3, -------------|---4, 5, 6 ----------------]
Output over time:
[1]-----------------[2,3]---[4]------------------[5,6]
is there a way to do this?
I think this can be solved by dividing your stream into two, firstValue$ and afterFirstValue$, and then merging them.
import { merge } from 'rxjs';
import { take, skip, bufferTime } from 'rxjs/operators';
...
firstValue$ = source$.pipe(
take(1)
);
afterFirstValue$ = source$.pipe(
skip(1),
bufferTime(5000)
);
merge(firstValue$, afterFirstValue$)
.subscribe(result => {
// Do something
});
Answer to follow up question concerning subject
So I have done it so that the original source is a subject here. It is not exactly how you described it, but I think maybe this is what you want.
import { merge, Subject } from 'rxjs';
import { take, skip, bufferTime } from 'rxjs/operators';
import { Source } from 'webpack-sources';
...
source$ = new Subject();
firstValue$ = source$.pipe(
take(1)
);
afterFirstValue$ = source$.pipe(
skip(1),
bufferTime(5000)
);
merge(firstValue$, afterFirstValue$)
.subscribe(result => {
// Do something
});
source$.next(1);
source$.next(1);
source$.next(1);
You can use multicast to split the stream into two and just pass the first value through.
import { concat, Subject } from “rxjs”;
import { multicast, take, bufferCount } from “rxjs/operators”;
source.pipe(
multicast(
new Subject(),
s => concat(
s.pipe(take(1)),
s.pipe(bufferCount(X)),
)
),
);
I got really good answers that enlightened my view of the problem and made me come up with the real thing that I was needing, that was something like this:
function getLeadingBufferSubject (bufferTimeArg) {
const source = new Subject()
const result = new Subject()
let didOutputLeading = false
const buffered$ = source
.pipe(bufferTime(bufferTimeArg))
.pipe(filter(ar => ar.length > 0))
.pipe(map(ar => [...new Set(ar)]))
buffered$.subscribe(v => {
didOutputLeading = false
const slicedArray = v.slice(1)
// emits buffered values (except the first) and set flag to false
if (.length > 0) result.next(v.slice(1))
})
// emits first value if buffer is empty
source.subscribe(v => {
if (!didOutputLeading) {
didOutputLeading = true
result.next(v)
}
})
// call .next(value) on "source"
// subscribe for results on "result"
return {
source,
result
}
}
I had the same problem and after playing around with it, I found this additional solution:
source$.pipe(
buffer(source$.pipe(
throttleTime(bufferTime, asyncScheduler, {leading: true, trailing: true}),
delay(10) // <-- This here bugs me like crazy though!
)
)
Because throttle already features a leading option, you can just use it to trigger buffer emits manually.
I would really like to get rid of that delay here though. This is necessary because the inner observable is triggered first causing the buffer to emit prematurely.

How to ignore new values in Observer during execution

I have some Subject. And one Observer subscribed to it. How to omit all Observer invocations if it is already processing one?
var subject = new Subject();
var observer = {
next: x => {
//... some long processing is here
console.log('Observer got a next value: ' + x)
}
};
subject.subscribe(observer);
subject.next(0);
subject.next(1);// <-- if 0 value is not processed in the observer then skip it
subject.next(2);// <-- if 0 value is not processed in the observer then skip it
I of cause can introduce some flag, set it in Observer before execution and clear it after. And apply filter operator, like this:
var subject = new Subject();
var flag = true;
var observer = {
next: x => {
flag = false;
//... some long processing is here
console.log('Observer got a next value: ' + x)
flag = true;
}
};
subject.filter(() => flag).subscribe(observer);
subject.next(0);
subject.next(1);// <-- if previous value is not processed in the observer then skip it
subject.next(2);// <-- if 0 value is not processed in the observer then skip it
But I believe that exists more elegant and efficient way to achieve that.
Use the exhaustMap operator instead of trying roll your own backpressure. It is designed to ignore new events while waiting for the current one to complete.
const clicks = fromEvent(document, 'click');
const result = clicks.pipe(
exhaustMap((ev) => interval(1000).pipe(take(5))),
);
result.subscribe(x => console.log(x));

Resources