Window before nested emits - rxjs

I have observable [1,1,1,2,2,1,1], and want to convert it in observable of sums of same elements group, so result would be [3,4,2]. I wrote this code:
from([1, 1, 1, 2, 2, 1, 1])
.pipe(
connect((numbers$) =>
numbers$.pipe(window(checkChange(numbers$)), mergeMap(sumNumbers))
)
)
.subscribe(console.log);
function checkChange(obs$: Observable<number>): Observable<any> {
return obs$.pipe(
pairwise(),
filter(([a, b]) => a !== b),
map(() => "change")
);
}
function sumNumbers(obs$: Observable<number>): Observable<number> {
return obs$.pipe(reduce((acc, n) => acc + n, 0));
}
But it returns observable [5,3,1]. This is because original observable windows after element where change happens, and not before. So it groups by [[1,1,1,2],[2,1],[1]], and not [[1,1,1],[2,2],[1,1]]. How can I fix it?

You could achieve it using other notifier observable that runs before the window observable gets processed.
You could do something like this
const source = from([1, 1, 1, 2, 2, 1, 1]);
const myNotifier = new Subject<number>(); //Subject to be used as notifier
source
.pipe(
connect((shared$) =>
merge(
shared$.pipe(notifyOnChange(myNotifier)), // 1st run the notifier stream
shared$.pipe(window(myNotifier)) // 2nd the windowed one.
)
),
mergeMap(sumNumbers)
)
.subscribe(console.log);
function notifyOnChange<T>(notifier: Subject<T>) {
return (obs$: Observable<T>): Observable<never> =>
obs$.pipe(
distinctUntilChanged(), // only lets through the value if different to previous one
skip(1), // skip the first emission
tap(notifier), // notify the change
ignoreElements() //prevent the next notification propagation
);
}
function sumNumbers(obs$: Observable<number>): Observable<number> {
return obs$.pipe(reduce((acc, n) => acc + n, 0));
}
Cheers

I would use reduce instead to maintain the current value being counted, running count of correct value, and an array of previous counts.
import {from} from 'rxjs';
import {reduce,map} from 'rxjsoperators';
from([1, 1, 1, 2, 2, 1, 1]).pipe(
reduce(
({ prevCounts, prev, count }, cur) => (count === undefined) ? {
prevCounts,
prev : cur,
count : 1
} : (prev === cur) ? {
prevCounts,
prev,
count: count + 1
} : {
prevCounts: [...prevCounts, count],
prev: cur,
count: 1
},
{ prevCounts : [] }
),
// append count to prevCounts array
map(({ prevCounts, count }) => (count) ? [...prevCounts, count] : prevCounts)
).subscribe({
next: x => console.log(x)
});
This code doesn't use array.push because that changes the array in place and while in this case it wouldn't be a bad thing I've learned it's safer to have the functional programming habit of either returning unchanged objects or new objects.

Related

rxjs: extract slice of a collection given start and end matching rules

I'm trying to write a reactive function with rxjs that, given a potentially infinite array:
Rule 1: Skip initial null items
Rule 2: Extract the items between two '*' appearances
Rule 3: If first item after nulls is not an '*', must fail (or return an empty array)
Rule 4: Process no more than N items
Rule 5: If there's no a second '*', must fail (or return an empty array)
So, with N = 10:
Case 1: [null, null, '*', 1, 2, 3, '*', 4, 5] -> [1, 2, 3]
Case 2: [null, null, 1, '*', 2, 3, '*', 4, 5] -> [] // Breaks rule 3
Case 3: [null, null, '*', 1, 2, 3, 4, 5, 6, 7, 8, '*'] -> [] // Breaks rule 5 (second * is at position > N)
For the case 1, there's no problem. But I don't find the set of operator to enforce the rules 3 and 5
This example illustrates the problem:
const { from } = require('rxjs');
const { take, takeWhile, skipWhile, toArray } = require('rxjs/operators');
function *infinite(items) {
for (let i = 0; ; i++) {
yield i < items.length ? items[i] : `fake${i}`
}
}
const extract = ({
source,
limit = 10,
}) => new Promise(resolve => {
source
.pipe(...[
take(limit),
skipWhile(item => item === null),
skipWhile(item => item === '*'),
takeWhile(item => item !== '*'),
toArray(),
])
.subscribe(result => {
resolve(result)
})
})
;(async () => {
console.log(await extract({ source: from(infinite([null, '*', 1, 2, 3, '*', 4, 5, 6])) }))
console.log(await extract({ source: from(infinite([null, 'a', '*', 1, 2, 3, '*', 4, 5, 6])) }))
console.log(await extract({ source: from(infinite([null, '*', 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12])) }))
})()
Edit: I realized the operation:
skipWhile(item => item === '*'),
is not accurate. Should be something like
skipThisSingleItemIfMatchAsteriskOtherwiseFail
A possible solution for your problem is the following. Comments are inline
function extract(c: any[], n: number) {
// first you create an src stream where all the leading nulls are removed
const src = from(c).pipe(
// just take the first n elements
take(n),
// filter to remove the nulls - this can be a problem if you have nulls between the 2 '*'
filter((item) => item !== null),
// share is used to avoid having more than one subscription to this stream
share()
);
const core = src.pipe(
// here we consider all elements until the second '*' is met
// And what about the first '*'? see the rest of the code, there is the explanation
takeWhile((item) => item !== "*", true),
// create an array which cumulates all the values received until the stream completes
toArray(),
// if the array of all elements received is empty or if the last element is not a '*' return []
// else return the elements received a part the last '*'
map((arr) => {
return arr.length === 0
? []
: arr[arr.length - 1] !== "*"
? []
: arr.slice(0, arr.length - 1);
})
);
// this is the stream returned by the extract function
// it starts from the src stream we have created above
return src.pipe(
// the first element is taken
// since the src stream is shared between this stream and the stream we have called "core" and have built above
// then it means that the first element is consumed here and will not be encountered in the "core" stream
first(),
// if the first element is not a '*' an error is thrown
tap((d) => {
if (d !== "*") {
throw new Error("First not null val is not *");
}
}),
// if no error is thrown then we return the stream "core"
concatMap((firstItem) => {
return core;
}),
// if an error is thrown then we return an Observable which emits []
catchError((e) => of([]))
);
}
In order to use this function you can write the following code
const resp = extract(source, 10);
resp.subscribe((d) => {
// do stuff with the result, for instance
console.log(d);
});
Here a stackblitz that reproduces this logic
How about something with custom RxJS operators? Try the following
const { Observable, from } = rxjs;
const { take, filter, reduce } = rxjs.operators;
const onlyIfFirst = predicate => {
let first = true;
return source =>
new Observable(subscriber =>
source.subscribe({
next(value) {
if (first) {
first = false;
if (predicate(value)) {
subscriber.next(value);
} else {
subscriber.next([]);
subscriber.complete();
}
} else {
subscriber.next(value);
}
},
complete() {
subscriber.complete();
}
})
);
};
const toArrayWhen = (predicate, count) => {
let id = 0;
let times = count * 2;
return source =>
source.pipe(
reduce((acc, curr) => {
if (!!id) {
if (predicate(curr)) id++;
if (id < times && !predicate(curr)) {
acc = [...acc, curr];
}
} else {
if (predicate(curr)) id++;
}
return acc;
}, [])
);
};
const input = [null, null, '*', 1, 2, 3, '*', 3, '*', 4, '*'];
from(input)
.pipe(
take(10),
filter(value => value !== null),
onlyIfFirst(value => value === '*'),
toArrayWhen(value => value === '*', 1)
)
.subscribe({
next: value => console.log('Next:', value),
error: error => console.log('Error:', error),
complete: () => console.log('Complete')
});
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>
Note: I'm fairly certain the count variable behavior for > 1 is buggy at the moment. But as long as you only need the first instead of values between two asterisks *, it should be find.

Keep state while operating in switchMap

Suppose that you have a function that returns an rxjs observable that contains a list of objects.
const getItems = () =>
of([
{
id: 1,
value: 10
},
{
id: 2,
value: 20
},
{
id: 3,
value: 30
}
]);
and a second function that returns an observable with a single object
const getItem = id =>
of({
id,
value: Math.floor(Math.random() * 30) + 1
});
Now we want to create an observable that will get the first list and at a regular interval will randomly update any list item.
const source = getItems().pipe(
switchMap(items =>
interval(5000).pipe(
switchMap(x => {
// pick up a random id
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
)
);
})
)
)
);
source.subscribe(x => console.log(JSON.stringify(x)));
The problem with the above code is that each time the interval is triggered the items from the previous iteration reset to their initial form. e.g,
[{"id":1,"value":10},{"id":2,"value":13},{"id":3,"value":30}]
[{"id":1,"value":10},{"id":2,"value":20},{"id":3,"value":18}]
[{"id":1,"value":10},{"id":2,"value":16},{"id":3,"value":30}]
[{"id":1,"value":21},{"id":2,"value":20},{"id":3,"value":30}]
As you see, on each interval our code is resetting the list and updates a new item (eg value 13 is lost in the second iteration and reverts to 20).
The behaviour seems reasonable since the items argument in the first switchMap acts like a closure.
I managed to somehow solve the issue by using BehaviorSubject but i think that my solution is somehow dirty.
const items$ = new BehaviorSubject([]);
const source = getItems().pipe(
tap(items => items$.next(items)),
switchMap(() =>
interval(5000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items$
.getValue()
.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
),
tap(items => items$.next(items)),
switchMap(() => items$)
);
})
)
)
);
Is there a better approach ?
Example code can be found here
I believe this should be doing what you want:
const source = getItems().pipe(
switchMap(items =>
interval(1000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId);
}),
scan((acc, item) => {
acc[acc.findIndex(i => i.id === item.id)] = item;
return acc;
}, items),
)
)
);
It's basically what you're doing but I'm using scan (that is initialized with the original items) to keep the output array in acc so I can update it later again.
Live demo: https://stackblitz.com/edit/rxjs-kvygy1?file=index.ts

RxJS: sequentially concat a variable number of inner Observables

I need to sequentially concat a variable number of inner Observables and stop as soon as the first one resolved to a given result.
See example below or here on Stackblitz where I use concatMap, but this works with a fixed number of inner Observables only.
const values = [1, 4, 6, 3, 9];
getRemoteValue(values[0]).pipe(
concatMap(result => result ? of(result) : getRemoteValue(values[1])),
concatMap(result => result ? of(result) : getRemoteValue(values[2])),
concatMap(result => result ? of(result) : getRemoteValue(values[3])),
concatMap(result => result ? of(result) : getRemoteValue(values[4]))
).subscribe(success => console.log(success ? 'found it' : 'failed'));
function getRemoteValue(input: number): Observable<boolean> {
console.log(`checking ${input}`)
// this would be an async remote call in real life
const value = _.random(10);
return of(value === input);
}
import * as _ from 'lodash';
import { Observable, of, from, EMPTY } from 'rxjs';
import { concatMap, single, find } from 'rxjs/operators';
const values = [1, 4, 6, 3, 9];
function getRemoteValue(input: number): Observable<boolean> {
console.log(`checking ${input}`)
// this would be an async remote call in real life
const value = _.random(10);
return of(value === input);
}
from(values).pipe(
concatMap(getRemoteValue),
find((v) => typeof v === 'boolean' && v === true)
)
.subscribe(success => {
console.log('success:', success);
console.log(success ? 'found it' : 'failed');
}
);
stackblitz

rxJs scan - doesn't emit if observable is empty

I have a bit of code that looks like this:
function getEvensSquared(array) {
return rxjs.of(array).pipe(
rxjs.operators.flatMap(array => {
return rxjs.from(
array.filter(n => n % 2 === 0)
);
}),
rxjs.operators.switchMap(n => {
return rxjs.of(n * n);
}),
rxjs.operators.scan((acc, cur) => {
acc.push(cur);
return acc;
}, [])
);
}
getEvensSquared([1, 2, 3, 4]).subscribe(v => {
console.log("1,2,3,4");
console.log(v)
});
getEvensSquared([1, 3, 5]).subscribe(v => {
console.log("1,3,5"); //never prints
console.log(v)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.2/rxjs.umd.min.js"></script>
Essentially it is:
Get a stream of arrays of ObjectA.
Convert the array in to a stream of filtered ObjectA.
Convert the stream of ObjectA into a stream of ObjectB.
Accumulate the stream of ObjectB into a stream of arrays of ObjectB.
The problem is - if there are no approved bookings, the flatMap operator never emits. Is there a way to default in this case?
I tried putting defaultIfEmpty([]) after the scan - but that didn't work.
Positing this as an answer - because it works for the example I've given - defaultIfEmpty does work in this scenario.
function getEvensSquared(array) {
return rxjs.of(array).pipe(
rxjs.operators.flatMap(array => {
return rxjs.from(
array.filter(n => n % 2 === 0)
);
}),
rxjs.operators.switchMap(n => {
return rxjs.of(n * n);
}),
rxjs.operators.scan((acc, cur) => {
acc.push(cur);
return acc;
}, []),
rxjs.operators.defaultIfEmpty([])
);
}
getEvensSquared([1, 2, 3, 4]).subscribe(v => {
console.log("1,2,3,4");
console.log(v)
});
getEvensSquared([1, 3, 5]).subscribe(v => {
console.log("1,3,5"); //works
console.log(v)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.2/rxjs.umd.min.js"></script>

RxJS emit array items over time?

I'm trying to emit simple array values one after another with 500ms in between:
var a = Rx.Observable.from([1,2,3]);
a.interval(500).subscribe(function(b) { console.log(b); });
However, this throws an exception:
Uncaught TypeError: a.interval is not a function.
Three ways to do it, with RxJS version 6 :
1. Using concatMap
import { from, of, pipe } from 'rxjs';
import { concatMap, delay } from 'rxjs/operators';
const array = [1, 2, 3, 4, 5];
from(array)
.pipe(
concatMap(val => of(val).pipe(delay(1000))),
)
.subscribe(console.log);
2. Using zip and interval
import { from, pipe, interval } from 'rxjs';
import { delay, zip} from 'rxjs/operators';
const array = [1, 2, 3, 4, 5];
from(array)
.pipe(
zip(interval(1000), (a, b) => a),
)
.subscribe(console.log);
3. Using interval as source
import { interval, pipe } from 'rxjs';
import { map, take } from 'rxjs/operators';
const array = [1, 2, 3, 4, 5];
interval(1000)
.pipe(
take(array.length),
map(i => array[i])
)
.subscribe(console.log);
As already pointed out by xgrommx, interval is not an instance member of an observable but rather a static member of Rx.Observable.
Rx.Observable.fromArray([1,2,3]).zip(
Rx.Observable.interval(500), function(a, b) { return a; })
.subscribe(
function(x) { document.write(x + '<br \>'); },
null,
function() { document.write("complete"); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.5.2/rx.all.min.js"></script>
This is how I would do it:
var fruits = ['apple', 'orange', 'banana', 'apple'];
var observable = Rx.Observable.interval(1000).take(fruits.length).map(t => fruits[t]);
observable.subscribe(t => {
console.log(t);
document.body.appendChild(document.createTextNode(t + ', '));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.5.2/rx.all.min.js"></script>
var arrayList = [1,2,3,4,5];
var source = Rx.Observable
.interval(500/* ms */)
.timeInterval()
.take(arrayList.length);
source.subscribe(function(idx){
console.log(arrayList[idx]);
//or document.write or whatever needed
});
Pretty late but a simpler solution would be :
const arr = ["Hi,", "how", "may", "I", "help", "you?"];
Rx.Observable.interval(500)
.takeWhile(_ => _ < arr.length)
.map(_ => arr[_])
.subscribe(_ => console.log(_))
I find Weichhold technique to be the best but that it would gain in clarity of intent by extracting the zipped value outside of the zip:
// assume some input stream of values:
var inputs = Obs.of(1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8);
// emit each value from stream at a given interval:
var events = Obs.zip(inputs, Obs.interval(1000))
.map(val => val[0])
.forEach(console.log);
If you want to release samples over time, you can do something like this
const observable = interval(100).pipe(
scan((acc, value) => [value, ...acc], []),
sampleTime(10000),
map((acc) => acc[0])
);
I had a little different requirement, my array kept updating over time too. So basically I had to implement a queue which I can dequeue at a regular interval, but I didn't want to use an Interval.
If somebody has a need for something like this then probably this solution can help:
I have a function createQueue() that takes the array as an input and returns an Observable which we subscribe for listening to events from the Array at a regular interval.
The function also modifies the 'push()' method of the passes array so that whenever any item is pushed in the array, the Observable would emit.
createQueue(queue: string[]) {
return Observable.create((obs: Observer<void>) => {
const arrayPush = queue.push;
queue.push = (data: string) => {
const returnVal = arrayPush.call(queue, data);
obs.next();
return returnVal;
}
}).pipe(switchMap(() => {
return from([...queue])
.pipe(
concatMap(val => of(val)
.pipe(delay(1000)))
);
}), tap(_ => queue.shift()))
}
Lets say that the array is: taskQueue = [];
So, we need to pass it to the above function and subscribe to it.
createQueue(taskQueue).subscribe((data) => {
console.log('Data from queue => ', data);
});
Now, every time we do taskQueue.push('<something here>'), the subscription will trigger after a delay of "1000ms".
Please note: we should not be assigning a new array to the taskQueue after createQueue() has been called, or else we will loose the modified push().
Here is a dummy example for the above implementation: Test Example
Rx.Observable instance doesn't have interval method http://xgrommx.github.io/rx-book/content/core_objects/observable/observable_instance_methods/index.html. You can use like this.
Rx.Observable.interval(500)
.map(function(v) { return [1,2,3];})
.subscribe(console.log.bind(console));

Resources