How to call "defaultIfEmpty" when list is empty on RxJS? - rxjs

I've two lists with two distinct objects that need to be converted into the same type, the "second" list will be used only if the "first" list is empty, I tried to use the method defaultIfEmpty but it never return the second option.
const first = []; // could be [{code: 1}, {code: 2}]
const second = [{id: 1}, {id: 2}]
of(first).pipe(
map((value) => {number: value.code})
).pipe(
defaultIfEmpty(of(second).pipe(map((value) => {number: value.id})))
).subscribe(doSomething);
The desired output is:
[{number: 1}, {number: 2}]
On the example above, the map from defaultIfEmpty is never called;
how can I "switch" to another method source if the given source is empty?
will subscribe method be called after the map is complete, or it will be called for each item on map?

like this ?
const first = []; // could be [{code: 1}, {code: 2}]
const second = [{id: 1}, {id: 2}];
of(first).pipe(
filter(({length}) => length > 0),
defaultIfEmpty(second),
map((arr) => arr.map((x) => ({number: x.code ?? x.id})))
).subscribe(...);

If that's an option just create the right observable at runtime:
const makeObservable =
(arr1, arr2) =>
from(arr1.length ? arr1 : arr2)
.pipe(map(({code, id}) => ({number: code ?? id})));
const obs1$ = makeObservable([], [{id:1},{id:2}]);
const obs2$ = makeObservable([{code:2},{code:3}], []);
obs1$.subscribe(o => console.log(o));
obs2$.subscribe(o => console.log(o));
<script src="https://unpkg.com/rxjs#%5E7/dist/bundles/rxjs.umd.min.js"></script>
<script>
const {from} = rxjs;
const {map} = rxjs.operators;
</script>

const list1: { code: number }[] = [];
const list2 = [{ id: 1 }, { id: 2 }];
of(list1)
.pipe(
map((aList) => aList.map((v) => ({ 'number': v.code }))),
map((listModified) => {
return listModified?.length > 0
? listModified
: list2.map((value) => ({ number: value.id }));
})
)
.subscribe(console.log);
Don't get confused with map, one of them is from rxjs, the other is a function for arrays. In your first map, you are mapping the the whole array, not each element.
subscribe will be called when everything completes within the pipe.

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.

I want to use rx.js to merge multiple data sources and support adding and deleting data sources

I have multiple data sources, and they're hot Observable(BehaviorSubject)
const data1$ = new BehaviorSubject({key: 'a', value: 1})
const data2$ = new BehaviorSubject({key: 'b', value: 2})
const data3$ = new BehaviorSubject({key: 'c', value: 3})
And I have a root that aggregates other data sources
const root$ = new BehaviorSubject({ field: {/* Stores the values of other data sources*/ } })
The idea is that when I subscribe to root$, I get an aggregation of their data, and when dataX$ is updated(next), I get an aggregation of the updates.
Also, I would like to receive updates when adding/deleting data sources.
This is my code so far, and I can't think of a way to implement it.
const a1$ = new BehaviorSubject({key: 'a', value: 1})
const a2$ = new BehaviorSubject({key: 'b', value: 2})
const trunk$ = new BehaviorSubject([a1$, a2$])
function add(dataSourch) {
trunk$.next([...trunk$.getValue(), dataSourch])
}
function remove(key) {
const dataArr = trunk$.getValue()
const newArr = dataArr.filter((sourch) => sourch.key !== key)
trunk$.next([...newArr])
}
————————————————————————————————————
At present: my problem is to add a new data source in the following code, and the final data source will trigger multiple updates
I added additional information and code on this issue
I have some basic data sources, which are subscribed by different roles and modified by different roles.
At the same time, I have a aggregation data source, which aggregates the data of the basic data source to a field (the aggregation data source itself has other data),
and is subscribed by some roles.
And the basic data source management module supports adding and deleting data sources.
const data1$ = new BehaviorSubject({key: 'a', value: 1})
const data2$ = new BehaviorSubject({key: 'b', value: 1})
const data3$ = new BehaviorSubject({key: 'c', value: 1})
const data4$ = new BehaviorSubject({key: 'd', value: 1})
const addSourch = (curr: any) => (prev: any) => ([curr, ...prev])
const addSourch$ = new Subject()
const deleteSourchByOb = (curr: any) => (arr: any) => {
const newArr = arr.filter((ele: any) => ele !== curr)
return newArr
}
const deleteSourchByOb$ = new Subject()
const deleteAll = () => (prev: any) => ([])
const deleteAll$ = new Subject()
const sourch$ = new BehaviorSubject([data1$, data2$, data3$])
const root$ = merge(
sourch$.pipe(map(arr => (curr: any) => [...arr, ...curr])),
addSourch$.pipe(map(addSourch)),
deleteSourchByOb$.pipe(map(deleteSourchByOb)),
deleteAll$.pipe(map(deleteAll))
).pipe(
scan((state, fn: Function) => fn(state), [])
)
const thunk$ = new BehaviorSubject({ field: { }})
const root2$= root$.pipe(
mergeMap(arr => merge(...arr)),
map((v: any) => (curr: any) => {
const obj = cloneDeep(curr)
obj[v.key] = v
return obj
})
).pipe(
scan((state, fn) => fn(state), {})
)
const final$ = combineLatest([root2$, thunk$]).pipe(
map((arr) => {
arr[1].field = arr[0]
return arr[1]
})
)
final$.subscribe(console.log)
You can achieve this behavior by using the scan operator.
const { BehaviorSubject, Subject, merge } = rxjs;
const { map, scan } = rxjs.operators;
// Your functions that mutate your state/data aggregation
const add = (data) => (state) => ([...state, data]);
const deleteAll = () => (state) => ([]);
const data1$ = new BehaviorSubject({key: 'a', value: 1})
const data2$ = new BehaviorSubject({key: 'b', value: 2})
const data3$ = new BehaviorSubject({key: 'c', value: 3})
// Observable that represents all add events
const add$ = merge(data1$, data2$, data3$)
// Observable that represents all deleteAll events
const deleteAll$ = new Subject();
const data$ = merge(
// Applies the first (outer) mutate function to your event observables
add$.pipe(map(add)),
deleteAll$.pipe(map(deleteAll))
).pipe(
// Applies the second (inner) mutate function to finally mutate and return your updated state
scan((state, fn) => fn(state), [])
)
data$.subscribe(console.log)
// Sample further events
data1$.next({key: 'd', value: 4})
deleteAll$.next();
data1$.next({key: 'e', value: 5})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
If you need any more details, ask for it in the comments and I try to add them to the answer.
FYI: For the sake of the main reason for your question (mutate state over time in rxjs) I did not implement a remove function as you did in your question. You can easily adapt this function to the given add and deleteAll functions.

RXJS Emit array item on event trigger

Using RxJS Id like to emit each array item on an event/click.
I have below which works fine, but is there a cleaner way?
const testEventClick = new Subject();
const array = [1, 2, 3, 4, 5];
from(array)
.pipe(
concatMap(val => {
return new Observable(sub => {
testEventClick.subscribe(x => {
sub.next(val);
sub.complete();
});
});
})
)
.subscribe(console.log);
testEventClick.next();
testEventClick.next();
I would do the other way around and observe the subject.
Example: https://stackblitz.com/edit/rxjs-l4ttzh

RxJS unsubscribe with id

Upon unsubscribe, I need to call a function with an id that was originally sent in the data. What is the correct way of doing that?
It is a nested observable as seen in below snippet.
Observable.create(subscriber => {
const myData = nested.getData
.map(x => x > 1)
.subscribe(subscriber);
return () => {
myData.unsubscribe();
callWithIdThatIsInData({id:123})
};
})
When unsubscribed, I need to callWithIdThatIsInData() with a property what was sent in the nested observable..
Can anyone point me in the right direction please?
A solution is to store your properties which match your filter condition, during the subscription of the nested observable.
Here is an example :
const nested = Rx.Observable.from([1, 2, 3, 4, 5]);
const stream = Rx.Observable.create((subscriber) => {
const saved = [];
const myData = nested.filter(x => x > 1).subscribe(x => {
subscriber.next(x);
saved.push(x);
});
subscriber.complete();
return () => {
myData.unsubscribe();
//Here you have access to your data
console.log(saved);
}
});
stream.subscribe(x => console.log(x));

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