Skip to content

Commit

Permalink
fix(GeoJoinerOnSnapshot): emit snapshot on empty queries and update d…
Browse files Browse the repository at this point in the history
…ocs array when doc removed, fixes #73
  • Loading branch information
MichaelSolati committed Jan 15, 2019
1 parent 2f9b4db commit 85f2e26
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 35 deletions.
80 changes: 46 additions & 34 deletions src/GeoJoinerOnSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { validateQueryCriteria, calculateDistance } from './utils';
interface DocMap { change: GeoFirestoreTypes.web.DocumentChange; distance: number; emitted: boolean; }

/**
* A `GeoJoinerOnSnapshot` subscribes and aggregates multiple `onSnapshot` listeners
* A `GeoJoinerOnSnapshot` subscribes and aggregates multiple `onSnapshot` listeners
* while filtering out documents not in query radius.
*/
export class GeoJoinerOnSnapshot {
Expand All @@ -29,11 +29,9 @@ export class GeoJoinerOnSnapshot {
private _onNext: (snapshot: GeoQuerySnapshot) => void, private _onError?: (error: Error) => void
) {
validateQueryCriteria(_near);

this._queriesResolved = (new Array(_queries.length)).fill(0);

this._queriesResolved = new Array(_queries.length).fill(0);
_queries.forEach((value: GeoFirestoreTypes.web.Query, index: number) => {
const subscription = value.onSnapshot((snapshot) => this._processSnapshot(snapshot, index), (error) => this._error = error);
const subscription = value.onSnapshot(snapshot => this._processSnapshot(snapshot, index), error => (this._error = error));
this._subscriptions.push(subscription);
});

Expand All @@ -42,7 +40,7 @@ export class GeoJoinerOnSnapshot {

/**
* A functions that clears the interval and ends all query subscriptions.
*
*
* @return An unsubscribe function that can be called to cancel all snapshot listener.
*/
public unsubscribe(): () => void {
Expand Down Expand Up @@ -72,11 +70,19 @@ export class GeoJoinerOnSnapshot {
return result;
});

const docs = docChanges.map((change) => change.doc);
const docs = docChanges.reduce((filtered, change) => {
if (change.newIndex >= 0) {
filtered.push(change.doc);
} else {
this._docs.delete(change.doc.id);
}
return filtered;
}, []);

this._firstEmitted = true;
this._onNext(new GeoQuerySnapshot({
docs,
docChanges: () => docChanges
docs,
docChanges: () => docChanges
} as GeoFirestoreTypes.web.QuerySnapshot, this._near.center));
}

Expand All @@ -97,35 +103,41 @@ export class GeoJoinerOnSnapshot {

/**
* Parses `snapshot` and filters out documents not in query radius. Sets new values to `_docs` map.
*
*
* @param snapshot The `QuerySnapshot` of the query.
* @param index Index of query who's snapshot has been triggered.
*/
private _processSnapshot(snapshot: GeoFirestoreTypes.web.QuerySnapshot, index: number): void {
if (!this._firstRoundResolved) this._queriesResolved[index] = 1;
snapshot.docChanges().forEach((change) => {
const distance = change.doc.data().l ? calculateDistance(this._near.center, change.doc.data().l) : null;
const id = change.doc.id;
const fromMap = this._docs.get(id);
const doc: any = {
change: {
doc: change.doc,
oldIndex: fromMap && this._firstEmitted ? fromMap.change.oldIndex : -1,
newIndex: fromMap && this._firstEmitted ? fromMap.change.newIndex : -1,
type: fromMap && this._firstEmitted ? change.type : 'added'
}, distance, emitted: this._firstEmitted ? !!fromMap : false
};
// Ensure doc in query radius
if (this._near.radius >= distance) {
if (!fromMap && doc.change.type === 'removed') return; // Removed doc and wasn't in map
if (!fromMap && doc.change.type === 'modified') doc.change.type = 'added'; // Modified doc and wasn't in map
this._newValues = true;
this._docs.set(id, doc);
} else if (fromMap) {
doc.change.type = 'removed'; // Not in query anymore, mark for removal
this._newValues = true;
this._docs.set(id, doc);
}
});
if (snapshot.docChanges().length) {
snapshot.docChanges().forEach(change => {
const distance = change.doc.data().l ? calculateDistance(this._near.center, change.doc.data().l) : null;
const id = change.doc.id;
const fromMap = this._docs.get(id);
const doc: any = {
change: {
doc: change.doc,
oldIndex: (fromMap && this._firstEmitted) ? fromMap.change.oldIndex : -1,
newIndex: (fromMap && this._firstEmitted) ? fromMap.change.newIndex : -1,
type: (fromMap && this._firstEmitted) ? change.type : 'added'
}, distance, emitted: this._firstEmitted ? !!fromMap : false
};
// Ensure doc in query radius
if (this._near.radius >= distance) {
if (!fromMap && doc.change.type === 'removed') return; // Removed doc and wasn't in map
if (!fromMap && doc.change.type === 'modified') doc.change.type = 'added'; // Modified doc and wasn't in map
this._newValues = true;
this._docs.set(id, doc);
} else if (fromMap) {
doc.change.type = 'removed'; // Not in query anymore, mark for removal
this._newValues = true;
this._docs.set(id, doc);
} else if (!fromMap && !this._firstRoundResolved) {
this._newValues = true;
}
});
} else if (!this._firstRoundResolved) {
this._newValues = true;
}
}
}
61 changes: 60 additions & 1 deletion test/GeoQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { GeoFirestore } from '../src/GeoFirestore';
import { GeoQuery } from '../src/GeoQuery';
import {
afterEachHelper, beforeEachHelper, collection, dummyData,
firestore, invalidFirestores, stubDatabase, invalidLocations
firestore, invalidFirestores, stubDatabase, invalidLocations, geocollection
} from './common';

const expect = chai.expect;
Expand Down Expand Up @@ -84,6 +84,65 @@ describe('GeoQuery Tests:', () => {
});
});
});

it('onSnapshot returns no data, with geo related filters on an empty area', (done) => {
const center = new firebase.firestore.GeoPoint(-50, -50);
const query = new GeoQuery(collection);
stubDatabase().then(() => {
const subscription = query.near({ center, radius: 1 }).onSnapshot((snapshot) => {
subscription();
expect(snapshot.empty).to.equal(true);
done();
});
});
});

it('onSnapshot updates when a new document, that matches the query, is added to collection', (done) => {
const center = new firebase.firestore.GeoPoint(-50, -50);
const doc = geocollection.doc();
let runOnce = false;
const query = new GeoQuery(collection);
stubDatabase().then(() => {
const subscription = query.near({ center, radius: 1 }).onSnapshot((snapshot) => {
if (!runOnce) {
runOnce = true;
setTimeout(() => {
doc.set({ coordinates: center });
}, 100);
} else {
subscription();
const result = snapshot.docs.map(d => d.data());
expect(result).to.have.deep.members([{ coordinates: center }]);
done();
}
});
});
});

it('onSnapshot updates when a document, that belongs in the query, is removed from collection', (done) => {
const doc = dummyData[0];
let runOnce = false;
const query = new GeoQuery(collection);
stubDatabase().then(() => {
const subscription = query.near({ center: doc.coordinates, radius: 0.1 }).onSnapshot((snapshot) => {
if (!runOnce) {
runOnce = true;
expect(snapshot.empty).to.equal(false);
expect(snapshot.docChanges().length).to.equal(1);
expect(snapshot.docChanges()[0].type).to.equal('added');
setTimeout(() => {
geocollection.doc(doc.key).delete();
}, 100);
} else {
subscription();
expect(snapshot.empty).to.equal(true);
expect(snapshot.docChanges().length).to.equal(1);
expect(snapshot.docChanges()[0].type).to.equal('removed');
done();
}
});
});
});
});

describe('get():', () => {
Expand Down

0 comments on commit 85f2e26

Please sign in to comment.