export type CompareFunction = (a: object, b: object) => number; export type MergeQueryObserver = (docs: DocumentData[]) => void; export type MergeErrorObserver = ( e: Error | string | object, queryPosition: number ) => void; export class MergeQuery { private observers: Set<{ fn: MergeQueryObserver; err: MergeErrorObserver }>; private compareFunction = defaultCompareFunction; private docs: DocumentData[][] = [[], []]; private queryCancelers: Function[] = []; constructor(...queries: Query[]) { this.observers = new Set(); queries.forEach((query, position) => { this.queryCancelers.push( query.onSnapshot( snap => { this.updateDocs(snap.docs, position); }, e => this.err(e, position) ) ); }); } setCompareFunction(compareFx: CompareFunction): void { this.compareFunction = compareFx; } /** * Listen for updates to the merged data. Receives the entire array after each update. * * @param observer receives an array of data objects sorted using compareFunction. * @return an unsubscribe function */ subscribe( observer: MergeQueryObserver, errorObserver?: MergeErrorObserver ): () => void { const obs = { fn: observer, err: errorObserver || (() => { /* do nothing */ }) }; this.observers.add(obs); return () => { this.observers.delete(obs); }; } destroy() { this.queryCancelers.forEach(fn => fn()); this.queryCancelers.length = 0; this.docs.length = 0; this.observers.clear(); } private err(e: Error | object, pos: number) { this.observers.forEach(obs => { if (obs.err) obs.err(e, pos); }); } private updateDocs(docs: DocumentData[], position: number) { this.docs[position] = docs.map(dd => ({ $id: dd.id, ...dd.data() })); const sortedData = [...new Set(this.docs.flat())].sort( this.compareFunction ); this.observers.forEach(obs => obs.fn(sortedData)); } }