From 51e76bdaeef3bad607f134498d8a92301efc436e Mon Sep 17 00:00:00 2001 From: Michael Solati Date: Fri, 9 Mar 2018 01:43:53 -0500 Subject: [PATCH] feat(firestore): early implementation of geofirestore --- package.json | 6 +- src/{firebase/geoFire.ts => geoFire/index.ts} | 28 +- .../geoQuery.ts => geoFire/query.ts} | 16 +- src/geoFirestore/index.ts | 159 ++++++ src/geoFirestore/query.ts | 532 ++++++++++++++++++ src/index.ts | 16 +- src/interfaces/geoFirestoreQueryState.ts | 5 + src/interfaces/index.ts | 1 + .../callbackRegistration.ts} | 0 .../geoFireUtils.ts => tools/utils.ts} | 18 +- tests/common.ts | 6 +- .../index.test.ts} | 10 +- .../query.test.ts} | 54 +- .../callbackRegistration.test.ts} | 4 +- .../utils.test.ts} | 6 +- 15 files changed, 787 insertions(+), 74 deletions(-) rename src/{firebase/geoFire.ts => geoFire/index.ts} (84%) rename src/{firebase/geoQuery.ts => geoFire/query.ts} (97%) create mode 100644 src/geoFirestore/index.ts create mode 100644 src/geoFirestore/query.ts create mode 100644 src/interfaces/geoFirestoreQueryState.ts rename src/{firebase/geoCallbackRegistration.ts => tools/callbackRegistration.ts} (100%) rename src/{firebase/geoFireUtils.ts => tools/utils.ts} (96%) rename tests/{geoFire.test.ts => geoFire/index.test.ts} (99%) rename tests/{geoQuery.test.ts => geoFire/query.test.ts} (95%) rename tests/{geoCallbackRegistration.test.ts => tools/callbackRegistration.test.ts} (98%) rename tests/{geoFireUtils.test.ts => tools/utils.test.ts} (99%) diff --git a/package.json b/package.json index aa88592..b5b59de 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,10 @@ "uglify-js": "^3.3.13" }, "scripts": { - "browserify": "browserify dist/index.js -o dist/geofire.js", - "build": "tsc && npm run browserify && npm run uglify", + "browserify": "browserify dist/geoFire/index.js -o dist/browser/geofire.js && browserify dist/geoFirestore/index.js -o dist/browser/geofirestore.js", + "build": "rm -rf ./dist && tsc && npm run browserify && npm run uglify", "test": "mocha --reporter spec -r ts-node/register -r jsdom-global/register --timeout 5000 --exit 'tests/**/*.test.ts'", "travis": "npm run test && npm run build", - "uglify": "uglifyjs dist/geofire.js -c -m -o dist/geofire.min.js" + "uglify": "uglifyjs dist/browser/geofire.js -c -m -o dist/browser/geofire.min.js && uglifyjs dist/browser/geofirestore.js -c -m -o dist/browser/geofirestore.min.js" } } diff --git a/src/firebase/geoFire.ts b/src/geoFire/index.ts similarity index 84% rename from src/firebase/geoFire.ts rename to src/geoFire/index.ts index 4abb27b..f1794e7 100644 --- a/src/firebase/geoFire.ts +++ b/src/geoFire/index.ts @@ -1,7 +1,19 @@ +/*! + * GeoFire is an open-source library that allows you to store and query a set + * of keys based on their geographic location. At its heart, GeoFire simply + * stores locations with string keys. Its main benefit, however, is the + * possibility of retrieving only those keys within a given geographic area - + * all in realtime. + * + * GeoFire 0.0.0 + * https://github.com/firebase/geofire-js/ + * License: MIT + */ + import * as firebase from 'firebase'; -import { GeoQuery } from './geoQuery'; -import { decodeGeoFireObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from './geoFireUtils'; +import { GeoFireQuery } from './query'; +import { decodeGeoFireObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from '../tools/utils'; import { QueryCriteria } from '../interfaces'; @@ -89,7 +101,7 @@ export class GeoFire { const newData = {}; - Object.keys(locations).forEach(function (key) { + Object.keys(locations).forEach((key) => { validateKey(key); const location: number[] = locations[key]; @@ -108,13 +120,13 @@ export class GeoFire { }; /** - * Returns a new GeoQuery instance with the provided queryCriteria. + * Returns a new GeoFireQuery instance with the provided queryCriteria. * - * @param queryCriteria The criteria which specifies the GeoQuery's center and radius. - * @return A new GeoQuery object. + * @param queryCriteria The criteria which specifies the GeoFireQuery's center and radius. + * @return A new GeoFireQuery object. */ - public query(queryCriteria: QueryCriteria): GeoQuery { - return new GeoQuery(this._firebaseRef, queryCriteria); + public query(queryCriteria: QueryCriteria): GeoFireQuery { + return new GeoFireQuery(this._firebaseRef, queryCriteria); }; /********************/ diff --git a/src/firebase/geoQuery.ts b/src/geoFire/query.ts similarity index 97% rename from src/firebase/geoQuery.ts rename to src/geoFire/query.ts index cf0526f..1696580 100644 --- a/src/firebase/geoQuery.ts +++ b/src/geoFire/query.ts @@ -1,15 +1,15 @@ import * as firebase from 'firebase'; -import { GeoFire } from './geoFire'; -import { GeoCallbackRegistration } from './geoCallbackRegistration'; -import { decodeGeoFireObject, encodeGeohash, getKey, geohashQueries, validateCriteria, validateLocation } from './geoFireUtils'; +import { GeoFire } from './'; +import { GeoCallbackRegistration } from '../tools/callbackRegistration'; +import { decodeGeoFireObject, encodeGeohash, geoFireGetKey, geohashQueries, validateCriteria, validateLocation } from '../tools/utils'; import { QueryCriteria } from '../interfaces'; /** - * Creates a GeoQuery instance. + * Creates a GeoFireQuery instance. */ -export class GeoQuery { +export class GeoFireQuery { // Event callbacks private _callbacks: any = { ready: [], key_entered: [], key_exited: [], key_moved: [] }; // Variable to track when the query is cancelled @@ -235,7 +235,7 @@ export class GeoQuery { * @param locationDataSnapshot A snapshot of the data stored for this location. */ private _childAddedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void { - this._updateLocation(getKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); + this._updateLocation(geoFireGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); } /** @@ -244,7 +244,7 @@ export class GeoQuery { * @param locationDataSnapshot A snapshot of the data stored for this location. */ private _childChangedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void { - this._updateLocation(getKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); + this._updateLocation(geoFireGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); } /** @@ -253,7 +253,7 @@ export class GeoQuery { * @param locationDataSnapshot A snapshot of the data stored for this location. */ private _childRemovedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void { - const key: string = getKey(locationDataSnapshot); + const key: string = geoFireGetKey(locationDataSnapshot); if (this._locationsTracked.hasOwnProperty(key)) { this._firebaseRef.child(key).once('value', (snapshot: firebase.database.DataSnapshot) => { const location: number[] = (snapshot.val() === null) ? null : decodeGeoFireObject(snapshot.val()); diff --git a/src/geoFirestore/index.ts b/src/geoFirestore/index.ts new file mode 100644 index 0000000..1301f9e --- /dev/null +++ b/src/geoFirestore/index.ts @@ -0,0 +1,159 @@ +/*! + * GeoFire is an open-source library that allows you to store and query a set + * of keys based on their geographic location. At its heart, GeoFire simply + * stores locations with string keys. Its main benefit, however, is the + * possibility of retrieving only those keys within a given geographic area - + * all in realtime. + * + * GeoFire 0.0.0 + * https://github.com/firebase/geofire-js/ + * License: MIT + */ + +import * as firebase from 'firebase'; + +import { GeoFirestoreQuery } from './query'; +import { decodeGeoFireObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from '../tools/utils'; + +import { QueryCriteria, GeoFireObj } from '../interfaces'; + +/** + * Creates a GeoFirestore instance. + */ +export class GeoFirestore { + /** + * @param _collectionRef A Firestore Collection reference where the GeoFirestore data will be stored. + */ + constructor(private _collectionRef: firebase.firestore.CollectionReference) { + if (Object.prototype.toString.call(this._collectionRef) !== '[object Object]') { + throw new Error('collectionRef must be an instance of a Firestore Collection'); + } + } + + /********************/ + /* PUBLIC METHODS */ + /********************/ + /** + * Returns a promise fulfilled with the location corresponding to the provided key. + * + * If the provided key does not exist, the returned promise is fulfilled with null. + * + * @param key The key of the location to retrieve. + * @returns A promise that is fulfilled with the location of the given key. + */ + public get(key: string): Promise { + validateKey(key); + return this._collectionRef.doc(key).get().then((documentSnapshot: firebase.firestore.DocumentSnapshot) => { + const snapshotVal = documentSnapshot.data(); + if (snapshotVal === null) { + return null; + } else { + return decodeGeoFireObject(snapshotVal); + } + }); + }; + + /** + * Returns the Firestore Collection used to create this GeoFirestore instance. + * + * @returns The Firestore Collection used to create this GeoFirestore instance. + */ + public ref(): firebase.firestore.CollectionReference { + return this._collectionRef; + }; + + /** + * Removes the provided key from this GeoFirestore. Returns an empty promise fulfilled when the key has been removed. + * + * If the provided key is not in this GeoFirestore, the promise will still successfully resolve. + * + * @param key The key of the location to remove. + * @returns A promise that is fulfilled after the inputted key is removed. + */ + public remove(key: string): Promise { + return this.set(key, null); + }; + + /** + * Adds the provided key - location pair(s) to Firestore. Returns an empty promise which is fulfilled when the write is complete. + * + * If any provided key already exists in this GeoFirestore, it will be overwritten with the new location value. + * + * @param keyOrLocations The key representing the location to add or a mapping of key - location pairs which + * represent the locations to add. + * @param location The [latitude, longitude] pair to add. + * @returns A promise that is fulfilled when the write is complete. + */ + public set(keyOrLocations: string | any, location?: number[]): Promise { + const batch: firebase.firestore.WriteBatch = this._collectionRef.firestore.batch(); + let locations; + if (typeof keyOrLocations === 'string' && keyOrLocations.length !== 0) { + // If this is a set for a single location, convert it into a object + locations = {}; + locations[keyOrLocations] = location; + } else if (typeof keyOrLocations === 'object') { + if (typeof location !== 'undefined') { + throw new Error('The location argument should not be used if you pass an object to set().'); + } + locations = keyOrLocations; + } else { + throw new Error('keyOrLocations must be a string or a mapping of key - location pairs.'); + } + + Object.keys(locations).forEach((key) => { + validateKey(key); + + const ref = this._collectionRef.doc(key); + const location: number[] = locations[key]; + if (location === null) { + batch.delete(ref); + } else { + validateLocation(location); + + const geohash: string = encodeGeohash(location); + batch.set(ref, encodeGeoFireObject(location, geohash), { merge: true }); + } + }); + + return batch.commit(); + }; + + /** + * Returns a new GeoQuery instance with the provided queryCriteria. + * + * @param queryCriteria The criteria which specifies the GeoQuery's center and radius. + * @return A new GeoFirestoreQuery object. + */ + public query(queryCriteria: QueryCriteria): GeoFirestoreQuery { + return new GeoFirestoreQuery(this._collectionRef, queryCriteria); + }; + + /********************/ + /* STATIC METHODS */ + /********************/ + /** + * Static method which calculates the distance, in kilometers, between two locations, + * via the Haversine formula. Note that this is approximate due to the fact that the + * Earth's radius varies between 6356.752 km and 6378.137 km. + * + * @param location1 The [latitude, longitude] pair of the first location. + * @param location2 The [latitude, longitude] pair of the second location. + * @returns The distance, in kilometers, between the inputted locations. + */ + static distance(location1: number[], location2: number[]) { + validateLocation(location1); + validateLocation(location2); + + var radius = 6371; // Earth's radius in kilometers + var latDelta = degreesToRadians(location2[0] - location1[0]); + var lonDelta = degreesToRadians(location2[1] - location1[1]); + + var a = (Math.sin(latDelta / 2) * Math.sin(latDelta / 2)) + + (Math.cos(degreesToRadians(location1[0])) * Math.cos(degreesToRadians(location2[0])) * + Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2)); + + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return radius * c; + }; +} diff --git a/src/geoFirestore/query.ts b/src/geoFirestore/query.ts new file mode 100644 index 0000000..5900a84 --- /dev/null +++ b/src/geoFirestore/query.ts @@ -0,0 +1,532 @@ +import * as firebase from 'firebase'; + +import { GeoFirestore } from './'; +import { GeoCallbackRegistration } from '../tools/callbackRegistration'; +import { decodeGeoFireObject, encodeGeohash, geoFirestoreGetKey, geohashQueries, validateCriteria, validateLocation } from '../tools/utils'; + +import { QueryCriteria, GeoFireObj, GeoFirestoreQueryState } from '../interfaces'; + +/** + * Creates a GeoFirestoreQuery instance. + */ +export class GeoFirestoreQuery { + // Event callbacks + private _callbacks: any = { ready: [], key_entered: [], key_exited: [], key_moved: [] }; + // Variable to track when the query is cancelled + private _cancelled: boolean = false; + private _center: number[]; + // A dictionary of geohash queries which currently have an active callbacks + private _currentGeohashesQueried: any = {}; + // A dictionary of locations that a currently active in the queries + // Note that not all of these are currently within this query + private _locationsTracked: any = {}; + private _radius: number; + + // Variables used to keep track of when to fire the 'ready' event + private _valueEventFired: boolean = false; + private _outstandingGeohashReadyEvents: any; + // Every ten seconds, clean up the geohashes we are currently querying for. We keep these around + // for a little while since it's likely that they will need to be re-queried shortly after they + // move outside of the query's bounding box. + private _geohashCleanupScheduled: boolean = false; + private _cleanUpCurrentGeohashesQueriedInterval: NodeJS.Timer; + private _cleanUpCurrentGeohashesQueriedTimeout = null; + + /** + * @param _collectionRef A Firestore Collection reference where the GeoFirestore data will be stored. + * @param _queryCriteria The criteria which specifies the query's center and radius. + */ + constructor(private _collectionRef: firebase.firestore.CollectionReference, private _queryCriteria: QueryCriteria) { + // Firebase reference of the GeoFirestore which created this query + if (Object.prototype.toString.call(this._collectionRef) !== '[object Object]') { + throw new Error('firebaseRef must be an instance of Firestore'); + } + + this._cleanUpCurrentGeohashesQueriedInterval = setInterval(() => { + if (this._geohashCleanupScheduled === false) { + this._cleanUpCurrentGeohashesQueried(); + } + }, 10000); + + // Validate and save the query criteria + validateCriteria(_queryCriteria, true); + this._center = _queryCriteria.center; + this._radius = _queryCriteria.radius; + + // Listen for new geohashes being added around this query and fire the appropriate events + this._listenForNewGeohashes(); + } + + /********************/ + /* PUBLIC METHODS */ + /********************/ + /** + * Terminates this query so that it no longer sends location updates. All callbacks attached to this + * query via on() will be cancelled. This query can no longer be used in the future. + */ + public cancel(): void { + // Mark this query as cancelled + this._cancelled = true; + + // Cancel all callbacks in this query's callback list + this._callbacks = { ready: [], key_entered: [], key_exited: [], key_moved: [] }; + + // Turn off all Firebase listeners for the current geohashes being queried + const keys: string[] = Object.keys(this._currentGeohashesQueried); + keys.forEach((geohashQueryStr: string) => { + const query: string[] = this._stringToQuery(geohashQueryStr); + this._cancelGeohashQuery(query, this._currentGeohashesQueried[geohashQueryStr]); + delete this._currentGeohashesQueried[geohashQueryStr]; + }); + + // Delete any stored locations + this._locationsTracked = {}; + + // Turn off the current geohashes queried clean up interval + clearInterval(this._cleanUpCurrentGeohashesQueriedInterval); + }; + + /** + * Returns the location signifying the center of this query. + * + * @returns The [latitude, longitude] pair signifying the center of this query. + */ + public center(): number[] { + return this._center; + }; + + /** + * Attaches a callback to this query which will be run when the provided eventType fires. Valid eventType + * values are 'ready', 'key_entered', 'key_exited', and 'key_moved'. The ready event callback is passed no + * parameters. All other callbacks will be passed three parameters: (1) the location's key, (2) the location's + * [latitude, longitude] pair, and (3) the distance, in kilometers, from the location to this query's center + * + * 'ready' is used to signify that this query has loaded its initial state and is up-to-date with its corresponding + * GeoFirestore instance. 'ready' fires when this query has loaded all of the initial data from GeoFirestore and fired all + * other events for that data. It also fires every time updateCriteria() is called, after all other events have + * fired for the updated query. + * + * 'key_entered' fires when a key enters this query. This can happen when a key moves from a location outside of + * this query to one inside of it or when a key is written to GeoFirestore for the first time and it falls within + * this query. + * + * 'key_exited' fires when a key moves from a location inside of this query to one outside of it. If the key was + * entirely removed from GeoFire, both the location and distance passed to the callback will be null. + * + * 'key_moved' fires when a key which is already in this query moves to another location inside of it. + * + * Returns a GeoCallbackRegistration which can be used to cancel the callback. You can add as many callbacks + * as you would like for the same eventType by repeatedly calling on(). Each one will get called when its + * corresponding eventType fires. Each callback must be cancelled individually. + * + * @param eventType The event type for which to attach the callback. One of 'ready', 'key_entered', + * 'key_exited', or 'key_moved'. + * @param callback Callback function to be called when an event of type eventType fires. + * @returns A callback registration which can be used to cancel the provided callback. + */ + public on(eventType: string, callback: Function): GeoCallbackRegistration { + // Validate the inputs + if (['ready', 'key_entered', 'key_exited', 'key_moved'].indexOf(eventType) === -1) { + throw new Error('event type must be \'ready\', \'key_entered\', \'key_exited\', or \'key_moved\''); + } + if (typeof callback !== 'function') { + throw new Error('callback must be a function'); + } + + // Add the callback to this query's callbacks list + this._callbacks[eventType].push(callback); + + // If this is a 'key_entered' callback, fire it for every location already within this query + if (eventType === 'key_entered') { + const keys: string[] = Object.keys(this._locationsTracked); + keys.forEach((key: string) => { + const locationDict = this._locationsTracked[key]; + if (typeof locationDict !== 'undefined' && locationDict.isInQuery) { + callback(key, locationDict.location, locationDict.distanceFromCenter); + } + }); + } + + // If this is a 'ready' callback, fire it if this query is already ready + if (eventType === 'ready' && this._valueEventFired) { + callback(); + } + + // Return an event registration which can be used to cancel the callback + return new GeoCallbackRegistration(() => { + this._callbacks[eventType].splice(this._callbacks[eventType].indexOf(callback), 1); + }); + }; + + /** + * Returns the radius of this query, in kilometers. + * + * @returns The radius of this query, in kilometers. + */ + public radius(): number { + return this._radius; + }; + + /** + * Updates the criteria for this query. + * + * @param newQueryCriteria The criteria which specifies the query's center and radius. + */ + public updateCriteria(newQueryCriteria: QueryCriteria): void { + // Validate and save the new query criteria + validateCriteria(newQueryCriteria); + this._center = newQueryCriteria.center || this._center; + this._radius = newQueryCriteria.radius || this._radius; + + // Loop through all of the locations in the query, update their distance from the center of the + // query, and fire any appropriate events + const keys: string[] = Object.keys(this._locationsTracked); + for (const key of keys) { + // If the query was cancelled while going through this loop, stop updating locations and stop + // firing events + if (this._cancelled === true) { + break; + } + // Get the cached information for this location + const locationDict = this._locationsTracked[key]; + // Save if the location was already in the query + const wasAlreadyInQuery = locationDict.isInQuery; + // Update the location's distance to the new query center + locationDict.distanceFromCenter = GeoFirestore.distance(locationDict.location, this._center); + // Determine if the location is now in this query + locationDict.isInQuery = (locationDict.distanceFromCenter <= this._radius); + // If the location just left the query, fire the 'key_exited' callbacks + // Else if the location just entered the query, fire the 'key_entered' callbacks + if (wasAlreadyInQuery && !locationDict.isInQuery) { + this._fireCallbacksForKey('key_exited', key, locationDict.location, locationDict.distanceFromCenter); + } else if (!wasAlreadyInQuery && locationDict.isInQuery) { + this._fireCallbacksForKey('key_entered', key, locationDict.location, locationDict.distanceFromCenter); + } + } + + // Reset the variables which control when the 'ready' event fires + this._valueEventFired = false; + + // Listen for new geohashes being added to GeoFirestore and fire the appropriate events + this._listenForNewGeohashes(); + }; + + + /*********************/ + /* PRIVATE METHODS */ + /*********************/ + /** + * Turns off all callbacks for the provide geohash query. + * + * @param query The geohash query. + * @param queryState An object storing the current state of the query. + */ + private _cancelGeohashQuery(query: string[], queryState: GeoFirestoreQueryState): void { + queryState.childCallback(); + queryState.valueCallback(); + } + + /** + * Callback for child added events. + * + * @param locationDataSnapshot A snapshot of the data stored for this location. + */ + private _childAddedCallback(locationDataSnapshot: firebase.firestore.DocumentSnapshot): void { + this._updateLocation(geoFirestoreGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.data())); + } + + /** + * Callback for child changed events + * + * @param locationDataSnapshot A snapshot of the data stored for this location. + */ + private _childChangedCallback(locationDataSnapshot: firebase.firestore.DocumentSnapshot): void { + this._updateLocation(geoFirestoreGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.data())); + } + + /** + * Callback for child removed events + * + * @param locationDataSnapshot A snapshot of the data stored for this location. + */ + private _childRemovedCallback(locationDataSnapshot: firebase.firestore.DocumentSnapshot): void { + const key: string = geoFirestoreGetKey(locationDataSnapshot); + if (this._locationsTracked.hasOwnProperty(key)) { + this._collectionRef.doc(key).get().then((snapshot: firebase.firestore.DocumentSnapshot) => { + const location: number[] = (snapshot.data() === null) ? null : decodeGeoFireObject(snapshot.data()); + const geohash: string = (location !== null) ? encodeGeohash(location) : null; + // Only notify observers if key is not part of any other geohash query or this actually might not be + // a key exited event, but a key moved or entered event. These events will be triggered by updates + // to a different query + if (!this._geohashInSomeQuery(geohash)) { + this._removeLocation(key, location); + } + }); + } + } + + /** + * Removes unnecessary Firebase queries which are currently being queried. + */ + private _cleanUpCurrentGeohashesQueried(): void { + let keys: string[] = Object.keys(this._currentGeohashesQueried); + keys.forEach((geohashQueryStr: string) => { + const queryState: any = this._currentGeohashesQueried[geohashQueryStr]; + if (queryState.active === false) { + var query = this._stringToQuery(geohashQueryStr); + // Delete the geohash since it should no longer be queried + this._cancelGeohashQuery(query, queryState); + delete this._currentGeohashesQueried[geohashQueryStr]; + } + }); + + // Delete each location which should no longer be queried + keys = Object.keys(this._locationsTracked); + keys.forEach((key: string) => { + if (!this._geohashInSomeQuery(this._locationsTracked[key].geohash)) { + if (this._locationsTracked[key].isInQuery) { + throw new Error('Internal State error, trying to remove location that is still in query'); + } + delete this._locationsTracked[key]; + } + }); + + // Specify that this is done cleaning up the current geohashes queried + this._geohashCleanupScheduled = false; + + // Cancel any outstanding scheduled cleanup + if (this._cleanUpCurrentGeohashesQueriedTimeout !== null) { + clearTimeout(this._cleanUpCurrentGeohashesQueriedTimeout); + this._cleanUpCurrentGeohashesQueriedTimeout = null; + } + } + + /** + * Fires each callback for the provided eventType, passing it provided key's data. + * + * @param eventType The event type whose callbacks to fire. One of 'key_entered', 'key_exited', or 'key_moved'. + * @param key The key of the location for which to fire the callbacks. + * @param location The location as [latitude, longitude] pair + * @param distanceFromCenter The distance from the center or null. + */ + private _fireCallbacksForKey(eventType: string, key: string, location?: number[], distanceFromCenter?: number): void { + this._callbacks[eventType].forEach((callback) => { + if (typeof location === 'undefined' || location === null) { + callback(key, null, null); + } else { + callback(key, location, distanceFromCenter); + } + }); + } + + /** + * Fires each callback for the 'ready' event. + */ + private _fireReadyEventCallbacks(): void { + this._callbacks.ready.forEach((callback) => { + callback(); + }); + } + + /** + * Checks if this geohash is currently part of any of the geohash queries. + * + * @param geohash The geohash. + * @returns Returns true if the geohash is part of any of the current geohash queries. + */ + private _geohashInSomeQuery(geohash: string): boolean { + const keys: string[] = Object.keys(this._currentGeohashesQueried); + for (const queryStr of keys) { + if (this._currentGeohashesQueried.hasOwnProperty(queryStr)) { + var query = this._stringToQuery(queryStr); + if (geohash >= query[0] && geohash <= query[1]) { + return true; + } + } + } + + return false; + } + + /** + * Called once all geohash queries have received all child added events and fires the ready + * event if necessary. + */ + private _geohashQueryReadyCallback(queryStr?: string): void { + const index: number = this._outstandingGeohashReadyEvents.indexOf(queryStr); + if (index > -1) { + this._outstandingGeohashReadyEvents.splice(index, 1); + } + this._valueEventFired = (this._outstandingGeohashReadyEvents.length === 0); + + // If all queries have been processed, fire the ready event + if (this._valueEventFired) { + this._fireReadyEventCallbacks(); + } + } + + /** + * Attaches listeners to Firebase which track when new geohashes are added within this query's + * bounding box. + */ + private _listenForNewGeohashes(): void { + // Get the list of geohashes to query + let geohashesToQuery: string[] = geohashQueries(this._center, this._radius * 1000).map(this._queryToString); + + // Filter out duplicate geohashes + geohashesToQuery = geohashesToQuery.filter((geohash: string, i: number) => geohashesToQuery.indexOf(geohash) === i); + + // For all of the geohashes that we are already currently querying, check if they are still + // supposed to be queried. If so, don't re-query them. Otherwise, mark them to be un-queried + // next time we clean up the current geohashes queried dictionary. + const keys: string[] = Object.keys(this._currentGeohashesQueried); + keys.forEach((geohashQueryStr: string) => { + const index: number = geohashesToQuery.indexOf(geohashQueryStr); + if (index === -1) { + this._currentGeohashesQueried[geohashQueryStr].active = false; + } else { + this._currentGeohashesQueried[geohashQueryStr].active = true; + geohashesToQuery.splice(index, 1); + } + }); + + // If we are not already cleaning up the current geohashes queried and we have more than 25 of them, + // kick off a timeout to clean them up so we don't create an infinite number of unneeded queries. + if (this._geohashCleanupScheduled === false && Object.keys(this._currentGeohashesQueried).length > 25) { + this._geohashCleanupScheduled = true; + this._cleanUpCurrentGeohashesQueriedTimeout = setTimeout(this._cleanUpCurrentGeohashesQueried, 10); + } + + // Keep track of which geohashes have been processed so we know when to fire the 'ready' event + this._outstandingGeohashReadyEvents = geohashesToQuery.slice(); + + // Loop through each geohash to query for and listen for new geohashes which have the same prefix. + // For every match, attach a value callback which will fire the appropriate events. + // Once every geohash to query is processed, fire the 'ready' event. + geohashesToQuery.forEach((toQueryStr: string) => { + // decode the geohash query string + const query: string[] = this._stringToQuery(toQueryStr); + + // Create the Firebase query + const firestoreQuery: firebase.firestore.Query = this._collectionRef.orderBy('g').startAt(query[0]).endAt(query[1]); + + // For every new matching geohash, determine if we should fire the 'key_entered' event + const childCallback = firestoreQuery.onSnapshot((snapshot: firebase.firestore.QuerySnapshot) => { + snapshot.docChanges.forEach((change: firebase.firestore.DocumentChange) => { + if (change.type === 'added') { + this._childAddedCallback(change.doc); + } + if (change.type === 'modified') { + this._childChangedCallback(change.doc); + } + if (change.type === 'removed') { + this._childRemovedCallback(change.doc); + } + }); + }); + + // Once the current geohash to query is processed, see if it is the last one to be processed + // and, if so, mark the value event as fired. + // Note that Firebase fires the 'value' event after every 'added' event fires. + const valueCallback = firestoreQuery.onSnapshot(() => { + valueCallback(); + this._geohashQueryReadyCallback(toQueryStr); + }); + + // Add the geohash query to the current geohashes queried dictionary and save its state + this._currentGeohashesQueried[toQueryStr] = { + active: true, + childCallback: childCallback, + valueCallback: valueCallback + }; + }); + // Based upon the algorithm to calculate geohashes, it's possible that no 'new' + // geohashes were queried even if the client updates the radius of the query. + // This results in no 'READY' event being fired after the .updateCriteria() call. + // Check to see if this is the case, and trigger the 'READY' event. + if (geohashesToQuery.length === 0) { + this._geohashQueryReadyCallback(); + } + } + + /** + * Encodes a query as a string for easier indexing and equality. + * + * @param query The query to encode. + * @returns The encoded query as string. + */ + private _queryToString(query: string[]): string { + if (query.length !== 2) { + throw new Error('Not a valid geohash query: ' + query); + } + return query[0] + ':' + query[1]; + } + + /** + * Removes the location from the local state and fires any events if necessary. + * + * @param key The key to be removed. + * @param currentLocation The current location as [latitude, longitude] pair or null if removed. + */ + private _removeLocation(key: string, currentLocation?: number[]): void { + const locationDict = this._locationsTracked[key]; + delete this._locationsTracked[key]; + if (typeof locationDict !== 'undefined' && locationDict.isInQuery) { + const distanceFromCenter: number = (currentLocation) ? GeoFirestore.distance(currentLocation, this._center) : null; + this._fireCallbacksForKey('key_exited', key, currentLocation, distanceFromCenter); + } + } + + /** + * Decodes a query string to a query + * + * @param str The encoded query. + * @returns The decoded query as a [start, end] pair. + */ + private _stringToQuery(str: string): string[] { + const decoded: string[] = str.split(':'); + if (decoded.length !== 2) { + throw new Error('Invalid internal state! Not a valid geohash query: ' + str); + } + return decoded; + } + + /** + * Callback for any updates to locations. Will update the information about a key and fire any necessary + * events every time the key's location changes. + * + * When a key is removed from GeoFirestore or the query, this function will be called with null and performs + * any necessary cleanup. + * + * @param key The key of the GeoFirestore location. + * @param location The location as [latitude, longitude] pair. + */ + private _updateLocation(key: string, location?: number[]): void { + validateLocation(location); + // Get the key and location + let distanceFromCenter: number, isInQuery; + var wasInQuery: boolean = (this._locationsTracked.hasOwnProperty(key)) ? this._locationsTracked[key].isInQuery : false; + var oldLocation: number[] = (this._locationsTracked.hasOwnProperty(key)) ? this._locationsTracked[key].location : null; + + // Determine if the location is within this query + distanceFromCenter = GeoFirestore.distance(location, this._center); + isInQuery = (distanceFromCenter <= this._radius); + + // Add this location to the locations queried dictionary even if it is not within this query + this._locationsTracked[key] = { + location: location, + distanceFromCenter: distanceFromCenter, + isInQuery: isInQuery, + geohash: encodeGeohash(location) + }; + + // Fire the 'key_entered' event if the provided key has entered this query + if (isInQuery && !wasInQuery) { + this._fireCallbacksForKey('key_entered', key, location, distanceFromCenter); + } else if (isInQuery && oldLocation !== null && (location[0] !== oldLocation[0] || location[1] !== oldLocation[1])) { + this._fireCallbacksForKey('key_moved', key, location, distanceFromCenter); + } else if (!isInQuery && wasInQuery) { + this._fireCallbacksForKey('key_exited', key, location, distanceFromCenter); + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 48c7480..e9b19b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,3 @@ -/*! - * GeoFire is an open-source library that allows you to store and query a set - * of keys based on their geographic location. At its heart, GeoFire simply - * stores locations with string keys. Its main benefit, however, is the - * possibility of retrieving only those keys within a given geographic area - - * all in realtime. - * - * GeoFire 0.0.0 - * https://github.com/firebase/geofire-js/ - * License: MIT - */ - -export { GeoFire } from './firebase/geoFire'; \ No newline at end of file +export * from './interfaces'; +export { GeoFire } from './geoFire'; +export { GeoFirestore } from './geoFirestore'; \ No newline at end of file diff --git a/src/interfaces/geoFirestoreQueryState.ts b/src/interfaces/geoFirestoreQueryState.ts new file mode 100644 index 0000000..8fa27ca --- /dev/null +++ b/src/interfaces/geoFirestoreQueryState.ts @@ -0,0 +1,5 @@ +export interface GeoFirestoreQueryState { + active: boolean; + childCallback: Function; + valueCallback: Function; +} \ No newline at end of file diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 1c3818c..709e52e 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './geoFireObj'; +export * from './geoFirestoreQueryState'; export * from './queryCriteria'; \ No newline at end of file diff --git a/src/firebase/geoCallbackRegistration.ts b/src/tools/callbackRegistration.ts similarity index 100% rename from src/firebase/geoCallbackRegistration.ts rename to src/tools/callbackRegistration.ts diff --git a/src/firebase/geoFireUtils.ts b/src/tools/utils.ts similarity index 96% rename from src/firebase/geoFireUtils.ts rename to src/tools/utils.ts index 849a6ab..40c73e1 100644 --- a/src/firebase/geoFireUtils.ts +++ b/src/tools/utils.ts @@ -426,12 +426,26 @@ export function decodeGeoFireObject(geoFireObj: GeoFireObj): number[] { * Returns the key of a Firebase snapshot across SDK versions. * * @param A Firebase snapshot. - * @returns key The Firebase snapshot's key. + * @returns The Firebase snapshot's key. */ -export function getKey(snapshot: firebase.database.DataSnapshot): string { +export function geoFireGetKey(snapshot: firebase.database.DataSnapshot): string { let key: string; if (typeof snapshot.key === 'string' || snapshot.key === null) { key = snapshot.key; } return key; +} + +/** + * Returns the id of a Firestore snapshot across SDK versions. + * + * @param A Firestore snapshot. + * @returns The Firestore snapshot's id. + */ +export function geoFirestoreGetKey(snapshot: firebase.firestore.DocumentSnapshot): string { + let id: string; + if (typeof snapshot.id === 'string' || snapshot.id === null) { + id = snapshot.id; + } + return id; } \ No newline at end of file diff --git a/tests/common.ts b/tests/common.ts index 1c476a7..76a1f74 100644 --- a/tests/common.ts +++ b/tests/common.ts @@ -46,9 +46,9 @@ export function beforeEachHelper(done) { /* Helper function which runs after each Jasmine test has completed */ export function afterEachHelper(done) { - // Cancel each outstanding GeoQuery - geoQueries.forEach(function (geoQuery) { - geoQuery.cancel(); + // Cancel each outstanding GeoFireQuery + geoQueries.forEach(function (geoFireQuery) { + geoFireQuery.cancel(); }) geoFireRef.remove().then(function () { diff --git a/tests/geoFire.test.ts b/tests/geoFire/index.test.ts similarity index 99% rename from tests/geoFire.test.ts rename to tests/geoFire/index.test.ts index 35743db..a3a5952 100755 --- a/tests/geoFire.test.ts +++ b/tests/geoFire/index.test.ts @@ -1,11 +1,11 @@ import * as chai from 'chai'; -import { GeoFire } from '../src/firebase/geoFire'; -import { GeoQuery } from '../src/firebase/geoQuery'; +import { GeoFire } from '../../src/geoFire'; +import { GeoFireQuery } from '../../src/geoFire/query'; import { afterEachHelper, beforeEachHelper, Checklist, failTestOnCaughtError, geoFire, geoFireRef, getFirebaseData, geoQueries, invalidFirebaseRefs, invalidKeys, invalidLocations, invalidQueryCriterias, validKeys, validLocations, validQueryCriterias -} from './common'; +} from '../common'; const expect = chai.expect; @@ -819,10 +819,10 @@ describe('GeoFire Tests:', () => { }); describe('query():', () => { - it('query() returns GeoQuery instance', () => { + it('query() returns GeoFireQuery instance', () => { geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); - expect(geoQueries[0] instanceof GeoQuery).to.be.ok; + expect(geoQueries[0] instanceof GeoFireQuery).to.be.ok; }); it('query() does not throw errors given valid query criteria', () => { diff --git a/tests/geoQuery.test.ts b/tests/geoFire/query.test.ts similarity index 95% rename from tests/geoQuery.test.ts rename to tests/geoFire/query.test.ts index dfa4eeb..fb5895a 100644 --- a/tests/geoQuery.test.ts +++ b/tests/geoFire/query.test.ts @@ -2,11 +2,11 @@ import * as chai from 'chai'; import { afterEachHelper, beforeEachHelper, Checklist, failTestOnCaughtError, geoFire, geoQueries, invalidQueryCriterias, validQueryCriterias, wait -} from './common'; +} from '../common'; const expect = chai.expect; -describe('GeoQuery Tests:', () => { +describe('GeoFireQuery Tests:', () => { // Reset the Firebase before each test beforeEach((done) => { beforeEachHelper(done); @@ -84,7 +84,7 @@ describe('GeoQuery Tests:', () => { expect(geoQueries[0].radius()).to.equal(100); }); - it('updateCriteria() fires \'key_entered\' callback for locations which now belong to the GeoQuery', (done) => { + it('updateCriteria() fires \'key_entered\' callback for locations which now belong to the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'loc1 entered', 'loc4 entered'], expect, done); geoQueries.push(geoFire.query({ center: [90, 90], radius: 1000 })); @@ -109,7 +109,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('updateCriteria() fires \'key_entered\' callback for locations with complex keys which now belong to the GeoQuery', (done) => { + it('updateCriteria() fires \'key_entered\' callback for locations with complex keys which now belong to the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'loc:^:*1 entered', 'loc-+-+-4 entered'], expect, done); geoQueries.push(geoFire.query({ center: [90, 90], radius: 1000 })); @@ -134,7 +134,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('updateCriteria() fires \'key_exited\' callback for locations which no longer belong to the GeoQuery', (done) => { + it('updateCriteria() fires \'key_exited\' callback for locations which no longer belong to the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'loc1 exited', 'loc4 exited'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -455,7 +455,7 @@ describe('GeoQuery Tests:', () => { }); describe('\'key_moved\' event:', () => { - it('\'key_moved\' callback does not fire for brand new locations within or outside of the GeoQuery', (done) => { + it('\'key_moved\' callback does not fire for brand new locations within or outside of the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -477,7 +477,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback does not fire for locations outside of the GeoQuery which are moved somewhere else outside of the GeoQuery', (done) => { + it('\'key_moved\' callback does not fire for locations outside of the GeoFireQuery which are moved somewhere else outside of the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -506,7 +506,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback does not fire for locations outside of the GeoQuery which are moved within the GeoQuery', (done) => { + it('\'key_moved\' callback does not fire for locations outside of the GeoFireQuery which are moved within the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -535,7 +535,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback does not fire for locations within the GeoQuery which are moved somewhere outside of the GeoQuery', (done) => { + it('\'key_moved\' callback does not fire for locations within the GeoFireQuery which are moved somewhere outside of the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -564,7 +564,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback does not fires for a location within the GeoQuery which is set to the same location', (done) => { + it('\'key_moved\' callback does not fires for a location within the GeoFireQuery which is set to the same location', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc3 moved'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -594,7 +594,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback fires for locations within the GeoQuery which are moved somewhere else within the GeoQuery', (done) => { + it('\'key_moved\' callback fires for locations within the GeoFireQuery which are moved somewhere else within the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved', 'loc3 moved'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -681,7 +681,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback properly fires when multiple keys are at the same location within the GeoQuery and only one of them moves somewhere else within the GeoQuery', (done) => { + it('\'key_moved\' callback properly fires when multiple keys are at the same location within the GeoFireQuery and only one of them moves somewhere else within the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved', 'loc3 moved'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -710,7 +710,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_moved\' callback properly fires when a location within the GeoQuery moves somehwere else within the GeoQuery that is already occupied by another key', (done) => { + it('\'key_moved\' callback properly fires when a location within the GeoFireQuery moves somehwere else within the GeoFireQuery that is already occupied by another key', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved', 'loc3 moved'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -739,7 +739,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('multiple \'key_moved\' callbacks fire for locations within the GeoQuery which are moved somewhere else within the GeoQuery', (done) => { + it('multiple \'key_moved\' callbacks fire for locations within the GeoFireQuery which are moved somewhere else within the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved1', 'loc3 moved1', 'loc1 moved2', 'loc3 moved2'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -773,7 +773,7 @@ describe('GeoQuery Tests:', () => { }); describe('\'key_entered\' event:', () => { - it('\'key_entered\' callback fires when a location enters the GeoQuery before onKeyEntered() was called', (done) => { + it('\'key_entered\' callback fires when a location enters the GeoFireQuery before onKeyEntered() was called', (done) => { const cl = new Checklist(['p1', 'p2', 'loc1 entered', 'loc4 entered'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -797,7 +797,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_entered\' callback fires when a location enters the GeoQuery after onKeyEntered() was called', (done) => { + it('\'key_entered\' callback fires when a location enters the GeoFireQuery after onKeyEntered() was called', (done) => { const cl = new Checklist(['p1', 'p2', 'loc1 entered', 'loc4 entered'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -869,7 +869,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_entered\' callback properly fires when multiple keys are at the same location outside the GeoQuery and only one of them moves within the GeoQuery', (done) => { + it('\'key_entered\' callback properly fires when multiple keys are at the same location outside the GeoFireQuery and only one of them moves within the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 entered'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -895,7 +895,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_entered\' callback properly fires when a location outside the GeoQuery moves somewhere within the GeoQuery that is already occupied by another key', (done) => { + it('\'key_entered\' callback properly fires when a location outside the GeoFireQuery moves somewhere within the GeoFireQuery that is already occupied by another key', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 entered', 'loc3 entered'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -921,7 +921,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('multiple \'key_entered\' callbacks fire when a location enters the GeoQuery', (done) => { + it('multiple \'key_entered\' callbacks fire when a location enters the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'loc1 entered1', 'loc4 entered1', 'loc1 entered2', 'loc4 entered2'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -950,7 +950,7 @@ describe('GeoQuery Tests:', () => { }); describe('\'key_exited\' event:', () => { - it('\'key_exited\' callback fires when a location leaves the GeoQuery', (done) => { + it('\'key_exited\' callback fires when a location leaves the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited', 'loc4 exited'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -1069,7 +1069,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_exited\' callback fires when a location within the GeoQuery is entirely removed from GeoFire', (done) => { + it('\'key_exited\' callback fires when a location within the GeoFireQuery is entirely removed from GeoFire', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -1094,7 +1094,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_exited\' callback properly fires when multiple keys are at the same location inside the GeoQuery and only one of them moves outside the GeoQuery', (done) => { + it('\'key_exited\' callback properly fires when multiple keys are at the same location inside the GeoFireQuery and only one of them moves outside the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -1119,7 +1119,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('\'key_exited\' callback properly fires when a location inside the GeoQuery moves somewhere outside the GeoQuery that is already occupied by another key', (done) => { + it('\'key_exited\' callback properly fires when a location inside the GeoFireQuery moves somewhere outside the GeoFireQuery that is already occupied by another key', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -1145,7 +1145,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('multiple \'key_exited\' callbacks fire when a location leaves the GeoQuery', (done) => { + it('multiple \'key_exited\' callbacks fire when a location leaves the GeoFireQuery', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited1', 'loc4 exited1', 'loc1 exited2', 'loc4 exited2'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -1259,8 +1259,8 @@ describe('GeoQuery Tests:', () => { }); }); - describe('Cancelling GeoQuery:', () => { - it('cancel() prevents GeoQuery from firing any more \'key_*\' event callbacks', (done) => { + describe('Cancelling GeoFireQuery:', () => { + it('cancel() prevents GeoFireQuery from firing any more \'key_*\' event callbacks', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'loc1 entered', 'loc4 entered', 'loc1 moved', 'loc4 exited'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); @@ -1323,7 +1323,7 @@ describe('GeoQuery Tests:', () => { }).catch(failTestOnCaughtError); }); - it('Calling cancel() on one GeoQuery does not cancel other GeoQueries', (done) => { + it('Calling cancel() on one GeoFireQuery does not cancel other GeoQueries', (done) => { const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'loc1 entered1', 'loc1 entered2', 'loc4 entered1', 'loc4 entered2', 'loc1 moved1', 'loc1 moved2', 'loc4 exited1', 'loc4 exited2', 'loc1 exited2', 'loc5 entered2'], expect, done); geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); diff --git a/tests/geoCallbackRegistration.test.ts b/tests/tools/callbackRegistration.test.ts similarity index 98% rename from tests/geoCallbackRegistration.test.ts rename to tests/tools/callbackRegistration.test.ts index 892e807..65e4445 100644 --- a/tests/geoCallbackRegistration.test.ts +++ b/tests/tools/callbackRegistration.test.ts @@ -1,8 +1,8 @@ -import { GeoCallbackRegistration } from '../src/firebase/geoCallbackRegistration'; +import { GeoCallbackRegistration } from '../../src/tools/callbackRegistration'; import { afterEachHelper, beforeEachHelper, Checklist, failTestOnCaughtError, geoFire, geoQueries, wait -} from './common'; +} from '../common'; import * as chai from 'chai'; diff --git a/tests/geoFireUtils.test.ts b/tests/tools/utils.test.ts similarity index 99% rename from tests/geoFireUtils.test.ts rename to tests/tools/utils.test.ts index c80f730..d1da57e 100644 --- a/tests/geoFireUtils.test.ts +++ b/tests/tools/utils.test.ts @@ -1,14 +1,14 @@ import * as chai from 'chai'; -import { GeoFire } from '../src/firebase/geoFire'; +import { GeoFire } from '../../src/geoFire'; import { boundingBoxBits, degreesToRadians, encodeGeohash, geohashQuery, geohashQueries, g_GEOHASH_PRECISION, metersToLongitudeDegrees, validateCriteria, validateGeohash, validateKey, validateLocation, wrapLongitude -} from '../src/firebase/geoFireUtils'; +} from '../../src/tools/utils'; import { invalidGeohashes, invalidKeys, invalidLocations, invalidQueryCriterias, validGeohashes, validKeys, validLocations, validQueryCriterias -} from './common'; +} from '../common'; const expect = chai.expect;