geofire
Version:
Location-based querying and filtering using Firebase
1,315 lines (1,300 loc) • 668 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.geofire = {}));
}(this, (function (exports) { 'use strict';
/**
* Creates a GeoCallbackRegistration instance.
*/
var GeoCallbackRegistration = /** @class */ (function () {
/**
* @param _cancelCallback Callback to run when this callback registration is cancelled.
*/
function GeoCallbackRegistration(_cancelCallback) {
this._cancelCallback = _cancelCallback;
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.
*/
GeoCallbackRegistration.prototype.cancel = function () {
if (typeof this._cancelCallback !== 'undefined') {
this._cancelCallback();
this._cancelCallback = undefined;
}
};
return GeoCallbackRegistration;
}());
// Default geohash length
var GEOHASH_PRECISION = 10;
// Characters used in location geohashes
var BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
// The meridional circumference of the earth in meters
var EARTH_MERI_CIRCUMFERENCE = 40007860;
// Length of a degree latitude at the equator
var METERS_PER_DEGREE_LATITUDE = 110574;
// Number of bits per geohash character
var BITS_PER_CHAR = 5;
// Maximum length of a geohash in bits
var MAXIMUM_BITS_PRECISION = 22 * BITS_PER_CHAR;
// Equatorial radius of the earth in meters
var EARTH_EQ_RADIUS = 6378137.0;
// The following value assumes a polar radius of
// const EARTH_POL_RADIUS = 6356752.3;
// The formulate to calculate E2 is
// E2 == (EARTH_EQ_RADIUS^2-EARTH_POL_RADIUS^2)/(EARTH_EQ_RADIUS^2)
// The exact value is used here to avoid rounding errors
var E2 = 0.00669447819799;
// Cutoff for rounding errors on double calculations
var EPSILON = 1e-12;
function log2(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.
*/
function validateKey(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 + 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/<geohash>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.
*/
function validateLocation(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 geohash The geohash to be validated.
*/
function validateGeohash(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, geohash_1 = geohash; _i < geohash_1.length; _i++) {
var letter = geohash_1[_i];
if (BASE32.indexOf(letter) === -1) {
error = 'geohash cannot contain \'' + letter + '\'';
}
}
}
if (typeof error !== 'undefined') {
throw new Error('Invalid GeoFire geohash \'' + geohash + '\': ' + error);
}
}
/**
* 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.
*/
function degreesToRadians(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 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.
*/
function geohashForLocation(location, precision) {
if (precision === void 0) { precision = GEOHASH_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');
}
}
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;
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 += 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.
*/
function metersToLongitudeDegrees(distance, latitude) {
var radians = degreesToRadians(latitude);
var num = Math.cos(radians) * EARTH_EQ_RADIUS * Math.PI / 180;
var denom = 1 / Math.sqrt(1 - E2 * Math.sin(radians) * Math.sin(radians));
var deltaDeg = num * denom;
if (deltaDeg < 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.
*/
function longitudeBitsForResolution(resolution, latitude) {
var degs = metersToLongitudeDegrees(resolution, latitude);
return (Math.abs(degs) > 0.000001) ? Math.max(1, 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.
*/
function latitudeBitsForResolution(resolution) {
return Math.min(log2(EARTH_MERI_CIRCUMFERENCE / 2 / resolution), MAXIMUM_BITS_PRECISION);
}
/**
* Wraps the longitude to [-180,180].
*
* @param longitude The longitude to wrap.
* @returns longitude The resulting longitude.
*/
function wrapLongitude(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 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.
*/
function boundingBoxBits(coordinate, size) {
var latDeltaDegrees = size / 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, 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 in meters.
* @returns The center of the box, and the eight bounding box points.
*/
function boundingBoxCoordinates(center, radius) {
var latDegrees = radius / 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 geohash The geohash whose bounding box query to generate.
* @param bits The number of bits of precision.
* @returns A [start, end] pair of geohashes.
*/
function geohashQuery(geohash, bits) {
validateGeohash(geohash);
var precision = Math.ceil(bits / 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 = BASE32.indexOf(geohash.charAt(geohash.length - 1));
var significantBits = bits - (base.length * BITS_PER_CHAR);
var unusedBits = (BITS_PER_CHAR - significantBits);
// delete unused bits
var startValue = (lastValue >> unusedBits) << unusedBits;
var endValue = startValue + (1 << unusedBits);
if (endValue > 31) {
return [base + BASE32[startValue], base + '~'];
}
else {
return [base + BASE32[startValue], base + BASE32[endValue]];
}
}
/**
* Calculates a set of query bounds to fully contain a given circle, each being a [start, end] pair
* where any geohash is guaranteed to be lexicographically larger than 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 geohash query bounds, each containing a [start, end] pair.
*/
function geohashQueryBounds(center, radius) {
validateLocation(center);
var queryBits = Math.max(1, boundingBoxBits(center, radius));
var geohashPrecision = Math.ceil(queryBits / BITS_PER_CHAR);
var coordinates = boundingBoxCoordinates(center, radius);
var queries = coordinates.map(function (coordinate) {
return geohashQuery(geohashForLocation(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];
});
});
}
/**
* 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.
*/
function distanceBetween(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;
}
/**
* 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.
*/
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 geoFireObj The location encoded as GeoFire object.
* @returns The location as [latitude, longitude] pair or null if decoding fails.
*/
function decodeGeoFireObject(geoFireObj) {
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.
*/
function geoFireGetKey(snapshot) {
var 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;
}
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Firebase constants. Some of these (@defines) can be overridden at compile-time.
*/
const CONSTANTS = {
/**
* @define {boolean} Whether this is the client Node.js SDK.
*/
NODE_CLIENT: false,
/**
* @define {boolean} Whether this is the Admin Node.js SDK.
*/
NODE_ADMIN: false,
/**
* Firebase SDK Version
*/
SDK_VERSION: '${JSCORE_VERSION}'
};
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Throws an error if the provided assertion is falsy
*/
const assert = function (assertion, message) {
if (!assertion) {
throw assertionError(message);
}
};
/**
* Returns an Error object suitable for throwing.
*/
const assertionError = function (message) {
return new Error('Firebase Database (' +
CONSTANTS.SDK_VERSION +
') INTERNAL ASSERT FAILED: ' +
message);
};
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const stringToByteArray$1 = function (str) {
// TODO(user): Use native implementations if/when available
const out = [];
let p = 0;
for (let i = 0; i < str.length; i++) {
let c = str.charCodeAt(i);
if (c < 128) {
out[p++] = c;
}
else if (c < 2048) {
out[p++] = (c >> 6) | 192;
out[p++] = (c & 63) | 128;
}
else if ((c & 0xfc00) === 0xd800 &&
i + 1 < str.length &&
(str.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {
// Surrogate Pair
c = 0x10000 + ((c & 0x03ff) << 10) + (str.charCodeAt(++i) & 0x03ff);
out[p++] = (c >> 18) | 240;
out[p++] = ((c >> 12) & 63) | 128;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
}
else {
out[p++] = (c >> 12) | 224;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
}
}
return out;
};
/**
* Turns an array of numbers into the string given by the concatenation of the
* characters to which the numbers correspond.
* @param bytes Array of numbers representing characters.
* @return Stringification of the array.
*/
const byteArrayToString = function (bytes) {
// TODO(user): Use native implementations if/when available
const out = [];
let pos = 0, c = 0;
while (pos < bytes.length) {
const c1 = bytes[pos++];
if (c1 < 128) {
out[c++] = String.fromCharCode(c1);
}
else if (c1 > 191 && c1 < 224) {
const c2 = bytes[pos++];
out[c++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
}
else if (c1 > 239 && c1 < 365) {
// Surrogate Pair
const c2 = bytes[pos++];
const c3 = bytes[pos++];
const c4 = bytes[pos++];
const u = (((c1 & 7) << 18) | ((c2 & 63) << 12) | ((c3 & 63) << 6) | (c4 & 63)) -
0x10000;
out[c++] = String.fromCharCode(0xd800 + (u >> 10));
out[c++] = String.fromCharCode(0xdc00 + (u & 1023));
}
else {
const c2 = bytes[pos++];
const c3 = bytes[pos++];
out[c++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
}
}
return out.join('');
};
// We define it as an object literal instead of a class because a class compiled down to es5 can't
// be treeshaked. https://github.com/rollup/rollup/issues/1691
// Static lookup maps, lazily populated by init_()
const base64 = {
/**
* Maps bytes to characters.
*/
byteToCharMap_: null,
/**
* Maps characters to bytes.
*/
charToByteMap_: null,
/**
* Maps bytes to websafe characters.
* @private
*/
byteToCharMapWebSafe_: null,
/**
* Maps websafe characters to bytes.
* @private
*/
charToByteMapWebSafe_: null,
/**
* Our default alphabet, shared between
* ENCODED_VALS and ENCODED_VALS_WEBSAFE
*/
ENCODED_VALS_BASE: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789',
/**
* Our default alphabet. Value 64 (=) is special; it means "nothing."
*/
get ENCODED_VALS() {
return this.ENCODED_VALS_BASE + '+/=';
},
/**
* Our websafe alphabet.
*/
get ENCODED_VALS_WEBSAFE() {
return this.ENCODED_VALS_BASE + '-_.';
},
/**
* Whether this browser supports the atob and btoa functions. This extension
* started at Mozilla but is now implemented by many browsers. We use the
* ASSUME_* variables to avoid pulling in the full useragent detection library
* but still allowing the standard per-browser compilations.
*
*/
HAS_NATIVE_SUPPORT: typeof atob === 'function',
/**
* Base64-encode an array of bytes.
*
* @param input An array of bytes (numbers with
* value in [0, 255]) to encode.
* @param webSafe Boolean indicating we should use the
* alternative alphabet.
* @return The base64 encoded string.
*/
encodeByteArray(input, webSafe) {
if (!Array.isArray(input)) {
throw Error('encodeByteArray takes an array as a parameter');
}
this.init_();
const byteToCharMap = webSafe
? this.byteToCharMapWebSafe_
: this.byteToCharMap_;
const output = [];
for (let i = 0; i < input.length; i += 3) {
const byte1 = input[i];
const haveByte2 = i + 1 < input.length;
const byte2 = haveByte2 ? input[i + 1] : 0;
const haveByte3 = i + 2 < input.length;
const byte3 = haveByte3 ? input[i + 2] : 0;
const outByte1 = byte1 >> 2;
const outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4);
let outByte3 = ((byte2 & 0x0f) << 2) | (byte3 >> 6);
let outByte4 = byte3 & 0x3f;
if (!haveByte3) {
outByte4 = 64;
if (!haveByte2) {
outByte3 = 64;
}
}
output.push(byteToCharMap[outByte1], byteToCharMap[outByte2], byteToCharMap[outByte3], byteToCharMap[outByte4]);
}
return output.join('');
},
/**
* Base64-encode a string.
*
* @param input A string to encode.
* @param webSafe If true, we should use the
* alternative alphabet.
* @return The base64 encoded string.
*/
encodeString(input, webSafe) {
// Shortcut for Mozilla browsers that implement
// a native base64 encoder in the form of "btoa/atob"
if (this.HAS_NATIVE_SUPPORT && !webSafe) {
return btoa(input);
}
return this.encodeByteArray(stringToByteArray$1(input), webSafe);
},
/**
* Base64-decode a string.
*
* @param input to decode.
* @param webSafe True if we should use the
* alternative alphabet.
* @return string representing the decoded value.
*/
decodeString(input, webSafe) {
// Shortcut for Mozilla browsers that implement
// a native base64 encoder in the form of "btoa/atob"
if (this.HAS_NATIVE_SUPPORT && !webSafe) {
return atob(input);
}
return byteArrayToString(this.decodeStringToByteArray(input, webSafe));
},
/**
* Base64-decode a string.
*
* In base-64 decoding, groups of four characters are converted into three
* bytes. If the encoder did not apply padding, the input length may not
* be a multiple of 4.
*
* In this case, the last group will have fewer than 4 characters, and
* padding will be inferred. If the group has one or two characters, it decodes
* to one byte. If the group has three characters, it decodes to two bytes.
*
* @param input Input to decode.
* @param webSafe True if we should use the web-safe alphabet.
* @return bytes representing the decoded value.
*/
decodeStringToByteArray(input, webSafe) {
this.init_();
const charToByteMap = webSafe
? this.charToByteMapWebSafe_
: this.charToByteMap_;
const output = [];
for (let i = 0; i < input.length;) {
const byte1 = charToByteMap[input.charAt(i++)];
const haveByte2 = i < input.length;
const byte2 = haveByte2 ? charToByteMap[input.charAt(i)] : 0;
++i;
const haveByte3 = i < input.length;
const byte3 = haveByte3 ? charToByteMap[input.charAt(i)] : 64;
++i;
const haveByte4 = i < input.length;
const byte4 = haveByte4 ? charToByteMap[input.charAt(i)] : 64;
++i;
if (byte1 == null || byte2 == null || byte3 == null || byte4 == null) {
throw Error();
}
const outByte1 = (byte1 << 2) | (byte2 >> 4);
output.push(outByte1);
if (byte3 !== 64) {
const outByte2 = ((byte2 << 4) & 0xf0) | (byte3 >> 2);
output.push(outByte2);
if (byte4 !== 64) {
const outByte3 = ((byte3 << 6) & 0xc0) | byte4;
output.push(outByte3);
}
}
}
return output;
},
/**
* Lazy static initialization function. Called before
* accessing any of the static map variables.
* @private
*/
init_() {
if (!this.byteToCharMap_) {
this.byteToCharMap_ = {};
this.charToByteMap_ = {};
this.byteToCharMapWebSafe_ = {};
this.charToByteMapWebSafe_ = {};
// We want quick mappings back and forth, so we precompute two maps.
for (let i = 0; i < this.ENCODED_VALS.length; i++) {
this.byteToCharMap_[i] = this.ENCODED_VALS.charAt(i);
this.charToByteMap_[this.byteToCharMap_[i]] = i;
this.byteToCharMapWebSafe_[i] = this.ENCODED_VALS_WEBSAFE.charAt(i);
this.charToByteMapWebSafe_[this.byteToCharMapWebSafe_[i]] = i;
// Be forgiving when decoding and correctly decode both encodings.
if (i >= this.ENCODED_VALS_BASE.length) {
this.charToByteMap_[this.ENCODED_VALS_WEBSAFE.charAt(i)] = i;
this.charToByteMapWebSafe_[this.ENCODED_VALS.charAt(i)] = i;
}
}
}
}
};
/**
* URL-safe base64 encoding
*/
const base64Encode = function (str) {
const utf8Bytes = stringToByteArray$1(str);
return base64.encodeByteArray(utf8Bytes, true);
};
/**
* URL-safe base64 encoding (without "." padding in the end).
* e.g. Used in JSON Web Token (JWT) parts.
*/
const base64urlEncodeWithoutPadding = function (str) {
// Use base64url encoding and remove padding in the end (dot characters).
return base64Encode(str).replace(/\./g, '');
};
/**
* URL-safe base64 decoding
*
* NOTE: DO NOT use the global atob() function - it does NOT support the
* base64Url variant encoding.
*
* @param str To be decoded
* @return Decoded result, if possible
*/
const base64Decode = function (str) {
try {
return base64.decodeString(str, true);
}
catch (e) {
console.error('base64Decode failed: ', e);
}
return null;
};
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Do a deep-copy of basic JavaScript Objects or Arrays.
*/
function deepCopy(value) {
return deepExtend(undefined, value);
}
/**
* Copy properties from source to target (recursively allows extension
* of Objects and Arrays). Scalar values in the target are over-written.
* If target is undefined, an object of the appropriate type will be created
* (and returned).
*
* We recursively copy all child properties of plain Objects in the source- so
* that namespace- like dictionaries are merged.
*
* Note that the target can be a function, in which case the properties in
* the source Object are copied onto it as static properties of the Function.
*
* Note: we don't merge __proto__ to prevent prototype pollution
*/
function deepExtend(target, source) {
if (!(source instanceof Object)) {
return source;
}
switch (source.constructor) {
case Date:
// Treat Dates like scalars; if the target date object had any child
// properties - they will be lost!
const dateValue = source;
return new Date(dateValue.getTime());
case Object:
if (target === undefined) {
target = {};
}
break;
case Array:
// Always copy the array source and overwrite the target.
target = [];
break;
default:
// Not a plain Object - treat it as a scalar.
return source;
}
for (const prop in source) {
// use isValidKey to guard against prototype pollution. See https://snyk.io/vuln/SNYK-JS-LODASH-450202
if (!source.hasOwnProperty(prop) || !isValidKey(prop)) {
continue;
}
target[prop] = deepExtend(target[prop], source[prop]);
}
return target;
}
function isValidKey(key) {
return key !== '__proto__';
}
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class Deferred {
constructor() {
this.reject = () => { };
this.resolve = () => { };
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
/**
* Our API internals are not promiseified and cannot because our callback APIs have subtle expectations around
* invoking promises inline, which Promises are forbidden to do. This method accepts an optional node-style callback
* and returns a node-style callback which will resolve or reject the Deferred's promise.
*/
wrapCallback(callback) {
return (error, value) => {
if (error) {
this.reject(error);
}
else {
this.resolve(value);
}
if (typeof callback === 'function') {
// Attaching noop handler just in case developer wasn't expecting
// promises
this.promise.catch(() => { });
// Some of our callbacks don't expect a value and our own tests
// assert that the parameter length is 1
if (callback.length === 1) {
callback(error);
}
else {
callback(error, value);
}
}
};
}
}
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Returns navigator.userAgent string or '' if it's not defined.
* @return user agent string
*/
function getUA() {
if (typeof navigator !== 'undefined' &&
typeof navigator['userAgent'] === 'string') {
return navigator['userAgent'];
}
else {
return '';
}
}
/**
* Detect Cordova / PhoneGap / Ionic frameworks on a mobile device.
*
* Deliberately does not rely on checking `file://` URLs (as this fails PhoneGap
* in the Ripple emulator) nor Cordova `onDeviceReady`, which would normally
* wait for a callback.
*/
function isMobileCordova() {
return (typeof window !== 'undefined' &&
// @ts-ignore Setting up an broadly applicable index signature for Window
// just to deal with this case would probably be a bad idea.
!!(window['cordova'] || window['phonegap'] || window['PhoneGap']) &&
/ios|iphone|ipod|ipad|android|blackberry|iemobile/i.test(getUA()));
}
/**
* Detect React Native.
*
* @return true if ReactNative environment is detected.
*/
function isReactNative() {
return (typeof navigator === 'object' && navigator['product'] === 'ReactNative');
}
/**
* Detect whether the current SDK build is the Node version.
*
* @return true if it's the Node SDK build.
*/
function isNodeSdk() {
return CONSTANTS.NODE_ADMIN === true;
}
/**
* This method checks if indexedDB is supported by current browser/service worker context
* @return true if indexedDB is supported by current browser/service worker context
*/
function isIndexedDBAvailable() {
return typeof indexedDB === 'object';
}
/**
* This method validates browser/sw context for indexedDB by opening a dummy indexedDB database and reject
* if errors occur during the database open operation.
*
* @throws exception if current browser/sw context can't run idb.open (ex: Safari iframe, Firefox
* private browsing)
*/
function validateIndexedDBOpenable() {
return new Promise((resolve, reject) => {
try {
let preExist = true;
const DB_CHECK_NAME = 'validate-browser-context-for-indexeddb-analytics-module';
const request = self.indexedDB.open(DB_CHECK_NAME);
request.onsuccess = () => {
request.result.close();
// delete database only when it doesn't pre-exist
if (!preExist) {
self.indexedDB.deleteDatabase(DB_CHECK_NAME);
}
resolve(true);
};
request.onupgradeneeded = () => {
preExist = false;
};
request.onerror = () => {
var _a;
reject(((_a = request.error) === null || _a === void 0 ? void 0 : _a.message) || '');
};
}
catch (error) {
reject(error);
}
});
}
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Standardized Firebase Error.
*
* Usage:
*
* // Typescript string literals for type-safe codes
* type Err =
* 'unknown' |
* 'object-not-found'
* ;
*
* // Closure enum for type-safe error codes
* // at-enum {string}
* var Err = {
* UNKNOWN: 'unknown',
* OBJECT_NOT_FOUND: 'object-not-found',
* }
*
* let errors: Map<Err, string> = {
* 'generic-error': "Unknown error",
* 'file-not-found': "Could not find file: {$file}",
* };
*
* // Type-safe function - must pass a valid error code as param.
* let error = new ErrorFactory<Err>('service', 'Service', errors);
*
* ...
* throw error.create(Err.GENERIC);
* ...
* throw error.create(Err.FILE_NOT_FOUND, {'file': fileName});
* ...
* // Service: Could not file file: foo.txt (service/file-not-found).
*
* catch (e) {
* assert(e.message === "Could not find file: foo.txt.");
* if ((e as FirebaseError)?.code === 'service/file-not-found') {
* console.log("Could not read file: " + e['file']);
* }
* }
*/
const ERROR_NAME = 'FirebaseError';
// Based on code from:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types
class FirebaseError extends Error {
constructor(
/** The error code for this error. */
code, message,
/** Custom data for this error. */
customData) {
super(message);
this.code = code;
this.customData = customData;
/** The custom name for all FirebaseErrors. */
this.name = ERROR_NAME;
// Fix For ES5
// https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, FirebaseError.prototype);
// Maintains proper stack trace for where our error was thrown.
// Only available on V8.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ErrorFactory.prototype.create);
}
}
}
class ErrorFactory {
constructor(service, serviceName, errors) {
this.service = service;
this.serviceName = serviceName;
this.errors = errors;
}
create(code, ...data) {
const customData = data[0] || {};
const fullCode = `${this.service}/${code}`;
const template = this.errors[code];
const message = template ? replaceTemplate(template, customData) : 'Error';
// Service Name: Error message (service/code).
const fullMessage = `${this.serviceName}: ${message} (${fullCode}).`;
const error = new FirebaseError(fullCode, fullMessage, customData);
return error;
}
}
function replaceTemplate(template, data) {
return template.replace(PATTERN, (_, key) => {
const value = data[key];
return value != null ? String(value) : `<${key}?>`;
});
}
const PATTERN = /\{\$([^}]+)}/g;
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Evaluates a JSON string into a javascript object.
*
* @param {string} str A string containing JSON.
* @return {*} The javascript object representing the specified JSON.
*/
function jsonEval(str) {
return JSON.parse(str);
}
/**
* Returns JSON representing a javascript object.
* @param {*} data Javascript object to be stringified.
* @return {string} The JSON contents of the object.
*/
function stringify(data) {
return JSON.stringify(data);
}
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Decodes a Firebase auth. token into constituent parts.
*
* Notes:
* - May return with invalid / incomplete claims if there's no native base64 decoding support.
* - Doesn't check if the token is actually valid.
*/
const decode = function (token) {
let header = {}, claims = {}, data = {}, signature = '';
try {
const parts = token.split('.');
header = jsonEval(base64Decode(parts[0]) || '');
claims = jsonEval(base64Decode(parts[1]) || '');
signature = parts[2];
data = claims['d'] || {};
delete claims['d'];
}
catch (e) { }
return {
header,
claims,
data,
signature
};
};
/**
* Decodes a Firebase auth. token and checks the validity of its format. Expects a valid issued-at time.
*
* Notes:
* - May return a false negative if there's no native base64 decoding support.
* - Doesn't check if the token is actually valid.
*/
const isValidFormat = function (token) {
const decoded = decode(token), claims = decoded.claims;
return !!claims && typeof claims === 'object' && claims.hasOwnProperty('iat');
};
/**
* Attempts to peer into an auth token and determine if it's an admin auth token by looking at the claims portion.
*
* Notes:
* - May return a false negative if there's no native base64 decoding support.
* - Doesn't check if the token is actually valid.
*/
const isAdmin = function (token) {
const claims = decode(token).claims;
return typeof claims === 'object' && claims['admin'] === true;
};
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function contains(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function safeGet(obj, key) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return obj[key];
}
else {
return undefined;
}
}
function isEmpty(obj) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
function map(obj, fn, contextObj) {
const res = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
res[key] = fn.call(contextObj, obj[key], key, obj);
}
}
return res;
}
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Returns a querystring-formatted string (e.g. &arg=val&arg2=val2) from a
* params object (e.g. {arg: 'val', arg2: 'val2'})