@firebase/firestore
Version:
The Cloud Firestore component of the Firebase JS SDK.
1,452 lines (1,441 loc) • 1.52 MB
JavaScript
import { _isFirebaseServerApp, _getProvider, getApp, _removeServiceInstance } from '@firebase/app';
import { FirebaseError, getModularInstance, isCloudWorkstation, pingServer, deepEqual, createMockUserToken, getDefaultEmulatorHostnameAndPort, getGlobal, isIndexedDBAvailable, getUA, isSafari, isSafariOrWebkit } from '@firebase/util';
import { Integer, Md5 } from '@firebase/webchannel-wrapper/bloom-blob';
import { Logger, LogLevel } from '@firebase/logger';
import { TextEncoder, inspect, TextDecoder } from 'util';
import { randomBytes as randomBytes$1 } from 'crypto';
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { RE2JS } from 're2js';
const version = "12.17.0";
/**
* @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.
*/
/** The semver (www.semver.org) version of the SDK. */
let SDK_VERSION = version;
function setSDKVersion(version) {
SDK_VERSION = version;
}
/**
* @license
* Copyright 2020 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.
*/
/** Formats an object as a JSON string, suitable for logging. */
function formatJSON(value) {
// util.inspect() results in much more readable output than JSON.stringify()
return inspect(value, { depth: 100 });
}
/**
* @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 logClient = new Logger('@firebase/firestore');
// Helper methods are needed because variables can't be exported as read/write
function getLogLevel() {
return logClient.logLevel;
}
/**
* Sets the verbosity of Cloud Firestore logs (debug, error, or silent).
*
* @param logLevel - The verbosity you set for activity and error logging. Can
* be any of the following values:
*
* <ul>
* <li>`debug` for the most verbose logging level, primarily for
* debugging.</li>
* <li>`error` to log errors only.</li>
* <li><code>`silent` to turn off logging.</li>
* </ul>
*/
function setLogLevel(logLevel) {
logClient.setLogLevel(logLevel);
}
function logDebug(msg, ...obj) {
if (logClient.logLevel <= LogLevel.DEBUG) {
const args = obj.map(argToString);
logClient.debug(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
}
function logError(msg, ...obj) {
if (logClient.logLevel <= LogLevel.ERROR) {
const args = obj.map(argToString);
logClient.error(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
}
/**
* @internal
*/
function logWarn(msg, ...obj) {
if (logClient.logLevel <= LogLevel.WARN) {
const args = obj.map(argToString);
logClient.warn(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
}
/**
* Converts an additional log parameter to a string representation.
*/
function argToString(obj) {
if (typeof obj === 'string') {
return obj;
}
else {
try {
return formatJSON(obj);
}
catch (e) {
// Converting to JSON failed, just log the object directly
return obj;
}
}
}
/**
* @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 fail(id, messageOrContext, context) {
let message = 'Unexpected state';
if (typeof messageOrContext === 'string') {
message = messageOrContext;
}
else {
context = messageOrContext;
}
_fail(id, message, context);
}
function _fail(id, failure, context) {
// Log the failure in addition to throw an exception, just in case the
// exception is swallowed.
let message = `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ${failure} (ID: ${id.toString(16)})`;
if (context !== undefined) {
try {
const stringContext = JSON.stringify(context);
message += ' CONTEXT: ' + stringContext;
}
catch (e) {
message += ' CONTEXT: ' + context;
}
}
logError(message);
// NOTE: We don't use FirestoreError here because these are internal failures
// that cannot be handled by the user. (Also it would create a circular
// dependency between the error and assert modules which doesn't work.)
throw new Error(message);
}
function hardAssert(assertion, id, messageOrContext, context) {
let message = 'Unexpected state';
if (typeof messageOrContext === 'string') {
message = messageOrContext;
}
else {
context = messageOrContext;
}
if (!assertion) {
_fail(id, message, context);
}
}
/**
* Fails if the given assertion condition is false, throwing an Error with the
* given message if it did.
*
* The code of callsites invoking this function are stripped out in production
* builds. Any side-effects of code within the debugAssert() invocation will not
* happen in this case.
*
* @internal
*/
function debugAssert(assertion, message) {
if (!assertion) {
fail(0xdeb6, message);
}
}
/**
* Casts `obj` to `T`. In non-production builds, verifies that `obj` is an
* instance of `T` before casting.
*/
function debugCast(obj,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor) {
return obj;
}
/**
* @license
* Copyright 2020 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.
*/
/**
* Generates `nBytes` of random bytes.
*
* If `nBytes < 0` , an error will be thrown.
*/
function randomBytes(nBytes) {
return randomBytes$1(nBytes);
}
/**
* @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.
*/
/**
* A utility class for generating unique alphanumeric IDs of a specified length.
*
* @internal
* Exported internally for testing purposes.
*/
class AutoId {
static newId() {
// Alphanumeric characters
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// The largest byte value that is a multiple of `char.length`.
const maxMultiple = Math.floor(256 / chars.length) * chars.length;
let autoId = '';
const targetLength = 20;
while (autoId.length < targetLength) {
const bytes = randomBytes(40);
for (let i = 0; i < bytes.length; ++i) {
// Only accept values that are [0, maxMultiple), this ensures they can
// be evenly mapped to indices of `chars` via a modulo operation.
if (autoId.length < targetLength && bytes[i] < maxMultiple) {
autoId += chars.charAt(bytes[i] % chars.length);
}
}
}
return autoId;
}
}
function primitiveComparator(left, right) {
if (left < right) {
return -1;
}
if (left > right) {
return 1;
}
return 0;
}
/** Compare strings in UTF-8 encoded byte order */
function compareUtf8Strings(left, right) {
// Find the first differing character (a.k.a. "UTF-16 code unit") in the two strings and,
// if found, use that character to determine the relative ordering of the two strings as a
// whole. Comparing UTF-16 strings in UTF-8 byte order can be done simply and efficiently by
// comparing the UTF-16 code units (chars). This serendipitously works because of the way UTF-8
// and UTF-16 happen to represent Unicode code points.
//
// After finding the first pair of differing characters, there are two cases:
//
// Case 1: Both characters are non-surrogates (code points less than or equal to 0xFFFF) or
// both are surrogates from a surrogate pair (that collectively represent code points greater
// than 0xFFFF). In this case their numeric order as UTF-16 code units is the same as the
// lexicographical order of their corresponding UTF-8 byte sequences. A direct comparison is
// sufficient.
//
// Case 2: One character is a surrogate and the other is not. In this case the surrogate-
// containing string is always ordered after the non-surrogate. This is because surrogates are
// used to represent code points greater than 0xFFFF which have 4-byte UTF-8 representations
// and are lexicographically greater than the 1, 2, or 3-byte representations of code points
// less than or equal to 0xFFFF.
//
// An example of why Case 2 is required is comparing the following two Unicode code points:
//
// |-----------------------|------------|---------------------|-----------------|
// | Name | Code Point | UTF-8 Encoding | UTF-16 Encoding |
// |-----------------------|------------|---------------------|-----------------|
// | Replacement Character | U+FFFD | 0xEF 0xBF 0xBD | 0xFFFD |
// | Grinning Face | U+1F600 | 0xF0 0x9F 0x98 0x80 | 0xD83D 0xDE00 |
// |-----------------------|------------|---------------------|-----------------|
//
// A lexicographical comparison of the UTF-8 encodings of these code points would order
// "Replacement Character" _before_ "Grinning Face" because 0xEF is less than 0xF0. However, a
// direct comparison of the UTF-16 code units, as would be done in case 1, would erroneously
// produce the _opposite_ ordering, because 0xFFFD is _greater than_ 0xD83D. As it turns out,
// this relative ordering holds for all comparisons of UTF-16 code points requiring a surrogate
// pair with those that do not.
const length = Math.min(left.length, right.length);
for (let i = 0; i < length; i++) {
const leftChar = left.charAt(i);
const rightChar = right.charAt(i);
if (leftChar !== rightChar) {
return isSurrogate(leftChar) === isSurrogate(rightChar)
? primitiveComparator(leftChar, rightChar)
: isSurrogate(leftChar)
? 1
: -1;
}
}
// Use the lengths of the strings to determine the overall comparison result since either the
// strings were equal or one is a prefix of the other.
return primitiveComparator(left.length, right.length);
}
const MIN_SURROGATE$1 = 0xd800;
const MAX_SURROGATE$1 = 0xdfff;
function isSurrogate(s) {
const c = s.charCodeAt(0);
return c >= MIN_SURROGATE$1 && c <= MAX_SURROGATE$1;
}
/** Helper to compare arrays using isEqual(). */
function arrayEquals(left, right, comparator) {
if (left.length !== right.length) {
return false;
}
return left.every((value, index) => comparator(value, right[index]));
}
/**
* Verifies equality for an optional value.
*/
function isOptionalEqual(left, right, equalityTest) {
if (left === undefined && right === undefined) {
return true;
}
if (left === undefined || right === undefined) {
return false;
}
return equalityTest(left, right);
}
/**
* Returns the immediate lexicographically-following string. This is useful to
* construct an inclusive range for indexeddb iterators.
*/
function immediateSuccessor(s) {
// Return the input string, with an additional NUL byte appended.
return s + '\0';
}
/**
* @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.
*/
// An immutable sorted map implementation, based on a Left-leaning Red-Black
// tree.
class SortedMap {
constructor(comparator, root) {
this.comparator = comparator;
this.root = root ? root : LLRBNode.EMPTY;
}
// Returns a copy of the map, with the specified key/value added or replaced.
insert(key, value) {
return new SortedMap(this.comparator, this.root
.insert(key, value, this.comparator)
.copy(null, null, LLRBNode.BLACK, null, null));
}
// Returns a copy of the map, with the specified key removed.
remove(key) {
return new SortedMap(this.comparator, this.root
.remove(key, this.comparator)
.copy(null, null, LLRBNode.BLACK, null, null));
}
// Returns the value of the node with the given key, or null.
get(key) {
let node = this.root;
while (!node.isEmpty()) {
const cmp = this.comparator(key, node.key);
if (cmp === 0) {
return node.value;
}
else if (cmp < 0) {
node = node.left;
}
else if (cmp > 0) {
node = node.right;
}
}
return null;
}
// Returns the index of the element in this sorted map, or -1 if it doesn't
// exist.
indexOf(key) {
// Number of nodes that were pruned when descending right
let prunedNodes = 0;
let node = this.root;
while (!node.isEmpty()) {
const cmp = this.comparator(key, node.key);
if (cmp === 0) {
return prunedNodes + node.left.size;
}
else if (cmp < 0) {
node = node.left;
}
else {
// Count all nodes left of the node plus the node itself
prunedNodes += node.left.size + 1;
node = node.right;
}
}
// Node not found
return -1;
}
isEmpty() {
return this.root.isEmpty();
}
// Returns the total number of nodes in the map.
get size() {
return this.root.size;
}
// Returns the minimum key in the map.
minKey() {
return this.root.minKey();
}
// Returns the maximum key in the map.
maxKey() {
return this.root.maxKey();
}
// Traverses the map in key order and calls the specified action function
// for each key/value pair. If action returns true, traversal is aborted.
// Returns the first truthy value returned by action, or the last falsey
// value returned by action.
inorderTraversal(action) {
return this.root.inorderTraversal(action);
}
forEach(fn) {
this.inorderTraversal((k, v) => {
fn(k, v);
return false;
});
}
toString() {
const descriptions = [];
this.inorderTraversal((k, v) => {
descriptions.push(`${k}:${v}`);
return false;
});
return `{${descriptions.join(', ')}}`;
}
// Traverses the map in reverse key order and calls the specified action
// function for each key/value pair. If action returns true, traversal is
// aborted.
// Returns the first truthy value returned by action, or the last falsey
// value returned by action.
reverseTraversal(action) {
return this.root.reverseTraversal(action);
}
// Returns an iterator over the SortedMap.
getIterator() {
return new SortedMapIterator(this.root, null, this.comparator, false);
}
getIteratorFrom(key) {
return new SortedMapIterator(this.root, key, this.comparator, false);
}
getReverseIterator() {
return new SortedMapIterator(this.root, null, this.comparator, true);
}
getReverseIteratorFrom(key) {
return new SortedMapIterator(this.root, key, this.comparator, true);
}
} // end SortedMap
// An iterator over an LLRBNode.
class SortedMapIterator {
constructor(node, startKey, comparator, isReverse) {
this.isReverse = isReverse;
this.nodeStack = [];
let cmp = 1;
while (!node.isEmpty()) {
cmp = startKey ? comparator(node.key, startKey) : 1;
// flip the comparison if we're going in reverse
if (startKey && isReverse) {
cmp *= -1;
}
if (cmp < 0) {
// This node is less than our start key. ignore it
if (this.isReverse) {
node = node.left;
}
else {
node = node.right;
}
}
else if (cmp === 0) {
// This node is exactly equal to our start key. Push it on the stack,
// but stop iterating;
this.nodeStack.push(node);
break;
}
else {
// This node is greater than our start key, add it to the stack and move
// to the next one
this.nodeStack.push(node);
if (this.isReverse) {
node = node.right;
}
else {
node = node.left;
}
}
}
}
getNext() {
let node = this.nodeStack.pop();
const result = { key: node.key, value: node.value };
if (this.isReverse) {
node = node.left;
while (!node.isEmpty()) {
this.nodeStack.push(node);
node = node.right;
}
}
else {
node = node.right;
while (!node.isEmpty()) {
this.nodeStack.push(node);
node = node.left;
}
}
return result;
}
hasNext() {
return this.nodeStack.length > 0;
}
peek() {
if (this.nodeStack.length === 0) {
return null;
}
const node = this.nodeStack[this.nodeStack.length - 1];
return { key: node.key, value: node.value };
}
} // end SortedMapIterator
// Represents a node in a Left-leaning Red-Black tree.
class LLRBNode {
constructor(key, value, color, left, right) {
this.key = key;
this.value = value;
this.color = color != null ? color : LLRBNode.RED;
this.left = left != null ? left : LLRBNode.EMPTY;
this.right = right != null ? right : LLRBNode.EMPTY;
this.size = this.left.size + 1 + this.right.size;
}
// Returns a copy of the current node, optionally replacing pieces of it.
copy(key, value, color, left, right) {
return new LLRBNode(key != null ? key : this.key, value != null ? value : this.value, color != null ? color : this.color, left != null ? left : this.left, right != null ? right : this.right);
}
isEmpty() {
return false;
}
// Traverses the tree in key order and calls the specified action function
// for each node. If action returns true, traversal is aborted.
// Returns the first truthy value returned by action, or the last falsey
// value returned by action.
inorderTraversal(action) {
return (this.left.inorderTraversal(action) ||
action(this.key, this.value) ||
this.right.inorderTraversal(action));
}
// Traverses the tree in reverse key order and calls the specified action
// function for each node. If action returns true, traversal is aborted.
// Returns the first truthy value returned by action, or the last falsey
// value returned by action.
reverseTraversal(action) {
return (this.right.reverseTraversal(action) ||
action(this.key, this.value) ||
this.left.reverseTraversal(action));
}
// Returns the minimum node in the tree.
min() {
if (this.left.isEmpty()) {
return this;
}
else {
return this.left.min();
}
}
// Returns the maximum key in the tree.
minKey() {
return this.min().key;
}
// Returns the maximum key in the tree.
maxKey() {
if (this.right.isEmpty()) {
return this.key;
}
else {
return this.right.maxKey();
}
}
// Returns new tree, with the key/value added.
insert(key, value, comparator) {
let n = this;
const cmp = comparator(key, n.key);
if (cmp < 0) {
n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
}
else if (cmp === 0) {
n = n.copy(null, value, null, null, null);
}
else {
n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
}
return n.fixUp();
}
removeMin() {
if (this.left.isEmpty()) {
return LLRBNode.EMPTY;
}
let n = this;
if (!n.left.isRed() && !n.left.left.isRed()) {
n = n.moveRedLeft();
}
n = n.copy(null, null, null, n.left.removeMin(), null);
return n.fixUp();
}
// Returns new tree, with the specified item removed.
remove(key, comparator) {
let smallest;
let n = this;
if (comparator(key, n.key) < 0) {
if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
n = n.moveRedLeft();
}
n = n.copy(null, null, null, n.left.remove(key, comparator), null);
}
else {
if (n.left.isRed()) {
n = n.rotateRight();
}
if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
n = n.moveRedRight();
}
if (comparator(key, n.key) === 0) {
if (n.right.isEmpty()) {
return LLRBNode.EMPTY;
}
else {
smallest = n.right.min();
n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
}
}
n = n.copy(null, null, null, null, n.right.remove(key, comparator));
}
return n.fixUp();
}
isRed() {
return this.color;
}
// Returns new tree after performing any needed rotations.
fixUp() {
let n = this;
if (n.right.isRed() && !n.left.isRed()) {
n = n.rotateLeft();
}
if (n.left.isRed() && n.left.left.isRed()) {
n = n.rotateRight();
}
if (n.left.isRed() && n.right.isRed()) {
n = n.colorFlip();
}
return n;
}
moveRedLeft() {
let n = this.colorFlip();
if (n.right.left.isRed()) {
n = n.copy(null, null, null, null, n.right.rotateRight());
n = n.rotateLeft();
n = n.colorFlip();
}
return n;
}
moveRedRight() {
let n = this.colorFlip();
if (n.left.left.isRed()) {
n = n.rotateRight();
n = n.colorFlip();
}
return n;
}
rotateLeft() {
const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
return this.right.copy(null, null, this.color, nl, null);
}
rotateRight() {
const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
return this.left.copy(null, null, this.color, null, nr);
}
colorFlip() {
const left = this.left.copy(null, null, !this.left.color, null, null);
const right = this.right.copy(null, null, !this.right.color, null, null);
return this.copy(null, null, !this.color, left, right);
}
// For testing.
checkMaxDepth() {
const blackDepth = this.check();
if (Math.pow(2.0, blackDepth) <= this.size + 1) {
return true;
}
else {
return false;
}
}
// In a balanced RB tree, the black-depth (number of black nodes) from root to
// leaves is equal on both sides. This function verifies that or asserts.
check() {
if (this.isRed() && this.left.isRed()) {
throw fail(0xaad2, {
key: this.key,
value: this.value
});
}
if (this.right.isRed()) {
throw fail(0x3721, {
key: this.key,
value: this.value
});
}
const blackDepth = this.left.check();
if (blackDepth !== this.right.check()) {
throw fail(0x6d2d);
}
else {
return blackDepth + (this.isRed() ? 0 : 1);
}
}
} // end LLRBNode
// Empty node is shared between all LLRB trees.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
LLRBNode.EMPTY = null;
LLRBNode.RED = true;
LLRBNode.BLACK = false;
// Represents an empty node (a leaf node in the Red-Black Tree).
class LLRBEmptyNode {
constructor() {
this.size = 0;
}
get key() {
throw fail(0xe1a6);
}
get value() {
throw fail(0x3f0d);
}
get color() {
throw fail(0x4157);
}
get left() {
throw fail(0x741e);
}
get right() {
throw fail(0x901e);
}
// Returns a copy of the current node.
copy(key, value, color, left, right) {
return this;
}
// Returns a copy of the tree, with the specified key/value added.
insert(key, value, comparator) {
return new LLRBNode(key, value);
}
// Returns a copy of the tree, with the specified key removed.
remove(key, comparator) {
return this;
}
isEmpty() {
return true;
}
inorderTraversal(action) {
return false;
}
reverseTraversal(action) {
return false;
}
minKey() {
return null;
}
maxKey() {
return null;
}
isRed() {
return false;
}
// For testing.
checkMaxDepth() {
return true;
}
check() {
return 0;
}
} // end LLRBEmptyNode
LLRBNode.EMPTY = new LLRBEmptyNode();
/**
* @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.
*/
/**
* SortedSet is an immutable (copy-on-write) collection that holds elements
* in order specified by the provided comparator.
*
* NOTE: if provided comparator returns 0 for two elements, we consider them to
* be equal!
*/
class SortedSet {
constructor(comparator) {
this.comparator = comparator;
this.data = new SortedMap(this.comparator);
}
has(elem) {
return this.data.get(elem) !== null;
}
first() {
return this.data.minKey();
}
last() {
return this.data.maxKey();
}
get size() {
return this.data.size;
}
indexOf(elem) {
return this.data.indexOf(elem);
}
/** Iterates elements in order defined by "comparator" */
forEach(cb) {
this.data.inorderTraversal((k, v) => {
cb(k);
return false;
});
}
/** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
forEachInRange(range, cb) {
const iter = this.data.getIteratorFrom(range[0]);
while (iter.hasNext()) {
const elem = iter.getNext();
if (this.comparator(elem.key, range[1]) >= 0) {
return;
}
cb(elem.key);
}
}
/**
* Iterates over `elem`s such that: start <= elem until false is returned.
*/
forEachWhile(cb, start) {
let iter;
if (start !== undefined) {
iter = this.data.getIteratorFrom(start);
}
else {
iter = this.data.getIterator();
}
while (iter.hasNext()) {
const elem = iter.getNext();
const result = cb(elem.key);
if (!result) {
return;
}
}
}
/** Finds the least element greater than or equal to `elem`. */
firstAfterOrEqual(elem) {
const iter = this.data.getIteratorFrom(elem);
return iter.hasNext() ? iter.getNext().key : null;
}
getIterator() {
return new SortedSetIterator(this.data.getIterator());
}
getIteratorFrom(key) {
return new SortedSetIterator(this.data.getIteratorFrom(key));
}
/** Inserts or updates an element */
add(elem) {
return this.copy(this.data.remove(elem).insert(elem, true));
}
/** Deletes an element */
delete(elem) {
if (!this.has(elem)) {
return this;
}
return this.copy(this.data.remove(elem));
}
isEmpty() {
return this.data.isEmpty();
}
unionWith(other) {
let result = this;
// Make sure `result` always refers to the larger one of the two sets.
if (result.size < other.size) {
result = other;
other = this;
}
other.forEach(elem => {
result = result.add(elem);
});
return result;
}
isEqual(other) {
if (!(other instanceof SortedSet)) {
return false;
}
if (this.size !== other.size) {
return false;
}
const thisIt = this.data.getIterator();
const otherIt = other.data.getIterator();
while (thisIt.hasNext()) {
const thisElem = thisIt.getNext().key;
const otherElem = otherIt.getNext().key;
if (this.comparator(thisElem, otherElem) !== 0) {
return false;
}
}
return true;
}
toArray() {
const res = [];
this.forEach(targetId => {
res.push(targetId);
});
return res;
}
toString() {
const result = [];
this.forEach(elem => result.push(elem));
return 'SortedSet(' + result.toString() + ')';
}
copy(data) {
const result = new SortedSet(this.comparator);
result.data = data;
return result;
}
}
class SortedSetIterator {
constructor(iter) {
this.iter = iter;
}
getNext() {
return this.iter.getNext().key;
}
hasNext() {
return this.iter.hasNext();
}
}
/**
* Compares two sorted sets for equality using their natural ordering. The
* method computes the intersection and invokes `onAdd` for every element that
* is in `after` but not `before`. `onRemove` is invoked for every element in
* `before` but missing from `after`.
*
* The method creates a copy of both `before` and `after` and runs in O(n log
* n), where n is the size of the two lists.
*
* @param before - The elements that exist in the original set.
* @param after - The elements to diff against the original set.
* @param comparator - The comparator for the elements in before and after.
* @param onAdd - A function to invoke for every element that is part of `
* after` but not `before`.
* @param onRemove - A function to invoke for every element that is part of
* `before` but not `after`.
*/
function diffSortedSets(before, after, comparator, onAdd, onRemove) {
const beforeIt = before.getIterator();
const afterIt = after.getIterator();
let beforeValue = advanceIterator(beforeIt);
let afterValue = advanceIterator(afterIt);
// Walk through the two sets at the same time, using the ordering defined by
// `comparator`.
while (beforeValue || afterValue) {
let added = false;
let removed = false;
if (beforeValue && afterValue) {
const cmp = comparator(beforeValue, afterValue);
if (cmp < 0) {
// The element was removed if the next element in our ordered
// walkthrough is only in `before`.
removed = true;
}
else if (cmp > 0) {
// The element was added if the next element in our ordered walkthrough
// is only in `after`.
added = true;
}
}
else if (beforeValue != null) {
removed = true;
}
else {
added = true;
}
if (added) {
onAdd(afterValue);
afterValue = advanceIterator(afterIt);
}
else if (removed) {
onRemove(beforeValue);
beforeValue = advanceIterator(beforeIt);
}
else {
beforeValue = advanceIterator(beforeIt);
afterValue = advanceIterator(afterIt);
}
}
}
/**
* Returns the next element from the iterator or `undefined` if none available.
*/
function advanceIterator(it) {
return it.hasNext() ? it.getNext() : undefined;
}
/**
* @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 Code = {
// Causes are copied from:
// https://github.com/grpc/grpc/blob/bceec94ea4fc5f0085d81235d8e1c06798dc341a/include/grpc%2B%2B/impl/codegen/status_code_enum.h
/** Not an error; returned on success. */
OK: 'ok',
/** The operation was cancelled (typically by the caller). */
CANCELLED: 'cancelled',
/** Unknown error or an error from a different error domain. */
UNKNOWN: 'unknown',
/**
* Client specified an invalid argument. Note that this differs from
* FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are
* problematic regardless of the state of the system (e.g., a malformed file
* name).
*/
INVALID_ARGUMENT: 'invalid-argument',
/**
* Deadline expired before operation could complete. For operations that
* change the state of the system, this error may be returned even if the
* operation has completed successfully. For example, a successful response
* from a server could have been delayed long enough for the deadline to
* expire.
*/
DEADLINE_EXCEEDED: 'deadline-exceeded',
/** Some requested entity (e.g., file or directory) was not found. */
NOT_FOUND: 'not-found',
/**
* Some entity that we attempted to create (e.g., file or directory) already
* exists.
*/
ALREADY_EXISTS: 'already-exists',
/**
* The caller does not have permission to execute the specified operation.
* PERMISSION_DENIED must not be used for rejections caused by exhausting
* some resource (use RESOURCE_EXHAUSTED instead for those errors).
* PERMISSION_DENIED must not be used if the caller cannot be identified
* (use UNAUTHENTICATED instead for those errors).
*/
PERMISSION_DENIED: 'permission-denied',
/**
* The request does not have valid authentication credentials for the
* operation.
*/
UNAUTHENTICATED: 'unauthenticated',
/**
* Some resource has been exhausted, perhaps a per-user quota, or perhaps the
* entire file system is out of space.
*/
RESOURCE_EXHAUSTED: 'resource-exhausted',
/**
* Operation was rejected because the system is not in a state required for
* the operation's execution. For example, directory to be deleted may be
* non-empty, an rmdir operation is applied to a non-directory, etc.
*
* A litmus test that may help a service implementor in deciding
* between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE:
* (a) Use UNAVAILABLE if the client can retry just the failing call.
* (b) Use ABORTED if the client should retry at a higher-level
* (e.g., restarting a read-modify-write sequence).
* (c) Use FAILED_PRECONDITION if the client should not retry until
* the system state has been explicitly fixed. E.g., if an "rmdir"
* fails because the directory is non-empty, FAILED_PRECONDITION
* should be returned since the client should not retry unless
* they have first fixed up the directory by deleting files from it.
* (d) Use FAILED_PRECONDITION if the client performs conditional
* REST Get/Update/Delete on a resource and the resource on the
* server does not match the condition. E.g., conflicting
* read-modify-write on the same resource.
*/
FAILED_PRECONDITION: 'failed-precondition',
/**
* The operation was aborted, typically due to a concurrency issue like
* sequencer check failures, transaction aborts, etc.
*
* See litmus test above for deciding between FAILED_PRECONDITION, ABORTED,
* and UNAVAILABLE.
*/
ABORTED: 'aborted',
/**
* Operation was attempted past the valid range. E.g., seeking or reading
* past end of file.
*
* Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed
* if the system state changes. For example, a 32-bit file system will
* generate INVALID_ARGUMENT if asked to read at an offset that is not in the
* range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from
* an offset past the current file size.
*
* There is a fair bit of overlap between FAILED_PRECONDITION and
* OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error)
* when it applies so that callers who are iterating through a space can
* easily look for an OUT_OF_RANGE error to detect when they are done.
*/
OUT_OF_RANGE: 'out-of-range',
/** Operation is not implemented or not supported/enabled in this service. */
UNIMPLEMENTED: 'unimplemented',
/**
* Internal errors. Means some invariants expected by underlying System has
* been broken. If you see one of these errors, Something is very broken.
*/
INTERNAL: 'internal',
/**
* The service is currently unavailable. This is a most likely a transient
* condition and may be corrected by retrying with a backoff.
*
* See litmus test above for deciding between FAILED_PRECONDITION, ABORTED,
* and UNAVAILABLE.
*/
UNAVAILABLE: 'unavailable',
/** Unrecoverable data loss or corruption. */
DATA_LOSS: 'data-loss'
};
/** An error returned by a Firestore operation. */
class FirestoreError extends FirebaseError {
/** @hideconstructor */
constructor(
/**
* The backend error code associated with this error.
*/
code,
/**
* A custom error description.
*/
message) {
super(code, message);
this.code = code;
this.message = message;
// HACK: We write a toString property directly because Error is not a real
// class and so inheritance does not work correctly. We could alternatively
// do the same "back-door inheritance" trick that FirebaseError does.
this.toString = () => `${this.name}: [code=${this.code}]: ${this.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 DOCUMENT_KEY_NAME = '__name__';
const UPDATE_TIME_NAME = '__update_time__';
const CREATE_TIME_NAME = '__create_time__';
/**
* Path represents an ordered sequence of string segments.
*/
class BasePath {
constructor(segments, offset, length) {
if (offset === undefined) {
offset = 0;
}
else if (offset > segments.length) {
fail(0x027d, {
offset,
range: segments.length
});
}
if (length === undefined) {
length = segments.length - offset;
}
else if (length > segments.length - offset) {
fail(0x06d2, {
length,
range: segments.length - offset
});
}
this.segments = segments;
this.offset = offset;
this.len = length;
}
get length() {
return this.len;
}
isEqual(other) {
return BasePath.comparator(this, other) === 0;
}
child(nameOrPath) {
const segments = this.segments.slice(this.offset, this.limit());
if (nameOrPath instanceof BasePath) {
nameOrPath.forEach(segment => {
segments.push(segment);
});
}
else {
segments.push(nameOrPath);
}
return this.construct(segments);
}
/** The index of one past the last segment of the path. */
limit() {
return this.offset + this.length;
}
popFirst(size) {
size = size === undefined ? 1 : size;
return this.construct(this.segments, this.offset + size, this.length - size);
}
popLast() {
return this.construct(this.segments, this.offset, this.length - 1);
}
firstSegment() {
return this.segments[this.offset];
}
lastSegment() {
return this.get(this.length - 1);
}
get(index) {
return this.segments[this.offset + index];
}
isEmpty() {
return this.length === 0;
}
isPrefixOf(other) {
if (other.length < this.length) {
return false;
}
for (let i = 0; i < this.length; i++) {
if (this.get(i) !== other.get(i)) {
return false;
}
}
return true;
}
isImmediateParentOf(potentialChild) {
if (this.length + 1 !== potentialChild.length) {
return false;
}
for (let i = 0; i < this.length; i++) {
if (this.get(i) !== potentialChild.get(i)) {
return false;
}
}
return true;
}
forEach(fn) {
for (let i = this.offset, end = this.limit(); i < end; i++) {
fn(this.segments[i]);
}
}
toArray() {
return this.segments.slice(this.offset, this.limit());
}
/**
* Compare 2 paths segment by segment, prioritizing numeric IDs
* (e.g., "__id123__") in numeric ascending order, followed by string
* segments in lexicographical order.
*/
static comparator(p1, p2) {
const len = Math.min(p1.length, p2.length);
for (let i = 0; i < len; i++) {
const comparison = BasePath.compareSegments(p1.get(i), p2.get(i));
if (comparison !== 0) {
return comparison;
}
}
return primitiveComparator(p1.length, p2.length);
}
static compareSegments(lhs, rhs) {
const isLhsNumeric = BasePath.isNumericId(lhs);
const isRhsNumeric = BasePath.isNumericId(rhs);
if (isLhsNumeric && !isRhsNumeric) {
// Only lhs is numeric
return -1;
}
else if (!isLhsNumeric && isRhsNumeric) {
// Only rhs is numeric
return 1;
}
else if (isLhsNumeric && isRhsNumeric) {
// both numeric
return BasePath.extractNumericId(lhs).compare(BasePath.extractNumericId(rhs));
}
else {
// both non-numeric
return compareUtf8Strings(lhs, rhs);
}
}
// Checks if a segment is a numeric ID (starts with "__id" and ends with "__").
static isNumericId(segment) {
return segment.startsWith('__id') && segment.endsWith('__');
}
static extractNumericId(segment) {
return Integer.fromString(segment.substring(4, segment.length - 2));
}
}
/**
* A slash-separated path for navigating resources (documents and collections)
* within Firestore.
*
* @internal
*/
class ResourcePath extends BasePath {
construct(segments, offset, length) {
return new ResourcePath(segments, offset, length);
}
canonicalString() {
// NOTE: The client is ignorant of any path segments containing escape
// sequences (e.g. __id123__) and just passes them through raw (they exist
// for legacy reasons and should not be used frequently).
return this.toArray().join('/');
}
toString() {
return this.canonicalString();
}
toStringWithLeadingSlash() {
return `/${this.canonicalString()}`;
}
/**
* Returns a string representation of this path
* where each path segment has been encoded with
* `encodeURIComponent`.
*/
toUriEncodedString() {
return this.toArray().map(encodeURIComponent).join('/');
}
/**
* Creates a resource path from the given slash-delimited string. If multiple
* arguments are provided, all components are combined. Leading and trailing
* slashes from all components are ignored.
*/
static fromString(...pathComponents) {
// NOTE: The client is ignorant of any path segments containing escape
// sequences (e.g. __id123__) and just passes them through raw (they exist
// for legacy reasons and should not be used frequently).
const segments = [];
for (const path of pathComponents) {
if (path.indexOf('//') >= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid segment (${path}). Paths must not contain // in them.`);
}
// Strip leading and trailing slashed.
segments.push(...path.split('/').filter(segment => segment.length > 0));
}
return new ResourcePath(segments);
}
static emptyPath() {
return new ResourcePath([]);
}
}
const identifierRegExp = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
/**
* A dot-separated path for navigating sub-objects within a document.
* @internal
*/
let FieldPath$1 = class FieldPath extends BasePath {
construct(segments, offset, length) {
return new FieldPath(segments, offset, length);
}
/**
* Returns true if the string could be used as a segment in a field path
* without escaping.
*/
static isValidIdentifier(segment) {
return identifierRegExp.test(segment);
}
canonicalString() {
return this.toA