RxJS provides the sequenceEqual operator to compare two streams in order. How would one go about testing equality of two streams regardless of order?
Pseudocode:
//how do we implement sequenceEqualUnordered?
from([1,2,3]).pipe(sequenceEqualUnordered(from([3,2,1]))).subscribe((eq) =>
console.log("Eq should be true because both observables contain the same values")
)
In my particular use case I need to wait until a certain set of values has been emitted or error but I don't care what order they're emitted in. I just care that each value of interest is emitted once.
Here's my solution:
import { Observable, OperatorFunction, Subscription } from 'rxjs';
export function sequenceEqualUnordered<T>(compareTo: Observable<T>, comparator?: (a: T, b: T) => number): OperatorFunction<T, boolean> {
return (source: Observable<T>) => new Observable<boolean>(observer => {
const sourceValues: T[] = [];
const destinationValues: T[] = [];
let sourceCompleted = false;
let destinationCompleted = false;
function onComplete() {
if (sourceCompleted && destinationCompleted) {
if (sourceValues.length !== destinationValues.length) {
emit(false);
return;
}
sourceValues.sort(comparator);
destinationValues.sort(comparator);
emit(JSON.stringify(sourceValues) === JSON.stringify(destinationValues));
}
}
function emit(value: boolean) {
observer.next(value);
observer.complete();
}
const subscriptions = new Subscription();
subscriptions.add(source.subscribe({
next: next => sourceValues.push(next),
error: error => observer.error(error),
complete: () => {
sourceCompleted = true;
onComplete();
}
}));
subscriptions.add(compareTo.subscribe({
next: next => destinationValues.push(next),
error: error => observer.error(error),
complete: () => {
destinationCompleted = true;
onComplete();
}
}));
return () => subscriptions.unsubscribe();
});
}
As many of RxJS operators have some input parameters and as all of them return functions, sequenceEqualUnordered also has some input parameter (mostly the same as Rx's sequenceEqual operator) and it returns a function. And this returned function has the Observable<T> as the source type, and has Observable<boolean> as the return type.
Creating a new Observable that will emit boolean values is exactly what you need. You'd basically want to collect all the values from both source and compareTo Observables (and store them to sourceValues and destinationValues arrays). To do this, you need to subscribe to both source and compareTo Observables. But, to be able to handle subscriptions, a subscriptions object has to be created. When creating a new subscriptions to source and compareTo, just add those subscriptions to subscriptions object.
When subscribing to any of them, collect emitted values to appropriate sourceValues or destinationValues arrays in next handlers. Should any errors happen, propagate them to the observer in error handlers. And in complete handlers, set the appropriate sourceCompleted or destinationCompleted flags to indicate which Observable has completed.
Then, in onComplete check if both of them have completed, and if they all are, compare the emitted values and emit appropriate boolean value. If sourceValues and destinationValues arrays don't have the same lengths, they can't equal the same, so emit false. After that, basically sort the arrays and then compare the two.
When emitting, emit both the value and complete notification.
Also, the return value of function passed to the new Observable<boolean> should be the unsubscribe function. Basically, when someone unsubscribes from new Observable<boolean>, it should also unsubscribe from both source and compareTo Observables and this is done by calling () => subscriptions.unsubscribe(). subscriptions.unsubscribe() will unsubscribe from all subscriptions that are added to it.
TBH, I haven't wrote any tests for this operator, so I'm not entirely sure that I have covered all edge cases.
My first idea. Use toArray on both then zip them together finally sort and compare results?
Related
I have an outer observable that i use with its result in the inner observable
and that I need to return the result from the inner observable
In the following example, I need to return the result allPersons from the second observable
the result from that function is Observable I want the it will return Observable
getAllPerson(): Observable<Data1[]> {
return this.dataService.getIds().subscribe(
(ids) => {
return this.dataService.getPersons().pipe(
map((allPersons) => {
console.log(ids);
//filter persons according to ids
return allPersons;
})
})
);
}
Also tried: and get
Argument of type 'Observable' is not assignable to parameter of type 'OperatorFunction<any, any>'.
getAllPerson(): Observable<any> {
return this. dataService.getIds().pipe(
switchMap((data) => {
this.dataService.getPersons().subscribe(
(allPersons) => {
console.log(ids);
//filter persons according to ids
return allPersons;
})
})
);
}
It's going to be somthing like that :
function getAllPerson(): Observable<Data1[]> {
return this.dataService.getIds().pipe(
switchMap((ids) => {
return this.dataService.getPersons().pipe(
map((allPersons) => {
return allPersons.filter(...); //filter persons according to ids
})
);
})
);
}
And subscribe the whole thing.
Okay, I see you're using TypeScript (nice), but there's a type error in the first two lines of your code.
getAllPerson(): Observable<Data1[]> {
return this.dataService.getIds().subscribe( /*more code here */ );
The type check is going to look at this is complain. It will say something like, "Denotationally, you've declared that this function returns an Observable object. By inference, I can see you're returning a Subscription object. As far as I can tell, Observable objects and subscription objects cannot be unified. This is a type error.
It's right.
The issue is that once you subscribe to an observable, you're no longer in RxJS land. You're left with an imperative way to end an observable, but you're done dealing with observable.
Think about subscribing as your way to exist the RxJS library. So if operators like of, from, fromEvent, new Subject, new BehaviorSubject, ect are ways to enter the RxJS library, then subscribe, lastValueFrom, firstValueFrom, behaviorSub.value, etc are ways to exit the RxJS library.
So how to avoid that dreaded subscribe. This is where RxJS hiher order operators come in. They let you chain, combine, merge, etc streams
for Example:
function getAllPerson(): Observable<Data1[]> {
return this.dataService.getIds().pipe(
switchMap(ids => this.dataService.getPersons().pipe(
map(allPersons => allPersons.filter(/* filter code ...*/))
))
);
}
I want combineLatest functional but for variable count of observables.
Something like:
// init combineLatest of three observables
[1, 2, 3]
// first observable produced new value "2"
[2, 2, 3]
// third observable ended
[2, 2]
// first observable produced new value "1"
[1, 2]
// new observable added
[2, 2, 4]
Is it possible in RxJS?
If I understand the problem right, the solution is pretty tricky for something that looks innocent.
I try to go step by step to explain a potential solution.
First of all we need understand that there are 3 different events that we need to manage:
the fact that one Observable completes
the fact that one Observable is added to the array which is given to combineLatest
the fact that a new array of Observables has to be passed to combineLatest, either because we are at the beginning of the processing (i.e. with the initial array) or because we have added a new Observable or because one Observable has completed
The second thing that we need to recognize is that we need to store the array of Observables we pass to combineLatest in a variable, otherwise we are not able to add or remove Obaservables from there.
Once these things are clear, we can build a solution in the form of a function that returns 2 things:
the Observable that we want to subscribe to and that should have the behavior that we are looking for
a Subject that we can use to communicate that we want to add a new Observable to the combineLatest function
The last point we need to recognize is that any time we change the list of Observable, either because we add or because we remove an Observable (because it completed), we need to run the combineLatest function with the new fresh list of Observables.
Now that all this has been clarified, this is the code of the function that returns an Observable which behaves as described
function dynamicCombineLatest(startingObservables: Observable<any>[]) {
// this is the variable holding the array of Observables
let observables = startingObservables;
// this is the array that contains the list of Observables which have been, potentially, transformed to emit
// immediately the last value emitted - this happens when a new Observable is added to the array
let observablesPotentiallyWithLastValueImmediatelyEmitted =
startingObservables;
// this is the variable holding the array of values last notified by each Observable
// we will use it when we need to add a new Observable to the list
const lastValues = [];
// this are the Subjects used to notify the 3 different types of events
const start = new BehaviorSubject<Observable<any>[]>(observables);
const add = new Subject<Observable<any>>();
const remove = new Subject<Observable<any>>();
let skipFirst = false;
// this is the chain of operations which must happen when a new Observable is added
const addToObservables = add.pipe(
tap({
next: (obs) => {
console.log("add");
// we need to make sure that the Observables in the list will immediately start to emit
// the last value they emitted. In this way we are sure that, as soon as the new added Observable emits somthing,
// the last value emitted by the previous Observables will be considered
observablesPotentiallyWithLastValueImmediatelyEmitted = observables.map(
(o, i) => {
return startWith(lastValues[i])(o);
}
);
// the new Observable is added to the list
observables.push(obs);
observablesPotentiallyWithLastValueImmediatelyEmitted.push(obs);
},
})
);
// this is the chain of operations which must happen when an Observable is removed
const removeFromObservables = remove.pipe(
tap({
next: (obs) => {
const index =
observablesPotentiallyWithLastValueImmediatelyEmitted.indexOf(obs);
console.log("remove");
// we simply remove the Observable from the list and it "last value"
observablesPotentiallyWithLastValueImmediatelyEmitted.splice(index, 1);
observables.splice(index, 1);
lastValues.splice(index, 1);
// we make sure that the Observables in the list will immediately start to emit with the last value they emitted
observablesPotentiallyWithLastValueImmediatelyEmitted = observables.map(
(o, i) => {
return lastValues[i] ? startWith(lastValues[i])(o) : o;
}
);
// we set that the first value of the new combineLatest Observable will be skipped
skipFirst = true;
},
})
);
// here we merge the 2 chains of operations so that both add and remove logic will be executed
// when the relative Subjects emit
merge(addToObservables, removeFromObservables).subscribe({
next: () => {
console.log("new start");
// we notify that a change in the Observable list has occurred and therefore we need to unsubscribe the previous "combineLatest"
// and subscribe to the new one we are going to build
start.next(observablesPotentiallyWithLastValueImmediatelyEmitted);
},
});
// this is where we switch to a new Observable, result of the "combineLatest" operation,
// any time the start Subject emits a new Observable list
const dynamicObservables = start.pipe(
switchMap((_observables) => {
const _observablesSavingLastValueAndSignallingRemove = _observables.map(
(o, i) =>
o.pipe(
tap({
next: (v) => {
// here we save the last value emitted by each Observable
lastValues[i] = v;
},
complete: () => {
// here we notify that the Observable has completed and we need to remove it from the list
remove.next(o);
},
})
)
);
console.log("add or remove");
// eventually this is the Observable created by combineLatest with the expected array of Observables
const _combineLatest = combineLatest(
_observablesSavingLastValueAndSignallingRemove
);
const ret = skipFirst ? _combineLatest.pipe(skip(1)) : _combineLatest;
skipFirst = false;
return ret;
})
);
// here we return the Observable which will be subscribed to and the add Subject to be used to add new Observables
return { dynamicObservables, add };
}
You can look at this stackblitz for an example.
Buffer and combine based on a key
Here's a slight variant of what you're asking for. It works just like mergeAll, only it keeps a buffer and emits the latest for any observable that have emitted so far.
The varient here is that you need to supply string keys for your values to get attached to. You should be able to see how to turn this into array indices if you so choose.
The reason I haven't done this with an array is because there's no much undefined behavior. For example, if the first observable completes and the second observable emits, your elements are all opaquely re-ordered.
Using keys returns control back to the caller, who can just use Object.keys() if they don't care about indices/labels for their data.
Here you are:
interface LabeledObservable<T> {
label: string,
stream: Observable<T>
}
interface CombinedLatest<T> {
[key:string]: T
}
function combineLatestAll<T>():
OperatorFunction<
LabeledObservable<T>,
CombinedLatest<T>
>
{
return source$ => defer(() => {
const buffer = {};
return source$.pipe(
mergeMap(({label, stream}) => stream.pipe(
map(v => {
buffer[label] = v;
return {...buffer};
}),
finalize(() => {
delete buffer[label];
})
))
);
});
}
Subject for new observables
If you like the idea of a subject you can use to inject new observables into your combineLatest operator, this still allows that. The only alteration needed is that you must supply unique labels for your observables. If you don't care about the labels, you can just use any ID generator pattern (Like incrementing a global id counter or something).
const startingObservables: Observable<any>[] = /*some observables */;
const add = new Subject<LabeledObservable<any>>();
add.pipe(
combineLatestAll()
).subscribe(console.log);
startingObservables.forEach((stream,i) => {
add.next({label: "" + i, stream});
});
rxjs collects the values sent by Subject and then sends the past values as an array
import { Subject } from "rxjs";
const list$ = new Subject<number>();
list$
.pipe(/* Which pipeline should I use to achieve the following log information */)
.subscribe(console.log);
list$.next(1); // log: [1]
list$.next(2); // log: [1,2]
In RxJS .pipe() is where you list a series of operators. Each operator is a pure functinon that receives the emitted value from the previous observable (or operator), and returns a new value for the next operator.
Because of the utility that operators provide, the subscription method can be kept rather simple, or completely empty.
Given your requirement to convert each emitted value to an array, I would recommend the scan() operator. Similar to Array.reduce(), the scan() operator gives you an aggregate value you can add to for each emission from the source observable (in this case, your subject).
const list = new Subject<number>();
list.pipe(
scan((numArray, number)=>
[...numArray, number], [] as number[]
)
).subscribe(numArray=>console.log(numArray));
list.next(1); // [1]
list.next(2); // [1,2]
The reason I didn't include $ in the variable name, is that this is typically reserved only for public observables (not subjects or subscriptions, which shouldn't be exposed to other classes/modules/files).
I customized Observable to solve the problem, but I want to know how to use the pipe that comes with rxjs
import { Observable, Subject } from "rxjs";
const list$ = new Subject<number>();
var observable = new Observable((subscribe) => {
const cache = [];
const sub = list$.subscribe((v) => {
cache.push(v);
subscribe.next(cache);
});
return function unsubscribe() {
sub.unsubscribe();
};
});
const sub = observable.subscribe(console.log);
list$.next(1); // log: [1]
list$.next(2); // log: [1,2]
I have a function in which there could be a number of unknown observables defined:
class Foo {
show$(multipleObservables$: Observable<boolean>[]): void {
// ...
}
}
And in the locations where I use this class, the number of observables vary. It could be 1, but it could also be three:
const observableOne$ = of(false);
const observableTwo$ = of(true);
const observableThree$ = of(false);
const observables$ = [observableOne$, observableTwo$, observableThree$];
Foo.show$(observables$);
How can I write the show$ function to return true when any observable is true, or false when they are all false?
I've read this post, but that assumes you already know the number of observables. Is there a way that you can create this function without knowing the observables on forehand?
This is going to depend on whether the source Observables are going to complete or not. If you're going to use just of() you could do it like this:
show$(multipleObservables$: Observable<boolean>[]) {
return forkJoin(multipleObservables$)
.pipe(
map((results: boolean[]) => results.some(Boolean)),
);
}
Is there a better way to re-write this code and avoid chaining of subscriptions ?
Why am I chaining? because I need to the output of source1$ in child subscriptions
And also I have if conditions because I want to call child subscriptions conditionally
PS i checked solution in this post
Here is the stackblitz link and code
import { from } from 'rxjs';
//emit array as a sequence of values
const source1$ = from([1]);
const source2$ = from([2]);
const source3$ = from([3]);
const useCond1 = true; // this is dynamic can be false too
const useCond2 = true; // this is dynamic can be false too
source1$.subscribe(val => {
if (useCond1) {
source2$.subscribe(() => {
console.log('val from source1 in source2', val);
});
}
if (useCond2) {
source3$.subscribe(() => {
console.log('val from source1 in source3', val);
});
}
});
Not sure, but it seems that you need switchMap or mergeMap and iif
from rxjx doc:
import { fromEvent, iif, of } from 'rxjs';
import { mergeMap, map, throttleTime, filter } from 'rxjs/operators';
const r$ = of(`I'm saying R!!`);
const x$ = of(`X's always win!!`);
fromEvent(document, 'mousemove')
.pipe(
throttleTime(50),
filter((move: MouseEvent) => move.clientY < 210),
map((move: MouseEvent) => move.clientY),
mergeMap(yCoord => iif(() => yCoord < 110, r$, x$))
)
.subscribe(console.log);
Yes, there is a better way!
RxJS provides many different operators and static functions for combining, filtering, and transforming observables. When you use what the library provides, you do not need to have nested subscriptions.
In general, I find it simpler to not do any logic at all inside the subscribe, but rather design observables that emit the exact data that is needed.
A simplistic example could look like this:
someValue$ = source1$.pipe(
switchMap(val1 => useCond1 ? source2$ : of(val1))
);
someValue$.subscribe();
switchMap will subscribe to an "inner observable" whenever it receives an emission. The logic above says to either return the value emitted from source1$ (val1) or return whatever source2$ emits depending on the value of useCond1.
So source2$ will only get subscribed to when useCond1 is true;
Note: the function inside switchMap should return an observable (because switchMap subscribes to it), so of was used to turn the emitted value into an observable.
In your case, let's assume you want to emit some calculated value, based possibly on the other two sources.
We can use combineLatest to create a single observable based on 3 different sources. Since you only want to optionally call source2$ and source3$, we can define the sources based on your conditions. We can then use map to transform the array of values from the 3 sources, into the desired output:
someValue$ = source1$.pipe(
switchMap(val1 => {
const s1$ = of(val1);
const s2$ = useCond1 ? source2$ : of('default val2');
const s3$ = useCond2 ? source3$ : of('default val3');
return combineLatest([s1$, s2$, s3$]);
}),
map(([val1, val2, val3]) => {
return ... // your logic to return desired value
})
);
combineLatest will emit an array containing the latest emissions from each source whenever any source emits. This means someValue$ will emit the latest calculated value whenever any of the sources change.