RxJS5 - skipWhile but keep values while skipping? - rxjs5

I have a stream which needs to skip while bool is true.
However, when that bool is set back to false, I need to apply the last stream value missed when it was skipping.
Rx.Observable
.interval(1000)
.skipWhile(val => true|false)<---when back to false, get last missed value?
.subscribe(val => console.log(val));

Use multicast to share start and the rest values. pairwise to pairt prev and next value and concat to combine start and the rest.
Rx.Observable.interval(500)
.multicast(new Rx.Subject(), shared => {
let start$ = shared
.pairwise()
.skipWhile((x)=>x[1]<5) // this is the condition to skip
.first()
.switchMap(el=>Rx.Observable.concat(Rx.Observable.of(el[0]),
Rx.Observable.of(el[1])));
return Rx.Observable.concat(start$, shared);
}).subscribe(x=>console.log(x));

Related

RxJS: execute concatMap i parallel

Is it possible to execute a high-order observable in parallel, but still preserve the order when merging the results?
I have something looking like this:
invoker$: Observable<void>;
fetch: (index: number) => Observable<T[]>;
invoker$
.pipe(
concatMap((_, index) => fetch(index)),
scan((acc: T[], values) => [...acc, ...values], [])
)
.subscribe(/* Do something with the array */);
The idea is having an observable that invokes a callback (e.g. backend call that takes a considerable amount of time) generating a new observable that emits a single value (array of some generic type). The returned values should be concatenated in another array while preserve their original fetch order.
I would, however, like the requests to be fired in parallel. So if the invoker$ is called rapidly, the requests are made in parallel and the results are merged as they complete.
My understanding is that the concatMap will wait for one observable to complete, before starting the next one. mergeMap will do it parallel, but won't do anything to preserve the order.
You can do it using mergeMap.
First, you need to pass the index together with the async response down the stream.
Then you can sort based on the index from the previous step.
Then you have two choices:
if the stream needs to end once all the requests are made and handle only once all the responses you can use reduce https://rxmarbles.com/#reduce
if the stream needs to continue for another batch of requests you need to use scan and later filter until you reach the needed event count. https://rxmarbles.com/#scan and https://rxmarbles.com/#filter
I am going to give you some pseudo-code for both examples:
In the reduce case, the stream ends once all requests are sent:
invoker$
.pipe(
mergeMap((_, index) => fetch(index).then(value => {value, index})),
reduce((acc: T[], singleValue) => [...acc, ...singleValue], []),
map(array => array.sort(/*Sort on index here*/).map(valueWithIndex => valueWithIndex.value))
)
.subscribe(/* Do something with the array */);
In the multiple-use case, I am assuming the size of the batch to be constant:
invoker$
.pipe(
mergeMap((_, index) => fetch(index).then(value => {value, index})),
scan((acc: T[], singleValue) => {
let resp = [...acc, ...singleValue];
// The scan can accumulate more than the batch size,
// so we need to limit it and restart for the new batch
if(resp.length > BATCH_SIZE) {
resp = [singleValue];
}
return resp;
}, []),
filter(array => array.length == BATCH_SIZE),
map(array =>
array
.sort(/*Sort on index here*/)
.map(valueWithIndex => valueWithIndex.value))
)
.subscribe(/* Do something with the array */);
2.1. In case the batch size is dynamic:
invoker$
.pipe(
mergeMap((_, index) => fetch(index).then(value => {value, index})),
withLatestFrom(batchSizeStream),
scan((acc: [T[], number], [singleValue, batchSize]) => {
let resp = [[...acc[0], ...singleValue], batchSize];
// The scan can accumulate more than the batch size,
// so we need to limit it and restart for the new batch
// NOTE: the batch size is dynamic and we do not want to drop data
// once the buffer size changes, so we need to drop the buffer
// only if the batch size did not change
if(resp[0].length > batchSize && acc[1] == batchSize) {
resp = [[singleValue], batchSize];
}
return resp;
}, [[],0]),
filter(arrayWithBatchSize =>
arrayWithBatchSize[0].length >= arrayWithBatchSize[1]),
map(arrayWithBatchSize =>
arrayWithBatchSize[0]
.sort(/*Sort on index here*/)
.map(valueWithIndex => valueWithIndex.value))
)
.subscribe(/* Do something with the array */);
EDIT: optimized sorting, added dynamic batch size case
I believe that the operator you are looking for is forkJoin.
This operator will take as input a list of observables, fire them in parallel and will return a list of the last emitted value of each observable once they all complete.
forkJoin({
invoker: invoker$,
fetch: fetch$,
})
.subscribe(({invoker, fetch}) => {
console.log(invoker, fetch);
});
Seems like this behavior is provided by the concatMapEager operator from the cartant/rxjs-etc library - written by Nicholas Jamieson
(cartant) who's a developer on the core RxJS team.

Using RxJS to manipulate a stream of items to obtain an array of streamed items

i'm kinda new to rxjs and can't get my head around this problem:
I have two streams:
one with incoming objects
---a----b----c----d----->
one with the selected object from a list
-------------------c---->
From the incoming objects stream make a stream of the list of objects (with scan operator)
incoming: ----a--------b-------c----------d----------------\>
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]---------\>
When a list object is selected (n), start a new stream
the first value of the new stream is the last value of the list sliced ( list.slice(n))
incoming: ----a--------b-------c----------d--------------------e-------->
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]--------->
selected object: ---------------------------------c------->
new stream of list: ------[c,d]-----[c,d,e]--->
i can't get the last value of the list stream when the object is selected,,,
made a marble diagram for better understanding,
selectedObject$ = new BehaviorSubject(0);
incomingObjects$ = new Subject();
list$ = incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
newList$ = selectedObject$.pipe(
withLastFrom(list$),
switchMap(([index,list])=> incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, list.slice(index))
))
)
A common pattern I use along with the scan operator is passing reducer functions instead of values to scan so that the current value can be used in the update operation. In this case you can link the two observables with a merge operator and map their values to functions that are appropriate - either adding to a list, or slicing the list after a selection.
// these are just timers for demonstration, any observable should be fine.
const incoming$ = timer(1000, 1000).pipe(map(x => String.fromCharCode(x + 65)), take(10));
const selected$ = timer(3000, 3000).pipe(map(x => String.fromCharCode(x * 2 + 66)), take(2));
merge(
incoming$.pipe(map(x => (s) => [...s, x])), // append to list
selected$.pipe(map(x => (s) => { // slice list starting from selection
const index = s.indexOf(x);
return (index !== -1) ? s.slice(index) : s;
}))
).pipe(
scan((list, reducer) => reducer(list), []) // run reducer
).subscribe(x => console.log(x)); // display list state as demonstration.
If I understand the problem right, you could follow the following approach.
The key point is to recognize that the list Observable (i.e. the Observable obtained with the use of scan) should be an hot Observable, i.e. an Observable that notifies independent on whether or not it is subscribed. The reason is that each new stream you want to create should have always the same source Observable as its upstream.
Then, as you already hint, the act of selecting a value should be modeled with a BehaviorSubject.
As soon as the select BehaviorSubject notifies a value selected, the previous stream has to complete and a new one has to be subscribed. This is the job of switchMap.
The rest is to slice the arrays of numbers in the right way.
This is the complete code of this approach
const selectedObject$ = new BehaviorSubject(1);
const incomingObjects$ = interval(1000).pipe(take(10));
const incomingObjectsHot$ = new ReplaySubject<number[]>(1);
incomingObjects$
.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
.subscribe(incomingObjectsHot$);
selectedObject$
.pipe(
switchMap((selected) =>
incomingObjectsHot$.pipe(
map((nums) => {
const selIndex = nums.indexOf(selected);
if (selIndex > 0) {
return nums.slice(selIndex);
}
})
)
),
filter(v => !!v)
)
.subscribe(console.log);
An example can be seen in this stackblitz.

Cancel repeated subscription in mergeMap

How to combine the distinct, switchMap and mergeMap operators, so that when the source emits repeated values (detected by distinct.keySelector), the previous subscription is canceled (as in the switchMap), but if the value is not repeated follow the behavior of mergeMap?
Example:
source = from(1, 2, 1, 2, 3) // 'abcde'
result = source.pipe(delay(), combination() // '--cde'
I'm currently doing something like:
const activeSubscriptions = new Map();
source$.pipe(
mergeMap((value) => {
const pendingSubscription = activeSubscriptions.get(value);
if (pendingSubscription) {
pendingSubscription.unsubscribe();
activeSubscriptions.delete(value);
}
const request$ = new Subject();
const subscription = this.service.get(value).subscribe({
complete: () => request$.complete(),
error: (err) => request$.error(err),
next: (value) => request$.next(value),
});
activeSubscriptions.set(value, subscription);
return request$;
})
);
But looking for a better way to do that.
Thank you in advance
I think you can use the windowToggle operator for this:
src$ = src$.pipe(shareReplay(1));
src$.pipe(
ignoreElements(),
windowToggle(src$.pipe(observeOn(asyncScheduler)), openValue => src$.pipe(skip(1), filter(v => v === openValue))),
mergeMap(
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
),
)
A replacement for observeOn(asyncScheduler) could also be delay(0), the important thing is to make sure the order in which the src$'s subscribers receive the value is correct. In this case, we want to make sure that when src$ emits, the clean-up takes place first, so that's why we're using src$.pipe(observeOn(asyncScheduler)).
ignoreElements() is used because each window is paired to only one value, the one which has created the window. The first argument(s) passed to windowToggle will describe the observable(s) which can create the windows. So, we only need those, since we're able to get the last value with the help of
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
By the way, a window is nothing but a Subject.
Lastly, if you want to perform async operations inside the window's pipe, you'll have to make sure that everything is unsubscribed when the window is completed(closed). To do that, you could try this:
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
switchMap(val => /* some async action which uses `val` */),
takeUntil(window.pipe(isEmpty()))
)
where isEmpty will emit either true or false when the source(in this case, the window) completes. false means that the source had emitted at least one value before emitting a complete notification, and true otherwise. In this case, I'd say it's irrelevant whether it's true or false, since the window will not emit any values by itself(because we have used ignoreElements, which ignores everything except error and complete notifications).

Reset Scan Accumulator RxJS after certain time

I have a fromEvent attached to input keydown event. This way I can listen to KeyEvents.
Inside the pipe, I use the scan operator, so I can accumulate the latest 3 keys introduced by the user.
I check in the scan, the accumulator length, so if it's already three, I clean it up (manual reset).
I need a way that when the user types in, within the next 3000ms, he can keep typing until reaching the limit (3 keys) but if the user is slower than the time limit (3s), next time he types in, I will reset the accumulator manually.
fromEvent(myInput.nativeElement, 'keydown').pipe(
tap(e => e.preventDefault()),
scan((acc: KeyboardEvent[], val: KeyboardEvent) => {
// Add condition here to manually reset the accumulator...
if (acc.length === 3) {
acc = [];
}
return [...acc, val];
}, []),
takeUntil(this.destroy$)
).subscribe((events: KeyboardEvent[]) => console.log(events));
I have tried to merge this with a timer in some way, but I can't figure out how. Not sure how to get there.
you can use the timeInterval operator here, which gives you the time passed between emissions along with the value. set up along the lines of:
fromEvent(myInput.nativeElement, 'keydown').pipe(
tap(e => e.preventDefault()),
timeInterval(), // just add this operator, will convert to shape {value: T, interval: number}
scan((acc: KeyboardEvent[], val) => {
// Add condition here to manually reset the accumulator...
// also check the interval
if (acc.length === 3 || val.interval > 3000) {
acc = [];
}
return [...acc, val.value];
}, []),
takeUntil(this.destroy$)
).subscribe((events: KeyboardEvent[]) => console.log(events));
here is a working blitz: https://stackblitz.com/edit/rxjs-dxfb37?file=index.ts
not an operator I've ever had a use case for before, but seems to solve your issue here pretty effectively.

RxJS: How to ignore item in stream when outdated

I'm trying to subscribe to a "loading" Observable which returns a boolean. The goal is to have a delay for the items in the stream if loading is true, but not if loading is false. It should also be possible to ignore an outdated item, lets assume following case:
In the stream are two items:
Item A with value true, and is passed immediately
Item B with value false, and is passed 200ms later
Item A has value true, because of that it will be delayed by 500ms, Item B will not be delayed. In this situation Item B will arrive before Item A. I'm searching for an solution to completely ignore Item A for the mentioned case.
Here you see my current not-working solution:
this.loading$
.pipe(
distinctUntilChanged(),
delayWhen(loading => timer(loading ? 500 : 0))
)
.subscribe(loading => {
// Do something
});
Just replace delayWhen with switchMap and it should work. switchMap can cancel outdated inner stream. And you probably an one-time observable rather than an timer:
this.loading$
.pipe(
distinctUntilChanged(),
switchMap(
loading =>
of(loading).pipe(delay(loading ? 500 : 0)))
)
.subscribe(loading => {
// Do something
});

Resources