I have an array of objects (not primitives) called "streaks" on my state tree and I want to observe only the changes to that array, not simply emit the entire array every time it changes. When I try this using pairwise() I get two identical arrays every time, even though I thought pairwise() would join the previous version and the current version. Why is pairwise() sending two identical arrays? NOTE streaks[1] and streaks[0] are identical, so _.differenceBy() isn't finding any changes because the two arrays are the same.
import {from} from "rxjs";
import {map, pairwise} from "rxjs/operators";
import * as _ from 'lodash';
const state$ = from(store);
const streaks$ = state$.pipe(
map(state => state.streaks),
// distinctUntilChanged(), <-- i've tried this and nothing is output at all
pairwise(),
map(streaks => {
let diff = _.differenceBy(streaks[0], streaks[1], _.isEqual);
console.log('diff', diff); //<-- this is an empty array
return diff;
})
);
streaks$.subscribe((streaksArray) => {
console.log('STREAKS$ 0', streaksArray); //<-- this is never even hit
} );
I solved this issue by creating an distinctUntilChangedArray operator that uses a self written compareArray function
Create compare array function
const compareArray<T> = (first: T[], second: T[], comparator=: (obj: T, obj2: T) => boolean): boolean {
return (first.length === 0 && second.length === 0)
|| (first.length === second.length && first.every((value, index) => {
return comparator
? comparator(value, second[index])
: JSON.stringify(value) === JSON.stringify(second[index]);
}))
}
Maybe you find a better array comparator. This is just my personal implementation of it
Create distinctUntilChangedArray operator
const distinctUntilChangedArray<T> = (comparator?: (prev: T, curr: T) => boolean): MonoTypeOperatorFunction<T[]> {
return distinctUntilChanged((prev: T[], curr: T[]) => {
return compareArray(first, second, comparator);
}
}
Final usage
interface nonPrimitive {
id: number;
name: string;
}
const nonPrimitiveComparator = (prev: nonPrimitive, curr: nonPrimitive): boolean => {
return prev.id === curr.id && prev.name === curr.name;
}
const source$: Observable<nonPrimitive>;
const distinctedSource$ = source$.pipe(
distinctUntilChangedArray(nonPrimitiveComparator)
);
Related
I need to handle a situation where I have 3 endpoints to call and would like to get the data in the most convenient/efficient way. The first call can be handled independently and returns a single result. The second endpoint returns a collection but will need to initiate 0-* subsequent calls, where a given key is present.
Ideally would like to receive the collection (from the 2nd endpoint call) as a mutated/new collection that includes the result from the 3rd endpoint call.
I am currently using forkJoin(observableA$, observableB$) to handle the first 2 calls in parallel but I cannot work out how to include the sequential calls and have the data included in observableB$
//Customer observable
const customer$ = this._customerManagementService.getCustomer(
accountNumber
);
return forkJoin({
customer: customer$,
saleCycles: saleCyclesWithVehicle$
}).pipe(finalize(() => this._loaderFactoryService.hide()));
getSalesWithVehicle(accountNumber: string, dealerKey: string) {
return this._salesCycleService
.getCyclesForCustomer({
customerNumber: accountNumber,
dealerKey: dealerKey
})
.pipe(
concatMap((results: ISaleCycle[]) => {
return results.map(cycle => {
return this._purchaseVehicleService.getPurchaseVehicle(
cycle.vehicleKey
);
});
})
);
}
I expect the collection to include further data as a new property on the original collection
UPDATE
After a bit more thought maybe I should be using reduce somewhere in the solution. This way I can be in control of what's getting push into the array and it could be dynamic?
getSalesWithVehicle(accountNumber: string, dealerKey: string) {
return this._salesCycleService
.getCyclesForCustomer({
customerNumber: accountNumber,
dealerKey: dealerKey
})
.pipe(
switchMap((results: ISaleCycle[]) => {
return results.map(cycle => {
if (cycle.vehicleKey) {
return this._purchaseVehicleService
.getPurchaseVehicle(cycle.vehicleKey)
.pipe(
reduce((acc, vehicle) => {
return { cycle: cycle, vehicle: vehicle };
}, []),
toArray()
);
}
else {
///No extra data to be had
}
});
}),
concatAll()
);
}
I would use concatMap() to merge the responses of HTTP requests 2 and 3.
import { of } from 'rxjs';
import { map, concatMap } from 'rxjs/operators';
const pretendGetCustomer = of({accountNumber: 123, name:"John Doe"});
const pretendGetVehiculeHttpRequest = (customerNumber) => {
return of([{custNum: 123, vehicleId:"2"}, {custNum: 123, vehicleId:"1"}]);
}
const pretendGetCyclesHttpRequest = (cycleIds) => {
return of([{id:"1", name:"yellow bike", retailPrice:"$10"}, {id:"2", name:"red bike", retailPrice:"$20"}]);
}
const yourFunction = () => {
pretendGetCustomer.subscribe(customer => {
// Assuming you do other things here with cust, reason why we are subscribing to this separately
// isHappy(customer)
// Your second & third calls
pretendGetVehiculeHttpRequest(customer.accountNumber).pipe(
// Need to use concatMap() to subscribe to new stream
// Note: use mergeMap() if you don't need the 1st stream to be completed
// before calling the rest
concatMap(purchases => {
const cyclesIds = purchases.map(p => p.vehicleId);
// concatMap() requires an Observable in return
return pretendGetCyclesHttpRequest(cyclesIds).pipe(
// Use map() here because we just need to use the data,
// don't need to subscribe to another stream
map(cycles=>{
// Retrun whatever object you need in your subscription
return {
customerNumber: customer.accountNumber,
customerName: customer.name,
purchases: purchases.map(p => cycles.find(c => p.vehicleId === c.id))
}
})
);
})
).subscribe(resultof2and3 => {
// Do something with the new/mutated Object which is a result of
// your HTTP calls #2 and #3
console.log(resultof2and3);
});
});
}
yourFunction();
I made a stackblitz if you want to see the above run (see console): https://stackblitz.com/edit/rxjs-nqi7f1
This is the solution I eventually came up with. I've taken the advice from BoDeX and used concatMap(). In my mind it was clear that I wanted to use forkJoin and be able to reference the results by object key, I.e customer or saleCycles.
In the scenario where a vehicleKey was present I needed to return the results in a defined data structure, using map(). Likewise, if no vehicle was found then I just needed the outer observable.
const customer$ = this._customerManagementService.getCustomer(accountNumber);
const saleCyclesWithVehicle$ = this.getSalesWithVehicle(accountNumber,dealerKey);
getSalesWithVehicle(accountNumber: string, dealerKey: string) {
return this._salesCycleService
.getCyclesForCustomer({
customerNumber: accountNumber,
dealerKey: dealerKey
})
.pipe(
concatMap(cycles => {
return from(cycles).pipe(
concatMap((cycle: ISaleCycle) => {
if (cycle.vehicleKey) {
return this._purchaseVehicleService
.getPurchaseVehicle(cycle.vehicleKey)
.pipe(
map(vehicle => {
return { cycle: cycle, vehicle: vehicle };
})
);
} else {
return of({ cycle: cycle });
}
}),
toArray()
);
})
);
}
return forkJoin({
customer: customer$,
saleCycles: saleCyclesWithVehicle$
}).pipe(finalize(() => this._loaderFactoryService.hide()));
How i can write generic function, which take Array of Objects (any type of Object, possible even null and undefined), and filter it to return just valid items of array? If i write it lite this, i will lose genericity :/
// #flow
// Types
type Person = {
id: string,
name: string,
};
type Car = {
id: string,
color: string,
};
// Function definition
const isNotUndefinedOrNull = item => !(item === null || item === undefined);
export const trimList = (list: Array<any> | $ReadOnlyArray<any>): Array<any> => {
return list.filter(isNotUndefinedOrNull);
};
// Constants
const persons = [{ id: 'p1', name: 'Johny' }, null, undefined];
const cars = [{ id: 'c1', color: 'red' }, null, undefined];
// Calls
const trimmedPersons = trimList(persons);
const trimmedCars = trimList(cars);
PROBLEM is, there i have trimmed cars and persons, but flow doesnt know, there is Cars in the trimmedCars list and neither know there is Persons in trimmedPersons list. Flow see just Array and i dont know, how to write is right, to not lose this info.
Flow try
As flow has a bug with Refine array types using filter we use explicit type casting ((res): any): T[]).
function filterNullable<T>(items: (?T)[]): T[] {
const res = items.filter(item => !(item === null || item === undefined);
return ((res): any): T[]);
}
// Example
const a: number[] = filterNullable([1, 2, null, undefined]);
i found it :)
export function trimList<V>(list: Array<?V> | $ReadOnlyArray<?V>): Array<V> {
return R.filter(isNotUndefinedOrNull, list);
}
I'm trying to query an empty firebase list. The problem is that the observable method subscribe never finish and I can't show to user that ddbb list is empty.
The function getUserAppointmentsByDate(...) is calling getUserAppointments(...), where this.database.list('/appointment/users/' + user_uid) is an empty firebase list for the input user (user_uid).
how should I manage an empty query to firebase?
thanks in advance!
getUserAppointmentsByDate(user_uid: string, start: string, end: string) {
if (typeof (user_uid) == "undefined" || typeof (start) == "undefined" || typeof (end) == "undefined") {
console.error("invalid argument for getPatientReport");
return;
}
return this.getUserAppointments(user_uid)
.map(
(appointment) => {
return appointment
.filter((appointment) => {
var appointmentStart = new Date(appointment.start);
var startFilter = new Date(start);
var endFilter = new Date(end);
//Filter old, not cancelled and not deleted
return (appointmentStart.getTime() < endFilter.getTime())
&& (appointmentStart.getTime() > startFilter.getTime())
&& (appointment.status != AppointmentStatus.CANCELLED);
});
})
}
getUserAppointments(user_uid: string): any {
return this.database.list('/appointment/users/' + user_uid) //*THIS IS AN EMPTY LIST
.mergeMap((appointments) => {
return Observable.forkJoin(appointments.map(
(appointment) => this.database.object('/appointment/list/' + appointment.$key)
.take(1)))
})
}
As the this.database.list('/appointment/users/' + user_uid) return a empty array. Observable.forkJoin(appointments.map( complete without emit any value (that is the expected way of forkJoin works). In this case, you have two options, handling in the complete function.
.subscribe(
res => console.log('I got values'),
err => console.log('I got errors'),
// do it whatever you want here
() => console.log('I complete with any values')
)
or handle in an if statement:
import { of } from 'rxjs/observable/of';
...
return this.database.list('/appointment/users/' + user_uid)
.mergeMap((appointments) => {
if (appointments.length === 0) return of([]);
return Observable.forkJoin(appointments.map(
(appointment) => this.database.object('/appointment/list/' + appointment.$key)
.take(1)))
})
I'd like to have a generic function that takes a parameter of type (() => T) | T, like
const myGenericFunction = <T>(param: ((() => T) | T)): T => {
if (typeof param === 'function') { return param(); }
return param;
};
However, in this case, flow doesn't know that if I pass a param of () => 1 then T is number, as opposed to () => number. How can I help flow understand that T should not be a function?
Flow doesn't contain a "not" type (such as $Not<Function>), so I don't think there's a completely generic way to do this. However, if T can only be a subset of types, you can accomplish this with bound generics:
const myGenericFunction = <T: number | string | {}>(
param: ((() => T) | T)
): T => {
if (typeof param === 'function') { return param(); }
return param;
};
I need to get a subset from a firebase list. Let's say I have two sets:
set1 = {
dog:true,
cat:true,
snake:true,
bull:true
}
and
set2 = {
dog:true,
cat:true
}
What I need is to get the "in set1, but not in set2", in this case it would return:
setresult = {
snake:true,
bull:true
}
I tried to achieve this with map:
this.setresult = this.af.database.list('/set2/').map((animals) => {
return animals.map((animal) => {
if (auth.uid == _user.$key || af.database.object('set2/' + animal.$key) == null) {}
else {
return af.database.object('set1/' + animal.$key);
}
})
})
But I end up with a list with nulls, and I need only the result set.
Thanks in advance.
You can use the combineLatest operator to compose an observable that emits an object containing the keys/values from set1 that are not in set2:
import * as Rx from "rxjs/Rx";
let set = Rx.Observable.combineLatest(
// Combine the latest values from set1 and set2
// using AngularFire2 object observables.
this.af.database.object('/set1/'),
this.af.database.object('/set2/'),
// Use the operator's project function to emit an
// object containing the required values.
(set1, set2) => {
let result = {};
Object.keys(set1).forEach((key) => {
if (!set2[key]) {
result[key] = set1[key];
}
});
return result;
}
);
set.subscribe((set) => { console.log(set); });