diff --git a/.gitignore b/.gitignore index 228345a4..29d7970d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ dist/ node_modules/ -bower_components/ -tests/coverage/ \ No newline at end of file +coverage/ +.DS_Store +.nyc_output/ +.rpt2_cache \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 545892b3..00000000 --- a/.jshintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "predef": [ - ], - "bitwise": true, - "curly": true, - "eqeqeq": true, - "forin": true, - "freeze": true, - "indent": 2, - "latedef": false, - "node": true, - "noempty": true, - "nonbsp": true, - "quotmark": "double", - "strict": true, - "trailing": true, - "undef": true, - "unused": true -} diff --git a/.travis.yml b/.travis.yml index 33db554e..143470d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,10 @@ language: node_js node_js: -- '0.12' -#- stable + - 8 sudo: false -before_install: -- export DISPLAY=:99.0 -- sh -e /etc/init.d/xvfb start install: -- npm install -g bower -- npm install -- bower install + - npm install script: -- npm run travis -after_script: -- cat ./tests/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js + - npm run test +after_success: + - npm run coverage \ No newline at end of file diff --git a/bower.json b/bower.json deleted file mode 100644 index 08ff6227..00000000 --- a/bower.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "geofire", - "description": "Location-based querying and filtering using Firebase", - "version": "4.1.2", - "authors": [ - "Firebase (https://firebase.google.com/)" - ], - "homepage": "https://github.com/firebase/geofire-js/", - "repository": { - "type": "git", - "url": "https://github.com/firebase/geofire-js.git" - }, - "license": "MIT", - "keywords": [ - "geoquery", - "location", - "firebase", - "realtime", - "geolocation" - ], - "main": "dist/geofire.js", - "ignore": [ - "**/.*", - "src", - "build", - "tests", - "examples", - "node_modules", - "bower_components", - "firebase.json", - "package.json", - "gulpfile.js", - "changelog.txt" - ], - "dependencies": { - "firebase": "^2.4.0 || 3.x.x" - }, - "devDependencies": { - "jasmine": "~2.0.0", - "rsvp": "^3.1.0" - } -} diff --git a/build/footer b/build/footer deleted file mode 100644 index 7026eef9..00000000 --- a/build/footer +++ /dev/null @@ -1,7 +0,0 @@ - return GeoFire; -})(); - -// Export GeoFire if this is being run in node -if (typeof module !== "undefined" && typeof process !== "undefined") { - module.exports = GeoFire; -} \ No newline at end of file diff --git a/build/header b/build/header deleted file mode 100644 index 3ff8c820..00000000 --- a/build/header +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * 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 - */ - -var GeoFire = (function() { - "use strict"; diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 4f3e6604..00000000 --- a/gulpfile.js +++ /dev/null @@ -1,115 +0,0 @@ -/**************/ -/* REQUIRES */ -/**************/ -var gulp = require("gulp"); - -// File IO -var streamqueue = require("streamqueue"); -var concat = require("gulp-concat"); -var jshint = require("gulp-jshint"); -var uglify = require("gulp-uglify"); -var runSequence = require('run-sequence'); - -// Testing -var karma = require("gulp-karma"); - - -/****************/ -/* FILE PATHS */ -/****************/ -var paths = { - destDir: "dist", - - scripts: { - src: { - dir: "src", - files: [ - "src/*.js" - ] - }, - dest: { - dir: "dist", - files: { - unminified: "geofire.js", - minified: "geofire.min.js" - } - } - }, - - tests: { - config: "tests/karma.conf.js", - files: [ - "bower_components/firebase/firebase.js", - "bower_components/rsvp/rsvp.min.js", - "src/*.js", - "tests/specs/*.spec.js" - ] - } -}; - - -/***********/ -/* TASKS */ -/***********/ -/* Lints, minifies, and concatenates the script files */ -gulp.task("scripts", function() { - // Concatenate all src files together - var stream = streamqueue({ objectMode: true }); - stream.queue(gulp.src("build/header")); - stream.queue(gulp.src(paths.scripts.src.files)); - stream.queue(gulp.src("build/footer")); - - // Output the final concatenated script file - return stream.done() - // Rename file - .pipe(concat(paths.scripts.dest.files.unminified)) - - // Lint - .pipe(jshint()) - .pipe(jshint.reporter("jshint-stylish")) - .pipe(jshint.reporter("fail")) - .on("error", function(error) { - throw error; - }) - - // Write un-minified version - .pipe(gulp.dest(paths.scripts.dest.dir)) - - // Minify - .pipe(uglify({ - preserveComments: "some" - })) - - // Rename file - .pipe(concat(paths.scripts.dest.files.minified)) - - // Write minified version to the distribution directory - .pipe(gulp.dest(paths.scripts.dest.dir)); -}); - -/* Uses the Karma test runner to run the Jasmine tests */ -gulp.task("test", function() { - return gulp.src(paths.tests.files) - .pipe(karma({ - configFile: paths.tests.config, - action: "run" - })) - .on("error", function(error) { - throw error; - }); -}); - -/* Re-runs the "scripts" task every time a script file changes */ -gulp.task("watch", function() { - gulp.watch(["build/*", paths.scripts.src.dir + "/**/*"], ["scripts"]); -}); - -/* Builds the distribution files */ -gulp.task("build", ["scripts"]); - -/* Runs the "test" and "scripts" tasks by default */ -gulp.task("default", function(done) { - runSequence("scripts", "test", function(error) { - done(error && error.err); - }); -}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..32da0090 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5249 @@ +{ + "name": "geofire", + "version": "4.1.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz", + "integrity": "sha1-vs2AVIJzREDJ0TfkbXc0DmTX9Rs=", + "dev": true, + "requires": { + "@babel/highlight": "7.0.0-beta.49" + } + }, + "@babel/generator": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.49.tgz", + "integrity": "sha1-6c/9qROZaszseTu8JauRvBnQv3o=", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.49", + "jsesc": "2.5.1", + "lodash": "4.17.10", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.49.tgz", + "integrity": "sha1-olwRGbnwNSeGcBJuAiXAMEHI3jI=", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.49", + "@babel/template": "7.0.0-beta.49", + "@babel/types": "7.0.0-beta.49" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.49.tgz", + "integrity": "sha1-z1Aj8y0q2S0Ic3STnOwJUby1FEE=", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.49" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.49.tgz", + "integrity": "sha1-QNeO2glo0BGxxShm5XRs+yPldUg=", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.49" + } + }, + "@babel/highlight": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.49.tgz", + "integrity": "sha1-lr3GtD4TSCASumaRsQGEktOWIsw=", + "dev": true, + "requires": { + "chalk": "2.4.1", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "@babel/parser": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.49.tgz", + "integrity": "sha1-lE0MW6KBK7FZ7b0iZ0Ov0mUXm9w=", + "dev": true + }, + "@babel/template": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.49.tgz", + "integrity": "sha1-44q+ghfLl5P0YaUwbXrXRdg+HSc=", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.49", + "@babel/parser": "7.0.0-beta.49", + "@babel/types": "7.0.0-beta.49", + "lodash": "4.17.10" + } + }, + "@babel/traverse": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.49.tgz", + "integrity": "sha1-TypzaCoYM07WYl0QCo0nMZ98LWg=", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.49", + "@babel/generator": "7.0.0-beta.49", + "@babel/helper-function-name": "7.0.0-beta.49", + "@babel/helper-split-export-declaration": "7.0.0-beta.49", + "@babel/parser": "7.0.0-beta.49", + "@babel/types": "7.0.0-beta.49", + "debug": "3.1.0", + "globals": "11.5.0", + "invariant": "2.2.4", + "lodash": "4.17.10" + } + }, + "@babel/types": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.49.tgz", + "integrity": "sha1-t+Oxw/TUz+Eb34yJ8e/V4WF7h6Y=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "lodash": "4.17.10", + "to-fast-properties": "2.0.0" + } + }, + "@firebase/app": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.3.3.tgz", + "integrity": "sha512-V5fMC2Ysx1TlHD6x7vj7EOtoyJSU/ts+fp9qxt0E3TA+DbWgKFrkcL+o2jZhi30h0sXKV7oW0vh67YZdZylqOg==", + "dev": true, + "requires": { + "@firebase/app-types": "0.3.2", + "@firebase/util": "0.2.1", + "dom-storage": "2.1.0", + "tslib": "1.9.0", + "xmlhttprequest": "1.8.0" + } + }, + "@firebase/app-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.3.2.tgz", + "integrity": "sha512-ZD8lTgW07NGgo75bTyBJA8Lt9+NweNzot7lrsBtIvfciwUzaFJLsv2EShqjBeuhF7RpG6YFucJ6m67w5buCtzw==", + "dev": true + }, + "@firebase/auth": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.5.3.tgz", + "integrity": "sha512-/oxprMSyOhc4HXFBqNnBrY26e5DyTfSacyw7xBGuW1zMQrZuUwPM+6Cr0vLuParln28bmRXIinM5g2R9zVdchw==", + "dev": true, + "requires": { + "@firebase/auth-types": "0.3.3" + } + }, + "@firebase/auth-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.3.3.tgz", + "integrity": "sha512-7DJwHDjZMrRHuMB3y2z6HukaLt42I1GcUIQ2wO96G3NxpAUkQ1bDlMguwymFk0UFrPsdi3lv1mEwxCGTRNHAiA==", + "dev": true + }, + "@firebase/database": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.3.3.tgz", + "integrity": "sha512-8r/snlMuOB6QXdkfjbZaFxK1U8CbQgCG0rVfMHclRVNQhZLtTEtiVNDTQk75bfxY0k+iqyg+6fsNWClA92glcg==", + "dev": true, + "requires": { + "@firebase/database-types": "0.3.2", + "@firebase/logger": "0.1.1", + "@firebase/util": "0.2.1", + "faye-websocket": "0.11.1", + "tslib": "1.9.0" + } + }, + "@firebase/database-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.3.2.tgz", + "integrity": "sha512-9ZYdvYQ6r3aaHJarhUM5Hf6lQWu3ZJme+RR0o8qfBb9L04TL3uNjt+AJFku1ysVPntTn+9GqJjiIB2/OC3JtwA==", + "dev": true + }, + "@firebase/firestore": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-0.5.4.tgz", + "integrity": "sha512-OdeF8vU+rn7aklumjc9ga9+QnT+LSl+2uqgLE30wdCpYTEKI6kpb5NxhIqZ+dEspdpgW6xQPM6Uy2IKbJAsjBQ==", + "dev": true, + "requires": { + "@firebase/firestore-types": "0.4.3", + "@firebase/logger": "0.1.1", + "@firebase/webchannel-wrapper": "0.2.8", + "grpc": "1.11.3", + "tslib": "1.9.0" + } + }, + "@firebase/firestore-types": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-0.4.3.tgz", + "integrity": "sha512-QdFBhH0Bcw7TRodN6nJ1pQq0AGAgMfpIUQguKcogTE/2L/lAECxbUfTWtBcGenKcSKpae5xJuuhGZxaPGkQv7A==", + "dev": true + }, + "@firebase/functions": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.2.4.tgz", + "integrity": "sha512-HSa2MdBRPWltv1gvE1nWmbzxtWt5jJt4gZ8wje5qEIld7tbLvaWCJRvLGwH/dX1r9s6u6GtDF9FD4Gn2z2xFiQ==", + "dev": true, + "requires": { + "@firebase/functions-types": "0.1.3", + "@firebase/messaging-types": "0.2.3", + "isomorphic-fetch": "2.2.1", + "tslib": "1.9.0" + } + }, + "@firebase/functions-types": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.1.3.tgz", + "integrity": "sha512-9cOrF5b2B3kfdwsRm3owSXZciIQbupbEKhq4WTsLRXU8mz8ol7WxuOuG/FwYaHryeI8z29gIzP9NpiGoKJgo4w==", + "dev": true + }, + "@firebase/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-5jn3HHbEfdOwychyIEIkP1cik+MW/vvoOavTOzwDkH+fv6Bx+HBUOzh09M7sCYzXFtKzjbUax9+g39mJNBLklQ==", + "dev": true + }, + "@firebase/messaging": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.3.4.tgz", + "integrity": "sha512-MWewMZdHs7odZ4nTY9QVEQtq2g6dEw9LtSg63Z5FLxN/Mlj+GtEmUnoerprhzR7eo80DCH1Xd++tMmGk4rIz3g==", + "dev": true, + "requires": { + "@firebase/messaging-types": "0.2.3", + "@firebase/util": "0.2.1", + "tslib": "1.9.0" + } + }, + "@firebase/messaging-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-types/-/messaging-types-0.2.3.tgz", + "integrity": "sha512-avwCgZzcx2uxIW/wT3p3G/EyHftIrvMyiTS7AA7dxDlzfx+8dpAeTsb1+jsHJT4F6foSh5HG17Nw8sDzYuxH1Q==", + "dev": true + }, + "@firebase/polyfill": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.3.3.tgz", + "integrity": "sha512-xs8IZf1WEbufYXyfV8YjmiFZOaujRRq0T03NteihYfuGVTTym7z5SmvLvEHLEUjf2fgeobPEzZ2JgrCQHS+QHw==", + "dev": true, + "requires": { + "core-js": "2.5.5", + "promise-polyfill": "7.1.2", + "whatwg-fetch": "2.0.4" + } + }, + "@firebase/storage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.2.3.tgz", + "integrity": "sha512-2sq5jckWszW53gfQMkPNc7EumJ92oErRhzGJANbVzBumwR8qwKZU8/I+/uV9SPK1tVmSUc3S21jdoW5oOJVEuA==", + "dev": true, + "requires": { + "@firebase/storage-types": "0.2.3", + "tslib": "1.9.0" + } + }, + "@firebase/storage-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.2.3.tgz", + "integrity": "sha512-RaZeam2LgsB7xwAtOQr4G0Geoyf7D5TnLF3a12By6Rh0Z9PqBSlWn0SVYGW3SkmxIdqvWZMZvCyamUlqQvQzWw==", + "dev": true + }, + "@firebase/util": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.1.tgz", + "integrity": "sha512-KPNcIK5+bUUBMII87NqGu+tRUnMcY95xujS2z0QyAfoQCKe11DMHICv3M6uweiLSXqdQwrMTyFtiql1q+0UOYQ==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@firebase/webchannel-wrapper": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.8.tgz", + "integrity": "sha512-ToJbeJnxDc3O325FvcKVb3yHO1hvgHjCFvhKol6Z17GiB7vL104POjFQT4RnlLiAGSRCBAMxinDec9y9vQYdyg==", + "dev": true + }, + "@types/chai": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.3.tgz", + "integrity": "sha512-f5dXGzOJycyzSMdaXVhiBhauL4dYydXwVpavfQ1mVCaGjR56a9QfklXObUxlIY9bGTmCPHEEZ04I16BZ/8w5ww==", + "dev": true + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.2.tgz", + "integrity": "sha512-tfg9rh2qQhBW6SBqpvfqTgU6lHWGhQURoTrn7NeDF+CEkO9JGYbkzU23EXu6p3bnvDxLeeSX8ohAA23urvWeNw==", + "dev": true + }, + "@types/node": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.2.tgz", + "integrity": "sha512-9NfEUDp3tgRhmoxzTpTo+lq+KIVFxZahuRX0LHF/9IzKHaWuoWsIrrJ61zw5cnnlGINX8lqJzXYfQTOICS5Q+A==", + "dev": true + }, + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", + "dev": true + }, + "acorn": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.2.tgz", + "integrity": "sha512-zUzo1E5dI2Ey8+82egfnttyMlMZ2y0D8xOCO3PNPPlYXpl8NZvF6Qk9L9BEtJs+43FqEmfBViDqc5d1ckRDguw==", + "dev": true + }, + "acorn-globals": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", + "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "dev": true, + "requires": { + "acorn": "5.6.2" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.2" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "dev": true, + "requires": { + "colour": "0.7.1", + "optjs": "3.2.2" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browser-process-hrtime": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", + "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", + "dev": true + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", + "dev": true + }, + "builtin-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-2.0.0.tgz", + "integrity": "sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg==", + "dev": true + }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", + "dev": true, + "requires": { + "long": "3.2.0" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", + "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "coveralls": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.1.tgz", + "integrity": "sha512-FAzXwiDOYLGDWH+zgoIA+8GbWv50hlx+kpEJyvzLKOdnIBv9uWoVl4DhqGgyUHpiRjAlF8KYZSipWXYtllWH6Q==", + "dev": true, + "requires": { + "js-yaml": "3.12.0", + "lcov-parse": "0.0.10", + "log-driver": "1.2.7", + "minimist": "1.2.0", + "request": "2.87.0" + } + }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", + "dev": true + }, + "cssstyle": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.3.1.tgz", + "integrity": "sha512-tNvaxM5blOnxanyxI6panOsnfiyLRj3HV4qjqqS45WPNS1usdYWRUQjqTEEELK73lpeP/1KoIGYUwrBn/VcECA==", + "dev": true, + "requires": { + "cssom": "0.3.2" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "data-urls": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz", + "integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==", + "dev": true, + "requires": { + "abab": "1.0.4", + "whatwg-mimetype": "2.1.0", + "whatwg-url": "6.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dom-storage": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", + "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "4.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "0.4.23" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz", + "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==", + "dev": true, + "requires": { + "esprima": "3.1.3", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "estree-walker": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.2.tgz", + "integrity": "sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.4" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "3.0.0", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "firebase": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-5.0.4.tgz", + "integrity": "sha512-UIlUOcISw6uiAwbsj/bamDDKJtdjx9GU2m9XgPF58DSA8IJaIbfdL6jT5C03R5mL+mMpXoKDAOeOq+ynf+R09w==", + "dev": true, + "requires": { + "@firebase/app": "0.3.3", + "@firebase/auth": "0.5.3", + "@firebase/database": "0.3.3", + "@firebase/firestore": "0.5.4", + "@firebase/functions": "0.2.4", + "@firebase/messaging": "0.3.4", + "@firebase/polyfill": "0.3.3", + "@firebase/storage": "0.2.3" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz", + "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "grpc": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.11.3.tgz", + "integrity": "sha512-7fJ40USpnP7hxGK0uRoEhJz6unA5VUdwInfwAY2rK2+OVxdDJSdTZQ/8/M+1tW68pHZYgHvg2ohvJ+clhW3ANg==", + "dev": true, + "requires": { + "lodash": "4.17.10", + "nan": "2.10.0", + "node-pre-gyp": "0.10.0", + "protobufjs": "5.0.3" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "minipass": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "dev": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "needle": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.1.tgz", + "integrity": "sha512-t/ZswCM9JTWjAdXS9VpvqhI2Ct2sL2MdY4fUXqGJaGBk13ge99ObqRksRTbBE56K+wxUXwwfZYOuZHifFW9q+Q==", + "dev": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.19", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "dev": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.1", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.2" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "dev": true + }, + "npm-packlist": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "dev": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "rc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "dev": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "tar": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.2.tgz", + "integrity": "sha512-BfkE9CciGGgDsATqkikUHrQrraBCO+ke/1f6SFAEMnxyyfN9lxC+nW1NFWMpqH865DhHIy9vQi682gk1X7friw==", + "dev": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.2", + "yallist": "3.0.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "1.0.3" + } + }, + "http-parser-js": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dev": true, + "requires": { + "node-fetch": "1.7.3", + "whatwg-fetch": "2.0.4" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz", + "integrity": "sha512-yMSw5xLIbdaxiVXHk3amfNM2WeBxLrwH/BCyZ9HvA/fylwziAIJOG2rKqWyLqEJqwKT725vxxqidv+SyynnGAA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-2.2.0.tgz", + "integrity": "sha512-ozQGtlIw+/a/F3n6QwWiuuyRAPp64+g2GVsKYsIez0sgIEzkU5ZpL2uZ5pmAzbEJ82anlRaPlOQZzkRXspgJyg==", + "dev": true, + "requires": { + "@babel/generator": "7.0.0-beta.49", + "@babel/parser": "7.0.0-beta.49", + "@babel/template": "7.0.0-beta.49", + "@babel/traverse": "7.0.0-beta.49", + "@babel/types": "7.0.0-beta.49", + "istanbul-lib-coverage": "2.0.0", + "semver": "5.5.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jsdom": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.11.0.tgz", + "integrity": "sha512-ou1VyfjwsSuWkudGxb03FotDajxAto6USAlmMZjE2lc0jCznt7sBWkhfRBRaWwbnmDqdMSTKTLT5d9sBFkkM7A==", + "dev": true, + "requires": { + "abab": "1.0.4", + "acorn": "5.6.2", + "acorn-globals": "4.1.0", + "array-equal": "1.0.0", + "cssom": "0.3.2", + "cssstyle": "0.3.1", + "data-urls": "1.0.0", + "domexception": "1.0.1", + "escodegen": "1.10.0", + "html-encoding-sniffer": "1.0.2", + "left-pad": "1.3.0", + "nwsapi": "2.0.3", + "parse5": "4.0.0", + "pn": "1.1.0", + "request": "2.87.0", + "request-promise-native": "1.0.5", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.3.4", + "w3c-hr-time": "1.0.1", + "webidl-conversions": "4.0.2", + "whatwg-encoding": "1.0.3", + "whatwg-mimetype": "2.1.0", + "whatwg-url": "6.5.0", + "ws": "4.1.0", + "xml-name-validator": "3.0.0" + } + }, + "jsdom-global": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsdom-global/-/jsdom-global-3.0.2.tgz", + "integrity": "sha1-a9KZwTsMRiay2iwDk81DhdYGrLk=", + "dev": true + }, + "jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "magic-string": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", + "dev": true, + "requires": { + "vlq": "0.2.3" + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "dev": true, + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.3.tgz", + "integrity": "sha512-zFJF9lOpg2+uicP0BQKOAfIOqeTp/p8PC669mewxgRkR1hGjne8BMUHk4wpRS9o5Z0icA5Nv04HmGkW31KfMKw==", + "dev": true + }, + "nyc": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-12.0.2.tgz", + "integrity": "sha1-ikpO1pCWbBHsWH/4fuoMEsl0upk=", + "dev": true, + "requires": { + "archy": "1.0.0", + "arrify": "1.0.1", + "caching-transform": "1.0.1", + "convert-source-map": "1.5.1", + "debug-log": "1.0.1", + "default-require-extensions": "1.0.0", + "find-cache-dir": "0.1.1", + "find-up": "2.1.0", + "foreground-child": "1.5.6", + "glob": "7.1.2", + "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-hook": "1.1.0", + "istanbul-lib-instrument": "2.2.0", + "istanbul-lib-report": "1.1.3", + "istanbul-lib-source-maps": "1.2.5", + "istanbul-reports": "1.4.1", + "md5-hex": "1.3.0", + "merge-source-map": "1.1.0", + "micromatch": "3.1.10", + "mkdirp": "0.5.1", + "resolve-from": "2.0.0", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "spawn-wrap": "1.4.2", + "test-exclude": "4.2.1", + "yargs": "11.1.0", + "yargs-parser": "8.1.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, + "caching-transform": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-1.0.1.tgz", + "integrity": "sha1-bb2y8g+Nj7znnz6U6dF0Lc31wKE=", + "dev": true, + "requires": { + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "4.1.3", + "which": "1.3.1" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", + "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.3", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha1-IyNbKasjDFdqqw1PE/wEawsDgiI=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha1-dkZiRnH9fqVYzNmieVGC8pWPGyQ=", + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha1-ACbjf1RU1z41bf5lZGmYZ8an8P8=", + "dev": true + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha1-99jy5CuX43/nlhFMsPnWi146Q0E=", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz", + "integrity": "sha1-hTjZcDcss3FtU+VVI91UtVeo2Js=", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.3.tgz", + "integrity": "sha1-LfEhiMD6d5kMDSF20tC6M5QYglk=", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha1-/+a+Tnq4bTYD5CkNVJkLFFBvybE=", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + } + }, + "istanbul-reports": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.4.1.tgz", + "integrity": "sha1-Ty6OkoqnoF0dpsQn1AmLJlXsczQ=", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha1-oRdc80lt/IQ2wVbDNLSVWZK85pw=", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "md5-hex": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-1.3.0.tgz", + "integrity": "sha1-0sSv6YPENwZiF5uMrRRSGRNQRsQ=", + "dev": true, + "requires": { + "md5-o-matic": "0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/md5-o-matic/-/md5-o-matic-0.1.1.tgz", + "integrity": "sha1-givM1l4RfFFPqxdrJZRdVBAKA8M=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha1-L93n5gIJOfcJBqaPLXrmheTIxkY=", + "dev": true, + "requires": { + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "dev": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha1-h59xUMstq3pHElkGbBBO7m4Pp8I=", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha1-DpK2vty1nwIsE9DxlJ3ILRWQnxw=", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.2", + "use": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", + "dev": true, + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spawn-wrap": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", + "integrity": "sha1-z/WOc6giRhe2Vhq9wyWG6gyCJIw=", + "dev": true, + "requires": { + "foreground-child": "1.5.6", + "mkdirp": "0.5.1", + "os-homedir": "1.0.2", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "which": "1.3.1" + } + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha1-BaW01xU6GVvJLDxCW2nzsqlSTII=", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha1-LHrmEFbHFKW5ubKyr30xHvXHj+k=", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha1-enzShHDMbToc/m1miG9rxDDTrIc=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "test-exclude": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", + "integrity": "sha1-36Ii8DSAvKaSB8pyizfXS0X3JPo=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "3.1.10", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha1-FHFr8D/f79AwQK71jYtLhfOnxUQ=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha1-gWQ7y+8b3+zUYjeT3EZIlIupgzg=", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha1-kLhpk07W6HERXqL/WLA/RyTtLXc=", + "dev": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha1-NIQi2+gtgAswIu709qwQvy5NG0k=", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", + "integrity": "sha1-8TdqM7Ziml0GN4KUTacyYx6WaVA=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } + } + } + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "promise-polyfill": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-7.1.2.tgz", + "integrity": "sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==", + "dev": true + }, + "protobufjs": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.3.tgz", + "integrity": "sha512-55Kcx1MhPZX0zTbVosMQEO5R6/rikNXd9b6RQK4KSPcrSIIwoXTtebIczUrXlwaSrbz4x8XUVThGPob1n8I4QA==", + "dev": true, + "requires": { + "ascli": "1.0.1", + "bytebuffer": "5.0.1", + "glob": "7.1.2", + "yargs": "3.32.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "dev": true, + "requires": { + "is-number": "4.0.0", + "kind-of": "6.0.2", + "math-random": "1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dev": true, + "requires": { + "lodash": "4.17.10" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "dev": true, + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "1.1.1", + "tough-cookie": "2.3.4" + } + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "rollup": { + "version": "0.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.60.4.tgz", + "integrity": "sha512-55x0sY6LPF7waMOaDa6QvTfEngyYpMNh48UWNEiZzwGA8x+sP2aH4c+H+1fTpFLay8Q6w1eE5Asrty7NW0nfrQ==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "@types/node": "10.3.2" + } + }, + "rollup-plugin-commonjs": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.3.tgz", + "integrity": "sha512-g91ZZKZwTW7F7vL6jMee38I8coj/Q9GBdTmXXeFL7ldgC1Ky5WJvHgbKlAiXXTh762qvohhExwUgeQGFh9suGg==", + "dev": true, + "requires": { + "estree-walker": "0.5.2", + "magic-string": "0.22.5", + "resolve": "1.7.1", + "rollup-pluginutils": "2.3.0" + } + }, + "rollup-plugin-node-resolve": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz", + "integrity": "sha512-9zHGr3oUJq6G+X0oRMYlzid9fXicBdiydhwGChdyeNRGPcN/majtegApRKHLR5drboUvEWU+QeUmGTyEZQs3WA==", + "dev": true, + "requires": { + "builtin-modules": "2.0.0", + "is-module": "1.0.0", + "resolve": "1.7.1" + } + }, + "rollup-plugin-typescript2": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.15.0.tgz", + "integrity": "sha512-0SiFfXhs41wQ94RIzw8EDM9vblyLNFNDgyqX1rci6e1KpYcxIbNH67nzv0DCS31LnA7K4F9irTl+ZPezvm/zug==", + "dev": true, + "requires": { + "fs-extra": "5.0.0", + "resolve": "1.7.1", + "rollup-pluginutils": "2.3.0", + "tslib": "1.9.2" + }, + "dependencies": { + "tslib": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==", + "dev": true + } + } + }, + "rollup-plugin-uglify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-4.0.0.tgz", + "integrity": "sha512-f6W31EQLzxSEYfN3x6/lyljHqXSoCjXKcTsnwz3evQvHgU1+qTzU2SE0SIG7tbAvaCewp2UaZ5x3k6nYsxOP9A==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.49", + "uglify-js": "3.4.0" + } + }, + "rollup-pluginutils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.3.0.tgz", + "integrity": "sha512-xB6hsRsjdJdIYWEyYUJy/3ki5g69wrf0luHPGNK3ZSocV6HLNfio59l3dZ3TL4xUwEKgROhFi9jOCt6c5gfUWw==", + "dev": true, + "requires": { + "estree-walker": "0.5.2", + "micromatch": "2.3.11" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "requires": { + "buffer-from": "1.1.0", + "source-map": "0.6.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "ts-node": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.1.1.tgz", + "integrity": "sha512-79FnymLGDBd/nXoiak1L6w6fd9Zz9Ge/x8/Aglaeh31KkqRLDzbfT1vBGlO5dqc76WzufTlW4IYl7e01CVUF5A==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "diff": "3.5.0", + "make-error": "1.3.4", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.5.6", + "yn": "2.0.0" + } + }, + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tslint": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", + "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.4.1", + "commander": "2.15.1", + "diff": "3.5.0", + "glob": "7.1.2", + "js-yaml": "3.12.0", + "minimatch": "3.0.4", + "resolve": "1.7.1", + "semver": "5.5.0", + "tslib": "1.9.0", + "tsutils": "2.27.1" + }, + "dependencies": { + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + } + } + }, + "tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.1.tgz", + "integrity": "sha512-h6pM2f/GDchCFlldnriOhs1QHuwbnmj6/v7499eMHqPeW4V2G0elua2eIc2nu8v2NdHV0Gm+tzX83Hr6nUFjQA==", + "dev": true + }, + "uglify-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.0.tgz", + "integrity": "sha512-Jcf5naPkX3rVPSQpRn9Vm6Rr572I1gTtR9LnqKgXjmOgfYQ/QS0V2WRStFR53Bdj520M66aCZqt9uzYXgtGrJQ==", + "dev": true, + "requires": { + "commander": "2.15.1", + "source-map": "0.6.1" + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", + "dev": true + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "0.1.2" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": "0.4.13", + "websocket-extensions": "0.1.3" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", + "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.19" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + } + } + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", + "dev": true + }, + "whatwg-mimetype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", + "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "4.7.0", + "tr46": "1.0.1", + "webidl-conversions": "4.0.2" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.2" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "os-locale": "1.4.0", + "string-width": "1.0.2", + "window-size": "0.1.4", + "y18n": "3.2.1" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/package.json b/package.json index e6e2433f..90b88d15 100644 --- a/package.json +++ b/package.json @@ -19,40 +19,56 @@ "realtime", "geolocation" ], - "main": "dist/geofire.js", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", "files": [ "dist/**", "LICENSE", "README.md", "package.json" ], - "dependencies": {}, "peerDependencies": { - "firebase": "^2.4.0 || 3.x.x" + "firebase": "^2.4.0 || 3.x.x || 4.x.x || 5.x.x" }, "devDependencies": { - "coveralls": "2.11.4", - "firebase": "3.x.x", - "gulp": "3.9.0", - "gulp-concat": "2.6.0", - "gulp-ext-replace": "0.2.0", - "gulp-jshint": "1.11.2", - "gulp-karma": "0.0.5", - "gulp-uglify": "1.4.1", - "jasmine-core": "^2.5.2", - "jshint-stylish": "2.0.1", - "karma": "^0.13.22", - "karma-chrome-launcher": "0.2.0", - "karma-coverage": "0.5.2", - "karma-failed-reporter": "0.0.3", - "karma-firefox-launcher": "0.1.6", - "karma-jasmine": "0.3.6", - "karma-spec-reporter": "0.0.20", - "run-sequence": "^1.1.4", - "streamqueue": "0.1.1" + "@types/chai": "^4.1.3", + "@types/mocha": "^5.2.2", + "@types/node": "^10.3.2", + "chai": "^4.1.2", + "coveralls": "^3.0.1", + "firebase": "5.x.x", + "jsdom": "^11.11.0", + "jsdom-global": "^3.0.2", + "mocha": "^5.2.0", + "nyc": "^12.0.2", + "rollup": "^0.60.4", + "rollup-plugin-commonjs": "^9.1.3", + "rollup-plugin-node-resolve": "^3.3.0", + "rollup-plugin-typescript2": "^0.15.0", + "rollup-plugin-uglify": "^4.0.0", + "source-map-support": "^0.5.6", + "ts-node": "^6.1.1", + "tslint": "^5.10.0", + "typescript": "^2.9.1" }, "scripts": { - "test": "gulp test", - "travis": "gulp" + "build": "rollup -c", + "coverage": "nyc report --reporter=text-lcov | coveralls", + "test": "nyc --reporter=html --reporter=text mocha", + "prepare": "npm run build" + }, + "nyc": { + "extension": [ + ".ts" + ], + "exclude": [ + "**/*.d.ts", + "**/*.js" + ], + "reporter": [ + "html" + ], + "all": true } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..d354ca38 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,44 @@ +import commonjs from 'rollup-plugin-commonjs'; +import resolveModule from 'rollup-plugin-node-resolve'; +import typescript from 'rollup-plugin-typescript2'; +import { uglify } from 'rollup-plugin-uglify'; +import pkg from './package.json'; + +const GLOBAL_NAME = 'geofire'; + +const plugins = [ + resolveModule(), + typescript({ + typescript: require('typescript') + }), + commonjs() +]; + +const external = Object.keys(pkg.dependencies || {}); + +const completeBuilds = [{ + input: 'src/index.ts', + output: [{ + file: pkg.main, + format: 'cjs' + }, + { + file: pkg.module, + format: 'es' + } + ], + plugins, + external + }, + { + input: 'src/index.ts', + output: { + file: 'dist/geofire.js', + format: 'umd', + name: GLOBAL_NAME + }, + plugins: [...plugins, uglify()] + }, +]; + +export default [...completeBuilds]; \ No newline at end of file diff --git a/src/geoCallbackRegistration.js b/src/geoCallbackRegistration.js deleted file mode 100644 index f8ec8407..00000000 --- a/src/geoCallbackRegistration.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Creates a GeoCallbackRegistration instance. - * - * @constructor - * @this {GeoCallbackRegistration} - * @callback cancelCallback Callback to run when this callback registration is cancelled. - */ -var GeoCallbackRegistration = function(cancelCallback) { - /********************/ - /* PUBLIC METHODS */ - /********************/ - /** - * Cancels this callback registration so that it no longer fires its callback. This - * has no effect on any other callback registrations you may have created. - */ - this.cancel = function() { - if (typeof _cancelCallback !== "undefined") { - _cancelCallback(); - _cancelCallback = undefined; - } - }; - - /*****************/ - /* CONSTRUCTOR */ - /*****************/ - if (typeof cancelCallback !== "function") { - throw new Error("callback must be a function"); - } - - var _cancelCallback = cancelCallback; -}; \ No newline at end of file diff --git a/src/geoFire.js b/src/geoFire.js deleted file mode 100644 index c6f2c4a2..00000000 --- a/src/geoFire.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Creates a GeoFire instance. - * - * @constructor - * @this {GeoFire} - * @param {Firebase} firebaseRef A Firebase reference where the GeoFire data will be stored. - */ -var GeoFire = function(firebaseRef) { - /********************/ - /* PUBLIC METHODS */ - /********************/ - /** - * Returns the Firebase instance used to create this GeoFire instance. - * - * @return {Firebase} The Firebase instance used to create this GeoFire instance. - */ - this.ref = function() { - return _firebaseRef; - }; - - /** - * Adds the provided key - location pair(s) to Firebase. Returns an empty promise which is fulfilled when the write is complete. - * - * If any provided key already exists in this GeoFire, it will be overwritten with the new location value. - * - * @param {string|Object} keyOrLocations The key representing the location to add or a mapping of key - location pairs which - * represent the locations to add. - * @param {Array.|undefined} location The [latitude, longitude] pair to add. - * @return {Promise.<>} A promise that is fulfilled when the write is complete. - */ - this.set = function(keyOrLocations, location) { - var 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."); - } - - var newData = {}; - - Object.keys(locations).forEach(function(key) { - validateKey(key); - - var location = locations[key]; - if (location === null) { - // Setting location to null is valid since it will remove the key - newData[key] = null; - } else { - validateLocation(location); - - var geohash = encodeGeohash(location); - newData[key] = encodeGeoFireObject(location, geohash); - } - }); - - return _firebaseRef.update(newData); - }; - - /** - * 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 {string} key The key of the location to retrieve. - * @return {Promise.>} A promise that is fulfilled with the location of the given key. - */ - this.get = function(key) { - validateKey(key); - return _firebaseRef.child(key).once("value").then(function(dataSnapshot) { - var snapshotVal = dataSnapshot.val(); - if (snapshotVal === null) { - return null; - } else { - return decodeGeoFireObject(snapshotVal); - } - }); - }; - - /** - * Removes the provided key from this GeoFire. Returns an empty promise fulfilled when the key has been removed. - * - * If the provided key is not in this GeoFire, the promise will still successfully resolve. - * - * @param {string} key The key of the location to remove. - * @return {Promise.} A promise that is fulfilled after the inputted key is removed. - */ - this.remove = function(key) { - return this.set(key, null); - }; - - /** - * Returns a new GeoQuery instance with the provided queryCriteria. - * - * @param {Object} queryCriteria The criteria which specifies the GeoQuery's center and radius. - * @return {GeoQuery} A new GeoQuery object. - */ - this.query = function(queryCriteria) { - return new GeoQuery(_firebaseRef, queryCriteria); - }; - - /*****************/ - /* CONSTRUCTOR */ - /*****************/ - if (Object.prototype.toString.call(firebaseRef) !== "[object Object]") { - throw new Error("firebaseRef must be an instance of Firebase"); - } - - var _firebaseRef = firebaseRef; -}; - -/** - * 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 {Array.} location1 The [latitude, longitude] pair of the first location. - * @param {Array.} location2 The [latitude, longitude] pair of the second location. - * @return {number} The distance, in kilometers, between the inputted locations. - */ -GeoFire.distance = function(location1, location2) { - 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/geoFire/geoCallbackRegistration.ts b/src/geoFire/geoCallbackRegistration.ts new file mode 100644 index 00000000..773b9996 --- /dev/null +++ b/src/geoFire/geoCallbackRegistration.ts @@ -0,0 +1,27 @@ +/** + * Creates a GeoCallbackRegistration instance. + */ +export class GeoCallbackRegistration { + /** + * @param _cancelCallback Callback to run when this callback registration is cancelled. + */ + constructor(private _cancelCallback: Function) { + if (Object.prototype.toString.call(this._cancelCallback) !== '[object Function]') { + throw new Error('callback must be a function'); + } + } + + /********************/ + /* PUBLIC METHODS */ + /********************/ + /** + * Cancels this callback registration so that it no longer fires its callback. This + * has no effect on any other callback registrations you may have created. + */ + public cancel(): void { + if (typeof this._cancelCallback !== 'undefined') { + this._cancelCallback(); + this._cancelCallback = undefined; + } + } +} \ No newline at end of file diff --git a/src/geoFire/geoFireUtils.ts b/src/geoFire/geoFireUtils.ts new file mode 100644 index 00000000..8a4d3d91 --- /dev/null +++ b/src/geoFire/geoFireUtils.ts @@ -0,0 +1,470 @@ +import * as firebase from 'firebase'; + +import { GeoFireObj, QueryCriteria } from './interfaces'; + +// Default geohash length +export const g_GEOHASH_PRECISION: number = 10; + +// Characters used in location geohashes +export const g_BASE32: string = '0123456789bcdefghjkmnpqrstuvwxyz'; + +// The meridional circumference of the earth in meters +export const g_EARTH_MERI_CIRCUMFERENCE: number = 40007860; + +// Length of a degree latitude at the equator +export const g_METERS_PER_DEGREE_LATITUDE: number = 110574; + +// Number of bits per geohash character +export const g_BITS_PER_CHAR: number = 5; + +// Maximum length of a geohash in bits +export const g_MAXIMUM_BITS_PRECISION: number = 22 * g_BITS_PER_CHAR; + +// Equatorial radius of the earth in meters +export const g_EARTH_EQ_RADIUS: number = 6378137.0; + +// The following value assumes a polar radius of +// const g_EARTH_POL_RADIUS = 6356752.3; +// The formulate to calculate g_E2 is +// g_E2 == (g_EARTH_EQ_RADIUS^2-g_EARTH_POL_RADIUS^2)/(g_EARTH_EQ_RADIUS^2) +// The exact value is used here to avoid rounding errors +export const g_E2: number = 0.00669447819799; + +// Cutoff for rounding errors on double calculations +export const g_EPSILON: number = 1e-12; + +Math.log2 = Math.log2 || function (x) { + return Math.log(x) / Math.log(2); +}; + +/** + * Validates the inputted key and throws an error if it is invalid. + * + * @param key The key to be verified. + */ +export function validateKey(key: string): void { + let error: string; + + if (typeof key !== 'string') { + error = 'key must be a string'; + } else if (key.length === 0) { + error = 'key cannot be the empty string'; + } else if (1 + g_GEOHASH_PRECISION + key.length > 755) { + // Firebase can only stored child paths up to 768 characters + // The child path for this key is at the least: 'i/key' + error = 'key is too long to be stored in Firebase'; + } else if (/[\[\].#$\/\u0000-\u001F\u007F]/.test(key)) { + // Firebase does not allow node keys to contain the following characters + error = 'key cannot contain any of the following characters: . # $ ] [ /'; + } + + if (typeof error !== 'undefined') { + throw new Error('Invalid GeoFire key \'' + key + '\': ' + error); + } +}; + +/** + * Validates the inputted location and throws an error if it is invalid. + * + * @param location The [latitude, longitude] pair to be verified. + */ +export function validateLocation(location: number[]): void { + let error: string; + + if (!Array.isArray(location)) { + error = 'location must be an array'; + } else if (location.length !== 2) { + error = 'expected array of length 2, got length ' + location.length; + } else { + const latitude = location[0]; + const longitude = location[1]; + + if (typeof latitude !== 'number' || isNaN(latitude)) { + error = 'latitude must be a number'; + } else if (latitude < -90 || latitude > 90) { + error = 'latitude must be within the range [-90, 90]'; + } else if (typeof longitude !== 'number' || isNaN(longitude)) { + error = 'longitude must be a number'; + } else if (longitude < -180 || longitude > 180) { + error = 'longitude must be within the range [-180, 180]'; + } + } + + if (typeof error !== 'undefined') { + throw new Error('Invalid GeoFire location \'' + location + '\': ' + error); + } +}; + +/** + * Validates the inputted geohash and throws an error if it is invalid. + * + * @param geohash The geohash to be validated. + */ +export function validateGeohash(geohash: string): void { + let error; + + if (typeof geohash !== 'string') { + error = 'geohash must be a string'; + } else if (geohash.length === 0) { + error = 'geohash cannot be the empty string'; + } else { + for (const letter of geohash) { + if (g_BASE32.indexOf(letter) === -1) { + error = 'geohash cannot contain \'' + letter + '\''; + } + } + } + + if (typeof error !== 'undefined') { + throw new Error('Invalid GeoFire geohash \'' + geohash + '\': ' + error); + } +}; + +/** + * Validates the inputted query criteria and throws an error if it is invalid. + * + * @param newQueryCriteria The criteria which specifies the query's center and/or radius. + * @param requireCenterAndRadius The criteria which center and radius required. + */ +export function validateCriteria(newQueryCriteria: QueryCriteria, requireCenterAndRadius: boolean = false): void { + if (typeof newQueryCriteria !== 'object') { + throw new Error('query criteria must be an object'); + } else if (typeof newQueryCriteria.center === 'undefined' && typeof newQueryCriteria.radius === 'undefined') { + throw new Error('radius and/or center must be specified'); + } else if (requireCenterAndRadius && (typeof newQueryCriteria.center === 'undefined' || typeof newQueryCriteria.radius === 'undefined')) { + throw new Error('query criteria for a new query must contain both a center and a radius'); + } + + // Throw an error if there are any extraneous attributes + const keys: string[] = Object.keys(newQueryCriteria); + for (const key of keys) { + if (key !== 'center' && key !== 'radius') { + throw new Error('Unexpected attribute \'' + key + '\' found in query criteria'); + } + } + + // Validate the 'center' attribute + if (typeof newQueryCriteria.center !== 'undefined') { + validateLocation(newQueryCriteria.center); + } + + // Validate the 'radius' attribute + if (typeof newQueryCriteria.radius !== 'undefined') { + if (typeof newQueryCriteria.radius !== 'number' || isNaN(newQueryCriteria.radius)) { + throw new Error('radius must be a number'); + } else if (newQueryCriteria.radius < 0) { + throw new Error('radius must be greater than or equal to 0'); + } + } +}; + +/** + * Converts degrees to radians. + * + * @param degrees The number of degrees to be converted to radians. + * @returns The number of radians equal to the inputted number of degrees. + */ +export function degreesToRadians(degrees: number): number { + if (typeof degrees !== 'number' || isNaN(degrees)) { + throw new Error('Error: degrees must be a number'); + } + + return (degrees * Math.PI / 180); +}; + +/** + * Generates a geohash of the specified precision/string length from the [latitude, longitude] + * pair, specified as an array. + * + * @param location The [latitude, longitude] pair to encode into a geohash. + * @param precision The length of the geohash to create. If no precision is specified, the + * global default is used. + * @returns The geohash of the inputted location. + */ +export function encodeGeohash(location: number[], precision: number = g_GEOHASH_PRECISION): string { + validateLocation(location); + if (typeof precision !== 'undefined') { + if (typeof precision !== 'number' || isNaN(precision)) { + throw new Error('precision must be a number'); + } else if (precision <= 0) { + throw new Error('precision must be greater than 0'); + } else if (precision > 22) { + throw new Error('precision cannot be greater than 22'); + } else if (Math.round(precision) !== precision) { + throw new Error('precision must be an integer'); + } + } + + const latitudeRange = { + min: -90, + max: 90 + }; + const longitudeRange = { + min: -180, + max: 180 + }; + let hash: string = ''; + let hashVal = 0; + let bits: number = 0; + let even: number | boolean = 1; + + while (hash.length < precision) { + const val = even ? location[1] : location[0]; + const range = even ? longitudeRange : latitudeRange; + const mid = (range.min + range.max) / 2; + + if (val > mid) { + hashVal = (hashVal << 1) + 1; + range.min = mid; + } else { + hashVal = (hashVal << 1) + 0; + range.max = mid; + } + + even = !even; + if (bits < 4) { + bits++; + } else { + bits = 0; + hash += g_BASE32[hashVal]; + hashVal = 0; + } + } + + return hash; +}; + +/** + * Calculates the number of degrees a given distance is at a given latitude. + * + * @param distance The distance to convert. + * @param latitude The latitude at which to calculate. + * @returns The number of degrees the distance corresponds to. + */ +export function metersToLongitudeDegrees(distance: number, latitude: number): number { + const radians = degreesToRadians(latitude); + const num = Math.cos(radians) * g_EARTH_EQ_RADIUS * Math.PI / 180; + const denom = 1 / Math.sqrt(1 - g_E2 * Math.sin(radians) * Math.sin(radians)); + const deltaDeg = num * denom; + if (deltaDeg < g_EPSILON) { + return distance > 0 ? 360 : 0; + } + else { + return Math.min(360, distance / deltaDeg); + } +}; + +/** + * Calculates the bits necessary to reach a given resolution, in meters, for the longitude at a + * given latitude. + * + * @param resolution The desired resolution. + * @param latitude The latitude used in the conversion. + * @return The bits necessary to reach a given resolution, in meters. + */ +export function longitudeBitsForResolution(resolution: number, latitude: number): number { + const degs = metersToLongitudeDegrees(resolution, latitude); + return (Math.abs(degs) > 0.000001) ? Math.max(1, Math.log2(360 / degs)) : 1; +}; + +/** + * Calculates the bits necessary to reach a given resolution, in meters, for the latitude. + * + * @param resolution The bits necessary to reach a given resolution, in meters. + * @returns Bits necessary to reach a given resolution, in meters, for the latitude. + */ +export function latitudeBitsForResolution(resolution: number): number { + return Math.min(Math.log2(g_EARTH_MERI_CIRCUMFERENCE / 2 / resolution), g_MAXIMUM_BITS_PRECISION); +}; + +/** + * Wraps the longitude to [-180,180]. + * + * @param longitude The longitude to wrap. + * @returns longitude The resulting longitude. + */ +export function wrapLongitude(longitude: number): number { + if (longitude <= 180 && longitude >= -180) { + return longitude; + } + const adjusted = longitude + 180; + if (adjusted > 0) { + return (adjusted % 360) - 180; + } + else { + return 180 - (-adjusted % 360); + } +}; + +/** + * Calculates the maximum number of bits of a geohash to get a bounding box that is larger than a + * given size at the given coordinate. + * + * @param coordinate The coordinate as a [latitude, longitude] pair. + * @param size The size of the bounding box. + * @returns The number of bits necessary for the geohash. + */ +export function boundingBoxBits(coordinate: number[], size: number): number { + const latDeltaDegrees = size / g_METERS_PER_DEGREE_LATITUDE; + const latitudeNorth = Math.min(90, coordinate[0] + latDeltaDegrees); + const latitudeSouth = Math.max(-90, coordinate[0] - latDeltaDegrees); + const bitsLat = Math.floor(latitudeBitsForResolution(size)) * 2; + const bitsLongNorth = Math.floor(longitudeBitsForResolution(size, latitudeNorth)) * 2 - 1; + const bitsLongSouth = Math.floor(longitudeBitsForResolution(size, latitudeSouth)) * 2 - 1; + return Math.min(bitsLat, bitsLongNorth, bitsLongSouth, g_MAXIMUM_BITS_PRECISION); +}; + +/** + * Calculates eight points on the bounding box and the center of a given circle. At least one + * geohash of these nine coordinates, truncated to a precision of at most radius, are guaranteed + * to be prefixes of any geohash that lies within the circle. + * + * @param center The center given as [latitude, longitude]. + * @param radius The radius of the circle. + * @returns The eight bounding box points. + */ +export function boundingBoxCoordinates(center: number[], radius: number): number[][] { + const latDegrees = radius / g_METERS_PER_DEGREE_LATITUDE; + const latitudeNorth = Math.min(90, center[0] + latDegrees); + const latitudeSouth = Math.max(-90, center[0] - latDegrees); + const longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth); + const longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth); + const longDegs = Math.max(longDegsNorth, longDegsSouth); + return [ + [center[0], center[1]], + [center[0], wrapLongitude(center[1] - longDegs)], + [center[0], wrapLongitude(center[1] + longDegs)], + [latitudeNorth, center[1]], + [latitudeNorth, wrapLongitude(center[1] - longDegs)], + [latitudeNorth, wrapLongitude(center[1] + longDegs)], + [latitudeSouth, center[1]], + [latitudeSouth, wrapLongitude(center[1] - longDegs)], + [latitudeSouth, wrapLongitude(center[1] + longDegs)] + ]; +}; + +/** + * Calculates the bounding box query for a geohash with x bits precision. + * + * @param geohash The geohash whose bounding box query to generate. + * @param bits The number of bits of precision. + * @returns A [start, end] pair of geohashes. + */ +export function geohashQuery(geohash: string, bits: number): string[] { + validateGeohash(geohash); + const precision = Math.ceil(bits / g_BITS_PER_CHAR); + if (geohash.length < precision) { + return [geohash, geohash + '~']; + } + geohash = geohash.substring(0, precision); + const base = geohash.substring(0, geohash.length - 1); + const lastValue = g_BASE32.indexOf(geohash.charAt(geohash.length - 1)); + const significantBits = bits - (base.length * g_BITS_PER_CHAR); + const unusedBits = (g_BITS_PER_CHAR - significantBits); + // delete unused bits + const startValue = (lastValue >> unusedBits) << unusedBits; + const endValue = startValue + (1 << unusedBits); + if (endValue > 31) { + return [base + g_BASE32[startValue], base + '~']; + } else { + return [base + g_BASE32[startValue], base + g_BASE32[endValue]]; + } +}; + +/** + * Calculates a set of queries to fully contain a given circle. A query is a [start, end] pair + * where any geohash is guaranteed to be lexiographically larger then start and smaller than end. + * + * @param center The center given as [latitude, longitude] pair. + * @param radius The radius of the circle. + * @return An array of geohashes containing a [start, end] pair. + */ +export function geohashQueries(center: number[], radius: number): string[][] { + validateLocation(center); + const queryBits = Math.max(1, boundingBoxBits(center, radius)); + const geohashPrecision = Math.ceil(queryBits / g_BITS_PER_CHAR); + const coordinates = boundingBoxCoordinates(center, radius); + const queries = coordinates.map(function (coordinate) { + return geohashQuery(encodeGeohash(coordinate, geohashPrecision), queryBits); + }); + // remove duplicates + return queries.filter(function (query, index) { + return !queries.some(function (other, otherIndex) { + return index > otherIndex && query[0] === other[0] && query[1] === other[1]; + }); + }); +}; + +/** + * Encodes a location and geohash as a GeoFire object. + * + * @param location The location as [latitude, longitude] pair. + * @param geohash The geohash of the location. + * @returns The location encoded as GeoFire object. + */ +export function encodeGeoFireObject(location: number[], geohash: string): GeoFireObj { + validateLocation(location); + validateGeohash(geohash); + return { '.priority': geohash, 'g': geohash, 'l': location }; +} + +/** + * Decodes the location given as GeoFire object. Returns null if decoding fails. + * + * @param geoFireObj The location encoded as GeoFire object. + * @returns The location as [latitude, longitude] pair or null if decoding fails. + */ +export function decodeGeoFireObject(geoFireObj: GeoFireObj): number[] { + if (geoFireObj && 'l' in geoFireObj && Array.isArray(geoFireObj.l) && geoFireObj.l.length === 2) { + return geoFireObj.l; + } else { + throw new Error('Unexpected location object encountered: ' + JSON.stringify(geoFireObj)); + } +} + +/** + * Returns the key of a Firebase snapshot across SDK versions. + * + * @param A Firebase snapshot. + * @returns The Firebase snapshot's key. + */ +export function geoFireGetKey(snapshot: firebase.database.DataSnapshot): string { + let key; + if (typeof snapshot.key === 'string' || snapshot.key === null) { + key = snapshot.key; + } else if (typeof snapshot.key === 'function') { + // @ts-ignore + key = snapshot.key(); + } else { + // @ts-ignore + key = snapshot.name(); + } + + return key; +} + +/** + * 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. + */ +export function distance(location1: number[], location2: number[]): 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; +}; \ No newline at end of file diff --git a/src/geoFire/geoQuery.ts b/src/geoFire/geoQuery.ts new file mode 100644 index 00000000..f37add18 --- /dev/null +++ b/src/geoFire/geoQuery.ts @@ -0,0 +1,527 @@ +import * as firebase from 'firebase'; + +import { GeoCallbackRegistration } from './geoCallbackRegistration'; +import { decodeGeoFireObject, distance, encodeGeohash, geoFireGetKey, geohashQueries, validateCriteria, validateLocation } from './geoFireUtils'; + +import { GeoQueryCallbacks, GeoQueryState, LocationTracked, QueryCriteria } from './interfaces'; + +/** + * Creates a GeoQuery instance. + */ +export class GeoQuery { + // Event callbacks + private _callbacks: GeoQueryCallbacks = { 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: { [name: string]: GeoQueryState } = {}; + // A dictionary of locations that a currently active in the queries + // Note that not all of these are currently within this query + private _locationsTracked: { [name: string]: LocationTracked } = {}; + private _radius: number; + + // Variables used to keep track of when to fire the 'ready' event + private _valueEventFired: boolean = false; + private _outstandingGeohashReadyEvents: any; + + private _geohashCleanupScheduled: boolean = false; + private _cleanUpCurrentGeohashesQueriedInterval: any; + private _cleanUpCurrentGeohashesQueriedTimeout = null; + + /** + * @param _firebaseRef A Firebase reference where the GeoFire data will be stored. + * @param queryCriteria The criteria which specifies the query's center and radius. + */ + constructor(private _firebaseRef: firebase.database.Reference, queryCriteria: QueryCriteria) { + // Firebase reference of the GeoFire which created this query + if (Object.prototype.toString.call(this._firebaseRef) !== '[object Object]') { + throw new Error('firebaseRef must be an instance of Firebase'); + } + + // 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. + 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 + * GeoFire instance. 'ready' fires when this query has loaded all of the initial data from GeoFire 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 GeoFire 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 = 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 GeoFire 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: GeoQueryState): void { + const queryRef = this._firebaseRef.orderByChild('g').startAt(query[0]).endAt(query[1]); + queryRef.off('child_added', queryState.childAddedCallback); + queryRef.off('child_removed', queryState.childRemovedCallback); + queryRef.off('child_changed', queryState.childChangedCallback); + queryRef.off('value', queryState.valueCallback); + } + + /** + * Callback for child added events. + * + * @param locationDataSnapshot A snapshot of the data stored for this location. + */ + private _childAddedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void { + this._updateLocation(geoFireGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); + } + + /** + * Callback for child changed events + * + * @param locationDataSnapshot A snapshot of the data stored for this location. + */ + private _childChangedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void { + this._updateLocation(geoFireGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); + } + + /** + * Callback for child removed events + * + * @param locationDataSnapshot A snapshot of the data stored for this location. + */ + private _childRemovedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void { + const key: string = geoFireGetKey(locationDataSnapshot); + if (key in this._locationsTracked) { + this._firebaseRef.child(key).once('value', (snapshot: firebase.database.DataSnapshot) => { + const location: number[] = (snapshot.val() === null) ? null : decodeGeoFireObject(snapshot.val()); + 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) { + const 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 (queryStr in this._currentGeohashesQueried) { + const 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 firebaseQuery: firebase.database.Query = this._firebaseRef.orderByChild('g').startAt(query[0]).endAt(query[1]); + + // For every new matching geohash, determine if we should fire the 'key_entered' event + const childAddedCallback = firebaseQuery.on('child_added', (a) => this._childAddedCallback(a)); + const childRemovedCallback = firebaseQuery.on('child_removed', (a) => this._childRemovedCallback(a)); + const childChangedCallback = firebaseQuery.on('child_changed', (a) => this._childChangedCallback(a)); + + // 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 'child_added' event fires. + const valueCallback = firebaseQuery.on('value', () => { + firebaseQuery.off('value', valueCallback); + this._geohashQueryReadyCallback(toQueryStr); + }); + + // Add the geohash query to the current geohashes queried dictionary and save its state + this._currentGeohashesQueried[toQueryStr] = { + active: true, + childAddedCallback, + childRemovedCallback, + childChangedCallback, + 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) ? 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 GeoFire or the query, this function will be called with null and performs + * any necessary cleanup. + * + * @param key The key of the geofire 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; + const wasInQuery: boolean = (key in this._locationsTracked) ? this._locationsTracked[key].isInQuery : false; + const oldLocation: number[] = (key in this._locationsTracked) ? this._locationsTracked[key].location : null; + + // Determine if the location is within this query + distanceFromCenter = 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, + distanceFromCenter, + 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/geoFire/index.ts b/src/geoFire/index.ts new file mode 100644 index 00000000..080cf75e --- /dev/null +++ b/src/geoFire/index.ts @@ -0,0 +1,147 @@ +/*! + * 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, distance, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from './geoFireUtils'; + +import { QueryCriteria } from './interfaces'; + +/** + * Creates a GeoFire instance. + */ +export class GeoFire { + /** + * @param _firebaseRef A Firebase reference where the GeoFire data will be stored. + */ + constructor(private _firebaseRef: firebase.database.Reference) { + if (Object.prototype.toString.call(this._firebaseRef) !== '[object Object]') { + throw new Error('firebaseRef must be an instance of Firebase'); + } + } + + /********************/ + /* 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._firebaseRef.child(key).once('value').then((dataSnapshot: firebase.database.DataSnapshot) => { + const snapshotVal = dataSnapshot.val(); + if (snapshotVal === null) { + return null; + } else { + return decodeGeoFireObject(snapshotVal); + } + }); + }; + + /** + * Returns the Firebase instance used to create this GeoFire instance. + * + * @returns The Firebase instance used to create this GeoFire instance. + */ + public ref(): firebase.database.Reference { + return this._firebaseRef; + }; + + /** + * Removes the provided key from this GeoFire. Returns an empty promise fulfilled when the key has been removed. + * + * If the provided key is not in this GeoFire, 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 Firebase. Returns an empty promise which is fulfilled when the write is complete. + * + * If any provided key already exists in this GeoFire, 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 { + 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.'); + } + + const newData = {}; + + Object.keys(locations).forEach((key) => { + validateKey(key); + + const location: number[] = locations[key]; + if (location === null) { + // Setting location to null is valid since it will remove the key + newData[key] = null; + } else { + validateLocation(location); + + const geohash: string = encodeGeohash(location); + newData[key] = encodeGeoFireObject(location, geohash); + } + }); + + return this._firebaseRef.update(newData); + }; + + /** + * Returns a new GeoQuery instance with the provided queryCriteria. + * + * @param queryCriteria The criteria which specifies the GeoQuery's center and radius. + * @return A new GeoQuery object. + */ + public query(queryCriteria: QueryCriteria): GeoQuery { + return new GeoQuery(this._firebaseRef, 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[]): number { + return distance(location1, location2); + }; +} diff --git a/src/geoFire/interfaces/geoFireObj.ts b/src/geoFire/interfaces/geoFireObj.ts new file mode 100644 index 00000000..2a387460 --- /dev/null +++ b/src/geoFire/interfaces/geoFireObj.ts @@ -0,0 +1,5 @@ +export interface GeoFireObj { + '.priority': string; + g: string; + l: number[]; +} \ No newline at end of file diff --git a/src/geoFire/interfaces/geoQueryCallbacks.ts b/src/geoFire/interfaces/geoQueryCallbacks.ts new file mode 100644 index 00000000..6b2d22b2 --- /dev/null +++ b/src/geoFire/interfaces/geoQueryCallbacks.ts @@ -0,0 +1,9 @@ +export type ReadyCallback = () => void; +export type KeyCallback = (key?: string, location?: number[], distanceFromCenter?: number) => void; + +export interface GeoQueryCallbacks { + ready: ReadyCallback[]; + key_entered: KeyCallback[]; + key_exited: KeyCallback[]; + key_moved: KeyCallback[]; +} \ No newline at end of file diff --git a/src/geoFire/interfaces/geoQueryState.ts b/src/geoFire/interfaces/geoQueryState.ts new file mode 100644 index 00000000..7abfde13 --- /dev/null +++ b/src/geoFire/interfaces/geoQueryState.ts @@ -0,0 +1,11 @@ +import * as firebase from 'firebase'; + +export type GeoQueryStateCallback = (a: firebase.database.DataSnapshot | null, b?: string) => any + +export interface GeoQueryState { + active: boolean; + childAddedCallback: GeoQueryStateCallback; + childRemovedCallback: GeoQueryStateCallback; + childChangedCallback: GeoQueryStateCallback; + valueCallback: GeoQueryStateCallback; +} \ No newline at end of file diff --git a/src/geoFire/interfaces/index.ts b/src/geoFire/interfaces/index.ts new file mode 100644 index 00000000..cbb7a6b6 --- /dev/null +++ b/src/geoFire/interfaces/index.ts @@ -0,0 +1,5 @@ +export * from './geoFireObj'; +export * from './geoQueryCallbacks'; +export * from './geoQueryState'; +export * from './locationTracked'; +export * from './queryCriteria'; \ No newline at end of file diff --git a/src/geoFire/interfaces/locationTracked.ts b/src/geoFire/interfaces/locationTracked.ts new file mode 100644 index 00000000..99c285f4 --- /dev/null +++ b/src/geoFire/interfaces/locationTracked.ts @@ -0,0 +1,6 @@ +export interface LocationTracked { + location: number[]; + distanceFromCenter: number; + isInQuery: boolean; + geohash: string; +} \ No newline at end of file diff --git a/src/geoFire/interfaces/queryCriteria.ts b/src/geoFire/interfaces/queryCriteria.ts new file mode 100644 index 00000000..806b4bac --- /dev/null +++ b/src/geoFire/interfaces/queryCriteria.ts @@ -0,0 +1,4 @@ +export interface QueryCriteria { + center?: number[]; + radius?: number; +} \ No newline at end of file diff --git a/src/geoFireUtils.js b/src/geoFireUtils.js deleted file mode 100644 index 30651839..00000000 --- a/src/geoFireUtils.js +++ /dev/null @@ -1,468 +0,0 @@ -// Default geohash length -var g_GEOHASH_PRECISION = 10; - -// Characters used in location geohashes -var g_BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"; - -// The meridional circumference of the earth in meters -var g_EARTH_MERI_CIRCUMFERENCE = 40007860; - -// Length of a degree latitude at the equator -var g_METERS_PER_DEGREE_LATITUDE = 110574; - -// Number of bits per geohash character -var g_BITS_PER_CHAR = 5; - -// Maximum length of a geohash in bits -var g_MAXIMUM_BITS_PRECISION = 22*g_BITS_PER_CHAR; - -// Equatorial radius of the earth in meters -var g_EARTH_EQ_RADIUS = 6378137.0; - -// The following value assumes a polar radius of -// var g_EARTH_POL_RADIUS = 6356752.3; -// The formulate to calculate g_E2 is -// g_E2 == (g_EARTH_EQ_RADIUS^2-g_EARTH_POL_RADIUS^2)/(g_EARTH_EQ_RADIUS^2) -// The exact value is used here to avoid rounding errors -var g_E2 = 0.00669447819799; - -// Cutoff for rounding errors on double calculations -var g_EPSILON = 1e-12; - -Math.log2 = Math.log2 || function(x) { - return Math.log(x)/Math.log(2); -}; - -/** - * Validates the inputted key and throws an error if it is invalid. - * - * @param {string} key The key to be verified. - */ -var validateKey = function(key) { - var error; - - if (typeof key !== "string") { - error = "key must be a string"; - } - else if (key.length === 0) { - error = "key cannot be the empty string"; - } - else if (1 + g_GEOHASH_PRECISION + key.length > 755) { - // Firebase can only stored child paths up to 768 characters - // The child path for this key is at the least: "i/key" - error = "key is too long to be stored in Firebase"; - } - else if (/[\[\].#$\/\u0000-\u001F\u007F]/.test(key)) { - // Firebase does not allow node keys to contain the following characters - error = "key cannot contain any of the following characters: . # $ ] [ /"; - } - - if (typeof error !== "undefined") { - throw new Error("Invalid GeoFire key '" + key + "': " + error); - } -}; - -/** - * Validates the inputted location and throws an error if it is invalid. - * - * @param {Array.} location The [latitude, longitude] pair to be verified. - */ -var validateLocation = function(location) { - var error; - - if (!Array.isArray(location)) { - error = "location must be an array"; - } - else if (location.length !== 2) { - error = "expected array of length 2, got length " + location.length; - } - else { - var latitude = location[0]; - var longitude = location[1]; - - if (typeof latitude !== "number" || isNaN(latitude)) { - error = "latitude must be a number"; - } - else if (latitude < -90 || latitude > 90) { - error = "latitude must be within the range [-90, 90]"; - } - else if (typeof longitude !== "number" || isNaN(longitude)) { - error = "longitude must be a number"; - } - else if (longitude < -180 || longitude > 180) { - error = "longitude must be within the range [-180, 180]"; - } - } - - if (typeof error !== "undefined") { - throw new Error("Invalid GeoFire location '" + location + "': " + error); - } -}; - -/** - * Validates the inputted geohash and throws an error if it is invalid. - * - * @param {string} geohash The geohash to be validated. - */ -var validateGeohash = function(geohash) { - var error; - - if (typeof geohash !== "string") { - error = "geohash must be a string"; - } - else if (geohash.length === 0) { - error = "geohash cannot be the empty string"; - } - else { - for (var i = 0, length = geohash.length; i < length; ++i) { - if (g_BASE32.indexOf(geohash[i]) === -1) { - error = "geohash cannot contain \"" + geohash[i] + "\""; - } - } - } - - if (typeof error !== "undefined") { - throw new Error("Invalid GeoFire geohash '" + geohash + "': " + error); - } -}; - -/** - * Validates the inputted query criteria and throws an error if it is invalid. - * - * @param {Object} newQueryCriteria The criteria which specifies the query's center and/or radius. - */ -var validateCriteria = function(newQueryCriteria, requireCenterAndRadius) { - if (typeof newQueryCriteria !== "object") { - throw new Error("query criteria must be an object"); - } - else if (typeof newQueryCriteria.center === "undefined" && typeof newQueryCriteria.radius === "undefined") { - throw new Error("radius and/or center must be specified"); - } - else if (requireCenterAndRadius && (typeof newQueryCriteria.center === "undefined" || typeof newQueryCriteria.radius === "undefined")) { - throw new Error("query criteria for a new query must contain both a center and a radius"); - } - - // Throw an error if there are any extraneous attributes - var keys = Object.keys(newQueryCriteria); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var key = keys[i]; - if (key !== "center" && key !== "radius") { - throw new Error("Unexpected attribute '" + key + "'' found in query criteria"); - } - } - - // Validate the "center" attribute - if (typeof newQueryCriteria.center !== "undefined") { - validateLocation(newQueryCriteria.center); - } - - // Validate the "radius" attribute - if (typeof newQueryCriteria.radius !== "undefined") { - if (typeof newQueryCriteria.radius !== "number" || isNaN(newQueryCriteria.radius)) { - throw new Error("radius must be a number"); - } - else if (newQueryCriteria.radius < 0) { - throw new Error("radius must be greater than or equal to 0"); - } - } -}; - -/** - * Converts degrees to radians. - * - * @param {number} degrees The number of degrees to be converted to radians. - * @return {number} The number of radians equal to the inputted number of degrees. - */ -var degreesToRadians = function(degrees) { - if (typeof degrees !== "number" || isNaN(degrees)) { - throw new Error("Error: degrees must be a number"); - } - - return (degrees * Math.PI / 180); -}; - -/** - * Generates a geohash of the specified precision/string length from the [latitude, longitude] - * pair, specified as an array. - * - * @param {Array.} location The [latitude, longitude] pair to encode into a geohash. - * @param {number=} precision The length of the geohash to create. If no precision is - * specified, the global default is used. - * @return {string} The geohash of the inputted location. - */ -var encodeGeohash = function(location, precision) { - validateLocation(location); - if (typeof precision !== "undefined") { - if (typeof precision !== "number" || isNaN(precision)) { - throw new Error("precision must be a number"); - } - else if (precision <= 0) { - throw new Error("precision must be greater than 0"); - } - else if (precision > 22) { - throw new Error("precision cannot be greater than 22"); - } - else if (Math.round(precision) !== precision) { - throw new Error("precision must be an integer"); - } - } - - // Use the global precision default if no precision is specified - precision = precision || g_GEOHASH_PRECISION; - - var latitudeRange = { - min: -90, - max: 90 - }; - var longitudeRange = { - min: -180, - max: 180 - }; - var hash = ""; - var hashVal = 0; - var bits = 0; - var even = 1; - - while (hash.length < precision) { - var val = even ? location[1] : location[0]; - var range = even ? longitudeRange : latitudeRange; - var mid = (range.min + range.max) / 2; - - /* jshint -W016 */ - if (val > mid) { - hashVal = (hashVal << 1) + 1; - range.min = mid; - } - else { - hashVal = (hashVal << 1) + 0; - range.max = mid; - } - /* jshint +W016 */ - - even = !even; - if (bits < 4) { - bits++; - } - else { - bits = 0; - hash += g_BASE32[hashVal]; - hashVal = 0; - } - } - - return hash; -}; - -/** - * Calculates the number of degrees a given distance is at a given latitude. - * - * @param {number} distance The distance to convert. - * @param {number} latitude The latitude at which to calculate. - * @return {number} The number of degrees the distance corresponds to. - */ -var metersToLongitudeDegrees = function(distance, latitude) { - var radians = degreesToRadians(latitude); - var num = Math.cos(radians)*g_EARTH_EQ_RADIUS*Math.PI/180; - var denom = 1/Math.sqrt(1-g_E2*Math.sin(radians)*Math.sin(radians)); - var deltaDeg = num*denom; - if (deltaDeg < g_EPSILON) { - return distance > 0 ? 360 : 0; - } - else { - return Math.min(360, distance/deltaDeg); - } -}; - -/** - * Calculates the bits necessary to reach a given resolution, in meters, for the longitude at a - * given latitude. - * - * @param {number} resolution The desired resolution. - * @param {number} latitude The latitude used in the conversion. - * @return {number} The bits necessary to reach a given resolution, in meters. - */ -var longitudeBitsForResolution = function(resolution, latitude) { - var degs = metersToLongitudeDegrees(resolution, latitude); - return (Math.abs(degs) > 0.000001) ? Math.max(1, Math.log2(360/degs)) : 1; -}; - -/** - * Calculates the bits necessary to reach a given resolution, in meters, for the latitude. - * - * @param {number} resolution The bits necessary to reach a given resolution, in meters. - */ -var latitudeBitsForResolution = function(resolution) { - return Math.min(Math.log2(g_EARTH_MERI_CIRCUMFERENCE/2/resolution), g_MAXIMUM_BITS_PRECISION); -}; - -/** - * Wraps the longitude to [-180,180]. - * - * @param {number} longitude The longitude to wrap. - * @return {number} longitude The resulting longitude. - */ -var wrapLongitude = function(longitude) { - if (longitude <= 180 && longitude >= -180) { - return longitude; - } - var adjusted = longitude + 180; - if (adjusted > 0) { - return (adjusted % 360) - 180; - } - else { - return 180 - (-adjusted % 360); - } -}; - -/** - * Calculates the maximum number of bits of a geohash to get a bounding box that is larger than a - * given size at the given coordinate. - * - * @param {Array.} coordinate The coordinate as a [latitude, longitude] pair. - * @param {number} size The size of the bounding box. - * @return {number} The number of bits necessary for the geohash. - */ -var boundingBoxBits = function(coordinate, size) { - var latDeltaDegrees = size/g_METERS_PER_DEGREE_LATITUDE; - var latitudeNorth = Math.min(90, coordinate[0] + latDeltaDegrees); - var latitudeSouth = Math.max(-90, coordinate[0] - latDeltaDegrees); - var bitsLat = Math.floor(latitudeBitsForResolution(size))*2; - var bitsLongNorth = Math.floor(longitudeBitsForResolution(size, latitudeNorth))*2-1; - var bitsLongSouth = Math.floor(longitudeBitsForResolution(size, latitudeSouth))*2-1; - return Math.min(bitsLat, bitsLongNorth, bitsLongSouth, g_MAXIMUM_BITS_PRECISION); -}; - -/** - * Calculates eight points on the bounding box and the center of a given circle. At least one - * geohash of these nine coordinates, truncated to a precision of at most radius, are guaranteed - * to be prefixes of any geohash that lies within the circle. - * - * @param {Array.} center The center given as [latitude, longitude]. - * @param {number} radius The radius of the circle. - * @return {Array.>} The eight bounding box points. - */ -var boundingBoxCoordinates = function(center, radius) { - var latDegrees = radius/g_METERS_PER_DEGREE_LATITUDE; - var latitudeNorth = Math.min(90, center[0] + latDegrees); - var latitudeSouth = Math.max(-90, center[0] - latDegrees); - var longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth); - var longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth); - var longDegs = Math.max(longDegsNorth, longDegsSouth); - return [ - [center[0], center[1]], - [center[0], wrapLongitude(center[1] - longDegs)], - [center[0], wrapLongitude(center[1] + longDegs)], - [latitudeNorth, center[1]], - [latitudeNorth, wrapLongitude(center[1] - longDegs)], - [latitudeNorth, wrapLongitude(center[1] + longDegs)], - [latitudeSouth, center[1]], - [latitudeSouth, wrapLongitude(center[1] - longDegs)], - [latitudeSouth, wrapLongitude(center[1] + longDegs)] - ]; -}; - -/** - * Calculates the bounding box query for a geohash with x bits precision. - * - * @param {string} geohash The geohash whose bounding box query to generate. - * @param {number} bits The number of bits of precision. - * @return {Array.} A [start, end] pair of geohashes. - */ -var geohashQuery = function(geohash, bits) { - validateGeohash(geohash); - var precision = Math.ceil(bits/g_BITS_PER_CHAR); - if (geohash.length < precision) { - return [geohash, geohash+"~"]; - } - geohash = geohash.substring(0, precision); - var base = geohash.substring(0, geohash.length - 1); - var lastValue = g_BASE32.indexOf(geohash.charAt(geohash.length - 1)); - var significantBits = bits - (base.length*g_BITS_PER_CHAR); - var unusedBits = (g_BITS_PER_CHAR - significantBits); - /*jshint bitwise: false*/ - // delete unused bits - var startValue = (lastValue >> unusedBits) << unusedBits; - var endValue = startValue + (1 << unusedBits); - /*jshint bitwise: true*/ - if (endValue > 31) { - return [base+g_BASE32[startValue], base+"~"]; - } - else { - return [base+g_BASE32[startValue], base+g_BASE32[endValue]]; - } -}; - -/** - * Calculates a set of queries to fully contain a given circle. A query is a [start, end] pair - * where any geohash is guaranteed to be lexiographically larger then start and smaller than end. - * - * @param {Array.} center The center given as [latitude, longitude] pair. - * @param {number} radius The radius of the circle. - * @return {Array.>} An array of geohashes containing a [start, end] pair. - */ -var geohashQueries = function(center, radius) { - validateLocation(center); - var queryBits = Math.max(1, boundingBoxBits(center, radius)); - var geohashPrecision = Math.ceil(queryBits/g_BITS_PER_CHAR); - var coordinates = boundingBoxCoordinates(center, radius); - var queries = coordinates.map(function(coordinate) { - return geohashQuery(encodeGeohash(coordinate, geohashPrecision), queryBits); - }); - // remove duplicates - return queries.filter(function(query, index) { - return !queries.some(function(other, otherIndex) { - return index > otherIndex && query[0] === other[0] && query[1] === other[1]; - }); - }); -}; - -/** - * Encodes a location and geohash as a GeoFire object. - * - * @param {Array.} location The location as [latitude, longitude] pair. - * @param {string} geohash The geohash of the location. - * @return {Object} The location encoded as GeoFire object. - */ -function encodeGeoFireObject(location, geohash) { - validateLocation(location); - validateGeohash(geohash); - return { - ".priority": geohash, - "g": geohash, - "l": location - }; -} - -/** - * Decodes the location given as GeoFire object. Returns null if decoding fails. - * - * @param {Object} geoFireObj The location encoded as GeoFire object. - * @return {?Array.} location The location as [latitude, longitude] pair or null if - * decoding fails. - */ -function decodeGeoFireObject(geoFireObj) { - if (geoFireObj !== null && geoFireObj.hasOwnProperty("l") && Array.isArray(geoFireObj.l) && geoFireObj.l.length === 2) { - return geoFireObj.l; - } else { - throw new Error("Unexpected GeoFire location object encountered: " + JSON.stringify(geoFireObj)); - } -} - -/** - * Returns the key of a Firebase snapshot across SDK versions. - * - * @param {DataSnapshot} snapshot A Firebase snapshot. - * @return {string|null} key The Firebase snapshot's key. - */ - function getKey(snapshot) { - var key; - if (typeof snapshot.key === "function") { - key = snapshot.key(); - } else if (typeof snapshot.key === "string" || snapshot.key === null) { - key = snapshot.key; - } else { - key = snapshot.name(); - } - return key; - } diff --git a/src/geoQuery.js b/src/geoQuery.js deleted file mode 100644 index 73c4c8f1..00000000 --- a/src/geoQuery.js +++ /dev/null @@ -1,560 +0,0 @@ -/** - * Creates a GeoQuery instance. - * - * @constructor - * @this {GeoQuery} - * @param {Firebase} firebaseRef A Firebase reference. - * @param {Object} queryCriteria The criteria which specifies the query's center and radius. - */ -var GeoQuery = function (firebaseRef, queryCriteria) { - /*********************/ - /* PRIVATE METHODS */ - /*********************/ - /** - * Fires each callback for the provided eventType, passing it provided key's data. - * - * @param {string} eventType The event type whose callbacks to fire. One of "key_entered", "key_exited", or "key_moved". - * @param {string} key The key of the location for which to fire the callbacks. - * @param {?Array.} location The location as [latitude, longitude] pair - * @param {?double} distanceFromCenter The distance from the center or null. - */ - function _fireCallbacksForKey(eventType, key, location, distanceFromCenter) { - _callbacks[eventType].forEach(function(callback) { - if (typeof location === "undefined" || location === null) { - callback(key, null, null); - } - else { - callback(key, location, distanceFromCenter); - } - }); - } - - /** - * Fires each callback for the "ready" event. - */ - function _fireReadyEventCallbacks() { - _callbacks.ready.forEach(function(callback) { - callback(); - }); - } - - /** - * Decodes a query string to a query - * - * @param {string} str The encoded query. - * @return {Array.} The decoded query as a [start, end] pair. - */ - function _stringToQuery(string) { - var decoded = string.split(":"); - if (decoded.length !== 2) { - throw new Error("Invalid internal state! Not a valid geohash query: " + string); - } - return decoded; - } - - /** - * Encodes a query as a string for easier indexing and equality. - * - * @param {Array.} query The query to encode. - * @param {string} The encoded query as string. - */ - function _queryToString(query) { - if (query.length !== 2) { - throw new Error("Not a valid geohash query: " + query); - } - return query[0]+":"+query[1]; - } - - /** - * Turns off all callbacks for the provide geohash query. - * - * @param {Array.} query The geohash query. - * @param {Object} queryState An object storing the current state of the query. - */ - function _cancelGeohashQuery(query, queryState) { - var queryRef = _firebaseRef.orderByChild("g").startAt(query[0]).endAt(query[1]); - queryRef.off("child_added", queryState.childAddedCallback); - queryRef.off("child_removed", queryState.childRemovedCallback); - queryRef.off("child_changed", queryState.childChangedCallback); - queryRef.off("value", queryState.valueCallback); - } - - /** - * Removes unnecessary Firebase queries which are currently being queried. - */ - function _cleanUpCurrentGeohashesQueried() { - var keys = Object.keys(_currentGeohashesQueried); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var geohashQueryStr = keys[i]; - var queryState = _currentGeohashesQueried[geohashQueryStr]; - if (queryState.active === false) { - var query = _stringToQuery(geohashQueryStr); - // Delete the geohash since it should no longer be queried - _cancelGeohashQuery(query, queryState); - delete _currentGeohashesQueried[geohashQueryStr]; - } - } - - // Delete each location which should no longer be queried - keys = Object.keys(_locationsTracked); - numKeys = keys.length; - for (i = 0; i < numKeys; ++i) { - var key = keys[i]; - if (!_geohashInSomeQuery(_locationsTracked[key].geohash)) { - if (_locationsTracked[key].isInQuery) { - throw new Error("Internal State error, trying to remove location that is still in query"); - } - delete _locationsTracked[key]; - } - } - - // Specify that this is done cleaning up the current geohashes queried - _geohashCleanupScheduled = false; - - // Cancel any outstanding scheduled cleanup - if (_cleanUpCurrentGeohashesQueriedTimeout !== null) { - clearTimeout(_cleanUpCurrentGeohashesQueriedTimeout); - _cleanUpCurrentGeohashesQueriedTimeout = null; - } - } - - /** - * 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 GeoFire or the query, this function will be called with null and performs - * any necessary cleanup. - * - * @param {string} key The key of the geofire location. - * @param {?Array.} location The location as [latitude, longitude] pair. - */ - function _updateLocation(key, location) { - validateLocation(location); - // Get the key and location - var distanceFromCenter, isInQuery; - var wasInQuery = (_locationsTracked.hasOwnProperty(key)) ? _locationsTracked[key].isInQuery : false; - var oldLocation = (_locationsTracked.hasOwnProperty(key)) ? _locationsTracked[key].location : null; - - // Determine if the location is within this query - distanceFromCenter = GeoFire.distance(location, _center); - isInQuery = (distanceFromCenter <= _radius); - - // Add this location to the locations queried dictionary even if it is not within this query - _locationsTracked[key] = { - location: location, - distanceFromCenter: distanceFromCenter, - isInQuery: isInQuery, - geohash: encodeGeohash(location, g_GEOHASH_PRECISION) - }; - - // Fire the "key_entered" event if the provided key has entered this query - if (isInQuery && !wasInQuery) { - _fireCallbacksForKey("key_entered", key, location, distanceFromCenter); - } else if (isInQuery && oldLocation !== null && (location[0] !== oldLocation[0] || location[1] !== oldLocation[1])) { - _fireCallbacksForKey("key_moved", key, location, distanceFromCenter); - } else if (!isInQuery && wasInQuery) { - _fireCallbacksForKey("key_exited", key, location, distanceFromCenter); - } - } - - /** - * Checks if this geohash is currently part of any of the geohash queries. - * - * @param {string} geohash The geohash. - * @param {boolean} Returns true if the geohash is part of any of the current geohash queries. - */ - function _geohashInSomeQuery(geohash) { - var keys = Object.keys(_currentGeohashesQueried); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var queryStr = keys[i]; - if (_currentGeohashesQueried.hasOwnProperty(queryStr)) { - var query = _stringToQuery(queryStr); - if (geohash >= query[0] && geohash <= query[1]) { - return true; - } - } - } - return false; - } - - /** - * Removes the location from the local state and fires any events if necessary. - * - * @param {string} key The key to be removed. - * @param {?Array.} currentLocation The current location as [latitude, longitude] pair - * or null if removed. - */ - function _removeLocation(key, currentLocation) { - var locationDict = _locationsTracked[key]; - delete _locationsTracked[key]; - if (typeof locationDict !== "undefined" && locationDict.isInQuery) { - var distanceFromCenter = (currentLocation) ? GeoFire.distance(currentLocation, _center) : null; - _fireCallbacksForKey("key_exited", key, currentLocation, distanceFromCenter); - } - } - - /** - * Callback for child added events. - * - * @param {Firebase DataSnapshot} locationDataSnapshot A snapshot of the data stored for this location. - */ - function _childAddedCallback(locationDataSnapshot) { - _updateLocation(getKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); - } - - /** - * Callback for child changed events - * - * @param {Firebase DataSnapshot} locationDataSnapshot A snapshot of the data stored for this location. - */ - function _childChangedCallback(locationDataSnapshot) { - _updateLocation(getKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val())); - } - - /** - * Callback for child removed events - * - * @param {Firebase DataSnapshot} locationDataSnapshot A snapshot of the data stored for this location. - */ - function _childRemovedCallback(locationDataSnapshot) { - var key = getKey(locationDataSnapshot); - if (_locationsTracked.hasOwnProperty(key)) { - _firebaseRef.child(key).once("value", function(snapshot) { - var location = snapshot.val() === null ? null : decodeGeoFireObject(snapshot.val()); - var geohash = (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 (!_geohashInSomeQuery(geohash)) { - _removeLocation(key, location); - } - }); - } - } - - /** - * Called once all geohash queries have received all child added events and fires the ready - * event if necessary. - */ - function _geohashQueryReadyCallback(queryStr) { - var index = _outstandingGeohashReadyEvents.indexOf(queryStr); - if (index > -1) { - _outstandingGeohashReadyEvents.splice(index, 1); - } - _valueEventFired = (_outstandingGeohashReadyEvents.length === 0); - - // If all queries have been processed, fire the ready event - if (_valueEventFired) { - _fireReadyEventCallbacks(); - } - } - - /** - * Attaches listeners to Firebase which track when new geohashes are added within this query's - * bounding box. - */ - function _listenForNewGeohashes() { - // Get the list of geohashes to query - var geohashesToQuery = geohashQueries(_center, _radius*1000).map(_queryToString); - - // Filter out duplicate geohashes - geohashesToQuery = geohashesToQuery.filter(function(geohash, i){ - return 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. - var keys = Object.keys(_currentGeohashesQueried); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var geohashQueryStr = keys[i]; - var index = geohashesToQuery.indexOf(geohashQueryStr); - if (index === -1) { - _currentGeohashesQueried[geohashQueryStr].active = false; - } - else { - _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 (_geohashCleanupScheduled === false && Object.keys(_currentGeohashesQueried).length > 25) { - _geohashCleanupScheduled = true; - _cleanUpCurrentGeohashesQueriedTimeout = setTimeout(_cleanUpCurrentGeohashesQueried, 10); - } - - // Keep track of which geohashes have been processed so we know when to fire the "ready" event - _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(function(toQueryStr) { - // decode the geohash query string - var query = _stringToQuery(toQueryStr); - - // Create the Firebase query - var firebaseQuery = _firebaseRef.orderByChild("g").startAt(query[0]).endAt(query[1]); - - // For every new matching geohash, determine if we should fire the "key_entered" event - var childAddedCallback = firebaseQuery.on("child_added", _childAddedCallback); - var childRemovedCallback = firebaseQuery.on("child_removed", _childRemovedCallback); - var childChangedCallback = firebaseQuery.on("child_changed", _childChangedCallback); - - // 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 "child_added" event fires. - var valueCallback = firebaseQuery.on("value", function() { - firebaseQuery.off("value", valueCallback); - _geohashQueryReadyCallback(toQueryStr); - }); - - // Add the geohash query to the current geohashes queried dictionary and save its state - _currentGeohashesQueried[toQueryStr] = { - active: true, - childAddedCallback: childAddedCallback, - childRemovedCallback: childRemovedCallback, - childChangedCallback: childChangedCallback, - 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) { - _geohashQueryReadyCallback(); - } - } - - /********************/ - /* PUBLIC METHODS */ - /********************/ - /** - * Returns the location signifying the center of this query. - * - * @return {Array.} The [latitude, longitude] pair signifying the center of this query. - */ - this.center = function() { - return _center; - }; - - /** - * Returns the radius of this query, in kilometers. - * - * @return {number} The radius of this query, in kilometers. - */ - this.radius = function() { - return _radius; - }; - - /** - * Updates the criteria for this query. - * - * @param {Object} newQueryCriteria The criteria which specifies the query's center and radius. - */ - this.updateCriteria = function(newQueryCriteria) { - // Validate and save the new query criteria - validateCriteria(newQueryCriteria); - _center = newQueryCriteria.center || _center; - _radius = newQueryCriteria.radius || _radius; - - // Loop through all of the locations in the query, update their distance from the center of the - // query, and fire any appropriate events - var keys = Object.keys(_locationsTracked); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var key = keys[i]; - - // If the query was cancelled while going through this loop, stop updating locations and stop - // firing events - if (_cancelled === true) { - break; - } - - // Get the cached information for this location - var locationDict = _locationsTracked[key]; - - // Save if the location was already in the query - var wasAlreadyInQuery = locationDict.isInQuery; - - // Update the location's distance to the new query center - locationDict.distanceFromCenter = GeoFire.distance(locationDict.location, _center); - - // Determine if the location is now in this query - locationDict.isInQuery = (locationDict.distanceFromCenter <= _radius); - - // If the location just left the query, fire the "key_exited" callbacks - if (wasAlreadyInQuery && !locationDict.isInQuery) { - _fireCallbacksForKey("key_exited", key, locationDict.location, locationDict.distanceFromCenter); - } - - // If the location just entered the query, fire the "key_entered" callbacks - else if (!wasAlreadyInQuery && locationDict.isInQuery) { - _fireCallbacksForKey("key_entered", key, locationDict.location, locationDict.distanceFromCenter); - } - } - - // Reset the variables which control when the "ready" event fires - _valueEventFired = false; - - // Listen for new geohashes being added to GeoFire and fire the appropriate events - _listenForNewGeohashes(); - }; - - /** - * 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 - * GeoFire instance. "ready" fires when this query has loaded all of the initial data from GeoFire 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 GeoFire 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 {string} eventType The event type for which to attach the callback. One of "ready", "key_entered", - * "key_exited", or "key_moved". - * @callback callback Callback function to be called when an event of type eventType fires. - * @return {GeoCallbackRegistration} A callback registration which can be used to cancel the provided callback. - */ - this.on = function(eventType, callback) { - // 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 - _callbacks[eventType].push(callback); - - // If this is a "key_entered" callback, fire it for every location already within this query - if (eventType === "key_entered") { - var keys = Object.keys(_locationsTracked); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var key = keys[i]; - var locationDict = _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") { - if (_valueEventFired) { - callback(); - } - } - - // Return an event registration which can be used to cancel the callback - return new GeoCallbackRegistration(function() { - _callbacks[eventType].splice(_callbacks[eventType].indexOf(callback), 1); - }); - }; - - /** - * 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. - */ - this.cancel = function () { - // Mark this query as cancelled - _cancelled = true; - - // Cancel all callbacks in this query's callback list - _callbacks = { - ready: [], - key_entered: [], - key_exited: [], - key_moved: [] - }; - - // Turn off all Firebase listeners for the current geohashes being queried - var keys = Object.keys(_currentGeohashesQueried); - var numKeys = keys.length; - for (var i = 0; i < numKeys; ++i) { - var geohashQueryStr = keys[i]; - var query = _stringToQuery(geohashQueryStr); - _cancelGeohashQuery(query, _currentGeohashesQueried[geohashQueryStr]); - delete _currentGeohashesQueried[geohashQueryStr]; - } - - // Delete any stored locations - _locationsTracked = {}; - - // Turn off the current geohashes queried clean up interval - clearInterval(_cleanUpCurrentGeohashesQueriedInterval); - }; - - - /*****************/ - /* CONSTRUCTOR */ - /*****************/ - // Firebase reference of the GeoFire which created this query - if (Object.prototype.toString.call(firebaseRef) !== "[object Object]") { - throw new Error("firebaseRef must be an instance of Firebase"); - } - var _firebaseRef = firebaseRef; - - // Event callbacks - var _callbacks = { - ready: [], - key_entered: [], - key_exited: [], - key_moved: [] - }; - - // Variable to track when the query is cancelled - var _cancelled = false; - - // Variables used to keep track of when to fire the "ready" event - var _valueEventFired = false; - var _outstandingGeohashReadyEvents; - - // A dictionary of locations that a currently active in the queries - // Note that not all of these are currently within this query - var _locationsTracked = {}; - - // A dictionary of geohash queries which currently have an active callbacks - var _currentGeohashesQueried = {}; - - // 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. - var _geohashCleanupScheduled = false; - var _cleanUpCurrentGeohashesQueriedTimeout = null; - var _cleanUpCurrentGeohashesQueriedInterval = setInterval(function() { - if (_geohashCleanupScheduled === false) { - _cleanUpCurrentGeohashesQueried(); - } - }, 10000); - - // Validate and save the query criteria - validateCriteria(queryCriteria, /* requireCenterAndRadius */ true); - var _center = queryCriteria.center; - var _radius = queryCriteria.radius; - - // Listen for new geohashes being added around this query and fire the appropriate events - _listenForNewGeohashes(); -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..665071ad --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export { GeoCallbackRegistration } from './geoFire/geoCallbackRegistration'; +export { GeoFire } from './geoFire'; +export { GeoQuery } from './geoFire/geoQuery'; +export { GeoFireObj, QueryCriteria } from './geoFire/interfaces'; \ No newline at end of file diff --git a/test/common.ts b/test/common.ts new file mode 100644 index 00000000..8b2dec1a --- /dev/null +++ b/test/common.ts @@ -0,0 +1,131 @@ +import * as chai from 'chai'; +import * as firebase from 'firebase'; + +import { GeoFire } from '../src/geoFire'; +import { GeoQuery } from '../src/geoFire/geoQuery'; + + +/*************/ +/* GLOBALS */ +/*************/ +const expect = chai.expect; +// Define examples of valid and invalid parameters +export const invalidFirebaseRefs = [null, undefined, NaN, true, false, [], 0, 5, '', 'a', ['hi', 1]]; +export const validKeys = ['a', 'loc1', '(e@Xi:4t>*E2)hc<5oa:1s6{B0d?u', Array(700).join('a')]; +export const invalidKeys = ['', true, false, null, undefined, { a: 1 }, 'loc.1', 'loc$1', '[loc1', 'loc1]', 'loc#1', 'loc/1', 'a#i]$da[s', 'te/nst', 'te/rst', 'te/u0000st', 'te/u0015st', 'te/007Fst', Array(800).join('a')]; +export const validLocations = [[0, 0], [-90, 180], [90, -180], [23, 74], [47.235124363, 127.2379654226]]; +export const invalidLocations = [[-91, 0], [91, 0], [0, 181], [0, -181], [[0, 0], 0], ['a', 0], [0, 'a'], ['a', 'a'], [NaN, 0], [0, NaN], [undefined, NaN], [null, 0], [null, null], [0, undefined], [undefined, undefined], '', 'a', true, false, [], [1], {}, { a: 1 }, null, undefined, NaN]; +export const validGeohashes = ['4', 'd62dtu', '000000000000']; +export const invalidGeohashes = ['', 'aaa', 1, true, false, [], [1], {}, { a: 1 }, null, undefined, NaN]; +export const validQueryCriterias = [{ center: [0, 0], radius: 1000 }, { center: [1, -180], radius: 1.78 }, { center: [22.22, -107.77], radius: 0 }, { center: [0, 0] }, { center: [1, -180] }, { center: [22.22, -107.77] }, { radius: 1000 }, { radius: 1.78 }, { radius: 0 }]; +export const invalidQueryCriterias = [{}, { random: 100 }, { center: [91, 2], radius: 1000, random: 'a' }, { center: [91, 2], radius: 1000 }, { center: [1, -181], radius: 1000 }, { center: ['a', 2], radius: 1000 }, { center: [1, [1, 2]], radius: 1000 }, { center: [0, 0], radius: -1 }, { center: [null, 2], radius: 1000 }, { center: [1, undefined], radius: 1000 }, { center: [NaN, 0], radius: 1000 }, { center: [1, 2], radius: -10 }, { center: [1, 2], radius: 'text' }, { center: [1, 2], radius: [1, 2] }, { center: [1, 2], radius: null }, true, false, undefined, NaN, [], 'a', 1]; + +// Create global constiables to hold the Firebase and GeoFire constiables +export let geoFireRef: firebase.database.Reference, + geoFire: GeoFire, + geoQueries: GeoQuery[] = []; + +// Initialize Firebase +const config = { + apiKey: 'AIzaSyC5IcRccDo289TTRa3Y7qJIu8YPz3EnKAI', + databaseURL: 'https://geofire-9d0de.firebaseio.com', + projectId: 'geofire-9d0de' +}; +firebase.initializeApp(config); + +/**********************/ +/* HELPER FUNCTIONS */ +/**********************/ +/* Helper functions which runs before each Jasmine test has started */ +export function beforeEachHelper(done) { + // Create a new Firebase database ref at a random node + geoFireRef = firebase.database().ref().push(); + // Create a new GeoFire instance + geoFire = new GeoFire(geoFireRef); + + // Reset the GeoFireQueries + geoQueries = []; + + done(); +} + +/* Helper functions which runs after each Jasmine test has completed */ +export function afterEachHelper(done) { + // Cancel each outstanding GeoQuery + geoQueries.forEach((GeoQuery) => { + GeoQuery.cancel(); + }); + + geoFireRef.remove().then(() => { + // Wait for 50 milliseconds after each test to give enough time for old query events to expire + return wait(50); + }).then(done); +} + +/* Returns a random alphabetic string of constiable length */ +export function generateRandomString() { + const possibleCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const numPossibleCharacters = possibleCharacters.length; + + let text = ''; + for (let i = 0; i < 10; i++) { + text += possibleCharacters.charAt(Math.floor(Math.random() * numPossibleCharacters)); + } + + return text; +} + +/* Returns the current data in the Firebase */ +export function getFirebaseData() { + return geoFireRef.once('value').then((dataSnapshot: firebase.database.DataSnapshot) => { + return dataSnapshot.exportVal(); + }); +}; + +/* Returns a promise which is fulfilled after the inputted number of milliseconds pass */ +export function wait(milliseconds) { + return new Promise(function (resolve) { + const timeout = window.setTimeout(() => { + window.clearTimeout(timeout); + resolve(); + }, milliseconds); + }); +}; + +/* Keeps track of all the current asynchronous tasks being run */ +export function Checklist(items, expect, done) { + const eventsToComplete = items; + + /* Removes a task from the events list */ + this.x = function (item) { + const index = eventsToComplete.indexOf(item); + if (index === -1) { + expect('Attempting to delete unexpected item \'' + item + '\' from Checklist').toBeFalsy(); + } + else { + eventsToComplete.splice(index, 1); + if (this.isEmpty()) { + done(); + } + } + }; + + /* Returns the length of the events list */ + this.length = function () { + return eventsToComplete.length; + }; + + /* Returns true if the events list is empty */ + this.isEmpty = function () { + return (this.length() === 0); + }; +}; + +/* Common error handler for use in .catch() statements of promises. This will + * cause the test to fail, outputting the details of the exception. Otherwise, tests + * tend to fail due to the Jasmine ASYNC timeout and provide no details of what actually + * went wrong. + **/ +export function failTestOnCaughtError(error) { + expect(error).to.throw(); +} diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 00000000..c0dacf2f --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,6 @@ +--reporter spec +--require ts-node/register +--require source-map-support/register +--require jsdom-global/register +--timeout 30000 +--exit test/**/*.test.ts \ No newline at end of file diff --git a/test/tests/geoCallbackRegistration.test.ts b/test/tests/geoCallbackRegistration.test.ts new file mode 100644 index 00000000..0983410c --- /dev/null +++ b/test/tests/geoCallbackRegistration.test.ts @@ -0,0 +1,253 @@ +import { GeoCallbackRegistration } from '../../src/geoFire/geoCallbackRegistration'; +import { + afterEachHelper, beforeEachHelper, Checklist, + failTestOnCaughtError, geoFire, geoQueries, wait +} from '../common'; + +import * as chai from 'chai'; + +const expect = chai.expect; + +describe('GeoFire GeoCallbackRegistration Tests:', () => { + // Reset the Firebase before each test + beforeEach((done) => { + beforeEachHelper(done); + }); + + afterEach((done) => { + afterEachHelper(done); + }); + + describe('Constructor:', () => { + it('Constructor throws error given non-function', () => { + const createCallbackRegistration = () => { + // @ts-ignore + new GeoCallbackRegistration('nonFunction'); + } + + expect(() => createCallbackRegistration()).to.throw(null, 'callback must be a function'); + }); + }); + + describe('Cancelling event callbacks:', () => { + it('\'key_moved\' registrations can be cancelled', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'loc1 moved'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyMovedRegistration = geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [2, 2]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + onKeyMovedRegistration.cancel(); + cl.x('p3'); + + return geoFire.set('loc3', [1, 2]); + }).then(() => { + cl.x('p4'); + + return wait(100); + }).then(() => { + cl.x('p5'); + }).catch(failTestOnCaughtError);; + }); + + it('\'key_entered\' registrations can be cancelled', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'loc1 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyEnteredRegistration = geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [80, 80] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + onKeyEnteredRegistration.cancel(); + cl.x('p2'); + + return geoFire.set('loc3', [1, 2]); + }).then(() => { + cl.x('p3'); + + return wait(100); + }).then(() => { + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_exited\' registrations can be cancelled', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'loc1 exited'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyExitedRegistration = geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [80, 80]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + onKeyExitedRegistration.cancel(); + cl.x('p3'); + + return geoFire.set('loc3', [-80, -80]); + }).then(() => { + cl.x('p4'); + + return wait(100); + }).then(() => { + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('Cancelling a \'key_moved\' registration does not cancel all \'key_moved\' callbacks', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'loc1 moved1', 'loc1 moved2', 'loc3 moved2'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyMovedRegistration1 = geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved1'); + }); + const onKeyMovedRegistration2 = geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved2'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [2, 2]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + onKeyMovedRegistration1.cancel(); + cl.x('p3'); + + return geoFire.set('loc3', [1, 2]); + }).then(() => { + cl.x('p4'); + + return wait(100); + }).then(() => { + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('Cancelling a \'key_entered\' registration does not cancel all \'key_entered\' callbacks', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'loc1 entered1', 'loc1 entered2', 'loc3 entered2'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyEnteredRegistration1 = geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered1'); + }); + const onKeyEnteredRegistration2 = geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered2'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [80, 80] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + onKeyEnteredRegistration1.cancel(); + cl.x('p2'); + + return geoFire.set('loc3', [1, 2]); + }).then(() => { + cl.x('p3'); + + return wait(100); + }).then(() => { + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('Cancelling a \'key_exited\' registration does not cancel all \'key_exited\' callbacks', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'loc1 exited1', 'loc1 exited2', 'loc3 exited2'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyExitedRegistration1 = geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited1'); + }); + const onKeyExitedRegistration2 = geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited2'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [80, 80]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + onKeyExitedRegistration1.cancel(); + cl.x('p3'); + + return geoFire.set('loc3', [-80, -80]); + }).then(() => { + cl.x('p4'); + + return wait(100); + }).then(() => { + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('Calling cancel on a GeoCallbackRegistration twice does not throw', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const onKeyExitedRegistration = geoQueries[0].on('key_exited', () => { }); + + expect(() => onKeyExitedRegistration.cancel()).not.throw(); + expect(() => onKeyExitedRegistration.cancel()).not.throw(); + }); + }); +}); diff --git a/test/tests/geoFire.test.ts b/test/tests/geoFire.test.ts new file mode 100755 index 00000000..edbabb50 --- /dev/null +++ b/test/tests/geoFire.test.ts @@ -0,0 +1,846 @@ +import * as chai from 'chai'; + +import { GeoFire } from '../../src/geoFire'; +import { GeoQuery } from '../../src/geoFire/geoQuery'; +import { + afterEachHelper, beforeEachHelper, Checklist, failTestOnCaughtError, geoFire, geoFireRef, getFirebaseData, geoQueries, + invalidFirebaseRefs, invalidKeys, invalidLocations, invalidQueryCriterias, validKeys, validLocations, validQueryCriterias +} from '../common'; + +const expect = chai.expect; + +describe('GeoFire Tests:', () => { + // Reset the Firebase before each test + beforeEach((done) => { + beforeEachHelper(done); + }); + + afterEach((done) => { + afterEachHelper(done); + }); + + describe('Constructor:', () => { + it('Constructor throws errors given invalid Firebase references', () => { + invalidFirebaseRefs.forEach((invalidFirebaseRef) => { + // @ts-ignore + expect(() => new GeoFire(invalidFirebaseRef)).to.throw(null, 'firebaseRef must be an instance of Firebase'); + }); + }); + + it('Constructor does not throw errors given valid Firebase references', () => { + expect(() => new GeoFire(geoFireRef)).not.to.throw(); + }); + }); + + describe('ref():', () => { + it('ref() returns the Firebase reference used to create a GeoFire instance', () => { + expect(geoFire.ref()).to.deep.equal(geoFireRef); + }); + }); + + describe('Adding a single location via set():', () => { + it('set() returns a promise', (done) => { + + const cl = new Checklist(['p1'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + }); + }); + + it('set() updates Firebase when adding new locations', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.set('loc2', [50, 50]); + }).then(() => { + cl.x('p2'); + + return geoFire.set('loc3', [-90, -90]); + }).then(() => { + cl.x('p3'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc2': { '.priority': 'v0gs3y0zh7', 'l': { '0': 50, '1': 50 }, 'g': 'v0gs3y0zh7' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('set() handles decimal latitudes and longitudes', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4'], expect, done); + + geoFire.set('loc1', [0.254, 0]).then(() => { + cl.x('p1'); + + return geoFire.set('loc2', [50, 50.293403]); + }).then(() => { + cl.x('p2'); + + return geoFire.set('loc3', [-82.614, -90.938]); + }).then(() => { + cl.x('p3'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': 'ebpcrypzxv', 'l': { '0': 0.254, '1': 0 }, 'g': 'ebpcrypzxv' }, + 'loc2': { '.priority': 'v0gu2qnx15', 'l': { '0': 50, '1': 50.293403 }, 'g': 'v0gu2qnx15' }, + 'loc3': { '.priority': '1cr648sfx4', 'l': { '0': -82.614, '1': -90.938 }, 'g': '1cr648sfx4' } + }); + + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('set() updates Firebase when changing a pre-existing key', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.set('loc2', [50, 50]); + }).then(() => { + cl.x('p2'); + + return geoFire.set('loc3', [-90, -90]); + }).then(() => { + cl.x('p3'); + + return geoFire.set('loc1', [2, 3]); + }).then(() => { + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': 's065kk0dc5', 'l': { '0': 2, '1': 3 }, 'g': 's065kk0dc5' }, + 'loc2': { '.priority': 'v0gs3y0zh7', 'l': { '0': 50, '1': 50 }, 'g': 'v0gs3y0zh7' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('set() updates Firebase when changing a pre-existing key to the same location', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.set('loc2', [50, 50]); + }).then(() => { + cl.x('p2'); + + return geoFire.set('loc3', [-90, -90]); + }).then(() => { + cl.x('p3'); + + return geoFire.set('loc1', [0, 0]); + }).then(() => { + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc2': { '.priority': 'v0gs3y0zh7', 'l': { '0': 50, '1': 50 }, 'g': 'v0gs3y0zh7' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('set() handles multiple keys at the same location', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.set('loc2', [0, 0]); + }).then(() => { + cl.x('p2'); + + return geoFire.set('loc3', [0, 0]); + }).then(() => { + cl.x('p3'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc2': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc3': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' } + }); + + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('set() updates Firebase after complex operations', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9', 'p10', 'p11'], expect, done); + + geoFire.set('loc:1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.set('loc2', [50, 50]); + }).then(() => { + cl.x('p2'); + + return geoFire.set('loc%!A72f()3', [-90, -90]); + }).then(() => { + cl.x('p3'); + + return geoFire.remove('loc2'); + }).then(() => { + cl.x('p4'); + + return geoFire.set('loc2', [0.2358, -72.621]); + }).then(() => { + cl.x('p5'); + + return geoFire.set('loc4', [87.6, -130]); + }).then(() => { + cl.x('p6'); + + return geoFire.set('loc5', [5, 55.555]); + }).then(() => { + cl.x('p7'); + + return geoFire.set('loc5', null); + }).then(() => { + cl.x('p8'); + + return geoFire.set('loc:1', [87.6, -130]); + }).then(() => { + cl.x('p9'); + + return geoFire.set('loc6', [-72.258, 0.953215]); + }).then(() => { + cl.x('p10'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc:1': { '.priority': 'cped3g0fur', 'l': { '0': 87.6, '1': -130 }, 'g': 'cped3g0fur' }, + 'loc2': { '.priority': 'd2h376zj8h', 'l': { '0': 0.2358, '1': -72.621 }, 'g': 'd2h376zj8h' }, + 'loc%!A72f()3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' }, + 'loc4': { '.priority': 'cped3g0fur', 'l': { '0': 87.6, '1': -130 }, 'g': 'cped3g0fur' }, + 'loc6': { '.priority': 'h50svty4es', 'l': { '0': -72.258, '1': 0.953215 }, 'g': 'h50svty4es' } + }); + + cl.x('p11'); + }).catch(failTestOnCaughtError); + }); + + it('set() does not throw errors given valid keys', () => { + validKeys.forEach((validKey) => { + expect(() => { + geoFire.set(validKey, [0, 0]); + }).not.to.throw(); + }); + }); + + it('set() throws errors given invalid keys', () => { + invalidKeys.forEach((invalidKey) => { + expect(() => { + geoFire.set(invalidKey, [0, 0]); + }).to.throw(); + }); + }); + + it('set() does not throw errors given valid locations', () => { + validLocations.forEach((validLocation, i) => { + expect(() => { + geoFire.set('loc', validLocation); + }).not.to.throw(); + }); + }); + + it('set() throws errors given invalid locations', () => { + invalidLocations.forEach((invalidLocation, i) => { + // Setting location to null is valid since it will remove the key + if (invalidLocation !== null) { + expect(() => { + // @ts-ignore + geoFire.set('loc', invalidLocation); + }).to.throw(); + } + }); + }); + }); + + describe('Adding multiple locations via set():', () => { + it('set() returns a promise', (done) => { + + const cl = new Checklist(['p1'], expect, done); + + geoFire.set({ + 'loc1': [0, 0] + }).then(() => { + cl.x('p1'); + }); + }); + + it('set() updates Firebase when adding new locations', (done) => { + const cl = new Checklist(['p1', 'p2'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, 50], + 'loc3': [-90, -90] + }).then(() => { + cl.x('p1'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc2': { '.priority': 'v0gs3y0zh7', 'l': { '0': 50, '1': 50 }, 'g': 'v0gs3y0zh7' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('set() handles decimal latitudes and longitudes', (done) => { + const cl = new Checklist(['p1', 'p2'], expect, done); + + geoFire.set({ + 'loc1': [0.254, 0], + 'loc2': [50, 50.293403], + 'loc3': [-82.614, -90.938] + }).then(() => { + cl.x('p1'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': 'ebpcrypzxv', 'l': { '0': 0.254, '1': 0 }, 'g': 'ebpcrypzxv' }, + 'loc2': { '.priority': 'v0gu2qnx15', 'l': { '0': 50, '1': 50.293403 }, 'g': 'v0gu2qnx15' }, + 'loc3': { '.priority': '1cr648sfx4', 'l': { '0': -82.614, '1': -90.938 }, 'g': '1cr648sfx4' } + }); + + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('set() updates Firebase when changing a pre-existing key', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, 50], + 'loc3': [-90, -90] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 3] + }); + }).then(() => { + cl.x('p2'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': 's065kk0dc5', 'l': { '0': 2, '1': 3 }, 'g': 's065kk0dc5' }, + 'loc2': { '.priority': 'v0gs3y0zh7', 'l': { '0': 50, '1': 50 }, 'g': 'v0gs3y0zh7' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('set() updates Firebase when changing a pre-existing key to the same location', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, 50], + 'loc3': [-90, -90] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [0, 0] + }); + }).then(() => { + cl.x('p2'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc2': { '.priority': 'v0gs3y0zh7', 'l': { '0': 50, '1': 50 }, 'g': 'v0gs3y0zh7' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('set() handles multiple keys at the same location', (done) => { + const cl = new Checklist(['p1', 'p2'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [0, 0], + 'loc3': [0, 0] + }).then(() => { + cl.x('p1'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc2': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' }, + 'loc3': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' } + }); + + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('set() updates Firebase after complex operations', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'p6'], expect, done); + + geoFire.set({ + 'loc:1': [0, 0], + 'loc2': [50, 50], + 'loc%!A72f()3': [-90, -90] + }).then(() => { + cl.x('p1'); + + return geoFire.remove('loc2'); + }).then(() => { + cl.x('p2'); + + return geoFire.set({ + 'loc2': [0.2358, -72.621], + 'loc4': [87.6, -130], + 'loc5': [5, 55.555] + }); + }).then(() => { + cl.x('p3'); + + return geoFire.set({ + 'loc5': null + }); + }).then(() => { + cl.x('p4'); + + return geoFire.set({ + 'loc:1': [87.6, -130], + 'loc6': [-72.258, 0.953215] + }); + }).then(() => { + cl.x('p5'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc:1': { '.priority': 'cped3g0fur', 'l': { '0': 87.6, '1': -130 }, 'g': 'cped3g0fur' }, + 'loc2': { '.priority': 'd2h376zj8h', 'l': { '0': 0.2358, '1': -72.621 }, 'g': 'd2h376zj8h' }, + 'loc%!A72f()3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' }, + 'loc4': { '.priority': 'cped3g0fur', 'l': { '0': 87.6, '1': -130 }, 'g': 'cped3g0fur' }, + 'loc6': { '.priority': 'h50svty4es', 'l': { '0': -72.258, '1': 0.953215 }, 'g': 'h50svty4es' } + }); + + cl.x('p6'); + }).catch(failTestOnCaughtError); + }); + + it('set() does not throw errors given valid keys', () => { + validKeys.forEach((validKey) => { + expect(() => { + const locations = {}; + locations[validKey] = [0, 0]; + geoFire.set(locations); + }).not.to.throw(); + }); + }); + + it('set() throws errors given invalid keys', () => { + invalidKeys.forEach((invalidKey) => { + if (invalidKey !== null && invalidKey !== undefined && typeof invalidKey !== 'boolean') { + expect(() => { + const locations = {}; + // @ts-ignore + locations[invalidKey] = [0, 0]; + geoFire.set(locations); + }).to.throw(); + } + }); + }); + + it('set() throws errors given a location argument in combination with an object', () => { + expect(() => { + geoFire.set({ + 'loc': [0, 0] + }, [0, 0]); + }).to.throw(); + }); + + it('set() does not throw errors given valid locations', () => { + validLocations.forEach((validLocation, i) => { + expect(() => { + geoFire.set({ + 'loc': validLocation + }); + }).not.to.throw(); + }); + }); + + it('set() throws errors given invalid locations', () => { + invalidLocations.forEach((invalidLocation, i) => { + // Setting location to null is valid since it will remove the key + if (invalidLocation !== null) { + expect(() => { + geoFire.set({ + 'loc': invalidLocation + }); + }).to.throw(); + } + }); + }); + }); + + describe('Retrieving locations:', () => { + it('get() returns a promise', (done) => { + const cl = new Checklist(['p1'], expect, done); + + geoFire.get('loc1').then(() => { + cl.x('p1'); + }); + }); + + it('get() returns null for non-existent keys', (done) => { + const cl = new Checklist(['p1'], expect, done); + + geoFire.get('loc1').then((location) => { + expect(location).to.equal(null); + + cl.x('p1'); + }); + }); + + it('get() retrieves locations given existing keys', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, 50], + 'loc3': [-90, -90] + }).then(() => { + cl.x('p1'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + cl.x('p2'); + + return geoFire.get('loc2'); + }).then((location) => { + expect(location).to.deep.equal([50, 50]); + cl.x('p3'); + + return geoFire.get('loc3'); + }).then((location) => { + expect(location).to.deep.equal([-90, -90]); + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('get() does not throw errors given valid keys', () => { + validKeys.forEach((validKey) => { + expect(() => geoFire.get(validKey)).not.to.throw(); + }); + }); + + it('get() throws errors given invalid keys', () => { + invalidKeys.forEach((invalidKey) => { + // @ts-ignore + expect(() => geoFire.get(invalidKey)).to.throw(); + }); + }); + }); + + describe('Removing locations:', () => { + it('set() removes existing location given null', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [2, 3] + }).then(() => { + cl.x('p1'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + + cl.x('p2'); + + return geoFire.set('loc1', null); + }).then(() => { + cl.x('p3'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.equal(null); + + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc2': { '.priority': 's065kk0dc5', 'l': { '0': 2, '1': 3 }, 'g': 's065kk0dc5' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('set() does nothing given a non-existent location and null', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + + cl.x('p2'); + + return geoFire.set('loc2', null); + }).then(() => { + cl.x('p3'); + + return geoFire.get('loc2'); + }).then((location) => { + expect(location).to.equal(null); + + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('set() removes existing location given null', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [2, 3] + }).then(() => { + cl.x('p1'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + + cl.x('p2'); + + return geoFire.set({ + 'loc1': null, + 'loc3': [-90, -90] + }); + }).then(() => { + cl.x('p3'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.equal(null); + + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc2': { '.priority': 's065kk0dc5', 'l': { '0': 2, '1': 3 }, 'g': 's065kk0dc5' }, + 'loc3': { '.priority': '1bpbpbpbpb', 'l': { '0': -90, '1': -90 }, 'g': '1bpbpbpbpb' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('set() does nothing given a non-existent location and null', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': null + }).then(() => { + cl.x('p1'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + + cl.x('p2'); + + return geoFire.get('loc2'); + }).then((location) => { + expect(location).to.equal(null); + + cl.x('p3'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' } + }); + + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('remove() removes existing location', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set({ + 'loc:^%*1': [0, 0], + 'loc2': [2, 3] + }).then(() => { + cl.x('p1'); + + return geoFire.get('loc:^%*1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + + cl.x('p2'); + + return geoFire.remove('loc:^%*1'); + }).then(() => { + cl.x('p3'); + + return geoFire.get('loc:^%*1'); + }).then((location) => { + expect(location).to.equal(null); + + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc2': { '.priority': 's065kk0dc5', 'l': { '0': 2, '1': 3 }, 'g': 's065kk0dc5' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('remove() does nothing given a non-existent location', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5'], expect, done); + + geoFire.set('loc1', [0, 0]).then(() => { + cl.x('p1'); + + return geoFire.get('loc1'); + }).then((location) => { + expect(location).to.deep.equal([0, 0]); + + cl.x('p2'); + + return geoFire.remove('loc2'); + }).then(() => { + cl.x('p3'); + + return geoFire.get('loc2'); + }).then((location) => { + expect(location).to.equal(null); + + cl.x('p4'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc1': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' } + }); + + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('remove() only removes one key if multiple keys are at the same location', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [2, 3], + 'loc3': [0, 0] + }).then(() => { + cl.x('p1'); + + return geoFire.remove('loc1'); + }).then(() => { + cl.x('p2'); + + return getFirebaseData(); + }).then((firebaseData) => { + expect(firebaseData).to.deep.equal({ + 'loc2': { '.priority': 's065kk0dc5', 'l': { '0': 2, '1': 3 }, 'g': 's065kk0dc5' }, + 'loc3': { '.priority': '7zzzzzzzzz', 'l': { '0': 0, '1': 0 }, 'g': '7zzzzzzzzz' } + }); + + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('remove() does not throw errors given valid keys', () => { + validKeys.forEach((validKey) => { + expect(() => geoFire.remove(validKey)).not.to.throw(); + }); + }); + + it('remove() throws errors given invalid keys', () => { + invalidKeys.forEach((invalidKey) => { + // @ts-ignore + expect(() => geoFire.remove(invalidKey)).to.throw(); + }); + }); + }); + + describe('query():', () => { + it('query() returns GeoQuery instance', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + expect(geoQueries[0] instanceof GeoQuery).to.be.ok; + }); + + it('query() does not throw errors given valid query criteria', () => { + validQueryCriterias.forEach((validQueryCriteria) => { + if (typeof validQueryCriteria.center !== 'undefined' && typeof validQueryCriteria.radius !== 'undefined') { + expect(() => geoFire.query(validQueryCriteria)).not.to.throw(); + } + }); + }); + + it('query() throws errors given invalid query criteria', () => { + invalidQueryCriterias.forEach((invalidQueryCriteria) => { + // @ts-ignore + expect(() => geoFire.query(invalidQueryCriteria)).to.throw(); + }); + }); + }); +}); diff --git a/test/tests/geoFireUtils.test.ts b/test/tests/geoFireUtils.test.ts new file mode 100644 index 00000000..86ba97da --- /dev/null +++ b/test/tests/geoFireUtils.test.ts @@ -0,0 +1,298 @@ +import * as chai from 'chai'; + +import { GeoFire } from '../../src/geoFire'; +import { + boundingBoxBits, degreesToRadians, encodeGeohash, geohashQuery, geohashQueries, g_GEOHASH_PRECISION, + metersToLongitudeDegrees, validateCriteria, validateGeohash, validateKey, validateLocation, wrapLongitude +} from '../../src/geoFire/geoFireUtils'; +import { + invalidGeohashes, invalidKeys, invalidLocations, invalidQueryCriterias, + validGeohashes, validKeys, validLocations, validQueryCriterias +} from '../common'; + +const expect = chai.expect; + +describe('geoFireUtils Tests:', () => { + describe('Parameter validation:', () => { + it('validateKey() does not throw errors given valid keys', () => { + validKeys.forEach((validKey) => { + expect(() => validateKey(validKey)).not.to.throw(); + }); + }); + + it('validateKey() throws errors given invalid keys', () => { + invalidKeys.forEach((invalidKey) => { + // @ts-ignore + expect(() => validateKey(invalidKey)).to.throw(); + }); + }); + + it('validateLocation() does not throw errors given valid locations', () => { + validLocations.forEach((validLocation, i) => { + expect(() => validateLocation(validLocation)).not.to.throw(); + }); + }); + + it('validateLocation() throws errors given invalid locations', () => { + invalidLocations.forEach((invalidLocation, i) => { + // @ts-ignore + expect(() => validateLocation(invalidLocation)).to.throw(); + }); + }); + + it('validateGeohash() does not throw errors given valid geohashes', () => { + validGeohashes.forEach((validGeohash, i) => { + expect(() => validateGeohash(validGeohash)).not.to.throw(); + }); + }); + + it('validateGeohash() throws errors given invalid geohashes', () => { + invalidGeohashes.forEach((invalidGeohash, i) => { + // @ts-ignore + expect(() => validateGeohash(invalidGeohash)).to.throw(); + }); + }); + + it('validateCriteria(criteria, true) does not throw errors given valid query criteria', () => { + validQueryCriterias.forEach((validQueryCriteria) => { + if (typeof validQueryCriteria.center !== 'undefined' && typeof validQueryCriteria.radius !== 'undefined') { + expect(() => validateCriteria(validQueryCriteria, true)).not.to.throw(); + } + }); + }); + + it('validateCriteria(criteria) does not throw errors given valid query criteria', () => { + validQueryCriterias.forEach((validQueryCriteria) => { + expect(() => validateCriteria(validQueryCriteria)).not.to.throw(); + }); + }); + + it('validateCriteria(criteria, true) throws errors given invalid query criteria', () => { + invalidQueryCriterias.forEach((invalidQueryCriteria) => { + // @ts-ignore + expect(() => validateCriteria(invalidQueryCriteria, true)).to.throw(); + }); + expect(() => validateCriteria({ center: [0, 0] }, true)).to.throw(); + expect(() => validateCriteria({ radius: 1000 }, true)).to.throw(); + }); + + it('validateCriteria(criteria) throws errors given invalid query criteria', () => { + invalidQueryCriterias.forEach((invalidQueryCriteria) => { + // @ts-ignore + expect(() => validateCriteria(invalidQueryCriteria)).to.throw(); + }); + }); + }); + + describe('Distance calculations:', () => { + it('degreesToRadians() converts degrees to radians', () => { + expect(degreesToRadians(0)).to.be.closeTo(0, 0); + expect(degreesToRadians(45)).to.be.closeTo(0.7854, 4); + expect(degreesToRadians(90)).to.be.closeTo(1.5708, 4); + expect(degreesToRadians(135)).to.be.closeTo(2.3562, 4); + expect(degreesToRadians(180)).to.be.closeTo(3.1416, 4); + expect(degreesToRadians(225)).to.be.closeTo(3.9270, 4); + expect(degreesToRadians(270)).to.be.closeTo(4.7124, 4); + expect(degreesToRadians(315)).to.be.closeTo(5.4978, 4); + expect(degreesToRadians(360)).to.be.closeTo(6.2832, 4); + expect(degreesToRadians(-45)).to.be.closeTo(-0.7854, 4); + expect(degreesToRadians(-90)).to.be.closeTo(-1.5708, 4); + }); + + it('degreesToRadians() throws errors given invalid inputs', () => { + // @ts-ignore + expect(() => degreesToRadians('')).to.throw(); + // @ts-ignore + expect(() => degreesToRadians('a')).to.throw(); + // @ts-ignore + expect(() => degreesToRadians(true)).to.throw(); + // @ts-ignore + expect(() => degreesToRadians(false)).to.throw(); + // @ts-ignore + expect(() => degreesToRadians([1])).to.throw(); + // @ts-ignore + expect(() => degreesToRadians({})).to.throw(); + expect(() => degreesToRadians(null)).to.throw(); + expect(() => degreesToRadians(undefined)).to.throw(); + }); + + it('dist() calculates the distance between locations', () => { + expect(GeoFire.distance([90, 180], [90, 180])).to.be.closeTo(0, 0); + expect(GeoFire.distance([-90, -180], [90, 180])).to.be.closeTo(20015, 1); + expect(GeoFire.distance([-90, -180], [-90, 180])).to.be.closeTo(0, 1); + expect(GeoFire.distance([-90, -180], [90, -180])).to.be.closeTo(20015, 1); + expect(GeoFire.distance([37.7853074, -122.4054274], [78.216667, 15.55])).to.be.closeTo(6818, 1); + expect(GeoFire.distance([38.98719, -77.250783], [29.3760648, 47.9818853])).to.be.closeTo(10531, 1); + expect(GeoFire.distance([38.98719, -77.250783], [-54.933333, -67.616667])).to.be.closeTo(10484, 1); + expect(GeoFire.distance([29.3760648, 47.9818853], [-54.933333, -67.616667])).to.be.closeTo(14250, 1); + expect(GeoFire.distance([-54.933333, -67.616667], [-54, -67])).to.be.closeTo(111, 1); + }); + + it('dist() does not throw errors given valid locations', () => { + validLocations.forEach((validLocation, i) => { + expect(() => GeoFire.distance(validLocation, [0, 0])).not.to.throw(); + expect(() => GeoFire.distance([0, 0], validLocation)).not.to.throw(); + }); + }); + + it('dist() throws errors given invalid locations', () => { + invalidLocations.forEach((invalidLocation, i) => { + // @ts-ignore + expect(() => GeoFire.distance(invalidLocation, [0, 0])).to.throw(); + // @ts-ignore + expect(() => GeoFire.distance([0, 0], invalidLocation)).to.throw(); + }); + }); + }); + + describe('Geohashing:', () => { + it('encodeGeohash() encodes locations to geohashes given no precision', () => { + expect(encodeGeohash([-90, -180])).to.be.equal('000000000000'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([90, 180])).to.be.equal('zzzzzzzzzzzz'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([-90, 180])).to.be.equal('pbpbpbpbpbpb'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([90, -180])).to.be.equal('bpbpbpbpbpbp'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([37.7853074, -122.4054274])).to.be.equal('9q8yywe56gcf'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([38.98719, -77.250783])).to.be.equal('dqcjf17sy6cp'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([29.3760648, 47.9818853])).to.be.equal('tj4p5gerfzqu'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([78.216667, 15.55])).to.be.equal('umghcygjj782'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([-54.933333, -67.616667])).to.be.equal('4qpzmren1kwb'.slice(0, g_GEOHASH_PRECISION)); + expect(encodeGeohash([-54, -67])).to.be.equal('4w2kg3s54y7h'.slice(0, g_GEOHASH_PRECISION)); + }); + + it('encodeGeohash() encodes locations to geohashes given a custom precision', () => { + expect(encodeGeohash([-90, -180], 6)).to.be.equal('000000'); + expect(encodeGeohash([90, 180], 20)).to.be.equal('zzzzzzzzzzzzzzzzzzzz'); + expect(encodeGeohash([-90, 180], 1)).to.be.equal('p'); + expect(encodeGeohash([90, -180], 5)).to.be.equal('bpbpb'); + expect(encodeGeohash([37.7853074, -122.4054274], 8)).to.be.equal('9q8yywe5'); + expect(encodeGeohash([38.98719, -77.250783], 18)).to.be.equal('dqcjf17sy6cppp8vfn'); + expect(encodeGeohash([29.3760648, 47.9818853], 12)).to.be.equal('tj4p5gerfzqu'); + expect(encodeGeohash([78.216667, 15.55], 1)).to.be.equal('u'); + expect(encodeGeohash([-54.933333, -67.616667], 7)).to.be.equal('4qpzmre'); + expect(encodeGeohash([-54, -67], 9)).to.be.equal('4w2kg3s54'); + }); + + it('encodeGeohash() does not throw errors given valid locations', () => { + validLocations.forEach((validLocation, i) => { + expect(() => encodeGeohash(validLocation)).not.to.throw(); + }); + }); + + it('encodeGeohash() throws errors given invalid locations', () => { + invalidLocations.forEach((invalidLocation, i) => { + // @ts-ignore + expect(() => encodeGeohash(invalidLocation)).to.throw(); + }); + }); + + it('encodeGeohash() does not throw errors given valid precision', () => { + const validPrecisions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, undefined]; + + validPrecisions.forEach((validPrecision, i) => { + expect(() => encodeGeohash([0, 0], validPrecision)).not.to.throw(); + }); + }); + + it('encodeGeohash() throws errors given invalid precision', () => { + const invalidPrecisions = [0, -1, 1.5, 23, '', 'a', true, false, [], {}, [1], { a: 1 }, null]; + + invalidPrecisions.forEach((invalidPrecision, i) => { + // @ts-ignore + expect(() => encodeGeohash([0, 0], invalidPrecision)).to.throw(); + }); + }); + }); + + describe('Coordinate calculations:', () => { + it('metersToLongtitudeDegrees calculates correctly', () => { + expect(metersToLongitudeDegrees(1000, 0)).to.be.closeTo(0.008983, 5); + expect(metersToLongitudeDegrees(111320, 0)).to.be.closeTo(1, 5); + expect(metersToLongitudeDegrees(107550, 15)).to.be.closeTo(1, 5); + expect(metersToLongitudeDegrees(96486, 30)).to.be.closeTo(1, 5); + expect(metersToLongitudeDegrees(78847, 45)).to.be.closeTo(1, 5); + expect(metersToLongitudeDegrees(55800, 60)).to.be.closeTo(1, 5); + expect(metersToLongitudeDegrees(28902, 75)).to.be.closeTo(1, 5); + expect(metersToLongitudeDegrees(0, 90)).to.be.closeTo(0, 5); + expect(metersToLongitudeDegrees(1000, 90)).to.be.closeTo(360, 5); + expect(metersToLongitudeDegrees(1000, 89.9999)).to.be.closeTo(360, 5); + expect(metersToLongitudeDegrees(1000, 89.995)).to.be.closeTo(102.594208, 5); + }); + + it('wrapLongitude wraps correctly', () => { + expect(wrapLongitude(0)).to.be.closeTo(0, 6); + expect(wrapLongitude(180)).to.be.closeTo(180, 6); + expect(wrapLongitude(-180)).to.be.closeTo(-180, 6); + expect(wrapLongitude(182)).to.be.closeTo(-178, 6); + expect(wrapLongitude(270)).to.be.closeTo(-90, 6); + expect(wrapLongitude(360)).to.be.closeTo(0, 6); + expect(wrapLongitude(540)).to.be.closeTo(-180, 6); + expect(wrapLongitude(630)).to.be.closeTo(-90, 6); + expect(wrapLongitude(720)).to.be.closeTo(0, 6); + expect(wrapLongitude(810)).to.be.closeTo(90, 6); + expect(wrapLongitude(-360)).to.be.closeTo(0, 6); + expect(wrapLongitude(-182)).to.be.closeTo(178, 6); + expect(wrapLongitude(-270)).to.be.closeTo(90, 6); + expect(wrapLongitude(-360)).to.be.closeTo(0, 6); + expect(wrapLongitude(-450)).to.be.closeTo(-90, 6); + expect(wrapLongitude(-540)).to.be.closeTo(180, 6); + expect(wrapLongitude(-630)).to.be.closeTo(90, 6); + expect(wrapLongitude(1080)).to.be.closeTo(0, 6); + expect(wrapLongitude(-1080)).to.be.closeTo(0, 6); + }); + }); + + describe('Bounding box bits:', () => { + it('boundingBoxBits must return correct number of bits', () => { + expect(boundingBoxBits([35, 0], 1000)).to.be.equal(28); + expect(boundingBoxBits([35.645, 0], 1000)).to.be.equal(27); + expect(boundingBoxBits([36, 0], 1000)).to.be.equal(27); + expect(boundingBoxBits([0, 0], 1000)).to.be.equal(28); + expect(boundingBoxBits([0, -180], 1000)).to.be.equal(28); + expect(boundingBoxBits([0, 180], 1000)).to.be.equal(28); + expect(boundingBoxBits([0, 0], 8000)).to.be.equal(22); + expect(boundingBoxBits([45, 0], 1000)).to.be.equal(27); + expect(boundingBoxBits([75, 0], 1000)).to.be.equal(25); + expect(boundingBoxBits([75, 0], 2000)).to.be.equal(23); + expect(boundingBoxBits([90, 0], 1000)).to.be.equal(1); + expect(boundingBoxBits([90, 0], 2000)).to.be.equal(1); + }); + }); + + describe('Geohash queries:', () => { + it('Geohash queries must be of the right size', () => { + expect(geohashQuery('64m9yn96mx', 6)).to.be.deep.equal(['60', '6h']); + expect(geohashQuery('64m9yn96mx', 1)).to.be.deep.equal(['0', 'h']); + expect(geohashQuery('64m9yn96mx', 10)).to.be.deep.equal(['64', '65']); + expect(geohashQuery('6409yn96mx', 11)).to.be.deep.equal(['640', '64h']); + expect(geohashQuery('64m9yn96mx', 11)).to.be.deep.equal(['64h', '64~']); + expect(geohashQuery('6', 10)).to.be.deep.equal(['6', '6~']); + expect(geohashQuery('64z178', 12)).to.be.deep.equal(['64s', '64~']); + expect(geohashQuery('64z178', 15)).to.be.deep.equal(['64z', '64~']); + }); + + it('Queries from geohashQueries must contain points in circle', () => { + function inQuery(queries, hash) { + for (let i = 0; i < queries.length; i++) { + if (hash >= queries[i][0] && hash < queries[i][1]) { + return true; + } + } + return false; + } + for (let i = 0; i < 200; i++) { + const centerLat = Math.pow(Math.random(), 5) * 160 - 80; + const centerLong = Math.pow(Math.random(), 5) * 360 - 180; + const radius = Math.random() * Math.random() * 100000; + const degreeRadius = metersToLongitudeDegrees(radius, centerLat); + const queries = geohashQueries([centerLat, centerLong], radius); + for (let j = 0; j < 1000; j++) { + const pointLat = Math.max(-89.9, Math.min(89.9, centerLat + Math.random() * degreeRadius)); + const pointLong = wrapLongitude(centerLong + Math.random() * degreeRadius); + if (GeoFire.distance([centerLat, centerLong], [pointLat, pointLong]) < radius / 1000) { + expect(inQuery(queries, encodeGeohash([pointLat, pointLong]))).to.be.true; + } + } + } + }); + }); +}); diff --git a/test/tests/geoQuery.test.ts b/test/tests/geoQuery.test.ts new file mode 100644 index 00000000..5e469828 --- /dev/null +++ b/test/tests/geoQuery.test.ts @@ -0,0 +1,1421 @@ +import * as chai from 'chai'; + +import { + afterEachHelper, beforeEachHelper, Checklist, failTestOnCaughtError, geoFire, geoQueries, invalidQueryCriterias, validQueryCriterias, wait +} from '../common'; + +const expect = chai.expect; + +describe('GeoQuery Tests:', () => { + // Reset the Firebase before each test + beforeEach((done) => { + beforeEachHelper(done); + }); + + afterEach((done) => { + afterEachHelper(done); + }); + + describe('Constructor:', () => { + it('Constructor stores query criteria', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + expect(geoQueries[0].center()).to.deep.equal([1, 2]); + expect(geoQueries[0].radius()).to.equal(1000); + }); + + it('Constructor throws error on invalid query criteria', () => { + expect(() => geoFire.query({})).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ random: 100 })).to.throw(); + expect(() => geoFire.query({ center: [1, 2] })).to.throw(); + expect(() => geoFire.query({ radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: [91, 2], radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: [1, -181], radius: 1000 })).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ center: ['text', 2], radius: 1000 })).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ center: [1, [1, 2]], radius: 1000 })).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ center: 1000, radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: null, radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: undefined, radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: [null, 2], radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: [1, undefined], radius: 1000 })).to.throw(); + expect(() => geoFire.query({ center: [1, 2], radius: -10 })).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ center: [1, 2], radius: 'text' })).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ center: [1, 2], radius: [1, 2] })).to.throw(); + expect(() => geoFire.query({ center: [1, 2], radius: null })).to.throw(); + expect(() => geoFire.query({ center: [1, 2], radius: undefined })).to.throw(); + // @ts-ignore + expect(() => geoFire.query({ center: [1, 2], radius: 1000, other: 'throw' })).to.throw(); + }); + }); + + describe('updateCriteria():', () => { + it('updateCriteria() updates query criteria', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + expect(geoQueries[0].center()).to.deep.equal([1, 2]); + expect(geoQueries[0].radius()).to.equal(1000); + + geoQueries[0].updateCriteria({ center: [2, 3], radius: 100 }); + + expect(geoQueries[0].center()).to.deep.equal([2, 3]); + expect(geoQueries[0].radius()).to.equal(100); + }); + + it('updateCriteria() updates query criteria when given only center', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + expect(geoQueries[0].center()).to.deep.equal([1, 2]); + expect(geoQueries[0].radius()).to.equal(1000); + + geoQueries[0].updateCriteria({ center: [2, 3] }); + + expect(geoQueries[0].center()).to.deep.equal([2, 3]); + expect(geoQueries[0].radius()).to.equal(1000); + }); + + it('updateCriteria() updates query criteria when given only radius', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + expect(geoQueries[0].center()).to.deep.equal([1, 2]); + expect(geoQueries[0].radius()).to.equal(1000); + + geoQueries[0].updateCriteria({ radius: 100 }); + + expect(geoQueries[0].center()).to.deep.equal([1, 2]); + expect(geoQueries[0].radius()).to.equal(100); + }); + + it('updateCriteria() fires \'key_entered\' callback for locations which now belong to the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'loc1 entered', 'loc4 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [90, 90], radius: 1000 })); + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + geoQueries[0].updateCriteria({ center: [1, 2], radius: 1000 }); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('updateCriteria() fires \'key_entered\' callback for locations with complex keys which now belong to the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'loc:^:*1 entered', 'loc-+-+-4 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [90, 90], radius: 1000 })); + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoFire.set({ + 'loc:^:*1': [2, 3], + 'loc:a:a:a:a:2': [50, -7], + 'loc%!@3': [16, -150], + 'loc-+-+-4': [5, 5], + 'loc:5': [67, 55] + }).then(() => { + cl.x('p1'); + + geoQueries[0].updateCriteria({ center: [1, 2], radius: 1000 }); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('updateCriteria() fires \'key_exited\' callback for locations which no longer belong to the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'loc1 exited', 'loc4 exited'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + geoQueries[0].updateCriteria({ center: [90, 90], radius: 1000 }); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('updateCriteria() does not cause event callbacks to fire on the previous criteria', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'loc1 entered', 'loc4 entered', 'loc1 exited', 'loc4 exited', 'loc4 entered', 'loc5 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [88, 88] + }).then(() => { + cl.x('p1'); + + geoQueries[0].updateCriteria({ center: [90, 90], radius: 1000 }); + + return wait(100); + }).then(() => { + cl.x('p2'); + + return geoFire.set({ + 'loc2': [1, 1], + 'loc4': [89, 90] + }); + }).then(() => { + cl.x('p3'); + + return wait(100); + }).then(() => { + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('updateCriteria() does not cause \'key_moved\' callbacks to fire for keys in both the previous and updated queries', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'loc1 entered', 'loc4 entered', 'loc4 exited', 'loc2 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [88, 88] + }).then(() => { + cl.x('p1'); + + geoQueries[0].updateCriteria({ center: [1, 1], radius: 1000 }); + + return wait(100); + }).then(() => { + cl.x('p2'); + + return geoFire.set({ + 'loc2': [1, 1], + 'loc4': [89, 90] + }); + }).then(() => { + cl.x('p3'); + + return wait(100); + }).then(() => { + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('updateCriteria() does not cause \'key_exited\' callbacks to fire twice for keys in the previous query but not in the updated query and which were moved after the query was updated', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'loc1 entered', 'loc4 entered', 'loc1 exited', 'loc4 exited', 'loc4 entered', 'loc5 entered', 'loc5 moved'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [88, 88] + }).then(() => { + cl.x('p1'); + + geoQueries[0].updateCriteria({ center: [90, 90], radius: 1000 }); + + return wait(100); + }).then(() => { + cl.x('p2'); + + return geoFire.set({ + 'loc2': [1, 1], + 'loc4': [89, 90] + }); + }).then(() => { + cl.x('p3'); + + return wait(100); + }).then(() => { + cl.x('p4'); + + return geoFire.set({ + 'loc2': [0, 0], + 'loc5': [89, 89] + }); + }).then(() => { + cl.x('p5'); + + return wait(100); + }).then(() => { + cl.x('p6'); + }).catch(failTestOnCaughtError); + }); + + it('updateCriteria() does not throw errors given valid query criteria', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + validQueryCriterias.forEach((validQueryCriteria) => { + expect(() => geoQueries[0].updateCriteria(validQueryCriteria)).not.to.throw(); + }); + }); + + it('updateCriteria() throws errors given invalid query criteria', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + invalidQueryCriterias.forEach((invalidQueryCriteria) => { + // @ts-ignore + expect(() => geoQueries[0].updateCriteria(invalidQueryCriteria)).to.throw(); + }); + }); + }); + + describe('on():', () => { + it('on() throws error given invalid event type', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const setInvalidEventType = () => { + geoQueries[0].on('invalid_event', () => { }); + } + + expect(setInvalidEventType).to.throw(); + }); + + it('on() throws error given invalid callback', () => { + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + const setInvalidCallback = () => { + // @ts-ignore + geoQueries[0].on('key_entered', 'non-function'); + } + + expect(setInvalidCallback).to.throw(); + }); + }); + + describe('\'ready\' event:', () => { + it('\'ready\' event fires after all \'key_entered\' events have fired', (done) => { + const cl = new Checklist(['p1', 'loc1 entered', 'loc2 entered', 'loc5 entered', 'loc6 entered', 'loc7 entered', 'loc10 entered', 'ready fired'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [1, 1], + 'loc3': [50, 50], + 'loc4': [14, 1], + 'loc5': [1, 2], + 'loc6': [1, 1], + 'loc7': [0, 0], + 'loc8': [-80, 44], + 'loc9': [1, -136], + 'loc10': [-2, -2] + }).then(() => { + cl.x('p1'); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoQueries[0].on('ready', () => { + expect(cl.length()).to.be.equal(1); + cl.x('ready fired'); + }); + }); + }); + + it('\'ready\' event fires immediately if the callback is added after the query is already ready', (done) => { + const cl = new Checklist(['p1', 'loc1 entered', 'loc2 entered', 'loc5 entered', 'loc6 entered', 'loc7 entered', 'loc10 entered', 'ready1 fired', 'ready2 fired'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [1, 1], + 'loc3': [50, 50], + 'loc4': [14, 1], + 'loc5': [1, 2], + 'loc6': [1, 1], + 'loc7': [0, 0], + 'loc8': [-80, 44], + 'loc9': [1, -136], + 'loc10': [-2, -2] + }).then(() => { + cl.x('p1'); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoQueries[0].on('ready', () => { + expect(cl.length()).to.be.equal(2); + cl.x('ready1 fired'); + geoQueries[0].on('ready', () => { + expect(cl.length()).to.be.equal(1); + cl.x('ready2 fired'); + }); + }); + }); + }); + + it('\'ready\' event fires after increasing the query radius, even if no new geohashes were queried', (done) => { + const cl = new Checklist(['ready1 fired', 'ready2 fired'], expect, done); + geoQueries.push(geoFire.query({ center: [37.7851382, -122.405893], radius: 6 })); + const onReadyCallbackRegistration1 = geoQueries[0].on('ready', () => { + cl.x('ready1 fired'); + onReadyCallbackRegistration1.cancel(); + geoQueries[0].updateCriteria({ + radius: 7 + }); + geoQueries[0].on('ready', () => { + cl.x('ready2 fired'); + }); + }); + }); + + it('updateCriteria() fires the \'ready\' event after all \'key_entered\' events have fired', (done) => { + const cl = new Checklist(['p1', 'loc1 entered', 'loc2 entered', 'loc5 entered', 'loc3 entered', 'loc1 exited', 'loc2 exited', 'loc5 exited', 'ready1 fired', 'ready2 fired'], expect, done); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [1, 1], + 'loc3': [50, 50], + 'loc4': [14, 1], + 'loc5': [1, 2] + }).then(() => { + cl.x('p1'); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + const onReadyCallbackRegistration1 = geoQueries[0].on('ready', () => { + expect(cl.length()).to.be.equal(6); + cl.x('ready1 fired'); + + onReadyCallbackRegistration1.cancel(); + + geoQueries[0].updateCriteria({ + center: [51, 51] + }); + + geoQueries[0].on('ready', () => { + expect(cl.length()).to.be.equal(1); + cl.x('ready2 fired'); + }); + }); + }); + }); + }); + + describe('\'key_moved\' event:', () => { + it('\'key_moved\' callback does not fire for brand new locations within or outside of the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [1, 90], + 'loc2': [50, -7], + 'loc3': [16, -150] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [1, 91], + 'loc3': [-50, -50] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_moved\' callback does not fire for locations outside of the GeoQuery which are moved within the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [1, 90], + 'loc2': [50, -7], + 'loc3': [16, -150] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [0, 0], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_moved\' callback does not fire for locations within the GeoQuery which are moved somewhere outside of the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [1, 90], + 'loc3': [-1, -90] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_moved\' callback does not fires for a location within the GeoQuery 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 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, -1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [0, 0], + 'loc2': [55, 55], + 'loc3': [1, 1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_moved\' callback fires for locations within the GeoQuery which are moved somewhere else within the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved', 'loc3 moved'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 2], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_moved\' callback gets passed correct location parameter', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved to 2,2', 'loc3 moved to -1,-1'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved to ' + location); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 2], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_moved\' callback gets passed correct distance parameter', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved (111.19 km from center)', 'loc3 moved (400.90 km from center)'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved (' + distance.toFixed(2) + ' km from center)'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 2], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved', 'loc3 moved'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [0, 0], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 2], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 moved', 'loc3 moved'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [2, 2], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 2], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('multiple \'key_moved\' callbacks fire for locations within the GeoQuery which are moved somewhere else within the GeoQuery', (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 })); + + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved1'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved2'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, -7], + 'loc3': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [2, 2], + 'loc3': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + }); + + describe('\'key_entered\' event:', () => { + it('\'key_entered\' callback fires when a location enters the GeoQuery 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 })); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_entered\' callback fires when a location enters the GeoQuery 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 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_entered\' callback gets passed correct location parameter', (done) => { + const cl = new Checklist(['p1', 'p2', 'loc1 entered at 2,3', 'loc4 entered at 5,5'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered at ' + location); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_entered\' callback gets passed correct distance parameter', (done) => { + const cl = new Checklist(['p1', 'p2', 'loc1 entered (157.23 km from center)', 'loc4 entered (555.66 km from center)'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered (' + distance.toFixed(2) + ' km from center)'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoFire.set({ + 'loc1': [50, 50], + 'loc2': [50, 50], + 'loc3': [18, -121] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [2, 2]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 entered', 'loc3 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + + geoFire.set({ + 'loc1': [50, 50], + 'loc2': [50, 50], + 'loc3': [0, 0] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [0, 0]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('multiple \'key_entered\' callbacks fire when a location enters the GeoQuery', (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 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered1'); + }); + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered2'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return wait(100); + }).then(() => { + cl.x('p2'); + }).catch(failTestOnCaughtError); + }); + }); + + describe('\'key_exited\' event:', () => { + it('\'key_exited\' callback fires when a location leaves the GeoQuery', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited', 'loc4 exited'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [25, 90], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_exited\' callback gets passed correct location parameter', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited to 25,90', 'loc4 exited to 25,5'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited to ' + location); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [5, 2], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [25, 90], + 'loc2': [5, 5], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_exited\' callback gets passed correct distance parameter', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited (9759.01 km from center)', 'loc4 exited (2688.06 km from center)'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited (' + distance.toFixed(2) + ' km from center)'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [5, 2], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [25, 90], + 'loc2': [5, 5], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_exited\' callback gets passed null for location and distance parameters if the key 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 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + expect(location).to.be.equal(null); + expect(distance).to.be.equal(null); + cl.x(key + ' exited'); + }); + + geoFire.set('loc1', [2, 3]).then(() => { + cl.x('p1'); + + return geoFire.remove('loc1'); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('\'key_exited\' callback fires when a location within the GeoQuery 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 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [2, 3] + }).then(() => { + cl.x('p1'); + + return geoFire.remove('loc1'); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited'], expect, done); + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [0, 0], + 'loc3': [18, -121] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [20, -55]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).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) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'loc1 exited'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + + geoFire.set({ + 'loc1': [0, 0], + 'loc2': [50, 50], + 'loc3': [18, -121] + }).then(() => { + cl.x('p1'); + + return geoFire.set('loc1', [18, -121]); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + + it('multiple \'key_exited\' callbacks fire when a location leaves the GeoQuery', (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 })); + + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited1'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited2'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [25, 90], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3'); + }).catch(failTestOnCaughtError); + }); + }); + + describe('\'key_*\' events combined:', () => { + it('\'key_*\' event callbacks fire when used all at the same time', (done) => { + const cl = new Checklist(['p1', 'p2', 'p3', 'p4', 'loc1 entered', 'loc4 entered', 'loc1 moved', 'loc4 exited', 'loc1 exited', 'loc5 entered'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [1, 1], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return geoFire.set({ + 'loc1': [10, -100], + 'loc2': [50, -50], + 'loc5': [5, 5] + }); + }).then(() => { + cl.x('p3'); + + return wait(100); + }).then(() => { + cl.x('p4'); + }).catch(failTestOnCaughtError); + }); + + it('location moving between geohash queries triggers a key_moved', (done) => { + const cl = new Checklist(['loc1 entered', 'loc2 entered', 'p1', 'loc1 moved', 'loc2 moved', 'p2'], expect, done); + + geoQueries.push(geoFire.query({ center: [0, 0], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [-1, -1], + 'loc2': [1, 1] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [1, 1], + 'loc2': [-1, -1] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).catch(failTestOnCaughtError); + }); + }); + + describe('Cancelling GeoQuery:', () => { + it('cancel() prevents GeoQuery 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 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [1, 1], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3') + + geoQueries[0].cancel(); + + return wait(1000); + }).then(() => { + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved'); + }); + + return geoFire.set({ + 'loc1': [10, -100], + 'loc2': [50, -50], + 'loc5': [5, 5] + }); + }).then(() => { + cl.x('p4'); + + return wait(100); + }).then(() => { + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('Calling cancel() on one GeoQuery 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 })); + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered1'); + }); + geoQueries[0].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited1'); + }); + geoQueries[0].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved1'); + }); + + geoQueries[1].on('key_entered', (key, location, distance) => { + cl.x(key + ' entered2'); + }); + geoQueries[1].on('key_exited', (key, location, distance) => { + cl.x(key + ' exited2'); + }); + geoQueries[1].on('key_moved', (key, location, distance) => { + cl.x(key + ' moved2'); + }); + + geoFire.set({ + 'loc1': [2, 3], + 'loc2': [50, -7], + 'loc3': [16, -150], + 'loc4': [5, 5], + 'loc5': [67, 55] + }).then(() => { + cl.x('p1'); + + return geoFire.set({ + 'loc1': [1, 1], + 'loc4': [25, 5] + }); + }).then(() => { + cl.x('p2'); + + return wait(100); + }).then(() => { + cl.x('p3') + + geoQueries[0].cancel(); + + return geoFire.set({ + 'loc1': [10, -100], + 'loc2': [50, -50], + 'loc5': [1, 2] + }); + }).then(() => { + cl.x('p4'); + + return wait(100); + }).then(() => { + cl.x('p5'); + }).catch(failTestOnCaughtError); + }); + + it('Calling cancel() in the middle of firing \'key_entered\' events is allowed', (done) => { + const cl = new Checklist(['p1', 'key entered', 'cancel query'], expect, done); + + geoQueries.push(geoFire.query({ center: [1, 2], radius: 1000 })); + + geoFire.set({ + 'loc1': [1, 2], + 'loc2': [1, 3], + 'loc3': [1, 4] + }).then(() => { + cl.x('p1'); + + let numKeyEnteredEventsFired = 0; + geoQueries[0].on('key_entered', (key, location, distance) => { + cl.x('key entered'); + numKeyEnteredEventsFired++; + if (numKeyEnteredEventsFired === 1) { + cl.x('cancel query'); + geoQueries[0].cancel(); + } + }); + }).catch(failTestOnCaughtError); + }); + }); +}); diff --git a/tests/index.html b/tests/index.html deleted file mode 100755 index d1c0f1d9..00000000 --- a/tests/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - GeoFire Test Suite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/karma.conf.js b/tests/karma.conf.js deleted file mode 100644 index 06c7f3c0..00000000 --- a/tests/karma.conf.js +++ /dev/null @@ -1,24 +0,0 @@ -// Configuration file for Karma test runner -module.exports = function(config) { - config.set({ - frameworks: ["jasmine"], - preprocessors: { - "../src/*.js": "coverage" - }, - reporters: ["spec", "failed", "coverage"], - coverageReporter: { - reporters: [ - { - type: "lcovonly", - dir: "coverage", - subdir: "." - }, - { - type: "text-summary" - } - ] - }, - browsers: ["Firefox"], - browserNoActivityTimeout: 30000 - }); -}; diff --git a/tests/specs/common.spec.js b/tests/specs/common.spec.js deleted file mode 100644 index dd10dc6e..00000000 --- a/tests/specs/common.spec.js +++ /dev/null @@ -1,125 +0,0 @@ -/*************/ -/* GLOBALS */ -/*************/ -// Override the default timeout interval for Jasmine -jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; - -// Define examples of valid and invalid parameters -var invalidFirebaseRefs = [null, undefined, NaN, true, false, [], 0, 5, "", "a", ["hi", 1]]; -var validKeys = ["a", "loc1", "(e@Xi:4t>*E2)hc<5oa:1s6{B0d?u", Array(700).join("a")]; -var invalidKeys = ["", true, false, null, undefined, {a: 1}, "loc.1", "loc$1", "[loc1", "loc1]", "loc#1", "loc/1", "a#i]$da[s", "te/nst", "te/rst", "te/u0000st", "te/u0015st", "te/007Fst", Array(800).join("a")]; -var validLocations = [[0, 0], [-90, 180], [90, -180], [23, 74], [47.235124363, 127.2379654226]]; -var invalidLocations = [[-91, 0], [91, 0], [0, 181], [0, -181], [[0, 0], 0], ["a", 0], [0, "a"], ["a", "a"], [NaN, 0], [0, NaN], [undefined, NaN], [null, 0], [null, null], [0, undefined], [undefined, undefined], "", "a", true, false, [], [1], {}, {a:1}, null, undefined, NaN]; -var validGeohashes = ["4", "d62dtu", "000000000000"]; -var invalidGeohashes = ["", "aaa", 1, true, false, [], [1], {}, {a:1}, null, undefined, NaN]; -var validQueryCriterias = [{center: [0,0], radius: 1000}, {center: [1,-180], radius: 1.78}, {center: [22.22,-107.77], radius: 0}, {center: [0,0]}, {center: [1,-180]}, {center: [22.22,-107.77]}, {radius: 1000}, {radius: 1.78}, {radius: 0}]; -var invalidQueryCriterias = [{}, {random: 100}, {center: [91,2], radius: 1000, random: "a"}, {center: [91,2], radius: 1000}, {center: [1,-181], radius: 1000}, {center: ["a",2], radius: 1000}, {center: [1,[1,2]], radius: 1000}, {center: [0,0], radius: -1}, {center: [null,2], radius: 1000}, {center: [1,undefined], radius: 1000}, {center: [NaN,0], radius: 1000}, {center: [1,2], radius: -10}, {center: [1,2], radius: "text"}, {center: [1,2], radius: [1,2]}, {center: [1,2], radius: null}, true, false, undefined, NaN, [], "a", 1]; - -// Create global variables to hold the Firebase and GeoFire variables -var geoFireRef, geoFire, geoQueries = []; - -// Initialize Firebase -var config = { - apiKey: "AIzaSyC5IcRccDo289TTRa3Y7qJIu8YPz3EnKAI", - databaseURL: "https://geofire-9d0de.firebaseio.com" -}; -firebase.initializeApp(config); - -/**********************/ -/* HELPER FUNCTIONS */ -/**********************/ -/* Helper function which runs before each Jasmine test has started */ -function beforeEachHelper(done) { - // Create a new Firebase database ref at a random node - geoFireRef = firebase.database().ref().push(); - - // Create a new GeoFire instance - geoFire = new GeoFire(geoFireRef); - - // Reset the GeoQueries - geoQueries = []; - - done(); -} - -/* Helper function which runs after each Jasmine test has completed */ -function afterEachHelper(done) { - // Cancel each outstanding GeoQuery - geoQueries.forEach(function(geoQuery) { - geoQuery.cancel(); - }) - - geoFireRef.remove().then(function() { - // Wait for 50 milliseconds after each test to give enough time for old query events to expire - return wait(50); - }).then(done); -} - -/* Returns a random alphabetic string of variable length */ -function generateRandomString() { - var possibleCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var numPossibleCharacters = possibleCharacters.length; - - var text = ""; - for (var i = 0; i < 10; i++) { - text += possibleCharacters.charAt(Math.floor(Math.random() * numPossibleCharacters)); - } - - return text; -} - -/* Returns the current data in the Firebase */ -function getFirebaseData() { - return geoFireRef.once("value").then(function(dataSnapshot) { - return dataSnapshot.exportVal(); - }); -}; - - -/* Returns a promise which is fulfilled after the inputted number of milliseconds pass */ -function wait(milliseconds) { - return new RSVP.Promise(function(resolve) { - var timeout = window.setTimeout(function() { - window.clearTimeout(timeout); - resolve(); - }, milliseconds); - }); -}; - -/* Keeps track of all the current asynchronous tasks being run */ -function Checklist(items, expect, done) { - var eventsToComplete = items; - - /* Removes a task from the events list */ - this.x = function(item) { - var index = eventsToComplete.indexOf(item); - if (index === -1) { - expect("Attempting to delete unexpected item '" + item + "' from Checklist").toBeFalsy(); - } - else { - eventsToComplete.splice(index, 1); - if (this.isEmpty()) { - done(); - } - } - }; - - /* Returns the length of the events list */ - this.length = function() { - return eventsToComplete.length; - }; - - /* Returns true if the events list is empty */ - this.isEmpty = function() { - return (this.length() === 0); - }; -}; - -/* Common error handler for use in .catch() statements of promises. This will - * cause the test to fail, outputting the details of the exception. Otherwise, tests - * tend to fail due to the Jasmine ASYNC timeout and provide no details of what actually - * went wrong. - **/ -function failTestOnCaughtError(error) { - expect(error).toBeNull(); -} diff --git a/tests/specs/geoCallbackRegistration.spec.js b/tests/specs/geoCallbackRegistration.spec.js deleted file mode 100644 index e3fe31fd..00000000 --- a/tests/specs/geoCallbackRegistration.spec.js +++ /dev/null @@ -1,242 +0,0 @@ -describe("GeoCallbackRegistration Tests:", function() { - // Reset the Firebase before each test - beforeEach(function(done) { - beforeEachHelper(done); - }); - - afterEach(function(done) { - afterEachHelper(done); - }); - - describe("Constructor:", function() { - it("Constructor throws error given non-function", function() { - var createCallbackRegistration = function() { - GeoCallbackRegistration("nonFunction"); - } - - expect(createCallbackRegistration).toThrow(); - }); - }); - - describe("Cancelling event callbacks:", function() { - it("\"key_moved\" registrations can be cancelled", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "loc1 moved"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyMovedRegistration = geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [2, 2]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - onKeyMovedRegistration.cancel(); - cl.x("p3"); - - return geoFire.set("loc3", [1, 2]); - }).then(function() { - cl.x("p4"); - - return wait(100); - }).then(function() { - cl.x("p5"); - }).catch(failTestOnCaughtError);; - }); - - it("\"key_entered\" registrations can be cancelled", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "loc1 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyEnteredRegistration = geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [80, 80] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - onKeyEnteredRegistration.cancel(); - cl.x("p2"); - - return geoFire.set("loc3", [1, 2]); - }).then(function() { - cl.x("p3"); - - return wait(100); - }).then(function() { - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_exited\" registrations can be cancelled", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "loc1 exited"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyExitedRegistration = geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [80, 80]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - onKeyExitedRegistration.cancel(); - cl.x("p3"); - - return geoFire.set("loc3", [-80, -80]); - }).then(function() { - cl.x("p4"); - - return wait(100); - }).then(function() { - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("Cancelling a \"key_moved\" registration does not cancel all \"key_moved\" callbacks", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "loc1 moved1", "loc1 moved2", "loc3 moved2"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyMovedRegistration1 = geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved1"); - }); - var onKeyMovedRegistration2 = geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved2"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [2, 2]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - onKeyMovedRegistration1.cancel(); - cl.x("p3"); - - return geoFire.set("loc3", [1, 2]); - }).then(function() { - cl.x("p4"); - - return wait(100); - }).then(function() { - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("Cancelling a \"key_entered\" registration does not cancel all \"key_entered\" callbacks", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "loc1 entered1", "loc1 entered2", "loc3 entered2"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyEnteredRegistration1 = geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered1"); - }); - var onKeyEnteredRegistration2 = geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered2"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [80, 80] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - onKeyEnteredRegistration1.cancel(); - cl.x("p2"); - - return geoFire.set("loc3", [1, 2]); - }).then(function() { - cl.x("p3"); - - return wait(100); - }).then(function() { - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("Cancelling a \"key_exited\" registration does not cancel all \"key_exited\" callbacks", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "loc1 exited1", "loc1 exited2", "loc3 exited2"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyExitedRegistration1 = geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited1"); - }); - var onKeyExitedRegistration2 = geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited2"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [80, 80]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - onKeyExitedRegistration1.cancel(); - cl.x("p3"); - - return geoFire.set("loc3", [-80, -80]); - }).then(function() { - cl.x("p4"); - - return wait(100); - }).then(function() { - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("Calling cancel on a GeoCallbackRegistration twice does not throw", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var onKeyExitedRegistration = geoQueries[0].on("key_exited", function() {}); - - expect(function() { onKeyExitedRegistration.cancel() }).not.toThrow(); - expect(function() { onKeyExitedRegistration.cancel() }).not.toThrow(); - }); - }); -}); diff --git a/tests/specs/geoFire.spec.js b/tests/specs/geoFire.spec.js deleted file mode 100755 index f98bcc26..00000000 --- a/tests/specs/geoFire.spec.js +++ /dev/null @@ -1,829 +0,0 @@ -describe("GeoFire Tests:", function() { - // Reset the Firebase before each test - beforeEach(function(done) { - beforeEachHelper(done); - }); - - afterEach(function(done) { - afterEachHelper(done); - }); - - describe("Constructor:", function() { - it("Constructor throws errors given invalid Firebase references", function() { - invalidFirebaseRefs.forEach(function(invalidFirebaseRef) { - expect(function() { new GeoFire(invalidFirebaseRef); }).toThrow(); - }); - }); - - it("Constructor does not throw errors given valid Firebase references", function() { - expect(function() { new GeoFire(geoFireRef); }).not.toThrow(); - }); - }); - - describe("ref():", function() { - it("ref() returns the Firebase reference used to create a GeoFire instance", function() { - expect(geoFire.ref()).toBe(geoFireRef); - }); - }); - - describe("Adding a single location via set():", function() { - it("set() returns a promise", function(done) { - - var cl = new Checklist(["p1"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - }); - }); - - it("set() updates Firebase when adding new locations", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.set("loc2", [50, 50]); - }).then(function() { - cl.x("p2"); - - return geoFire.set("loc3", [-90, -90]); - }).then(function() { - cl.x("p3"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc2": { ".priority": "v0gs3y0zh7", "l": { "0": 50, "1": 50 }, "g": "v0gs3y0zh7" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("set() handles decimal latitudes and longitudes", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4"], expect, done); - - geoFire.set("loc1", [0.254, 0]).then(function() { - cl.x("p1"); - - return geoFire.set("loc2", [50, 50.293403]); - }).then(function() { - cl.x("p2"); - - return geoFire.set("loc3", [-82.614, -90.938]); - }).then(function() { - cl.x("p3"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "ebpcrypzxv", "l": { "0": 0.254, "1": 0 }, "g": "ebpcrypzxv" }, - "loc2": { ".priority": "v0gu2qnx15", "l": { "0": 50, "1": 50.293403 }, "g": "v0gu2qnx15" }, - "loc3": { ".priority": "1cr648sfx4", "l": { "0": -82.614, "1": -90.938 }, "g": "1cr648sfx4" } - }); - - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("set() updates Firebase when changing a pre-existing key", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.set("loc2", [50, 50]); - }).then(function() { - cl.x("p2"); - - return geoFire.set("loc3", [-90, -90]); - }).then(function() { - cl.x("p3"); - - return geoFire.set("loc1", [2, 3]); - }).then(function() { - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "s065kk0dc5", "l": { "0": 2, "1": 3 }, "g": "s065kk0dc5" }, - "loc2": { ".priority": "v0gs3y0zh7", "l": { "0": 50, "1": 50 }, "g": "v0gs3y0zh7" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("set() updates Firebase when changing a pre-existing key to the same location", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.set("loc2", [50, 50]); - }).then(function() { - cl.x("p2"); - - return geoFire.set("loc3", [-90, -90]); - }).then(function() { - cl.x("p3"); - - return geoFire.set("loc1", [0, 0]); - }).then(function() { - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc2": { ".priority": "v0gs3y0zh7", "l": { "0": 50, "1": 50 }, "g": "v0gs3y0zh7" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("set() handles multiple keys at the same location", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.set("loc2", [0, 0]); - }).then(function() { - cl.x("p2"); - - return geoFire.set("loc3", [0, 0]); - }).then(function() { - cl.x("p3"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc2": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc3": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" } - }); - - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("set() updates Firebase after complex operations", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10", "p11"], expect, done); - - geoFire.set("loc:1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.set("loc2", [50, 50]); - }).then(function() { - cl.x("p2"); - - return geoFire.set("loc%!A72f()3", [-90, -90]); - }).then(function() { - cl.x("p3"); - - return geoFire.remove("loc2"); - }).then(function() { - cl.x("p4"); - - return geoFire.set("loc2", [0.2358, -72.621]); - }).then(function() { - cl.x("p5"); - - return geoFire.set("loc4", [87.6, -130]); - }).then(function() { - cl.x("p6"); - - return geoFire.set("loc5", [5, 55.555]); - }).then(function() { - cl.x("p7"); - - return geoFire.set("loc5", null); - }).then(function() { - cl.x("p8"); - - return geoFire.set("loc:1", [87.6, -130]); - }).then(function() { - cl.x("p9"); - - return geoFire.set("loc6", [-72.258, 0.953215]); - }).then(function() { - cl.x("p10"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc:1": { ".priority": "cped3g0fur", "l": { "0": 87.6, "1": -130 }, "g": "cped3g0fur" }, - "loc2": { ".priority": "d2h376zj8h", "l": { "0": 0.2358, "1": -72.621 }, "g": "d2h376zj8h" }, - "loc%!A72f()3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" }, - "loc4": { ".priority": "cped3g0fur", "l": { "0": 87.6, "1": -130 }, "g": "cped3g0fur" }, - "loc6": { ".priority": "h50svty4es", "l": { "0": -72.258, "1": 0.953215 }, "g": "h50svty4es" } - }); - - cl.x("p11"); - }).catch(failTestOnCaughtError); - }); - - it("set() does not throw errors given valid keys", function() { - validKeys.forEach(function(validKey) { - expect(function() { - geoFire.set(validKey, [0, 0]); - }).not.toThrow(); - }); - }); - - it("set() throws errors given invalid keys", function() { - invalidKeys.forEach(function(invalidKey) { - expect(function() { - geoFire.set(invalidKey, [0, 0]); - }).toThrow(); - }); - }); - - it("set() does not throw errors given valid locations", function() { - validLocations.forEach(function(validLocation, i) { - expect(function() { - geoFire.set("loc", validLocation); - }).not.toThrow(); - }); - }); - - it("set() throws errors given invalid locations", function() { - invalidLocations.forEach(function(invalidLocation, i) { - // Setting location to null is valid since it will remove the key - if (invalidLocation !== null) { - expect(function() { - geoFire.set("loc", invalidLocation); - }).toThrow(); - } - }); - }); - }); - - describe("Adding multiple locations via set():", function() { - it("set() returns a promise", function(done) { - - var cl = new Checklist(["p1"], expect, done); - - geoFire.set({ - "loc1": [0, 0] - }).then(function() { - cl.x("p1"); - }); - }); - - it("set() updates Firebase when adding new locations", function(done) { - var cl = new Checklist(["p1", "p2"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, 50], - "loc3": [-90, -90] - }).then(function() { - cl.x("p1"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc2": { ".priority": "v0gs3y0zh7", "l": { "0": 50, "1": 50 }, "g": "v0gs3y0zh7" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("set() handles decimal latitudes and longitudes", function(done) { - var cl = new Checklist(["p1", "p2"], expect, done); - - geoFire.set({ - "loc1": [0.254, 0], - "loc2": [50, 50.293403], - "loc3": [-82.614, -90.938] - }).then(function() { - cl.x("p1"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "ebpcrypzxv", "l": { "0": 0.254, "1": 0 }, "g": "ebpcrypzxv" }, - "loc2": { ".priority": "v0gu2qnx15", "l": { "0": 50, "1": 50.293403 }, "g": "v0gu2qnx15" }, - "loc3": { ".priority": "1cr648sfx4", "l": { "0": -82.614, "1": -90.938 }, "g": "1cr648sfx4" } - }); - - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("set() updates Firebase when changing a pre-existing key", function(done) { - var cl = new Checklist(["p1", "p2", "p3"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, 50], - "loc3": [-90, -90] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 3] - }); - }).then(function() { - cl.x("p2"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "s065kk0dc5", "l": { "0": 2, "1": 3 }, "g": "s065kk0dc5" }, - "loc2": { ".priority": "v0gs3y0zh7", "l": { "0": 50, "1": 50 }, "g": "v0gs3y0zh7" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("set() updates Firebase when changing a pre-existing key to the same location", function(done) { - var cl = new Checklist(["p1", "p2", "p3"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, 50], - "loc3": [-90, -90] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [0, 0] - }); - }).then(function() { - cl.x("p2"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc2": { ".priority": "v0gs3y0zh7", "l": { "0": 50, "1": 50 }, "g": "v0gs3y0zh7" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("set() handles multiple keys at the same location", function(done) { - var cl = new Checklist(["p1", "p2"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [0, 0], - "loc3": [0, 0] - }).then(function() { - cl.x("p1"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc2": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" }, - "loc3": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" } - }); - - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("set() updates Firebase after complex operations", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "p6"], expect, done); - - geoFire.set({ - "loc:1": [0, 0], - "loc2": [50, 50], - "loc%!A72f()3": [-90, -90] - }).then(function() { - cl.x("p1"); - - return geoFire.remove("loc2"); - }).then(function() { - cl.x("p2"); - - return geoFire.set({ - "loc2": [0.2358, -72.621], - "loc4": [87.6, -130], - "loc5": [5, 55.555] - }); - }).then(function() { - cl.x("p3"); - - return geoFire.set({ - "loc5": null - }); - }).then(function() { - cl.x("p4"); - - return geoFire.set({ - "loc:1": [87.6, -130], - "loc6": [-72.258, 0.953215] - }); - }).then(function() { - cl.x("p5"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc:1": { ".priority": "cped3g0fur", "l": { "0": 87.6, "1": -130 }, "g": "cped3g0fur" }, - "loc2": { ".priority": "d2h376zj8h", "l": { "0": 0.2358, "1": -72.621 }, "g": "d2h376zj8h" }, - "loc%!A72f()3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" }, - "loc4": { ".priority": "cped3g0fur", "l": { "0": 87.6, "1": -130 }, "g": "cped3g0fur" }, - "loc6": { ".priority": "h50svty4es", "l": { "0": -72.258, "1": 0.953215 }, "g": "h50svty4es" } - }); - - cl.x("p6"); - }).catch(failTestOnCaughtError); - }); - - it("set() does not throw errors given valid keys", function() { - validKeys.forEach(function(validKey) { - expect(function() { - var locations = {}; - locations[validKey] = [0, 0]; - geoFire.set(locations); - }).not.toThrow(); - }); - }); - - it("set() throws errors given invalid keys", function() { - invalidKeys.forEach(function(invalidKey) { - if (invalidKey !== null && invalidKey !== undefined && typeof invalidKey !== "boolean") { - expect(function() { - var locations = {}; - locations[invalidKey] = [0, 0]; - geoFire.set(locations); - }).toThrow(); - } - }); - }); - - it("set() throws errors given a location argument in combination with an object", function() { - expect(function() { - geoFire.set({ - "loc": [0, 0] - }, [0, 0]); - }).toThrow(); - }); - - it("set() does not throw errors given valid locations", function() { - validLocations.forEach(function(validLocation, i) { - expect(function() { - geoFire.set({ - "loc": validLocation - }); - }).not.toThrow(); - }); - }); - - it("set() throws errors given invalid locations", function() { - invalidLocations.forEach(function(invalidLocation, i) { - // Setting location to null is valid since it will remove the key - if (invalidLocation !== null) { - expect(function() { - geoFire.set({ - "loc": invalidLocation - }); - }).toThrow(); - } - }); - }); - }); - - describe("Retrieving locations:", function() { - it("get() returns a promise", function(done) { - var cl = new Checklist(["p1"], expect, done); - - geoFire.get("loc1").then(function() { - cl.x("p1"); - }); - }); - - it("get() returns null for non-existent keys", function(done) { - var cl = new Checklist(["p1"], expect, done); - - geoFire.get("loc1").then(function(location) { - expect(location).toBeNull(); - - cl.x("p1"); - }); - }); - - it("get() retrieves locations given existing keys", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, 50], - "loc3": [-90, -90] - }).then(function() { - cl.x("p1"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - cl.x("p2"); - - return geoFire.get("loc2"); - }).then(function(location) { - expect(location).toEqual([50, 50]); - cl.x("p3"); - - return geoFire.get("loc3"); - }).then(function(location) { - expect(location).toEqual([-90, -90]); - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("get() does not throw errors given valid keys", function() { - validKeys.forEach(function(validKey) { - expect(function() { geoFire.get(validKey); }).not.toThrow(); - }); - }); - - it("get() throws errors given invalid keys", function() { - invalidKeys.forEach(function(invalidKey) { - expect(function() { geoFire.get(invalidKey); }).toThrow(); - }); - }); - }); - - describe("Removing locations:", function() { - it("set() removes existing location given null", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [2, 3] - }).then(function() { - cl.x("p1"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - - cl.x("p2"); - - return geoFire.set("loc1", null); - }).then(function() { - cl.x("p3"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toBeNull(); - - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc2": { ".priority": "s065kk0dc5", "l": { "0": 2, "1": 3 }, "g": "s065kk0dc5" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("set() does nothing given a non-existent location and null", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - - cl.x("p2"); - - return geoFire.set("loc2", null); - }).then(function() { - cl.x("p3"); - - return geoFire.get("loc2"); - }).then(function(location) { - expect(location).toBeNull(); - - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("set() removes existing location given null", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [2, 3] - }).then(function() { - cl.x("p1"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - - cl.x("p2"); - - return geoFire.set({ - "loc1": null, - "loc3": [-90, -90] - }); - }).then(function() { - cl.x("p3"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toBeNull(); - - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc2": { ".priority": "s065kk0dc5", "l": { "0": 2, "1": 3 }, "g": "s065kk0dc5" }, - "loc3": { ".priority": "1bpbpbpbpb", "l": { "0": -90, "1": -90 }, "g": "1bpbpbpbpb" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("set() does nothing given a non-existent location and null", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": null - }).then(function() { - cl.x("p1"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - - cl.x("p2"); - - return geoFire.get("loc2"); - }).then(function(location) { - expect(location).toBeNull(); - - cl.x("p3"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" } - }); - - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("remove() removes existing location", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set({ - "loc:^%*1": [0, 0], - "loc2": [2, 3] - }).then(function() { - cl.x("p1"); - - return geoFire.get("loc:^%*1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - - cl.x("p2"); - - return geoFire.remove("loc:^%*1"); - }).then(function() { - cl.x("p3"); - - return geoFire.get("loc:^%*1"); - }).then(function(location) { - expect(location).toBeNull(); - - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc2": { ".priority": "s065kk0dc5", "l": { "0": 2, "1": 3 }, "g": "s065kk0dc5" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("remove() does nothing given a non-existent location", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5"], expect, done); - - geoFire.set("loc1", [0, 0]).then(function() { - cl.x("p1"); - - return geoFire.get("loc1"); - }).then(function(location) { - expect(location).toEqual([0, 0]); - - cl.x("p2"); - - return geoFire.remove("loc2"); - }).then(function() { - cl.x("p3"); - - return geoFire.get("loc2"); - }).then(function(location) { - expect(location).toBeNull(); - - cl.x("p4"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc1": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" } - }); - - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("remove() only removes one key if multiple keys are at the same location", function(done) { - var cl = new Checklist(["p1", "p2", "p3"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [2, 3], - "loc3": [0, 0] - }).then(function() { - cl.x("p1"); - - return geoFire.remove("loc1"); - }).then(function() { - cl.x("p2"); - - return getFirebaseData(); - }).then(function(firebaseData) { - expect(firebaseData).toEqual({ - "loc2": { ".priority": "s065kk0dc5", "l": { "0": 2, "1": 3 }, "g": "s065kk0dc5" }, - "loc3": { ".priority": "7zzzzzzzzz", "l": { "0": 0, "1": 0 }, "g": "7zzzzzzzzz" } - }); - - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("remove() does not throw errors given valid keys", function() { - validKeys.forEach(function(validKey) { - expect(function() { geoFire.remove(validKey); }).not.toThrow(); - }); - }); - - it("remove() throws errors given invalid keys", function() { - invalidKeys.forEach(function(invalidKey) { - expect(function() { geoFire.remove(invalidKey); }).toThrow(); - }); - }); - }); - - describe("query():", function() { - it("query() returns GeoQuery instance", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - expect(geoQueries[0] instanceof GeoQuery).toBeTruthy(); - }); - - it("query() does not throw errors given valid query criteria", function() { - validQueryCriterias.forEach(function(validQueryCriteria) { - if (typeof validQueryCriteria.center !== "undefined" && typeof validQueryCriteria.radius !== "undefined") { - expect(function() { geoFire.query(validQueryCriteria); }).not.toThrow(); - } - }); - }); - - it("query() throws errors given invalid query criteria", function() { - invalidQueryCriterias.forEach(function(invalidQueryCriteria) { - expect(function() { geoFire.query(invalidQueryCriteria); }).toThrow(); - }); - }); - }); -}); diff --git a/tests/specs/geoFireUtils.spec.js b/tests/specs/geoFireUtils.spec.js deleted file mode 100644 index ce10ba54..00000000 --- a/tests/specs/geoFireUtils.spec.js +++ /dev/null @@ -1,269 +0,0 @@ -describe("geoFireUtils Tests:", function() { - describe("Parameter validation:", function() { - it("validateKey() does not throw errors given valid keys", function() { - validKeys.forEach(function(validKey) { - expect(function() { validateKey(validKey); }).not.toThrow(); - }); - }); - - it("validateKey() throws errors given invalid keys", function() { - invalidKeys.forEach(function(invalidKey) { - expect(function() { validateKey(invalidKey); }).toThrow(); - }); - }); - - it("validateLocation() does not throw errors given valid locations", function() { - validLocations.forEach(function(validLocation, i) { - expect(function() { validateLocation(validLocation); }).not.toThrow(); - }); - }); - - it("validateLocation() throws errors given invalid locations", function() { - invalidLocations.forEach(function(invalidLocation, i) { - expect(function() { validateLocation(invalidLocation); }).toThrow(); - }); - }); - - it("validateGeohash() does not throw errors given valid geohashes", function() { - validGeohashes.forEach(function(validGeohash, i) { - expect(function() { validateGeohash(validGeohash); }).not.toThrow(); - }); - }); - - it("validateGeohash() throws errors given invalid geohashes", function() { - invalidGeohashes.forEach(function(invalidGeohash, i) { - expect(function() { validateGeohash(invalidGeohash); }).toThrow(); - }); - }); - - it("validateCriteria(criteria, true) does not throw errors given valid query criteria", function() { - validQueryCriterias.forEach(function(validQueryCriteria) { - if (typeof validQueryCriteria.center !== "undefined" && typeof validQueryCriteria.radius !== "undefined") { - expect(function() { validateCriteria(validQueryCriteria, true); }).not.toThrow(); - } - }); - }); - - it("validateCriteria(criteria) does not throw errors given valid query criteria", function() { - validQueryCriterias.forEach(function(validQueryCriteria) { - expect(function() { validateCriteria(validQueryCriteria); }).not.toThrow(); - }); - }); - - it("validateCriteria(criteria, true) throws errors given invalid query criteria", function() { - invalidQueryCriterias.forEach(function(invalidQueryCriteria) { - expect(function() { validateCriteria(invalidQueryCriteria, true); }).toThrow(); - }); - expect(function() { validateCriteria({center: [0, 0]}, true); }).toThrow(); - expect(function() { validateCriteria({radius: 1000}, true); }).toThrow(); - }); - - it("validateCriteria(criteria) throws errors given invalid query criteria", function() { - invalidQueryCriterias.forEach(function(invalidQueryCriteria) { - expect(function() { validateCriteria(invalidQueryCriteria); }).toThrow(); - }); - }); - }); - - describe("Distance calculations:", function() { - it("degreesToRadians() converts degrees to radians", function() { - expect(degreesToRadians(0)).toBeCloseTo(0); - expect(degreesToRadians(45)).toBeCloseTo(0.7854, 4); - expect(degreesToRadians(90)).toBeCloseTo(1.5708, 4); - expect(degreesToRadians(135)).toBeCloseTo(2.3562, 4); - expect(degreesToRadians(180)).toBeCloseTo(3.1416, 4); - expect(degreesToRadians(225)).toBeCloseTo(3.9270, 4); - expect(degreesToRadians(270)).toBeCloseTo(4.7124, 4); - expect(degreesToRadians(315)).toBeCloseTo(5.4978, 4); - expect(degreesToRadians(360)).toBeCloseTo(6.2832, 4); - expect(degreesToRadians(-45)).toBeCloseTo(-0.7854, 4); - expect(degreesToRadians(-90)).toBeCloseTo(-1.5708, 4); - }); - - it("degreesToRadians() throws errors given invalid inputs", function() { - expect(function() { degreesToRadians(""); }).toThrow(); - expect(function() { degreesToRadians("a"); }).toThrow(); - expect(function() { degreesToRadians(true); }).toThrow(); - expect(function() { degreesToRadians(false); }).toThrow(); - expect(function() { degreesToRadians([1]); }).toThrow(); - expect(function() { degreesToRadians({}); }).toThrow(); - expect(function() { degreesToRadians(null); }).toThrow(); - expect(function() { degreesToRadians(undefined); }).toThrow(); - }); - - it("dist() calculates the distance between locations", function() { - expect(GeoFire.distance([90, 180], [90, 180])).toBeCloseTo(0, 0); - expect(GeoFire.distance([-90, -180], [90, 180])).toBeCloseTo(20015, 0); - expect(GeoFire.distance([-90, -180], [-90, 180])).toBeCloseTo(0, 0); - expect(GeoFire.distance([-90, -180], [90, -180])).toBeCloseTo(20015, 0); - expect(GeoFire.distance([37.7853074, -122.4054274], [78.216667, 15.55])).toBeCloseTo(6818, 0); - expect(GeoFire.distance([38.98719, -77.250783], [29.3760648, 47.9818853])).toBeCloseTo(10531, 0); - expect(GeoFire.distance([38.98719, -77.250783], [-54.933333, -67.616667])).toBeCloseTo(10484, 0); - expect(GeoFire.distance([29.3760648, 47.9818853], [-54.933333, -67.616667])).toBeCloseTo(14250, 0); - expect(GeoFire.distance([-54.933333, -67.616667], [-54, -67])).toBeCloseTo(111, 0); - }); - - it("dist() does not throw errors given valid locations", function() { - validLocations.forEach(function(validLocation, i) { - expect(function() { GeoFire.distance(validLocation, [0, 0]); }).not.toThrow(); - expect(function() { GeoFire.distance([0, 0], validLocation); }).not.toThrow(); - }); - }); - - it("dist() throws errors given invalid locations", function() { - invalidLocations.forEach(function(invalidLocation, i) { - expect(function() { GeoFire.distance(invalidLocation, [0, 0]); }).toThrow(); - expect(function() { GeoFire.distance([0, 0], invalidLocation); }).toThrow(); - }); - }); - }); - - describe("Geohashing:", function() { - it("encodeGeohash() encodes locations to geohashes given no precision", function() { - expect(encodeGeohash([-90, -180])).toBe("000000000000".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([90, 180])).toBe("zzzzzzzzzzzz".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([-90, 180])).toBe("pbpbpbpbpbpb".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([90, -180])).toBe("bpbpbpbpbpbp".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([37.7853074, -122.4054274])).toBe("9q8yywe56gcf".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([38.98719, -77.250783])).toBe("dqcjf17sy6cp".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([29.3760648, 47.9818853])).toBe("tj4p5gerfzqu".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([78.216667, 15.55])).toBe("umghcygjj782".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([-54.933333, -67.616667])).toBe("4qpzmren1kwb".slice(0, g_GEOHASH_PRECISION)); - expect(encodeGeohash([-54, -67])).toBe("4w2kg3s54y7h".slice(0, g_GEOHASH_PRECISION)); - }); - - it("encodeGeohash() encodes locations to geohashes given a custom precision", function() { - expect(encodeGeohash([-90, -180], 6)).toBe("000000"); - expect(encodeGeohash([90, 180], 20)).toBe("zzzzzzzzzzzzzzzzzzzz"); - expect(encodeGeohash([-90, 180], 1)).toBe("p"); - expect(encodeGeohash([90, -180], 5)).toBe("bpbpb"); - expect(encodeGeohash([37.7853074, -122.4054274], 8)).toBe("9q8yywe5"); - expect(encodeGeohash([38.98719, -77.250783], 18)).toBe("dqcjf17sy6cppp8vfn"); - expect(encodeGeohash([29.3760648, 47.9818853], 12)).toBe("tj4p5gerfzqu"); - expect(encodeGeohash([78.216667, 15.55], 1)).toBe("u"); - expect(encodeGeohash([-54.933333, -67.616667], 7)).toBe("4qpzmre"); - expect(encodeGeohash([-54, -67], 9)).toBe("4w2kg3s54"); - }); - - it("encodeGeohash() does not throw errors given valid locations", function() { - validLocations.forEach(function(validLocation, i) { - expect(function() { encodeGeohash(validLocation); }).not.toThrow(); - }); - }); - - it("encodeGeohash() throws errors given invalid locations", function() { - invalidLocations.forEach(function(invalidLocation, i) { - expect(function() { encodeGeohash(invalidLocation); }).toThrow(); - }); - }); - - it("encodeGeohash() does not throw errors given valid precision", function() { - var validPrecisions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, undefined]; - - validPrecisions.forEach(function(validPrecision, i) { - expect(function() { encodeGeohash([0, 0], validPrecision); }).not.toThrow(); - }); - }); - - it("encodeGeohash() throws errors given invalid precision", function() { - var invalidPrecisions = [0, -1, 1.5, 23, "", "a", true, false, [], {}, [1], {a:1}, null]; - - invalidPrecisions.forEach(function(invalidPrecision, i) { - expect(function() { encodeGeohash([0, 0], invalidPrecision); }).toThrow(); - }); - }); - }); - - describe("Coordinate calculations:", function() { - it("metersToLongtitudeDegrees calculates correctly", function() { - expect(metersToLongitudeDegrees(1000, 0)).toBeCloseTo(0.008983, 5); - expect(metersToLongitudeDegrees(111320, 0)).toBeCloseTo(1, 5); - expect(metersToLongitudeDegrees(107550, 15)).toBeCloseTo(1, 5); - expect(metersToLongitudeDegrees(96486, 30)).toBeCloseTo(1, 5); - expect(metersToLongitudeDegrees(78847, 45)).toBeCloseTo(1, 5); - expect(metersToLongitudeDegrees(55800, 60)).toBeCloseTo(1, 5); - expect(metersToLongitudeDegrees(28902, 75)).toBeCloseTo(1, 5); - expect(metersToLongitudeDegrees(0, 90)).toBeCloseTo(0, 5); - expect(metersToLongitudeDegrees(1000, 90)).toBeCloseTo(360, 5); - expect(metersToLongitudeDegrees(1000, 89.9999)).toBeCloseTo(360, 5); - expect(metersToLongitudeDegrees(1000, 89.995)).toBeCloseTo(102.594208, 5); - }); - - it("wrapLongitude wraps correctly", function() { - expect(wrapLongitude(0)).toBeCloseTo(0, 6); - expect(wrapLongitude(180)).toBeCloseTo(180, 6); - expect(wrapLongitude(-180)).toBeCloseTo(-180, 6); - expect(wrapLongitude(182)).toBeCloseTo(-178, 6); - expect(wrapLongitude(270)).toBeCloseTo(-90, 6); - expect(wrapLongitude(360)).toBeCloseTo(0, 6); - expect(wrapLongitude(540)).toBeCloseTo(-180, 6); - expect(wrapLongitude(630)).toBeCloseTo(-90, 6); - expect(wrapLongitude(720)).toBeCloseTo(0, 6); - expect(wrapLongitude(810)).toBeCloseTo(90, 6); - expect(wrapLongitude(-360)).toBeCloseTo(0, 6); - expect(wrapLongitude(-182)).toBeCloseTo(178, 6); - expect(wrapLongitude(-270)).toBeCloseTo(90, 6); - expect(wrapLongitude(-360)).toBeCloseTo(0, 6); - expect(wrapLongitude(-450)).toBeCloseTo(-90, 6); - expect(wrapLongitude(-540)).toBeCloseTo(180, 6); - expect(wrapLongitude(-630)).toBeCloseTo(90, 6); - expect(wrapLongitude(1080)).toBeCloseTo(0, 6); - expect(wrapLongitude(-1080)).toBeCloseTo(0, 6); - }); - }); - - describe("Bounding box bits:", function() { - it("boundingBoxBits must return correct number of bits", function() { - expect(boundingBoxBits([35,0], 1000)).toBe(28); - expect(boundingBoxBits([35.645,0], 1000)).toBe(27); - expect(boundingBoxBits([36,0], 1000)).toBe(27); - expect(boundingBoxBits([0,0], 1000)).toBe(28); - expect(boundingBoxBits([0,-180], 1000)).toBe(28); - expect(boundingBoxBits([0,180], 1000)).toBe(28); - expect(boundingBoxBits([0,0], 8000)).toBe(22); - expect(boundingBoxBits([45,0], 1000)).toBe(27); - expect(boundingBoxBits([75,0], 1000)).toBe(25); - expect(boundingBoxBits([75,0], 2000)).toBe(23); - expect(boundingBoxBits([90,0], 1000)).toBe(1); - expect(boundingBoxBits([90,0], 2000)).toBe(1); - }); - }); - - describe("Geohash queries:", function() { - it("Geohash queries must be of the right size", function() { - expect(geohashQuery("64m9yn96mx",6)).toEqual(["60", "6h"]); - expect(geohashQuery("64m9yn96mx",1)).toEqual(["0", "h"]); - expect(geohashQuery("64m9yn96mx",10)).toEqual(["64", "65"]); - expect(geohashQuery("6409yn96mx",11)).toEqual(["640", "64h"]); - expect(geohashQuery("64m9yn96mx",11)).toEqual(["64h", "64~"]); - expect(geohashQuery("6",10)).toEqual(["6", "6~"]); - expect(geohashQuery("64z178",12)).toEqual(["64s", "64~"]); - expect(geohashQuery("64z178",15)).toEqual(["64z", "64~"]); - }); - - it("Queries from geohashQueries must contain points in circle", function() { - function inQuery(queries, hash) { - for (var i = 0; i < queries.length; i++) { - if (hash >= queries[i][0] && hash < queries[i][1]) { - return true; - } - } - return false; - } - for (var i = 0; i < 200; i++) { - var centerLat = Math.pow(Math.random(),5)*160-80; - var centerLong = Math.pow(Math.random(),5)*360-180; - var radius = Math.random()*Math.random()*100000; - var degreeRadius = metersToLongitudeDegrees(radius, centerLat); - var queries = geohashQueries([centerLat, centerLong], radius); - for (var j = 0; j < 1000; j++) { - var pointLat = Math.max(-89.9, Math.min(89.9, centerLat + Math.random()*degreeRadius)); - var pointLong = wrapLongitude(centerLong + Math.random()*degreeRadius); - if (GeoFire.distance([centerLat, centerLong], [pointLat, pointLong]) < radius/1000) { - expect(inQuery(queries, encodeGeohash([pointLat, pointLong]))).toBe(true); - } - } - } - }); - }); -}); diff --git a/tests/specs/geoQuery.spec.js b/tests/specs/geoQuery.spec.js deleted file mode 100644 index e846ac94..00000000 --- a/tests/specs/geoQuery.spec.js +++ /dev/null @@ -1,1404 +0,0 @@ -describe("GeoQuery Tests:", function() { - // Reset the Firebase before each test - beforeEach(function(done) { - beforeEachHelper(done); - }); - - afterEach(function(done) { - afterEachHelper(done); - }); - - describe("Constructor:", function() { - it("Constructor stores query criteria", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - expect(geoQueries[0].center()).toEqual([1,2]); - expect(geoQueries[0].radius()).toEqual(1000); - }); - - it("Constructor throws error on invalid query criteria", function() { - expect(function() { geoFire.query({}) }).toThrow(); - expect(function() { geoFire.query({random: 100}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2]}) }).toThrow(); - expect(function() { geoFire.query({radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: [91,2], radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: [1,-181], radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: ["text",2], radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: [1,[1,2]], radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: 1000, radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: null, radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: undefined, radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: [null,2], radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: [1,undefined], radius: 1000}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2], radius: -10}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2], radius: "text"}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2], radius: [1,2]}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2], radius: null}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2], radius: undefined}) }).toThrow(); - expect(function() { geoFire.query({center: [1,2], radius: 1000, other: "throw"}) }).toThrow(); - }); - }); - - describe("updateCriteria():", function() { - it("updateCriteria() updates query criteria", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - expect(geoQueries[0].center()).toEqual([1,2]); - expect(geoQueries[0].radius()).toEqual(1000); - - geoQueries[0].updateCriteria({center: [2,3], radius: 100}); - - expect(geoQueries[0].center()).toEqual([2,3]); - expect(geoQueries[0].radius()).toEqual(100); - }); - - it("updateCriteria() updates query criteria when given only center", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - expect(geoQueries[0].center()).toEqual([1,2]); - expect(geoQueries[0].radius()).toEqual(1000); - - geoQueries[0].updateCriteria({center: [2,3]}); - - expect(geoQueries[0].center()).toEqual([2,3]); - expect(geoQueries[0].radius()).toEqual(1000); - }); - - it("updateCriteria() updates query criteria when given only radius", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - expect(geoQueries[0].center()).toEqual([1,2]); - expect(geoQueries[0].radius()).toEqual(1000); - - geoQueries[0].updateCriteria({radius: 100}); - - expect(geoQueries[0].center()).toEqual([1,2]); - expect(geoQueries[0].radius()).toEqual(100); - }); - - it("updateCriteria() fires \"key_entered\" callback for locations which now belong to the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 entered", "loc4 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [90,90], radius: 1000})); - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - geoQueries[0].updateCriteria({center: [1,2], radius: 1000}); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("updateCriteria() fires \"key_entered\" callback for locations with complex keys which now belong to the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "loc:^:*1 entered", "loc-+-+-4 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [90,90], radius: 1000})); - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoFire.set({ - "loc:^:*1": [2, 3], - "loc:a:a:a:a:2": [50, -7], - "loc%!@3": [16, -150], - "loc-+-+-4": [5, 5], - "loc:5": [67, 55] - }).then(function() { - cl.x("p1"); - - geoQueries[0].updateCriteria({center: [1,2], radius: 1000}); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("updateCriteria() fires \"key_exited\" callback for locations which no longer belong to the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 exited", "loc4 exited"], expect, done); - - geoQueries.push(geoFire.query({center: [1, 2], radius: 1000})); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - geoQueries[0].updateCriteria({center: [90,90], radius: 1000}); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("updateCriteria() does not cause event callbacks to fire on the previous criteria", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "loc1 entered", "loc4 entered", "loc1 exited", "loc4 exited", "loc4 entered", "loc5 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1, 2], radius: 1000})); - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [88, 88] - }).then(function() { - cl.x("p1"); - - geoQueries[0].updateCriteria({center: [90, 90], radius: 1000}); - - return wait(100); - }).then(function() { - cl.x("p2"); - - return geoFire.set({ - "loc2": [1, 1], - "loc4": [89, 90] - }); - }).then(function() { - cl.x("p3"); - - return wait(100); - }).then(function() { - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("updateCriteria() does not cause \"key_moved\" callbacks to fire for keys in both the previous and updated queries", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "loc1 entered", "loc4 entered", "loc4 exited", "loc2 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1, 2], radius: 1000})); - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [88, 88] - }).then(function() { - cl.x("p1"); - - geoQueries[0].updateCriteria({center: [1, 1], radius: 1000}); - - return wait(100); - }).then(function() { - cl.x("p2"); - - return geoFire.set({ - "loc2": [1, 1], - "loc4": [89, 90] - }); - }).then(function() { - cl.x("p3"); - - return wait(100); - }).then(function() { - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it("updateCriteria() does not cause \"key_exited\" callbacks to fire twice for keys in the previous query but not in the updated query and which were moved after the query was updated", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "p5", "p6", "loc1 entered", "loc4 entered", "loc1 exited", "loc4 exited", "loc4 entered", "loc5 entered", "loc5 moved"], expect, done); - - geoQueries.push(geoFire.query({center: [1, 2], radius: 1000})); - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [88, 88] - }).then(function() { - cl.x("p1"); - - geoQueries[0].updateCriteria({center: [90, 90], radius: 1000}); - - return wait(100); - }).then(function() { - cl.x("p2"); - - return geoFire.set({ - "loc2": [1, 1], - "loc4": [89, 90] - }); - }).then(function() { - cl.x("p3"); - - return wait(100); - }).then(function() { - cl.x("p4"); - - return geoFire.set({ - "loc2": [0, 0], - "loc5": [89, 89] - }); - }).then(function() { - cl.x("p5"); - - return wait(100); - }).then(function() { - cl.x("p6"); - }).catch(failTestOnCaughtError); - }); - - it("updateCriteria() does not throw errors given valid query criteria", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - validQueryCriterias.forEach(function(validQueryCriteria) { - expect(function() { geoQueries[0].updateCriteria(validQueryCriteria); }).not.toThrow(); - }); - }); - - it("updateCriteria() throws errors given invalid query criteria", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - invalidQueryCriterias.forEach(function(invalidQueryCriteria) { - expect(function() { geoQueries[0].updateCriteria(invalidQueryCriteria); }).toThrow(); - }); - }); - }); - - describe("on():", function() { - it("on() throws error given invalid event type", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var setInvalidEventType = function() { - geoQueries[0].on("invalid_event", function() { }); - } - - expect(setInvalidEventType).toThrow(); - }); - - it("on() throws error given invalid callback", function() { - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - var setInvalidCallback = function() { - geoQueries[0].on("key_entered", "non-function"); - } - - expect(setInvalidCallback).toThrow(); - }); - }); - - describe("\"ready\" event:", function() { - it("\"ready\" event fires after all \"key_entered\" events have fired", function(done) { - var cl = new Checklist(["p1", "loc1 entered", "loc2 entered", "loc5 entered", "loc6 entered", "loc7 entered", "loc10 entered", "ready fired"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [1, 1], - "loc3": [50, 50], - "loc4": [14, 1], - "loc5": [1, 2], - "loc6": [1, 1], - "loc7": [0, 0], - "loc8": [-80, 44], - "loc9": [1, -136], - "loc10": [-2, -2] - }).then(function() { - cl.x("p1"); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoQueries[0].on("ready", function() { - expect(cl.length()).toBe(1); - cl.x("ready fired"); - }); - }); - }); - - it("\"ready\" event fires immediately if the callback is added after the query is already ready", function(done) { - var cl = new Checklist(["p1", "loc1 entered", "loc2 entered", "loc5 entered", "loc6 entered", "loc7 entered", "loc10 entered", "ready1 fired", "ready2 fired"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [1, 1], - "loc3": [50, 50], - "loc4": [14, 1], - "loc5": [1, 2], - "loc6": [1, 1], - "loc7": [0, 0], - "loc8": [-80, 44], - "loc9": [1, -136], - "loc10": [-2, -2] - }).then(function() { - cl.x("p1"); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoQueries[0].on("ready", function() { - expect(cl.length()).toBe(2); - cl.x("ready1 fired"); - geoQueries[0].on("ready", function() { - expect(cl.length()).toBe(1); - cl.x("ready2 fired"); - }); - }); - }); - }); - - it("\"ready\" event fires after increasing the query radius, even if no new geohashes were queried", function(done) { - var cl = new Checklist(["ready1 fired","ready2 fired"], expect, done); - geoQueries.push(geoFire.query({center: [37.7851382,-122.405893], radius: 6})); - var onReadyCallbackRegistration1 = geoQueries[0].on("ready", function() { - cl.x("ready1 fired"); - onReadyCallbackRegistration1.cancel(); - geoQueries[0].updateCriteria({ - radius: 7 - }); - geoQueries[0].on("ready", function() { - cl.x("ready2 fired"); - }); - }); - }); - - it("updateCriteria() fires the \"ready\" event after all \"key_entered\" events have fired", function(done) { - var cl = new Checklist(["p1", "loc1 entered", "loc2 entered", "loc5 entered", "loc3 entered", "loc1 exited", "loc2 exited", "loc5 exited", "ready1 fired", "ready2 fired"], expect, done); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [1, 1], - "loc3": [50, 50], - "loc4": [14, 1], - "loc5": [1, 2] - }).then(function() { - cl.x("p1"); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - var onReadyCallbackRegistration1 = geoQueries[0].on("ready", function() { - expect(cl.length()).toBe(6); - cl.x("ready1 fired"); - - onReadyCallbackRegistration1.cancel(); - - geoQueries[0].updateCriteria({ - center: [51, 51] - }); - - geoQueries[0].on("ready", function() { - expect(cl.length()).toBe(1); - cl.x("ready2 fired"); - }); - }); - }); - }); - }); - - describe("\"key_moved\" event:", function() { - it("\"key_moved\" callback does not fire for brand new locations within or outside of the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback does not fire for locations outside of the GeoQuery which are moved somewhere else outside of the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "p3"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [1, 90], - "loc2": [50, -7], - "loc3": [16, -150] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [1, 91], - "loc3": [-50, -50] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback does not fire for locations outside of the GeoQuery which are moved within the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "p3"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [1, 90], - "loc2": [50, -7], - "loc3": [16, -150] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [0, 0], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback does not fire for locations within the GeoQuery which are moved somewhere outside of the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "p3"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [1, 90], - "loc3": [-1, -90] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback does not fires for a location within the GeoQuery which is set to the same location", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc3 moved"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, -1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [0, 0], - "loc2": [55, 55], - "loc3": [1, 1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback fires for locations within the GeoQuery which are moved somewhere else within the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 moved", "loc3 moved"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 2], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback gets passed correct location parameter", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 moved to 2,2", "loc3 moved to -1,-1"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved to " + location); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 2], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_moved\" callback gets passed correct distance parameter", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 moved (111.19 km from center)", "loc3 moved (400.90 km from center)"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved (" + distance.toFixed(2) + " km from center)"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 2], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).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", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 moved", "loc3 moved"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [0, 0], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 2], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).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", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 moved", "loc3 moved"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [2, 2], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 2], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("multiple \"key_moved\" callbacks fire for locations within the GeoQuery which are moved somewhere else within the GeoQuery", function(done) { - var 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})); - - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved1"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved2"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, -7], - "loc3": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [2, 2], - "loc3": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - }); - - describe("\"key_entered\" event:", function() { - it("\"key_entered\" callback fires when a location enters the GeoQuery before onKeyEntered() was called", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 entered", "loc4 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_entered\" callback fires when a location enters the GeoQuery after onKeyEntered() was called", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 entered", "loc4 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_entered\" callback gets passed correct location parameter", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 entered at 2,3", "loc4 entered at 5,5"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered at " + location); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_entered\" callback gets passed correct distance parameter", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 entered (157.23 km from center)", "loc4 entered (555.66 km from center)"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered (" + distance.toFixed(2) + " km from center)"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).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", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoFire.set({ - "loc1": [50, 50], - "loc2": [50, 50], - "loc3": [18, -121] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [2, 2]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).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", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 entered", "loc3 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - - geoFire.set({ - "loc1": [50, 50], - "loc2": [50, 50], - "loc3": [0, 0] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [0, 0]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("multiple \"key_entered\" callbacks fire when a location enters the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "loc1 entered1", "loc4 entered1", "loc1 entered2", "loc4 entered2"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered1"); - }); - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered2"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return wait(100); - }).then(function() { - cl.x("p2"); - }).catch(failTestOnCaughtError); - }); - }); - - describe("\"key_exited\" event:", function() { - it("\"key_exited\" callback fires when a location leaves the GeoQuery", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited", "loc4 exited"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [25, 90], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_exited\" callback gets passed correct location parameter", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited to 25,90", "loc4 exited to 25,5"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited to " + location); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [5, 2], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [25, 90], - "loc2": [5, 5], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_exited\" callback gets passed correct distance parameter", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited (9759.01 km from center)", "loc4 exited (2688.06 km from center)"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited (" + distance.toFixed(2) + " km from center)"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [5, 2], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [25, 90], - "loc2": [5, 5], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_exited\" callback gets passed null for location and distance parameters if the key is entirely removed from GeoFire", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - expect(location).toBeNull(); - expect(distance).toBeNull(); - cl.x(key + " exited"); - }); - - geoFire.set("loc1", [2, 3]).then(function() { - cl.x("p1"); - - return geoFire.remove("loc1"); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("\"key_exited\" callback fires when a location within the GeoQuery is entirely removed from GeoFire", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [2, 3] - }).then(function() { - cl.x("p1"); - - return geoFire.remove("loc1"); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).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", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited"], expect, done); - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [0, 0], - "loc3": [18, -121] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [20, -55]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).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", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "loc1 exited"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - - geoFire.set({ - "loc1": [0, 0], - "loc2": [50, 50], - "loc3": [18, -121] - }).then(function() { - cl.x("p1"); - - return geoFire.set("loc1", [18, -121]); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - - it("multiple \"key_exited\" callbacks fire when a location leaves the GeoQuery", function(done) { - var 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})); - - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited1"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited2"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [25, 90], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3"); - }).catch(failTestOnCaughtError); - }); - }); - - describe("\"key_*\" events combined:", function() { - it ("\"key_*\" event callbacks fire when used all at the same time", function(done) { - var cl = new Checklist(["p1", "p2", "p3", "p4", "loc1 entered", "loc4 entered", "loc1 moved", "loc4 exited", "loc1 exited", "loc5 entered"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [1, 1], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return geoFire.set({ - "loc1": [10, -100], - "loc2": [50, -50], - "loc5": [5, 5] - }); - }).then(function() { - cl.x("p3"); - - return wait(100); - }).then(function() { - cl.x("p4"); - }).catch(failTestOnCaughtError); - }); - - it ("location moving between geohash queries triggers a key_moved", function(done) { - var cl = new Checklist(["loc1 entered", "loc2 entered", "p1", "loc1 moved", "loc2 moved", "p2"], expect, done); - - geoQueries.push(geoFire.query({center: [0,0], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [-1, -1], - "loc2": [1, 1] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [1, 1], - "loc2": [-1, -1] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).catch(failTestOnCaughtError); - }); - }); - - describe("Cancelling GeoQuery:", function() { - it("cancel() prevents GeoQuery from firing any more \"key_*\" event callbacks", function(done) { - var 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})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [1, 1], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3") - - geoQueries[0].cancel(); - - return wait(1000); - }).then(function() { - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved"); - }); - - return geoFire.set({ - "loc1": [10, -100], - "loc2": [50, -50], - "loc5": [5, 5] - }); - }).then(function() { - cl.x("p4"); - - return wait(100); - }).then(function() { - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("Calling cancel() on one GeoQuery does not cancel other GeoQueries", function(done) { - var 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})); - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x(key + " entered1"); - }); - geoQueries[0].on("key_exited", function(key, location, distance) { - cl.x(key + " exited1"); - }); - geoQueries[0].on("key_moved", function(key, location, distance) { - cl.x(key + " moved1"); - }); - - geoQueries[1].on("key_entered", function(key, location, distance) { - cl.x(key + " entered2"); - }); - geoQueries[1].on("key_exited", function(key, location, distance) { - cl.x(key + " exited2"); - }); - geoQueries[1].on("key_moved", function(key, location, distance) { - cl.x(key + " moved2"); - }); - - geoFire.set({ - "loc1": [2, 3], - "loc2": [50, -7], - "loc3": [16, -150], - "loc4": [5, 5], - "loc5": [67, 55] - }).then(function() { - cl.x("p1"); - - return geoFire.set({ - "loc1": [1, 1], - "loc4": [25, 5] - }); - }).then(function() { - cl.x("p2"); - - return wait(100); - }).then(function() { - cl.x("p3") - - geoQueries[0].cancel(); - - return geoFire.set({ - "loc1": [10, -100], - "loc2": [50, -50], - "loc5": [1, 2] - }); - }).then(function() { - cl.x("p4"); - - return wait(100); - }).then(function() { - cl.x("p5"); - }).catch(failTestOnCaughtError); - }); - - it("Calling cancel() in the middle of firing \"key_entered\" events is allowed", function(done) { - var cl = new Checklist(["p1", "key entered", "cancel query"], expect, done); - - geoQueries.push(geoFire.query({center: [1,2], radius: 1000})); - - geoFire.set({ - "loc1": [1, 2], - "loc2": [1, 3], - "loc3": [1, 4] - }).then(function() { - cl.x("p1"); - - var numKeyEnteredEventsFired = 0; - geoQueries[0].on("key_entered", function(key, location, distance) { - cl.x("key entered"); - numKeyEnteredEventsFired++; - if (numKeyEnteredEventsFired === 1) { - cl.x("cancel query"); - geoQueries[0].cancel(); - } - }); - }).catch(failTestOnCaughtError); - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..c8a33765 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "declaration": true, + "importHelpers": true, + "lib": [ + "es2015", + "dom" + ], + "moduleResolution": "node", + "noImplicitAny": false, + "outDir": "dist", + "sourceMap": false, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ] + }, + "include": [ + "src" + ] +} \ No newline at end of file